Skip to content

fix(runtime): land the terminal fact a stop claims, and stop dropping non-terminal prior runs - #2078

Merged
Astro-Han merged 8 commits into
mainfrom
fix/runtime-stopped-run-terminal-fact
Aug 4, 2026
Merged

fix(runtime): land the terminal fact a stop claims, and stop dropping non-terminal prior runs#2078
Astro-Han merged 8 commits into
mainfrom
fix/runtime-stopped-run-terminal-fact

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

A run stopped while parked on AskUserQuestion left the Session projection saying "aborted" and the RuntimeEvent ledger saying nothing at all. Its header stayed waiting_for_user forever, 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:

  • stop lands the terminal fact it already claimed. stop() takes the Run's terminal claim, but only finalize() — 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.
  • Prior-run context stops silently deleting a run. buildPriorRuntimeContext treated 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 — repairRunTerminalFact returns false for 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 stop reporting 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 racing finalize commits 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.
  • Not run: Desktop E2E and Storybook. No renderer, IPC, or UI surface changes.

Root cause

Confirmed against a real Desktop store, not inferred. Across 157 runs, exactly 5 were non-terminal, and all 5 held an AskUserQuestion function_call with no matching function_response. For one of them the session-message layer was complete — turn_state{status:aborted, abortSource:renderer.stop_button} and an error tool_result — while the ledger held text×4, function_call×8, tool_dispatch×8, function_response×7 and no terminal event. The next run started 22s later and was built without any of it. Full evidence in #2074.

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
Astro-Han marked this pull request as ready for review August 4, 2026 05:24
@Astro-Han
Astro-Han merged commit d3b0bc8 into main Aug 4, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant