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.
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.
The previous handler folded every null target into one error code
("WORK_PRODUCT_NOT_FOUND: Work product not found or its workspace
is unresolved"). When a user couldn't open a file we had no way to
tell whether the row was archived, the provider/external_id was
empty, the workspace had no local_path, or the external_id escaped
the root.
Widen resolveWorkProductTarget's return from (target | null) to
(target | error) where error carries one of five reasons:
not_found row missing for id+company
archived status == "archived" tombstone
no_target provider != filesystem and no url
workspace_unresolved all three workspace_id / task_id / run_id
lookups returned no local_path
path_escape external_id is absolute or steps outside root
Each reason includes the concrete detail (ids checked, paths tried)
so the toast on the renderer side tells the user exactly which slot
is null without having to open the DB.
The IPC handler maps each reason to its own failure code and passes
the detail through, so WorkProductRow's toast now reads e.g.
"workspace_id=null task_id=null run_id=null — none led to a
workspace with a local_path" instead of a generic unresolved.
Typecheck and build:verify clean.
Purge remaining hardcoded color values from the component layer and main
process so every surface flows through the kami token system.
src/main/desktop-shell-window.ts
- BrowserWindow.backgroundColor no longer paints pure white before the
renderer mounts. It now picks kami parchment (#F5F4ED) or deep-dark
(#141413) based on nativeTheme.shouldUseDarkColors so the startup
flash respects the user's OS theme preference.
src/shared/constants.ts
- DEPARTMENT_COLORS collapses 12 unrelated hex brand colors (violet,
red, green, cyan, pink, ...) into six kami tokens: var(--primary),
var(--kami-ink), var(--kami-dark-warm), var(--kami-charcoal),
var(--kami-olive), var(--kami-stone). Departments are distinguished
by warm-neutral ramp + ink-blue, never by second chromatic hue.
src/renderer/components/communication/{MessageItem,SearchResults}.tsx
- Replace the string-concat tint pattern `${color}18` with
color-mix(in oklab, ${color}, transparent 91%). The old pattern
produced invalid CSS (e.g. `var(--primary)18`) whenever `color`
was a CSS custom property, silently falling back to no background.
color-mix works for both hex and var() inputs, eliminating the
latent bug at the source.
src/renderer/components/ui/ActionButton.tsx
- Replace hand-rolled rgba(74,93,122,0.35) and rgba(220,53,69,0.3)
drop shadows with kami ring shadows routed through color-mix over
var(--primary) and var(--destructive).
src/renderer/views/CommandPaletteView.tsx
- Replace rgba(15,23,42,0.08) cold-slate shadow with a kami whisper
shadow driven by color-mix over var(--foreground).
src/renderer/components/skills/SkillAgentToggles.tsx
src/renderer/components/execution/ModelManagementShared.tsx
- Toggle thumbs switch from bg-white to bg-background so they
inherit parchment in light mode and deep-dark in dark mode,
removing the last pure-white surfaces from the renderer.
Third-party brand hex values in src/renderer/components/social/
social-config.ts are retained: they represent external brand identity
(Twitter blue, YouTube red, ...), not kami chrome, and collapsing them
to one hue would destroy recognition.
Typecheck clean. No business-logic signatures changed.
The 1f2f977 hotfix repairs the projector for new runs, but files that
already exist on disk before the fix lands stay invisible to the
DocumentCenter — a baseline+diff projector reports "changed" only
when a file's mtime advances past the run-start baseline, and a
quiescent file written before the fix never advances.
This commit adds a bootstrap-time one-shot rescan:
- New module `bootstrap/backfill-workspace-artifacts.ts` walks the
primary/oldest workspace of every company exactly once (gated by
`artifact_rescan_v1_done` in the settings table), applies the same
registrability filter as `artifact-projector.ts` (skip .git/,
node_modules/, .DS_Store, .lock/.log/.map; ignore package-lock.json
/ pnpm-lock.yaml / yarn.lock; cap at 5 000 files per workspace),
and upserts a `work_products` row via `findWorkProductByExternalId`
→ `createWorkProduct` for any file the projector never registered.
- bootstrap-application.ts wires the call after recoverScheduledPosts
(DB ready, services running, before the first heartbeat tick), so
the rescan completes in the same boot tick that the user opens
the DocumentCenter.
Idempotency: subsequent boots short-circuit on the settings flag.
The post-fix projector handles all future file activity inside
each run, so the backfill never needs to run twice. Per-workspace
file cap prevents a pathological tree from locking the bootstrap
path; overflow files are still picked up by the next run that
touches them. No fallback, no mock — files that fail stat are
silently skipped (same policy as the projector).
typecheck green, 543/543 unit tests, 10/10 eval. The user's 19
existing markdown deliverables under ~/Documents/Github/test/
should appear in the DocumentCenter on the next `pnpm dev`.
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.
Closes the three specific_asks from Jobs's T1 review of the workspace
architecture fix (7829acc..15e3863):
1. docs/architecture/workspace-concurrency.md: decision table
upgraded from a code block to a real Markdown table so it reads
consistently with the failure-modes table lower in the file.
2. docs/architecture/workspace-concurrency.md + resolve-workspace.ts:
added "Sunset coding-role inference" section and a TODO marker on
CODING_ROLE_PATTERN tying the regex to the v52 transition window
(target 2026-05-20). Both documents now name the end-state
explicitly so a future engineer knows why the regex exists and
when it dies.
3. resolve-workspace.ts: enriched the WorkspaceConfigurationError
message to include taskId / agentId / projectId alongside the
existing companyId, so a misconfigured setup points operators at
the offending row instead of just the company.
No behaviour change, no fallback, no mock. typecheck green, 540 unit
tests green, 10 eval tests green.