Repository navigation
ai-chat status merges two orthogonal state machines, so archived+processing validates as legal #2439
Description
Problem
ai-chat nodes use one status key for two unrelated state machines. Since the schema now declares all four values as a single enum, nonsensical combinations validate as legal — archived + processing is accepted, and nothing rejects it.
The overlap is documented, deliberately, at packages/core/src/models/ai_chat_node.rs:12-18:
statusis intentionally a plainString, not an enum. Two orthogonal state machines currently share this one key: a turn state the daemon drives (processingwhile inference runs,idleonce it completes) and a session lifecycle the PTY path uses (active/archived). The struct captures the value verbatim rather than forcing a lossy mapping over both axes.
So the axes are:
| Axis | Values | Driven by |
|---|---|---|
| Turn state | processing / idle |
daemon, during inference |
| Session lifecycle | active / archived |
PTY path |
One key cannot hold both. Writing processing destroys the knowledge that a session was archived, and vice versa — whichever writer runs last wins.
How this surfaced
Not newly introduced — surfaced by PR #2436 (issue #2430), which removed a gate so that schema validation runs for every type declaring a schema, not just task. That exposed the ai-chat schema declaring only active/archived while the daemon writes idle/processing, breaking 16 daemon tests.
The immediate fix was to declare all four values, which was correct for that PR: the frontend's AiChatStatus (packages/desktop-app/src/lib/types/ai-chat-node.ts:9) already listed exactly those four, so the backend schema was the side out of step. The 16 failures were the schema being wrong, not the tests.
But widening the enum makes the schema agree with reality without making reality coherent. The single key remains two state machines in a trench coat, and the enum now formally sanctions every cross-product combination.
Proposed fix
Split into two properties, each with its own enum and its own writer:
- a turn/runtime state (
idle/processing) — daemon-owned - a session lifecycle (
active/archived) — PTY/user-owned
Each becomes independently valid and independently queryable. "Show me archived chats" stops depending on whether inference happened to be mid-flight.
Scope
Deliberately larger than a schema edit — it touches every reader and writer of the key:
packages/core/src/models/core_schemas.rs— theai-chatschema declarationpackages/core/src/models/ai_chat_node.rs— the strongly-typed struct and the doc comment above- the daemon's inference path, which writes
processing/idle - the PTY path, which writes
active/archived packages/desktop-app/src/lib/types/ai-chat-node.ts:9and every frontend consumer ofAiChatStatus- any query or filter selecting on
status
Per the greenfield rule (zero users, no releases): change the shape and reset the database. No backfill, no dual-read, no lazy on-read upgrade.
Acceptance criteria
-
statusis replaced by two independently-valued properties, one per axis - Each carries its own enum; no combination of the two is nonsensical
- Exactly one writer owns each property — the daemon cannot clobber lifecycle, the PTY path cannot clobber turn state
- The frontend type and all consumers are updated in the same PR; no compatibility shim
- The
ai_chat_node.rs:12-18doc comment is rewritten to describe the split, not the workaround - A test asserts a chat can be
archivedwhile its turn state is whatever it was, without either value being lost
Activity
malibio commentedon Sep 8, 2026
Frontend baseline: 232 test files passed, 4895 tests passed.
malibio commentedon Sep 8, 2026
Found during independent work on this issue: PR #2465 (branch issue-2439-ai-chat-status-split, opened by mstomar125) already implements this exact fix — turn_status/session_status split, same touch points, already through a couple of fix-up commits. My own independent implementation (turn_state/session_status naming) is complete and fully tested locally but was not pushed to avoid overwriting that existing PR. Deferring to #2465; closing out my side without opening a competing PR.
mstomar125 commentedon Sep 8, 2026
Resolved via PR #2465 (merged).
`ai-chat.status` split into two independently-valued, independently-owned properties:
No backward-compatibility shim per the greenfield rule — schema, backend model, wire contract, daemon inference/capture paths, and every frontend consumer updated together. Also fixed a latent bug in `AiChatNodeBehavior::validate` that had been silently dead on the update path (it only ever recognized `active`/`archived`, never `idle`/`processing`, because properties are already namespaced by the time it runs on update).
A real, adversarially-found bug surfaced during review and is worth recording: both Svelte write sites (`ai-chat-node-viewer.svelte`, `ai-chat-pty-session.svelte`) initially wrote `turnStatus`/`sessionStatus` (camelCase) into node properties. Since ai-chat has no dedicated typed write command like `task` does, the generic `updateNode` path forwards whatever key the frontend uses verbatim into storage — nothing recognized camelCase, so a real "Send" click or PTY session start would have silently done nothing in the shipped app. My first fix attempt (a camelCase-first dual-casing read fallback on the backend) was itself wrong: updates deep-merge onto existing stored properties rather than replacing them, so a stray camelCase key is never cleaned up, and prioritizing it means a stale value permanently shadows every later fresh canonical write. Caught this by actually running the real end-to-end inference test, not by reasoning alone. Fixed properly at the write side instead — every writer (frontend and daemon) now uses the canonical snake_case keys the schema declares.
Verified with two independent adversarial reviews (one caught the critical bug, one confirmed the fix and the fix's fix), full Rust + frontend test suites, and two full real-model inference runs through the actual Tauri command layer.