From f0c4a66cc1ec2abf181bc44f380ae519be1dcf87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 19 Apr 2026 17:21:31 -0400 Subject: [PATCH] Hide archived work_products from the visible surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retirement set `status = "archived"` but none of the downstream readers honoured it. The snapshot pulled every row, the task-scoped listing returned every row, the DocumentCenter rendered every row, and `resolveWorkProductTarget` would happily hand a deleted file's path to `shell.openPath`. The promise that archived rows "stop surfacing" was only true inside the projector's own doc comment. Pin the visibility gate at the three DB read paths: - `listSnapshot().workProducts` — the canonical renderer source — excludes archived; - `listWorkProductsForTask` (task API + thread view) excludes archived; - `listWorkProductsForCompany` excludes archived. `resolveWorkProductTarget` now refuses to resolve an archived row, so `openWorkProduct` fails with `WORK_PRODUCT_NOT_FOUND` instead of routing the OS shell at a dead path. Archived rows stay in the table as retirement tombstones — operators still have the audit trail; they just no longer leak into the operator UI. --- src/main/database/documents.ts | 26 ++++++++++++++++++-------- src/main/database/snapshot.ts | 6 +++++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/main/database/documents.ts b/src/main/database/documents.ts index abaa1cbb..fe9907db 100644 --- a/src/main/database/documents.ts +++ b/src/main/database/documents.ts @@ -353,11 +353,17 @@ export function buildDocumentMethods( */ resolveWorkProductTarget(workProductId: string, companyId: string): { kind: "url" | "path"; value: string } | null { const row = getDb().prepare( - "SELECT provider, external_id, url, task_id, created_by_run_id, last_touched_by_run_id, workspace_id FROM work_products WHERE id = ? AND company_id = ?", + "SELECT provider, external_id, url, status, task_id, created_by_run_id, last_touched_by_run_id, workspace_id FROM work_products WHERE id = ? AND company_id = ?", ).get(workProductId, companyId) as - | { provider: string; external_id: string | null; url: string | null; task_id: string | null; created_by_run_id: string | null; last_touched_by_run_id: string | null; workspace_id: string | null } + | { provider: string; external_id: string | null; url: string | null; status: string; task_id: string | null; created_by_run_id: string | null; last_touched_by_run_id: string | null; workspace_id: string | null } | undefined; if (!row) return null; + // Archived rows are retirement tombstones — the file no longer + // exists (or was explicitly retired) and the row is kept for + // audit only. Refuse to resolve a target so `openWorkProduct` + // can fail cleanly instead of pointing the shell at a path that + // leads nowhere. + if (row.status === "archived") return null; if (row.url && row.url.length > 0) { return { kind: "url", value: row.url }; @@ -378,6 +384,10 @@ export function buildDocumentMethods( return { kind: "path", value: absolute }; }, + // Task-scoped visible listing. Archived rows stay in the table as + // retirement tombstones; the task API and the thread view only + // surface currently-active deliverables so a deleted file stops + // appearing in the task's artifact strip. listWorkProductsForTask(taskId: string, companyId: string) { return readAll<{ id: string; company_id: string; project_id: string | null; task_id: string; @@ -388,7 +398,7 @@ export function buildDocumentMethods( created_by_run_id: string | null; last_touched_by_run_id: string | null; created_at: string; updated_at: string; }>( - "SELECT * FROM work_products WHERE task_id = ? AND company_id = ? ORDER BY is_primary DESC, updated_at DESC", + "SELECT * FROM work_products WHERE task_id = ? AND company_id = ? AND status <> 'archived' ORDER BY is_primary DESC, updated_at DESC", taskId, companyId, ).map((r) => ({ id: r.id, companyId: r.company_id, projectId: r.project_id, taskId: r.task_id, @@ -405,10 +415,10 @@ export function buildDocumentMethods( /** * Company-scoped work-product listing, newest first. Used by the * renderer snapshot so the DocumentCenter can surface filesystem - * artifacts (type = "file") the agents produce via bash/write/edit - * — these are the deliverables operators expect to see alongside - * prose documents, so the page stops looking empty when every - * artifact of the night lives in `work_products`. + * artifacts (type = "file") the agents produce via bash/write/edit. + * Archived rows are excluded so retired files stop showing in the + * operator UI; callers that need the full audit trail go through + * `listArchivedWorkProductsForCompany`. */ listWorkProductsForCompany(companyId: string, limit = 500) { return readAll<{ @@ -420,7 +430,7 @@ export function buildDocumentMethods( created_by_run_id: string | null; last_touched_by_run_id: string | null; created_at: string; updated_at: string; }>( - "SELECT * FROM work_products WHERE company_id = ? ORDER BY updated_at DESC LIMIT ?", + "SELECT * FROM work_products WHERE company_id = ? AND status <> 'archived' ORDER BY updated_at DESC LIMIT ?", companyId, limit, ).map((r) => ({ id: r.id, companyId: r.company_id, projectId: r.project_id, taskId: r.task_id, diff --git a/src/main/database/snapshot.ts b/src/main/database/snapshot.ts index 6c3d14c4..e088dbb7 100644 --- a/src/main/database/snapshot.ts +++ b/src/main/database/snapshot.ts @@ -129,8 +129,12 @@ export function buildSnapshotMethods( goalId: r.goal_id, parentDocId: r.parent_doc_id, version: r.version, status: r.status, tagsJson: r.tags_json, latestRevisionId: r.latest_revision_id ?? null, createdAt: r.created_at, updatedAt: r.updated_at, })), + // Archived rows stay in the table as tombstones for audit but + // never surface as deliverables — the DocumentCenter and every + // downstream consumer read this snapshot, so the filter is + // pinned here as the canonical visibility gate. workProducts: readAll( - `select id, company_id, project_id, task_id, workspace_id, type, provider, external_id, title, url, status, review_state, is_primary, summary, metadata_json, created_by_run_id, last_touched_by_run_id, created_at, updated_at from work_products${companyWhere} order by updated_at desc limit 500`, + `select id, company_id, project_id, task_id, workspace_id, type, provider, external_id, title, url, status, review_state, is_primary, summary, metadata_json, created_by_run_id, last_touched_by_run_id, created_at, updated_at from work_products${companyWhere}${companyWhere ? " and" : " where"} status <> 'archived' order by updated_at desc limit 500`, ...companyArgs, ).map((r) => ({ id: r.id, companyId: r.company_id, projectId: r.project_id, taskId: r.task_id,