fix(projector): stop writing null workspace_id on work_products

Two sites in upsertFilesystemArtifact leaked null workspace_ids
into the work_products table and produced the orphan rows the read
path now has to heal:

1. Create path unconditionally forwarded ctx.workspaceId, which was
   null whenever a run was created without a workspace binding
   (historically cognitive runs, or runs that lost their link through
   the observationPath regression fixed in 1f2f977).

2. Update path passed {workspaceId: ctx.workspaceId} as part of the
   patch. updateWorkProduct treats any key present in patch as "write
   this value", so a run touching a previously-well-linked row with
   ctx.workspaceId=null would clobber the existing workspace_id back
   to null — actively destroying data.

Resolve both at the projector layer:

  - Add workspacesMod.findWorkspaceIdByLocalPath(companyId, localPath)
    as a reverse-lookup. workspacePath is the argument the projector
    is already walking; it came from some workspace row's local_path,
    so finding that row by path is deterministic.
  - Compute resolvedWorkspaceId = ctx.workspaceId ?? reverseLookup(...)
    once, and use it for both the identity lookup
    (findWorkProductByExternalId) and the subsequent write.
  - On the update path, only include workspaceId in the patch when
    resolvedWorkspaceId is non-null. Null now means "no info — leave
    the existing column alone" instead of "clobber to null".

Together with the read-path heal in cfb8ba5 this closes the loop:
new files never enter the table with a null workspace_id if we can
possibly resolve it, old files keep their links, and truly unlinkable
rows still fall back to the observation workspace on open.

Typecheck and build:verify clean.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-21 14:52:38 -04:00
1 parent cfb8ba5dc1
commit 9fba8dd6c3
2 files changed
+35 -5

No files matched your search

+18
View File
@@ -51,6 +51,24 @@ export function buildWorkspaceMethods(
return row ? { ...row, isPrimary: toBoolean(row.isPrimary) } : null;
},
/**
* Reverse-lookup a workspace's id by its local_path within a
* company. Used by the artifact projector to heal a run context
* whose workspaceId was never bound but whose workspacePath is
* still known — we look up the row that owns that path so the
* resulting work_product carries a real foreign key instead of
* another null that downstream readers would have to heal again.
* Returns null when the path does not match any workspace (the
* caller proceeds without a workspace_id, which is then handled
* defensively by updateWorkProduct).
*/
findWorkspaceIdByLocalPath(companyId: string, localPath: string): string | null {
const row = getDb()
.prepare("select id from workspaces where company_id = ? and local_path = ? limit 1")
.get(companyId, localPath) as { id: string } | undefined;
return row?.id ?? null;
},
getProjectPrimaryWorkspace(projectId: string | null | undefined, companyId: string): WorkspaceRecord | null {
if (!projectId) return null;
const row = getDb()