fix(docs): heal orphan work_products by falling back to observation workspace

Work-product rows written before the artifact projector tracked
workspace_id end-to-end can lose the link through all three joins:
workspace_id is NULL, task_id is NULL, and the row's run exists but
its runs.workspace_id is also NULL. Those orphans were readable in
the DocumentCenter (the listing doesn't need a workspace root) but
could not be opened — resolveWorkProductTarget returned null and the
toast now reports workspace_unresolved.

Add a fourth resolution tier in resolveWorkProductTarget: if the
three-step chain yields nothing, look up the company's observation
workspace (primary first, then oldest) and check whether the
external_id resolves to a real file inside that root. If it does,
return that absolute path and the row opens. If the file isn't
there, the error falls through with an extended detail line naming
the observation root that was tried, so the operator sees exactly
why the heal also failed.

This is deterministic — exactly one fallback workspace — and
side-effect-free on the read path: the row itself is left alone,
no stealth write-back. A follow-up commit will stop the projector
from writing NULL workspace_id in the first place.

Typecheck and build:verify clean.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-21 14:51:06 -04:00
1 parent 390f76fa76
commit cfb8ba5dc1
1 file changed
+35 -10
+35 -10
View File
@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto";
import { statSync } from "node:fs";
import { isAbsolute, resolve as resolvePath, relative as relativePath } from "node:path";
import type { DatabaseSync, SQLInputValue } from "node:sqlite";
import type { ReadAll, Transaction } from "./helpers";
@@ -379,19 +380,43 @@ export function buildDocumentMethods(
row.task_id,
row.last_touched_by_run_id ?? row.created_by_run_id,
);
if (!workspaceRoot) {
return {
kind: "error",
reason: "workspace_unresolved",
detail: `workspace_id=${row.workspace_id ?? "null"} task_id=${row.task_id ?? "null"} run_id=${row.last_touched_by_run_id ?? row.created_by_run_id ?? "null"} — none led to a workspace with a local_path`,
};
if (workspaceRoot) {
const absolute = joinWorkspaceRelative(workspaceRoot, row.external_id);
if (!absolute) {
return { kind: "error", reason: "path_escape", detail: `external_id=${row.external_id} escapes workspace root ${workspaceRoot}` };
}
return { kind: "path", value: absolute };
}
const absolute = joinWorkspaceRelative(workspaceRoot, row.external_id);
if (!absolute) {
return { kind: "error", reason: "path_escape", detail: `external_id=${row.external_id} escapes workspace root ${workspaceRoot}` };
// Fourth-tier heal: orphan rows created before the projector tracked
// workspace_id end-to-end lose the link through all three joins. If
// the externalId resolves to a real file inside the company's
// observation (primary/oldest) workspace, we treat that as the
// canonical root for this row. No guessing across all workspaces —
// just one deterministic fallback, and it only fires when the file
// actually exists on disk so the shell never sees a phantom path.
const observation = getDb()
.prepare(
"SELECT local_path as localPath FROM workspaces WHERE company_id = ? AND local_path IS NOT NULL ORDER BY is_primary DESC, created_at ASC LIMIT 1",
)
.get(companyId) as { localPath: string } | undefined;
if (observation?.localPath) {
const candidate = joinWorkspaceRelative(observation.localPath, row.external_id);
if (candidate) {
try {
if (statSync(candidate).isFile()) {
return { kind: "path", value: candidate };
}
} catch {
// fall through to error below
}
}
}
return { kind: "path", value: absolute };
return {
kind: "error",
reason: "workspace_unresolved",
detail: `workspace_id=${row.workspace_id ?? "null"} task_id=${row.task_id ?? "null"} run_id=${row.last_touched_by_run_id ?? row.created_by_run_id ?? "null"} — none led to a workspace with a local_path, and ${row.external_id} is not present in the company's observation workspace${observation?.localPath ? ` (${observation.localPath})` : " (no observation workspace exists)"}`,
};
},
// Task-scoped visible listing. Archived rows stay in the table as