diff options
| author | soryu <soryu@soryu.co> | 2026-05-01 23:56:51 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-01 23:56:51 +0100 |
| commit | e11759447b1ac00becfb1e979e488f7f9c9cf478 (patch) | |
| tree | f8a58368de3f6dda3f2f5c1af34e869a0e714205 /makima/src/db | |
| parent | 80085c7cfa9d679ed3e3fd54a7d55fa8ab1addef (diff) | |
| download | soryu-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.rs | 6 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 99 |
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 "#, ) |
