You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Chat session creation fails for "local" workspace — workspace_id inserted as default, violating NOT NULL #6230
Selecting the "local" entry (the root repo, not a worktree workspace) in the desktop app and sending a chat message fails with a database error. The insert into chat_sessions only sets v2_workspace_id and leaves the legacy workspace_id column as default, which the schema rejects. Worktree workspaces work fine — only "local" entries fail, and they fail in every repo. This predates 1.19.0; I saw the same on 1.18.x.
Sending the first message in a chat pane for a "local" entry fails with a database error while worktree workspaces work. "local" is the display name for a type: "main" workspace (apps/desktop/src/renderer/utils/getV2WorkspaceDisplayName/getV2WorkspaceDisplayName.ts:15), i.e. the repo checkout itself. The pasted query is a byte-for-byte match for the insert in the cloud tRPC chat.createSession mutation, so the failure is on the cloud side, not in the desktop app. I could not reproduce this against a live DB (read-only triage), but I believe the reported diagnosis is wrong and the real cause is the v2_workspace_idforeign key, not a NOT NULL violation on workspace_id.
Affected code
packages/trpc/src/router/chat/chat.ts:42-60 — the insert in the report. It sets only id, organizationId, createdBy, v2WorkspaceId; every other column falls back to default, which produces exactly the SQL in the issue (... values ($1, $2, $3, default, $4, default, ...) on conflict do nothing returning "id"). There is no try/catch, so the raw driver error (query text included) propagates to the client — which is why the user sees the query.
packages/db/src/schema/schema.ts:685-714 — chat_sessions. Note workspaceId at :695 has no.notNull(), and v2WorkspaceId at :698 references v2_workspaces.
Caller: apps/desktop/.../ChatPane/hooks/useWorkspaceChatController/useWorkspaceChatController.ts:41-50 and :129-158 (getOrCreateSession), invoked on send from ChatPaneInterface.tsx:529.
Ruled out: the NOT NULL reading
chat_sessions.workspace_id is nullable everywhere I can check:
added as a plain nullable uuid in packages/db/drizzle/0021_add_workspace_id_to_chat_sessions.sql:1, never altered afterwards (no other ALTER TABLE "chat_sessions" touches it),
"notNull": false in the current snapshot packages/db/drizzle/meta/0063_snapshot.json:3609-3614,
no .notNull() in the schema at schema.ts:695.
So omitting workspace_id is legal, and the same insert shape succeeds for worktrees — which it wouldn't if the column were NOT NULL.
Suspected root cause: FK chat_sessions_v2_workspace_id_v2_workspaces_id_fk
Workspaces became host-owned/local-first (#5731) and are no longer mirrored to cloud v2_workspaces:
packages/host-service/src/trpc/router/project/utils/ensure-main-workspace.ts:58-62 — "Fully local — workspaces have no cloud mirror." The main workspace id is minted by insertLocalWorkspace into host SQLite only (:108-116).
The dual-write reconciler is gone: pushWorkspaceCreateToCloud and workspace-cloud-sync.ts no longer exist in packages/host-service/src/; grepping v2Workspace.create outside plans/ finds only tests and apps/web/src/app/workspaces/page.tsx.
Meanwhile the FK is still in place — packages/db/drizzle/meta/0063_snapshot.json:3738-3750 — and dropping it is explicitly still pending work: plans/offline-first-workspace-follow-ups.md:80-81 ("Neon migration (user-run): drop chat_sessions.v2WorkspaceId FK → plain uuid tag, then drop v2_workspaces").
That combination fails any insert whose v2_workspace_id has no cloud row, with 23503 insert or update on table "chat_sessions" violates foreign key constraint "chat_sessions_v2_workspace_id_v2_workspaces_id_fk".
Why worktrees still work for the reporter but "local" never does: worktrees created during the dual-write era still have their v2_workspaces rows (that era is why packages/host-service/src/runtime/workspace-backfill.ts:34-65 can still read them back from cloud). Main workspaces, by contrast, are (re)created locally with a fresh UUID by runMainWorkspaceSweep → ensureMainWorkspace on boot for every project (packages/host-service/src/runtime/main-workspace-sweep.ts:16-33), so their ids were never in cloud — hence "fails in every repo". If this is right, a newly created worktree should fail the same way; that's the cheapest discriminating test.
Corroborating evidence that this failure mode is known: the legacy REST bootstrap already carries a defensive fallback for precisely this class of error — apps/api/src/app/api/chat/[sessionId]/route.ts:15-24 (shouldRetryWithoutWorkspaceId, matching "foreign key") and the retry at :85-102. The tRPC path has no equivalent.
Same crash should hit the agent/automation path, which uses the same mutation: packages/host-service/src/trpc/router/agents/agents.ts:259-262.
Reproduction
Manual (needs a DB where the FK still exists):
Create a new project in the desktop app, or delete the host's main workspace row so the boot sweep re-mints it — either way the type='main' row gets a fresh host-minted UUID.
Confirm with select id from v2_workspaces where id = '<the 4th param, e.g. 25433225-6c6a-4422-b142-c705a7ad807d>'; — I expect 0 rows. Also create a brand-new worktree and send a message there; I expect it to fail identically, which would confirm the cause is "no cloud row", not "type = main".
Please also paste the raw Postgres error (SQLSTATE + constraint name) rather than just the query — that single line settles FK-vs-NOT-NULL definitively.
Automated sketch (bun:test, against a throwaway Postgres branch with the migrations applied):
// expect 23503 on chat_sessions_v2_workspace_id_v2_workspaces_id_fkawaitexpect(db.insert(chatSessions).values({id: randomUUID(),organizationId: seededOrgId,createdBy: seededUserId,v2WorkspaceId: randomUUID(),// host-minted id, absent from v2_workspaces}).onConflictDoNothing(),).rejects.toThrow(/chat_sessions_v2_workspace_id_v2_workspaces_id_fk/);
Suggested fix direction
The principled fix is the already-planned migration: drop chat_sessions_v2_workspace_id_v2_workspaces_id_fk and keep v2_workspace_id as a plain uuid tag (plans/offline-first-workspace-follow-ups.md:80-81), i.e. remove the .references(() => v2Workspaces.id, ...) in schema.ts:698-700 and regenerate. The FK is unsatisfiable by construction now that the host owns workspace identity; dropping v2_workspaces itself can stay gated on the rest of R3.
Two notes on stopgaps:
Don't copy the REST fallback verbatim (retry with v2WorkspaceId nulled). The pane's session list filters on eq(chatSessions.v2WorkspaceId, workspaceId) (useWorkspaceChatController.ts:74-84), so a null-tagged session would be created but then vanish from the picker and history — a confusing second bug.
If the FK must survive for now, the alternative is restoring a cloud upsert for host-minted workspaces (call v2Workspace.create with the local id, including type: 'main') before chat.createSession. That reintroduces the dual-write the local-first work deliberately removed, so I'd only do it as a hotfix.
Independently of which route is taken, chat.createSession should wrap the insert and return a TRPCError with a human-readable message instead of leaking the driver's query text to the UI, and it currently does not verify that v2WorkspaceId belongs to ctx.activeOrganizationId — worth addressing in the same change since dropping the FK removes the last implicit check on that column.
Activity
github-actions commentedon Aug 6, 2026
Summary
Sending the first message in a chat pane for a "local" entry fails with a database error while worktree workspaces work. "local" is the display name for a
type: "main"workspace (apps/desktop/src/renderer/utils/getV2WorkspaceDisplayName/getV2WorkspaceDisplayName.ts:15), i.e. the repo checkout itself. The pasted query is a byte-for-byte match for the insert in the cloud tRPCchat.createSessionmutation, so the failure is on the cloud side, not in the desktop app. I could not reproduce this against a live DB (read-only triage), but I believe the reported diagnosis is wrong and the real cause is thev2_workspace_idforeign key, not aNOT NULLviolation onworkspace_id.Affected code
packages/trpc/src/router/chat/chat.ts:42-60— the insert in the report. It sets onlyid,organizationId,createdBy,v2WorkspaceId; every other column falls back todefault, which produces exactly the SQL in the issue (... values ($1, $2, $3, default, $4, default, ...) on conflict do nothing returning "id"). There is notry/catch, so the raw driver error (query text included) propagates to the client — which is why the user sees the query.packages/db/src/schema/schema.ts:685-714—chat_sessions. NoteworkspaceIdat:695has no.notNull(), andv2WorkspaceIdat:698referencesv2_workspaces.apps/desktop/.../ChatPane/hooks/useWorkspaceChatController/useWorkspaceChatController.ts:41-50and:129-158(getOrCreateSession), invoked on send fromChatPaneInterface.tsx:529.Ruled out: the
NOT NULLreadingchat_sessions.workspace_idis nullable everywhere I can check:packages/db/drizzle/0021_add_workspace_id_to_chat_sessions.sql:1, never altered afterwards (no otherALTER TABLE "chat_sessions"touches it),"notNull": falsein the current snapshotpackages/db/drizzle/meta/0063_snapshot.json:3609-3614,.notNull()in the schema atschema.ts:695.So omitting
workspace_idis legal, and the same insert shape succeeds for worktrees — which it wouldn't if the column wereNOT NULL.Suspected root cause: FK
chat_sessions_v2_workspace_id_v2_workspaces_id_fkWorkspaces became host-owned/local-first (#5731) and are no longer mirrored to cloud
v2_workspaces:packages/host-service/src/trpc/router/project/utils/ensure-main-workspace.ts:58-62— "Fully local — workspaces have no cloud mirror." The main workspace id is minted byinsertLocalWorkspaceinto host SQLite only (:108-116).packages/host-service/src/workspaces/local-workspace-store.ts:28-32— "Workspaces have no cloud mirror since local-first (feat(desktop,host-service): local-first projects — host.db owns projects, workspace cloud sync retired #5731), so the cloud capture inv2Workspace.create/deletenever fires for local rows."pushWorkspaceCreateToCloudandworkspace-cloud-sync.tsno longer exist inpackages/host-service/src/; greppingv2Workspace.createoutsideplans/finds only tests andapps/web/src/app/workspaces/page.tsx.Meanwhile the FK is still in place —
packages/db/drizzle/meta/0063_snapshot.json:3738-3750— and dropping it is explicitly still pending work:plans/offline-first-workspace-follow-ups.md:80-81("Neon migration (user-run): dropchat_sessions.v2WorkspaceIdFK → plain uuid tag, then dropv2_workspaces").That combination fails any insert whose
v2_workspace_idhas no cloud row, with23503 insert or update on table "chat_sessions" violates foreign key constraint "chat_sessions_v2_workspace_id_v2_workspaces_id_fk".Why worktrees still work for the reporter but "local" never does: worktrees created during the dual-write era still have their
v2_workspacesrows (that era is whypackages/host-service/src/runtime/workspace-backfill.ts:34-65can still read them back from cloud). Main workspaces, by contrast, are (re)created locally with a fresh UUID byrunMainWorkspaceSweep→ensureMainWorkspaceon boot for every project (packages/host-service/src/runtime/main-workspace-sweep.ts:16-33), so their ids were never in cloud — hence "fails in every repo". If this is right, a newly created worktree should fail the same way; that's the cheapest discriminating test.Corroborating evidence that this failure mode is known: the legacy REST bootstrap already carries a defensive fallback for precisely this class of error —
apps/api/src/app/api/chat/[sessionId]/route.ts:15-24(shouldRetryWithoutWorkspaceId, matching"foreign key") and the retry at:85-102. The tRPC path has no equivalent.Same crash should hit the agent/automation path, which uses the same mutation:
packages/host-service/src/trpc/router/agents/agents.ts:259-262.Reproduction
Manual (needs a DB where the FK still exists):
type='main'row gets a fresh host-minted UUID.getOrCreateSession→chat.createSession→ error.select id from v2_workspaces where id = '<the 4th param, e.g. 25433225-6c6a-4422-b142-c705a7ad807d>';— I expect 0 rows. Also create a brand-new worktree and send a message there; I expect it to fail identically, which would confirm the cause is "no cloud row", not "type = main".Please also paste the raw Postgres error (SQLSTATE + constraint name) rather than just the query — that single line settles FK-vs-NOT-NULL definitively.
Automated sketch (
bun:test, against a throwaway Postgres branch with the migrations applied):Suggested fix direction
The principled fix is the already-planned migration: drop
chat_sessions_v2_workspace_id_v2_workspaces_id_fkand keepv2_workspace_idas a plain uuid tag (plans/offline-first-workspace-follow-ups.md:80-81), i.e. remove the.references(() => v2Workspaces.id, ...)inschema.ts:698-700and regenerate. The FK is unsatisfiable by construction now that the host owns workspace identity; droppingv2_workspacesitself can stay gated on the rest of R3.Two notes on stopgaps:
v2WorkspaceIdnulled). The pane's session list filters oneq(chatSessions.v2WorkspaceId, workspaceId)(useWorkspaceChatController.ts:74-84), so a null-tagged session would be created but then vanish from the picker and history — a confusing second bug.v2Workspace.createwith the local id, includingtype: 'main') beforechat.createSession. That reintroduces the dual-write the local-first work deliberately removed, so I'd only do it as a hotfix.Independently of which route is taken,
chat.createSessionshould wrap the insert and return aTRPCErrorwith a human-readable message instead of leaking the driver's query text to the UI, and it currently does not verify thatv2WorkspaceIdbelongs toctx.activeOrganizationId— worth addressing in the same change since dropping the FK removes the last implicit check on that column.