July 3, 2026

Turn tracking gets a real state machine

Chalie's send path collapses onto one execution tracker and one step-loop, closing out the thread-system rebuild with a clean Sonar bill and a silent memory bug fixed along the way.

The thread-system rebuild has been converging for over a week: one API contract, independent lanes per thread, one getter for reading history. Today closed the loop on the piece still improvised — knowing, at any moment, exactly what state a turn is in.

Before today, “is this turn still running” was answered differently depending on which code path asked — the chat endpoint had one idea, the scheduler another, a background task a third. That ambiguity turns into real bugs: a stop button that doesn’t stop anything, a task that outlives the turn it’s attached to, a UI stuck on “thinking” for a reply that already finished. The fix is a dedicated execution tracker — a single service and backing database table owning a turn’s lifecycle from send to settle, with the step-chain that used to be separate handoffs collapsed onto one while-loop that owns the whole turn end to end. Sends now unify on one route, POST /api/thread/-1, instead of a bespoke case bolted onto the main chat handler. Every place that checked a raw cancellation event got renamed to what it actually is — a cooperative should_stop predicate — because a flag that gets checked, not a signal that gets caught, is the honest description of how a stop request propagates through a running turn. It shipped with matching tests: a new suite exercising turn-execution lifecycle directly, plus updates across the turn-signal, policy, dispatcher, and chat-endpoint contract suites.

A smaller but sharper fix rode along on the memory side: one malformed row in the behavioral-pattern table — a value that wasn’t valid JSON — was aborting the entire existing-patterns lookup every time Chalie’s background pattern-recognition tick ran. Not a crash, just a silent one, spamming a warning and quietly suppressing every valid pattern in the table because a single row couldn’t parse. The fix is a one-line guard — filter to rows where the JSON is actually valid before extracting — but the failure mode is worth naming: a corrupt row shouldn’t take down a feature for every row that’s fine. That generalizes past this one table.

The rest of the day was debt service: a whole-branch Sonar sweep through the backlog built up across the thread-system work — a blocker-severity validation-error leak that could surface internal detail through an error boundary, silent exception handling replaced with real logging across the scheduler and system layers, cognitive-complexity extractions on routing and episodic-memory code, duplicate literals hoisted into shared type aliases, dead argv-controllable paths removed from old migrations. None of it changes behavior — the gate was the full unit suite plus a clean frontend build and type-check — but it’s what keeps a fast-moving rewrite from quietly accumulating the smell of one. A last piece tidied a stop-handler to properly await its own cleanup call, and documented the new turn-execution vocabulary so the next reader doesn’t have to reverse-engineer should_stop from call sites.

None of today is visible in the product. What it buys is a thread system where “what is this turn doing right now” is answered by one piece of code instead of inferred from three, and a codebase clean enough that the next feature built on threads doesn’t inherit today’s ambiguity. That’s the work underneath everything that ships next.

  • Landed a dedicated ExecutionTracker service and TurnExecution model tracking per-turn lifecycle end to end, with sends unified onto one route and the old step-chain collapsed onto a single loop

  • Renamed the cancellation signal to should_stop across the dispatcher, policy manager, and delegate runner — a cooperative predicate, not an event, and now named like one

  • Fixed a silent memory bug where a single malformed JSON row in the pattern table was suppressing every valid behavioral pattern on each background tick

  • Cleared a full-branch Sonar backlog: a blocker-severity error-detail leak, silenced exceptions replaced with real logging, complexity extractions, and dead migration code removed, gated on the full test suite plus a clean type-check

  • Documented the new turn-execution vocabulary so the lifecycle model is legible to the next person working in this code