packages/store/src/json-file.ts:96, packages/store/src/workspace-store.ts:136, packages/store/src/connect.ts:123,133
connectAll("json:<dir>") hands the same base directory to both stores:
JsonFileConversationStore.open(base),
...
JsonFileWorkspaceStore.open(base),
Every other domain store namespaces itself into a subdirectory (projects/, requirements/, docs/, …). The workspace registry does not — it keeps the whole WorkspacesFile in one flat path.join(base, "workspaces.json"), landing directly among the conversation store's <encodeId(id)>.json rows.
JsonFileConversationStore.list scans that directory accepting anything ending in .json (json-file.ts:90), parses it, and unconditionally dereferences stored.messages.length (json-file.ts:96). workspaces.json parses fine but has {recent, by_conversation} and no messages:
list before touch: 1
CRASH: Cannot read properties of undefined (reading 'length')
Trigger is one ordinary user action — POST /v1/workspaces (packages/server/src/workspaces-routes.ts:52, the SPA's "Recent folders" picker) or any workspaces.bind(...) on conversation resume. From that point GET /v1/conversations — the SPA's conversation sidebar — fails for the lifetime of the data directory, since the file is never removed.
The collision cuts both ways: POST /v1/conversations {"id":"workspaces"} is accepted (only __-prefixed ids are reserved) and saveEnvelope atomically overwrites <base>/workspaces.json, destroying the recent-folders list and every conversation↔workspace binding.
SQLite is unaffected (dedicated workspace_recent / workspace_bindings tables). JSON is the default backend (json:///<data>/jarvis/conversations).
packages/store/src/json-file.ts:96,packages/store/src/workspace-store.ts:136,packages/store/src/connect.ts:123,133connectAll("json:<dir>")hands the same base directory to both stores:Every other domain store namespaces itself into a subdirectory (
projects/,requirements/,docs/, …). The workspace registry does not — it keeps the wholeWorkspacesFilein one flatpath.join(base, "workspaces.json"), landing directly among the conversation store's<encodeId(id)>.jsonrows.JsonFileConversationStore.listscans that directory accepting anything ending in.json(json-file.ts:90), parses it, and unconditionally dereferencesstored.messages.length(json-file.ts:96).workspaces.jsonparses fine but has{recent, by_conversation}and nomessages:Trigger is one ordinary user action —
POST /v1/workspaces(packages/server/src/workspaces-routes.ts:52, the SPA's "Recent folders" picker) or anyworkspaces.bind(...)on conversation resume. From that pointGET /v1/conversations— the SPA's conversation sidebar — fails for the lifetime of the data directory, since the file is never removed.The collision cuts both ways:
POST /v1/conversations {"id":"workspaces"}is accepted (only__-prefixed ids are reserved) andsaveEnvelopeatomically overwrites<base>/workspaces.json, destroying the recent-folders list and every conversation↔workspace binding.SQLite is unaffected (dedicated
workspace_recent/workspace_bindingstables). JSON is the default backend (json:///<data>/jarvis/conversations).