fix(runtime): land the terminal fact a stop claims, and stop dropping non-terminal prior runs - #2078
Merged
Merged
Conversation
A run stopped while parked on an interaction keeps a non-terminal header and a ledger with no terminal fact. buildPriorRuntimeContext treated that as skippable and dropped the whole run, so the turn's user message, assistant text, and tool activity disappeared from every later turn's model context — silently, and permanently, because the header never becomes terminal on its own. Read the ledger it does have instead: attempt the same repair the terminal-status branch already performs, and replay the events either way. Only the effective run header still depends on a terminal fact. Refs #2074
stop() takes the Run's terminal claim, but only finalize() — reached when the backend's event stream ends — ever cashed it. A turn parked on an unanswered interaction never ends that stream, so the stop projected an aborted turn into the Session while the Run stayed non-terminal in the ledger for good, and the backend generation it was waiting on could never wake it. Cash the claim where the stop completes. The claim itself keeps this idempotent: a stream that later produces its own terminal event finds the claim taken and writes nothing. Refs #2074
The ledger may already hold a terminal fact this Run never wrote — a Host recovery or a resumed continuation sealed it — and a sealed run rejects further appends. Read before landing one. Scope the settlement to embedded owners too. A Hosted Run's terminal fact belongs to the Host's terminal authority (#1359, #1996), which parks provider-indeterminate Runs that a stop must not resolve on their behalf. Refs #2074
… run repairRunTerminalFact returns false for a non-terminal header before it reads anything, so the call could only ever answer "no". It read as a recovery attempt that never happens. Refs #2074
…g stop commitTerminalRun checked terminalRunHeaderCommitted only on entry. A stop settling the claim and the stream's own finalize both pass that guard, then queue behind the same in-flight write. The claim slot dedupes the RuntimeEvent, but the run-store projection appended a second terminal AgentRunEvent for the one run. Re-check after the await. Before this, the new stop settlement made the duplicate reachable on the ordinary path: stopping mid-generation overlaps the stream's unwind. Refs #2074
Astro-Han
marked this pull request as ready for review
August 4, 2026 05:24
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.
Summary
A run stopped while parked on
AskUserQuestionleft the Session projection saying "aborted" and the RuntimeEvent ledger saying nothing at all. Its header stayedwaiting_for_userforever, and every later turn then dropped that whole turn — user message, assistant text, all tool activity — from the model's context. The agent read as amnesiac, permanently, because the header never becomes terminal on its own.Two changes, one at the source and one at the amplifier:
stoplands the terminal fact it already claimed.stop()takes the Run's terminal claim, but onlyfinalize()— reached when the backend's event stream ends — ever cashed it, and a turn parked on an unanswered interaction never ends that stream. The claim is now cashed where the stop completes, and the stop is fail-closed: a settlement that cannot land makes the stop fail rather than report a success it did not establish, and leaves the operation pending so a retry settles it again. The claim keeps the write idempotent — a stream that later produces its own terminal event finds it taken and writes nothing.buildPriorRuntimeContexttreated a non-terminal prior run as skippable. It now replays whatever ledger that run does have; only the effective run header still depends on a terminal fact. No repair is attempted on this branch —repairRunTerminalFactreturnsfalsefor a non-terminal header before reading anything, so the call could only ever answer "no".Either change alone leaves the defect reachable: the first does not help runs already stranded (a crash, an older build), and the second leaves
stopreporting a success it did not establish.Scope: embedded owners. A Hosted Run's terminal fact belongs to the Host's terminal authority (#1359, #1996), which also parks provider-indeterminate Runs that a stop must not resolve on their behalf.
Refs #2074
Verification
@maka/runtime— 3096 pass, 0 fail (9 skipped), including six new behavior tests: a stop whose backend stream never ends commits a terminal fact; a prior run left non-terminal by an unanswered interaction is replayed into model context; a settlement racingfinalizecommits exactly one terminal run event; a stop whose settlement fails stays retryable; a hosted stop leaves the terminal fact to the Host; a settlement finds an already sealed ledger and leaves it untouched. Each was confirmed red against the code without its guard first.@maka/runtime-host— 619 pass, 0 fail. This suite caught two real regressions from an earlier, wider version of the fix (a sealed-ledger append and a parked continuation being resolved by shutdown); both shaped the final scope.@maka/headless— 1338 pass (1 skipped).maka-agent(CLI/TUI) — 468 pass.npm run build(full workspace tsc),npm run lint,npm run format:check— clean.Root cause
Confirmed against a real Desktop store, not inferred. Across 157 runs, exactly 5 were non-terminal, and all 5 held an
AskUserQuestionfunction_callwith no matchingfunction_response. For one of them the session-message layer was complete —turn_state{status:aborted, abortSource:renderer.stop_button}and an errortool_result— while the ledger heldtext×4, function_call×8, tool_dispatch×8, function_response×7and no terminal event. The next run started 22s later and was built without any of it. Full evidence in #2074.