fix(runtime): guarantee terminal turn outcomes - #270
Merged
OmarB97 merged 3 commits intoJul 13, 2026
Conversation
OmarB97
marked this pull request as ready for review
July 12, 2026 22:37
OmarB97
marked this pull request as draft
July 12, 2026 23:14
OmarB97
marked this pull request as ready for review
July 13, 2026 00:05
OmarB97
added a commit
that referenced
this pull request
Jul 20, 2026
13 tasks
OmarB97
added a commit
that referenced
this pull request
Aug 2, 2026
#270 made the turn-outcome classifier the owner of message.complete's status. _derive_turn_outcome() treats a turn with no visible response as terminal status "failed" ("turn ended without a visible response"), and _freeze_turn_outcome() overrides the locally computed status with it, so the payload ships status="error". That is the deliberate contract, not a regression: #270's own tests/tui_gateway/test_turn_outcomes.py parametrizes {"completed": True, "final_response": ""} -> "failed" / "without a visible response". #270 added that file but never updated this one, so the pre-#270 expectation stayed red. The half of this test that still guards real behavior is the payload text: classifying the turn must not fabricate an "Error:" string. That assertion is unchanged and still passes. Co-authored-by: Omar Baradei <omar@kostudios.io> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 2, 2026
OmarB97
added a commit
that referenced
this pull request
Aug 2, 2026
…270 (#296) Two test doubles in this file drifted behind the code they stand in for. _session_info doubles (3 sites) The real signature is _session_info(agent, session=None), and the gateway turn path calls it with both. Three stubs still took a single argument, so the worker thread died with "lambda takes 1 positional argument but 2 were given" before the fake model ever streamed — surfacing as the misleading "fake model did not stream before activation". Most stubs in this file were already updated; these three were not. The two that were not yet failing reach the same call path on any future change. session.activate in-flight payload #270 stamps the in-flight turn id so a client can correlate a partial with the turn.outcome that closes it. Pin its shape and compare the stable fields exactly, since it is a fresh uuid per turn. golden transcript parity #270 added a leading session.info, a turn_id on message.start / message.complete, and a terminal turn.outcome to the in-process path. The _FakeSupervisor standing in for the compute host still emitted the old four-event sequence. The compute-host child proxies prompt.submit back into server.handle_request(), so it emits exactly what the in-process path emits; the double is what was stale, not the product. The transcript now also carries a fresh uuid and wall-clock timestamps, so the two runs can never be byte-equal again. Normalize those four volatile fields before comparing and keep asserting the sequence and every stable field. Co-authored-by: Omar Baradei <omar@kostudios.io> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
13 tasks
OmarB97
added a commit
that referenced
this pull request
Aug 2, 2026
…#309) `tests/tui_gateway/test_turn_outcomes.py` covers #270's "every started gateway turn has a durable terminal record" guarantee in 23 cases, all of them on the in-process path. None of them touch turn isolation, so nothing records which process owns the outcome when the turn body runs in the `python -m tui_gateway.compute_host` child. That gap invites a specific wrong fix. The parent's completion handler, `_on_compute_host_turn_done`, looks like the natural place to finalize: it runs on turn end, it clears the in-flight turn, and the parent did mint a turn id in `_begin_prompt_dispatch_locked` before routing to the child. It is the wrong place. `ComputeHost._run_real_turn` re-enters this same module -- `server._start_inflight_turn()` then `server._run_prompt_submit()` -- so the child already emits and persists the outcome on the ordinary in-process path, and reports `session_info_emitted: True` so the parent skips its `session.info` too. Finalizing in the parent therefore publishes a *second* terminal record under a different turn id, and the dedupe in `_finalize_turn_outcome()` cannot suppress it: that dedupe is per-session-dict in-process state, and the child holds its own session dict in another process. Four cases, no production change: * the child emits exactly one `turn.outcome` and persists exactly one row * its `turn.end` frame sets `session_info_emitted` * a raising agent still yields exactly one outcome, status `failed` * `_on_compute_host_turn_done` emits and persists nothing, while still releasing `inflight_turn`/`running` The tests drive the real `_run_real_turn` rather than asserting against a hand-written double, and reuse the file's existing `turn_harness`, `_Agent` and `_session` helpers. The fourth case is the regression guard, so it was mutation-tested rather than trusted: reintroducing a `_finalize_turn_outcome()` call into `_on_compute_host_turn_done` makes it fail, and removing it makes it pass again. Co-authored-by: Omar Baradei <omar@kostudios.io> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
turn_outcomesSQLite record that stays separate from model conversation historyturn.outcomeevent while allowing failed persistence to retry without duplicate live deliveryWhy
A provider timeout, HTTP 502, initialization error, or exception could terminate a turn without a durable explanation. Different clients also synthesized their own interruption markers, which made cancellation behavior inconsistent and could duplicate terminal state.
The root cause was that terminal state existed only in transient control flow and client-local rendering. It had no prompt-safe persistence model and no single idempotent delivery seam shared by normal, exceptional, interrupt, and reconnect paths.
Exact-head review fixes
message.completeturn_idthrough in-flight snapshots and hydrates desktop/Ink active-turn trackersImpact
Every started gateway turn now has a durable, redacted terminal record without adding synthetic messages to the model prompt or invalidating prompt caching. A delayed outcome cannot settle a newer resumed turn, a Stop-first turn cannot speak/title/continue after cancellation, and automatic work is recoverable after a synchronous dispatch failure.
Validation
scripts/run_tests.sh tests/tui_gateway/test_turn_outcomes.py -q— 23 passedscripts/run_tests.sh tests/test_hermes_state.py -q -k 'TestTurnOutcomes'— 3 passedscripts/run_tests.sh tests/hermes_cli/test_web_server.py -q -k 'get_session_messages_follows_compression_tip'— 1 passedgit diff --checkpassedSafety boundary
tests/test_tui_gateway_server.pywas deliberately excluded and never run. No Hermes, Chrome, TTS, or user app was restarted, installed, or launched. The branch is based on fork commits28f371dc8bande1754474b6, whose stable patch IDs match upstream Chrome/TTS containment fixes398ed14efand8f58ce2b55respectively.