summaryrefslogtreecommitdiff
path: root/makima/src/db
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-05-01 23:56:51 +0100
committerGitHub <noreply@github.com>2026-05-01 23:56:51 +0100
commite11759447b1ac00becfb1e979e488f7f9c9cf478 (patch)
treef8a58368de3f6dda3f2f5c1af34e869a0e714205 /makima/src/db
parent80085c7cfa9d679ed3e3fd54a7d55fa8ab1addef (diff)
downloadsoryu-e11759447b1ac00becfb1e979e488f7f9c9cf478.tar.gz
soryu-e11759447b1ac00becfb1e979e488f7f9c9cf478.zip
chore(cleanup): Phase 5 contracts removal + tmp directive + 30-day expiry + scroll fix (#118)
Sweeping cleanup across the surface and the wire. Net: -14k LOC of legacy contracts code, plus the tmp/scroll/UX fixes the user asked for. ## Sidebar/editor independent scroll Replace `height: calc(100vh - 80px)` (which assumed an 80px masthead and quietly clipped or pushed the whole page below the fold when the masthead was taller) with `h-screen + overflow-hidden` on the page root and proper `flex-1 min-h-0` sizing on `<main>`. Sidebar and editor pane now manage their own scroll independently; the page itself never scrolls. Same fix in /tmp/:taskId. ## tmp directive — real backing for orphans/ephemerals New migration `20260501100000_tmp_directive_and_clear_orphans.sql`: * Adds `directives.is_tmp` BOOLEAN NOT NULL DEFAULT false. * Partial unique index `(owner_id) WHERE is_tmp` — at most ONE tmp directive per owner. * Hard-deletes every existing orphan task (`directive_id IS NULL`). Per the user spec: "ALSO there are TOO MANY old tasks in tmp, we need to remove all of them as well." New repository helpers: * `get_or_create_tmp_directive(pool, owner_id) -> Directive` INSERT ON CONFLICT DO NOTHING + fallback SELECT, race-safe. * `list_all_tmp_directives` — drives the expiry sweep. * `delete_expired_tmp_tasks(tmp_directive_id) -> u64`. * `list_tmp_tasks_for_owner` (replaces `list_orphan_tasks_for_owner`). `mesh::create_task`: every top-level task must have a directive. If a caller doesn't supply `directive_id` and isn't a subtask, attach to the caller's tmp directive (auto-creating it on first use). `list_directives_for_owner` filters out `is_tmp=true` so the scratchpad directive doesn't pollute the contract list — surfaced via the sidebar's `tmp/` folder instead. ## 30-day expiry on tmp tasks New `phase_tmp_expiry` in the directive reconciler. Throttled to once per hour: enumerates every tmp directive, calls `delete_expired_tmp_tasks`, logs the count. The actual delete is `WHERE created_at < NOW() - INTERVAL '30 days'` and is fast on the existing index. Subtasks die via FK cascade. ## Phase 5 — contracts removed ### Frontend Deleted entire `/contracts` surface: * routes: `contracts.tsx`, `contract-file.tsx` * components/contracts: ContractList, ContractDetail, ContractCliInput, ContractContextMenu, CommandModePanel, PhaseBadge, PhaseHint, PhaseDeliverablesPanel, PhaseProgressBar, QuickActionButtons, RepositoryPanel, TaskDerivationPreview * (Kept `PhaseConfirmationModal` — used outside the contracts surface by `TaskOutput` and `PhaseConfirmationNotification`.) * Routes deregistered from `main.tsx`; nav entry removed from `NavStrip`. ### Backend handlers Deleted: `contracts.rs` (2.4k LOC), `contract_chat.rs` (3.2k LOC), `contract_daemon.rs` (~940 LOC), `contract_discuss.rs` (~590 LOC), `transcript_analysis.rs` (~690 LOC). All `/api/v1/contracts/*` routes deregistered. OpenAPI entries dropped. Module declarations removed from `server/handlers/mod.rs`. ### CLI Removed `makima contract` and `makima supervisor` subcommands. Deleted `daemon/cli/contract.rs` and `daemon/cli/supervisor.rs`. Bin dispatch trimmed (~377 LOC). ### Orchestrator Removed the contract-spawn path from `phase_execution` (`spawn_step_contract` and its caller). `directive_steps.contract_type` now logs a warning and falls through to standalone-task spawn. Column itself stays — old data still reads, just no longer triggers a contract+supervisor spawn. ### TUI `Action::PerformCreateContract` is now a no-op that surfaces a status message: "Contracts have been removed. Use directives instead." The TUI form is dead code pending a wider refresh. ## Out of scope (deliberately left) * Contracts DB tables (`contracts`, `contract_repositories`, `contract_chat_history`, `contract_events`, `contract_templates`) are retained for historical data + because some peripheral code still joins to them in TaskSummary queries. * `mesh_supervisor` handlers are retained — they aren't only used by contracts (some mesh-level supervisor behaviour persists), and the cross-cutting cleanup is bigger than this PR. * `directive_steps.contract_type` column itself isn't dropped; just no longer functional. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'makima/src/db')
-rw-r--r--makima/src/db/models.rs6
-rw-r--r--makima/src/db/repository.rs99
2 files changed, 98 insertions, 7 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 1fe6e35..44af939 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -2721,6 +2721,12 @@ pub struct Directive {
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
+ /// True for the per-owner scratchpad directive. Auto-created on first
+ /// orphan-task creation. Hidden from the directive list; surfaced to
+ /// users via the sidebar's `tmp/` folder. Tasks attached to a tmp
+ /// directive are auto-deleted after 30 days.
+ #[serde(default)]
+ pub is_tmp: bool,
}
/// A historical record of a directive goal change.
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index b41c74c..f91bfaa 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -1189,6 +1189,86 @@ pub async fn list_tasks_for_owner(
.await
}
+// =============================================================================
+// Tmp directive — per-owner scratchpad
+// =============================================================================
+
+/// Get the owner's tmp directive, creating it on the fly if absent. Idempotent
+/// thanks to the partial unique index on (owner_id) WHERE is_tmp.
+///
+/// We try an INSERT first with ON CONFLICT DO NOTHING; if a row was inserted
+/// it's returned, otherwise we fall back to a SELECT for the row some other
+/// request just created (or one that already existed).
+pub async fn get_or_create_tmp_directive(
+ pool: &PgPool,
+ owner_id: Uuid,
+) -> Result<Directive, sqlx::Error> {
+ // Try insert first. RETURNING fires only if a row was actually written;
+ // if the partial unique index trips (a tmp directive already exists)
+ // we get None and fall through to the SELECT.
+ let inserted = sqlx::query_as::<_, Directive>(
+ r#"
+ INSERT INTO directives
+ (owner_id, title, goal, status, reconcile_mode, is_tmp)
+ VALUES
+ ($1, 'tmp', '', 'idle', 'auto', true)
+ ON CONFLICT DO NOTHING
+ RETURNING *
+ "#,
+ )
+ .bind(owner_id)
+ .fetch_optional(pool)
+ .await?;
+
+ if let Some(d) = inserted {
+ return Ok(d);
+ }
+
+ // Pre-existing or just-created-by-someone-else: fetch.
+ sqlx::query_as::<_, Directive>(
+ r#"SELECT * FROM directives WHERE owner_id = $1 AND is_tmp = true LIMIT 1"#,
+ )
+ .bind(owner_id)
+ .fetch_one(pool)
+ .await
+}
+
+/// Find every tmp directive (across owners). Used by the 30-day expiry
+/// sweep — we need to know which directives are scratchpads so we know
+/// which tasks to age out.
+pub async fn list_all_tmp_directives(
+ pool: &PgPool,
+) -> Result<Vec<Directive>, sqlx::Error> {
+ sqlx::query_as::<_, Directive>(
+ r#"SELECT * FROM directives WHERE is_tmp = true"#,
+ )
+ .fetch_all(pool)
+ .await
+}
+
+/// Delete tasks attached to a tmp directive that are older than 30 days.
+/// Returns the number of rows deleted (informational; we log it).
+///
+/// We only sweep top-level tasks (parent_task_id IS NULL) — subtasks die
+/// when their parent dies via the FK cascade.
+pub async fn delete_expired_tmp_tasks(
+ pool: &PgPool,
+ tmp_directive_id: Uuid,
+) -> Result<u64, sqlx::Error> {
+ let result = sqlx::query(
+ r#"
+ DELETE FROM tasks
+ WHERE directive_id = $1
+ AND parent_task_id IS NULL
+ AND created_at < NOW() - INTERVAL '30 days'
+ "#,
+ )
+ .bind(tmp_directive_id)
+ .execute(pool)
+ .await?;
+ Ok(result.rows_affected())
+}
+
/// List ephemeral tasks attached to a directive — tasks with `directive_id`
/// set but no `directive_step_id`. These are the "spinoff" tasks the user
/// created via the directive folder context menu, distinct from
@@ -1223,14 +1303,15 @@ pub async fn list_ephemeral_directive_tasks_for_owner(
.await
}
-/// List "orphan" top-level tasks for an owner — tasks that are NOT attached
-/// to a directive and NOT a subtask of another task. These surface in the
-/// document-mode sidebar under a top-level `tmp/` folder. Hidden tasks
-/// excluded.
-pub async fn list_orphan_tasks_for_owner(
+/// List top-level tasks attached to the owner's tmp directive. These are
+/// the scratchpad / orphan tasks surfaced under the sidebar's `tmp/`
+/// folder. Auto-creates the tmp directive if it doesn't exist yet so the
+/// caller never has to handle "no tmp directive".
+pub async fn list_tmp_tasks_for_owner(
pool: &PgPool,
owner_id: Uuid,
) -> Result<Vec<TaskSummary>, sqlx::Error> {
+ let tmp = get_or_create_tmp_directive(pool, owner_id).await?;
sqlx::query_as::<_, TaskSummary>(
r#"
SELECT
@@ -1243,13 +1324,14 @@ pub async fn list_orphan_tasks_for_owner(
FROM tasks t
LEFT JOIN contracts c ON t.contract_id = c.id
WHERE t.owner_id = $1
+ AND t.directive_id = $2
AND t.parent_task_id IS NULL
- AND t.directive_id IS NULL
AND COALESCE(t.hidden, false) = false
ORDER BY t.priority DESC, t.created_at DESC
"#,
)
.bind(owner_id)
+ .bind(tmp.id)
.fetch_all(pool)
.await
}
@@ -5066,7 +5148,9 @@ pub async fn get_directive_with_steps_for_owner(
}
}
-/// List all directives for an owner with step counts.
+/// List all directives for an owner with step counts. Excludes the per-owner
+/// tmp directive (the scratchpad surface; surfaced via the sidebar's
+/// dedicated `tmp/` folder, not the regular directive list).
pub async fn list_directives_for_owner(
pool: &PgPool,
owner_id: Uuid,
@@ -5093,6 +5177,7 @@ pub async fn list_directives_for_owner(
WHERE directive_id = d.id
) s ON true
WHERE d.owner_id = $1
+ AND d.is_tmp = false
ORDER BY d.created_at DESC
"#,
)