Hotfix: separate workspace observation from lock claim — restore artifact projector for cognitive runs

Production regression of T1 (7829acc..15e3863). User reports "明明生成
了很多文件,但只有一个" — the filesystem shows 19 markdown deliverables
in ~/Documents/Github/test/{business-plan,operations,ceo-coordination}/
but the DocumentCenter surfaces only one row.

Root cause is structural, predicted in spirit by Karpathy's Round 1
take ("把'认知'和'执行'两件事一起锁是数据结构错"). The T1 rewrite
collapsed two concerns into one nullable field on the run context:

  workspacePath = workspace?.localPath ?? null
                  ^^^^^^^^^                ^^^^
                  lock target              observation root

For lockMode='none' (every cognitive C-suite agent post-T1) the
resolver returns workspace=null, so workspacePath was null, so
artifact-projector.ts:242 short-circuited every projection with
`{ kind: "skipped", reason: "no_workspace" }`. The agents kept
writing files via bash (cwd falls back to the company default
workspace at worker/runtime/index.ts:142), but nothing observed
them — the DocumentCenter only saw the one file that was projected
back when the agent still held an exclusive workspace.

Fix: split the two concerns. WorkspaceResolution gains a non-lock
`observationPath` field; the resolver populates it from the company
primary workspace (new `getCompanyObservationWorkspace` query —
primary first, oldest second, null only when company has zero
workspace rows) for lockMode='none' runs, and from `workspace.localPath`
itself for lockMode='exclusive' runs. RunDispatchParams threads the
field through; turn-context's `workspacePath` reads it first.
artifact-projector.ts is unchanged — it just sees a non-null path
again and resumes baselining, diffing, and upserting work_products
rows the way it did pre-T1.

Three new unit tests pin the regression: cognitive role with
workspace ⇒ observationPath set; cognitive role with no workspace ⇒
observationPath null; exclusive ⇒ observationPath equals
workspace.localPath. typecheck green, 543/543 unit tests, 10/10
eval. No fallback, no mock, no silent degradation.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-20 16:22:40 -04:00
1 parent e15bf67c9f
commit 1f2f9770a6
6 files changed
+132 -4

No files matched your search

+44 -2
View File
@@ -49,9 +49,33 @@ export type WorkspaceResolutionSource =
| "none";
export interface WorkspaceResolution {
/**
* The workspace whose lock the dispatcher will claim. Non-null iff
* `lockMode === 'exclusive'` — the type system enforces the pair.
*/
workspace: WorkspaceRecord | null;
lockMode: WorkspaceLockMode;
source: WorkspaceResolutionSource;
/**
* Where the artifact projector should *observe* for files this run
* may produce, independent of the lock decision. Set whenever the
* company has at least one workspace row, even when `lockMode='none'`
* — cognitive agents still write deliverables to disk via `bash`,
* and the projector needs an observation root to scan against the
* mtime baseline. Null only when the company has no workspace at
* all (genuinely nowhere to look).
*
* For `lockMode='exclusive'` this is always the same path as
* `workspace.localPath`. For `lockMode='none'` this is the
* company's primary/oldest workspace localPath, picked by
* `getCompanyObservationWorkspace` — *no lock claim*.
*
* Decoupling observation from locking is what fixes the regression
* where cognitive agents wrote 19 files but the DocumentCenter
* surfaced only 1: previously `workspacePath` shared the lock
* decision, so any non-exclusive run lost its projector.
*/
observationPath: string | null;
}
export class WorkspaceConfigurationError extends Error {
@@ -177,7 +201,19 @@ export function resolveWorkspaceWithLockMode(
const decision = inferLockMode(db, task, agent, companyId);
if (decision.lockMode === "none") {
return { workspace: null, lockMode: "none", source: "none" };
// No lock claim, but the cognitive run can still touch the
// filesystem via `bash`. Pick a stable observation root so the
// artifact projector can baseline + diff the run's footprint.
// `getCompanyObservationWorkspace` returns the primary or oldest
// workspace; null only when the company has no workspace row at
// all, in which case there is genuinely nothing to observe.
const observation = db.workspacesMod.getCompanyObservationWorkspace(companyId);
return {
workspace: null,
lockMode: "none",
source: "none",
observationPath: observation?.localPath ?? null,
};
}
if (!decision.workspaceId) {
@@ -198,5 +234,11 @@ export function resolveWorkspaceWithLockMode(
);
}
return { workspace, lockMode: "exclusive", source: decision.source };
return {
workspace,
lockMode: "exclusive",
source: decision.source,
// Exclusive runs observe the same path they lock — single source.
observationPath: workspace.localPath,
};
}