Skip to content

ai-chat status merges two orthogonal state machines, so archived+processing validates as legal #2439

Closed

Description

@malibio

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:

status is intentionally a plain String, not an enum. Two orthogonal state machines currently share this one key: a turn state the daemon drives (processing while inference runs, idle once 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 — the ai-chat schema declaration
  • packages/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:9 and every frontend consumer of AiChatStatus
  • 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

  • status is 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-18 doc comment is rewritten to describe the split, not the workaround
  • A test asserts a chat can be archived while its turn state is whatever it was, without either value being lost

Activity

  1. malibio commented on Sep 8, 2026

    @malibio
    CollaboratorAuthor

    Frontend baseline: 232 test files passed, 4895 tests passed.

  2. self-assigned this
    on Sep 8, 2026
  3. malibio commented on Sep 8, 2026

    @malibio
    CollaboratorAuthor

    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.

  4. mstomar125 commented on Sep 8, 2026

    @mstomar125
    Contributor

    Resolved via PR #2465 (merged).

    `ai-chat.status` split into two independently-valued, independently-owned properties:

    • `turn_status` (`"idle"`/`"processing"`) — daemon-owned, driven during inference turns
    • `session_status` (`"active"`/`"archived"`) — PTY-owned, set when a session ends

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

backendbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions