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.

“Is this turn still running” used to get a different answer depending on which code path you asked. The chat endpoint had one idea, the scheduler another, a background task a third. That’s not a philosophical problem, it’s a bug factory: a stop button that doesn’t stop anything, a task that outlives the turn it belongs to, a UI stuck on “thinking” for a reply that already finished. Today’s build closed that gap for the thread-system rebuild.

The fix is a dedicated execution tracker — one service and one backing table that owns a turn’s lifecycle from send to settle. The step-chain that used to hand off between separate pieces now collapses onto a single while-loop that owns the whole turn, start to finish. Sends unify on one route, POST /api/thread/-1, instead of a bespoke case bolted onto the chat handler. Every place that checked a raw cancellation event got renamed to what it actually does — a cooperative should_stop predicate. A flag you check is honest. A signal you catch implies something it doesn’t deliver. Shipped with a new lifecycle test suite plus updated contract tests across turn-signal, policy, dispatcher, and chat-endpoint.

Second fix, smaller, uglier bug: one malformed row in the behavioral-pattern table — a value that wasn’t valid JSON — was quietly killing the entire existing-patterns lookup on every background pattern tick. Not a crash. A warning nobody was reading, silently swallowing every valid pattern in the table because one row couldn’t parse. Guard’s one line: skip rows whose JSON doesn’t parse before extracting. Worth remembering past this table — a single bad row should never take the whole feature down with it.

Then a full-branch Sonar sweep to clear what a fast rewrite leaves behind: a blocker-severity validation error that could leak internal detail through an error boundary, silent exception handling swapped for real logging across the scheduler and system layers, complexity extractions on routing and episodic-memory code, duplicate string literals hoisted into shared type aliases, dead migration paths deleted. Gate was the full unit suite plus a clean frontend build and type-check. Senior review signed off. Also fixed a stop handler that wasn’t awaiting its own cleanup, and wrote down the turn-execution vocabulary so nobody has to reverse-engineer should_stop from call sites again.

One state machine instead of three competing guesses. That’s the whole point of today.

  • Landed a dedicated ExecutionTracker service and TurnExecution model owning per-turn lifecycle end to end, sends unified onto one route, 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 predicate you check, not an event you catch

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

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

  • Documented the new turn-execution vocabulary so the lifecycle model doesn’t need reverse-engineering next time