Repository navigation
Show a tool call you stopped as interrupted, not failed - #25181
Conversation
…al timeline folder Pure moves so a shared timeline assembler can use them: Codex's message ordinal counter becomes ProviderTurnMessageOrdinals and Claude's turn-row revision becomes the provider-neutral agent-journal turn-row revision. Only names and import paths change.
…found again after a restart - A sink transition is admitted whole or not at all; its steps run back to back at their turn in the journal's write queue, and each resolver reads the fold with every earlier write landed. A resolver may also say where the row belongs (turn scope, provider reference), and the writer always hears how the transition landed. A resolved lifecycle batch chooses its settlement mutations from the fold at execution. - New optional row field providerItemRef: the provider's own reference for the item a row is, written only where the row's identity cannot spell it (Codex keys messages by their place in the turn and renumbers its item ids on resume). Set by the creating write, kept by revisions, indexed by the journal fold, never read by clients. A downgrade test shows an older host and client render such rows unchanged. - Provider timeline identity schemes (shared legacy arm, Codex) and the join index that resolves a provider item to its row from memory or the fold: ordinals and request incarnations are read back from the rows, so a restart or an evicted entry finds the original row instead of placing a new one.
… joins read from a replaced epoch A fresh join index continued a turn's messages at the first free place, so a journal holding only a later ordinal (an imported or removed earlier row) had its sequence back-filled. The place is now one past the highest ordinal any row or echoed send holds there, read through a pure scheme reader. The join caches also drop what they read when the journal's epoch is replaced.
…t its turn in the journal Adapters translate their provider's dialect into a small grammar (turns, items, streamed text, requests, context facts, session end/reset); one shared assembler turns it into the journal rows every structured lane writes. Each event is planned as one sink transition. Which row a write lands on, whether a replay writes anything, and every change to what the assembler knows (its ledger) are decided by the transition's resolvers at the event's turn in the journal's write queue, against the fold as it stands then. A forecast (the ledger plus admitted events still queued) only answers apply() at once. So a refused event allocates nothing, a write the journal rejects leaves no trace in memory, and a restart or evicted cache finds the same rows again. Text and full snapshots of one provider item share one row and one lifecycle; reset always flushes text and settles the old session from the journal; the open-work budget is derived from what is actually open. Codex migration contracts compare against the existing Codex translator, including a restart mid-stream and a repeat that outlives the join cache.
…nd background work A third review found two blockers with the earlier rounds' cause, a remembered interpretation trusted after the journal moved on: - A reused request id was judged by its earlier prompt's settled turn before asking which turn the new one lands in, so a real approval in a later turn was dropped. The target turn now decides: the old turn again is a replay; a different live turn opens the next prompt beside it. - A text stream checked its row's turn only on its first write, and a turn's end released streams by the turn planning expected. Every write now checks the row, a turn's end stops the streams whose rows are in it, and turn status reads the journal first, so another writer's Stop wins. Also: a message boundary drawn by an event the journal held as a replay no longer splits an anonymous message; a send naming a turn not yet open waits for that turn; the budget charges a stream's thread and turn strings and the turn caches are byte-bounded; the open turn ends when the journal shows it settled; a turn's opener is read from the journal's row. Background work is now Orca's existing background-task row instead of a tool call flagged `outlivesTurn` (a flag remembered only in memory, so a restart failed the task). A turn's end never settles that row, so it survives restarts; session end leaves one in flight unverifiable. Three tests that opened a background tool call with `outlivesTurn` now open a background-task row and keep their original expectations about which turn the row stays in.
… streams A row kept for the anonymous stream that may continue it, and the marker that a stopped stream's queued writes write nothing, lived in the live-stream map and were never removed when no stream followed. They now live in their own bounded maps, so the live map holds open streams only.
…, not failed A call still running when its turn was interrupted, when the provider child was seen to exit, or that the provider cancelled now carries `endedAs: 'interrupted'` beside `state: 'failed'`. Every build that predates the field keeps reading the call as the failure it always showed; this build reads both through one shared reader and counts the call apart from failures in the run header, without the error tint on its partial output. The shared assembler, the restart settlement and the Codex lane settle a running call through one rule: only a proven interruption cuts it short, so a turn the provider completed around an unclosed call, or work the host lost track of, still reads failed.
A settle with no proof of the child's death closed its running calls as plain failed, so a proof written afterwards corrected the turn to interrupted but could not find the calls. The call now keeps endedAs: 'unverifiable' beside its failed state, and the stale settle revises exactly those calls whose owner the proof names. Older builds still read them as failed.
…ified-ending block
Keeps both multi-row writes: the transition's per-row write (each built after the one before it folds) and main's all-or-nothing queued-rejection write. Main's ledger receipt rides the shared single-row write. A resolved lifecycle batch cannot carry rejectsQueued, so it is never silently dropped.
…b2025/acp-c1-timeline-assembler
…imeline-assembler
…embler' into brennanb2025/acp-c5-tool-interrupted
…b2025/acp-c1-timeline-assembler
…embler' into brennanb2025/acp-c5-tool-interrupted
…timeline-transitions
…m the transition PR Nothing in production reaches the state they defended (an assembler that lost its memory while its child keeps streaming the same turn), and the stored Codex id was positional. The legacy identity scheme moves to the assembler PR with its first caller; the Codex scheme and any persisted reference wait for Codex to move onto the assembler. The Codex ordinal counter goes back to codex/, since no neutral code imports it.
A settlement too large for one row now commits all its rows or none, through the journal's existing all-or-nothing write, instead of a row-by-row writer. Every row is built before any commits, so a settlement naming one item twice is refused before anything is written.
…d-forget Nothing reads which steps of a transition wrote. A failed step fails the sink, leaving the steps before it written; the header says so, and tests cover it plus a settlement whose second row fails inside the transaction. writeAgentJournalTurnRow returns nothing again, as on main.
…ansitions' into brennanb2025/acp-c1-timeline-assembler
…embler' into brennanb2025/acp-c5-tool-interrupted
Review summaryThe problem. When a tool call is cut short, because you pressed Stop, the agent cancelled, a newer turn superseded it, or the agent process exited, whoever closes it out could only mark it User-facing change. A tool call cut short by a proven interruption now reads "1 interrupted" in the collapsed run header. It uses the same small grey style as "1 failed", has no check mark, and has the screen-reader label "Interrupted tool calls: 1". A mixed run reads "1 failed, 1 interrupted", and the partial output is no longer red, on desktop and mobile. Nothing else is added: no notice, no banner. A call Orca lost contact with still reads "failed", as it does today. Mechanism (unchanged by review). Tool-call rows keep Fixed during review.
Decisions to confirm (Brennan).
Deferred (temporary).
Verified.
Not verified.
|
…assembler The common pattern discards the history a provider replays while loading a session, so the assembler has no use for an input.history event.
Review follow-up to the adoption removal: drop the comment naming the provider's saved message, and make requestedAt required since every pending input comes from input.accepted.
- journal-store.ts imports: main #24576 dropped journalStoreLoadedFields; this branch types appendResolvedItem off JournalItemAppender, so neither it nor JournalResolvedItem is imported. - event-sink-queue: keeps this branch's journalItems; journalStopDecidesTurn takes main #24864's two arguments (openedBy plumbing removed on main).
…ansitions' into brennanb2025/acp-c1-timeline-assembler
agent-session-journal-schemas.ts: main #24576 (through C1a/C1b) made the known body union internal (KnownItemBody) behind an open union; this branch's TurnLifecycleFields, tool-call endedAs and the shared turn fields sit on it unchanged.
|
Merged the current C1b, then current main, into this branch (now
|
…CP foundation squashes) One conflict, in agent-session-journal-schemas.ts: main moved the row's failure fact into agent-session-failure-fact-schema.ts (#25073) right where this branch added TurnLifecycleFields. Took main's move (the old local FailureFact is gone) and kept this branch's TurnLifecycleFields.
…itions handle fix)
…opaque provider handle Main's #24991 replaced the {kind, ...} handle with {transport, agent, nativeId}; three test files from this stack still wrote the old shape. Same lines the downstream ACP branch uses.
|
Merged main after #25064/#25747 landed; head |
Main now carries A3 #25159 (squash 9905765) and C5 #25181 (squash c4ea14c); both squashes equal their final branch heads, so the merge was computed against main's tree with those heads as extra parents (scratch commit, never pushed) and committed as an ordinary two-parent merge of origin/main. Resolutions: dead-generation settlement keeps D3's move of the unfinished-work helpers; provider support, create support, the record's stored-agent check and its tests take main's A3 (records no longer admitted per registration). D3 callers of the removed registry follow: agent-launch-mode reads the registered agents from STRUCTURED_AGENT_RUNTIME_REGISTRATIONS; the Grok host rig opens the store without agents; the "admits Grok's records" registration test is gone with the registry.
…-history - #24660: the row writer's planned write is public writeRows (the new step writer calls it); the transactional write that also stores the chat's status is commitRows, so every path still writes the status row once per transaction. - #25181: the open settlement plan ends a running call as its turn's row ended (runningCallEnd / terminalAgentJournalBody) and revises calls an unproven settle closed once a proof names their owner; the status facts read requiresTerminalSettlement, the same facts as before. Liveness keeps main's lostLiveWorkJournalBody under the per-item roster revision. - #24660: the open plan settles what a gone process left queued through holdUnsentSends (a person's message becomes a held card); its leftovers are exactly the queued sends written before the open. - #25159: reading and settling a chat need no adapter. The persisted tab listing lists every tab with a record, startup settles any chat whose record exists, and adapterSupportsRecord / hostCanSettleRecord are gone, as on main. - Line caps: the runtime's startup step moves into its own layer (orca-runtime-structured-agent-session-startup-step.ts), and the host exposes its restore as one `startup` surface instead of four pass-through methods. - The golden digest is unchanged, so the status rules stay at 3.






Dependencies #25141 and #25064 have landed on main. This diff contains only the interrupted-call change; their transition and shared assembler code come from main.
ELI5
When you press Stop while the agent is running a command, that command never finishes. Today the chat tells you it failed, as if the command itself errored. It didn't fail; you stopped it. After this change the chat says it was interrupted.
What Changed
The problem. A structured chat's tool call has three states: running, completed, failed. When a turn or session ends while a call is still running, whoever settles it (the shared timeline assembler from #25064, the settlement that runs after a restart, the Codex lane) can only pick
failed. So every call cut short by Stop, by the agent cancelling it, or by the agent process exiting shows "1 failed" in the run header and its partial output in red. Grok over ACP (the Agent Client Protocol) makes this constant: on a cancel it leaves the running callin_progressforever, so Orca has to close it itself, and today that always reads as a failure.A second case, found in review: when Orca restarts and cannot yet prove the old agent process died, it closes the turn as "unverifiable" and the running call as plain failed. If proof of the death arrives later (the next time the chat is opened or a new agent process is started), the turn was already corrected to interrupted, but the call could not be: nothing on it said it had been closed for lack of proof, so it read failed forever.
What you see now.
Mechanism.
endedAs, written only besidestate: 'failed'. It has two values:interrupted(a proven interruption) andunverifiable(closed when its session ended and nothing proved why). One shared reader,agentJournalToolCallLifecycle, turns the pair intorunning | completed | failed | interrupted;unverifiablereads asfailed, exactly as today. The run header, the output tint, and the projection read through it.endedRunningAgentJournalToolCall, decides what a call still running at an end becomes. Only an interruption Orca knows happened cuts it short: the call's turn endedinterrupted(a person's Stop, a cancel, or a newer turn superseding it), a session that ended on an observed child exit, or the agent process found dead after a restart. A turn the provider completed around a call it never closed stays plain failed. An end the host could not verify (lost contact, a restart with no proof yet) also reads failed, but the call keepsendedAs: 'unverifiable'so a later proof can find it. That last case applies only to a call whose turn was still running when contact was lost: a call in a turn a person had already stopped takes the Stop'sinterrupted(next bullets).unverifiableturn tointerruptedonce death evidence named the agent process that wrote it (by its owner "fence", the number each agent process generation writes under). It now does the same for calls: each call withendedAs: 'unverifiable'whose creating fence is the one the evidence names becomesinterrupted. That is derived from the journal row itself on each settle, so there is no separate list of owed corrections. A call that failed on its own (noendedAs), a completed call, a call another agent process wrote, and evidence from an older build that names no owner are all left alone; once corrected, a call no longer matches, so re-running writes nothing.terminalAgentJournalBody(body, end)(injournal-terminal-settlement.ts), the "row that settles an item no one will finish" function that Add a shared timeline assembler for structured agent chats (not wired yet) #25064 made shared. Both the shared assembler's turn and session settlement and the restart (dead-generation) settlement every lane shares call it, so the ending is a parameter of that one function, not a second copy of the rule. The Codex lane's turn and session settlement uses the same rule on its in-memory open items (it already had a helper namedinterruptedBodythat wrotefailed).interrupted(outcome "cancellation") straight to the journal, and since Add a shared timeline assembler for structured agent chats (not wired yet) #25064's latest round a Stop only stops the turn's text and cancels its pending prompts; its running calls stay the agent's to finish, so a call the agent completes after the Stop lands completed. When the agent then ends that turn (or a newer turn or the session's end does), the calls still running take the Stop row's state, read when the settlement is written, not whatever the provider's later end reports. So a stopped turn's unfinished calls read interrupted even if the provider answers the cancel withcompleted, and whichever settlement closes them: the agent's turn end, a newer turn, the session's end (including one the host could not verify), or the restart settlement with no proof the agent died. A turn row another writer leftunverifiableleaves them unverified, open to a later proof; work outside any turn (background tasks, subagents) takes the settlement's own end. The rule lives in one place,runningCallEndinjournal-terminal-settlement.tsbesideterminalAgentJournalBody, and every settlement that ends running calls through that function asks it: the assembler's turn and session settlement, the restart settlement, and the live settlement when an agent process exits unexpectedly. Only a call whose turn row is still running when the settlement runs takes that settlement's own end; after a restart that end comes from the death evidence (proof: interrupted; none: failed withendedAs: 'unverifiable'). A settlement with no ending of its own (the Stop's) ends no calls at all.tool_call_updatewithstatus: cancelled, for the Grok adapter later in this stack) closes the item withinterruptedAgentJournalToolCall(body); the assembler keeps it, and a later turn end does not restate it.turnitem and legacystatus.turnLifecyclerepeated the same eight fields; they now share one definition. That freed the line the new field needed under the file's 300-line limit.Why
Why a field beside
failedand not a newinterruptedstate value. Persisted journal rows outlive the build that wrote them: after a downgrade an older Orca folds the same rows, and a phone or desktop client on an older version reads this host's items. Every build so far reads tool state with=== 'failed'/=== 'running'checks, so an unknown value counts as neither: the older run header would mark a stopped call done ✓, which is worse than today's "failed". Its rewind normalizer would also turn the row into a raw-text fallback. A capability negotiated at the RPC boundary can protect clients, but not persisted rows read by an older host. The additive field keeps every older reader on the existing failed fallback with no negotiation, and the shared reader keeps the concept in one place. The cross-version test proves this against a real older release for both endings, including that the bare new state value reads as a success there.Why record
unverifiableon the call instead of re-deriving it later. The alternatives were (a) to correct every failed call inside a turn once the turn is proven interrupted, which would wrongly relabel a call that genuinely failed before the stop, or (b) to work backwards from which settlement batch last wrote each row, which the journal does not expose per item and would tie correctness to settlement ids. Recording the unverified end on the row is the same model the turn already uses (turn.state: 'unverifiable'), keeps the fact with the thing it describes, and lets the existing re-run settlement find exactly the calls it closed.Why
interrupted(and the quiet header count). The common pattern models a call cut short as its own terminal state, neither error nor success, shown in muted text and counted apart from errors in a group summary, with no extra notice. "Interrupted" is also the word Orca already uses for the turn itself (turn.state: 'interrupted'), so the call and its turn agree.Why an unverified end still reads failed. Orca's execution rule (
docs/reference/ssh-execution-boundary.md) is that loss of contact is never evidence a process died. Marking work the host lost track of as "interrupted" would claim a stop nobody proved, so a call in a turn that was still running when contact was lost keeps today's reading until proof arrives. A turn a person already stopped is different: the Stop is the interruption, recorded on the turn before contact was lost, so its calls read interrupted whatever later happens to the process, just as the turn does.Differences from the common pattern
state: 'failed'+endedAsrather than a fourth state value. Safe because every reader in this build goes through one reader, and older builds read the existing failed fallback (proven by the cross-version test).endedAs: 'unverifiable'so that, unlike a permanent failure, a later proof corrects it. The common pattern has no unverified end, so it has nothing to correct later.interruptedflag is not read yet. Follow-up: read that flag in the Claude lane and writeendedAsfrom it.endedAs: 'unverifiable', so a neutral reading there would likewise be a reader-only change; that is left as an open question for review.interrupted/unverifiable), and the finer reason (Stop, newer turn, death) stays on the turn row.Linked Issue
Part of the ACP (Agent Client Protocol) native-chat stack. No separate issue.
Visual Proof
Desktop QA done on a separate Mac (the M4Air), 6 of 6 captures pass. The before/after screenshots are in the review comment on this PR. How it was run, plainly:
70fd4bac5bd; the before build wasmainat40f9e7a3fcd. Both ran as isolated dev builds with their own profiles.1 interrupted(screen reader "Interrupted tool calls: 1", no check mark), the mixed run reads1 failed, 1 interrupted, and the stopped call's partial output is in the normal muted text. Before:1 failed/2 failed, and the partial output is red.Exactly what changes on screen:
1 failed(screen reader: "Failed tool calls: 1"); no check mark1 interrupted(screen reader: "Interrupted tool calls: 1"); still no check mark2 failed1 failed, 1 interruptedtext-destructive)text-foreground/80)1 failed, red output1 failed, red output) until proof arrives, then as the first row1 failed, red output1 interrupted, as the first rowThe renderer test (
NativeChatToolRunCallCounts.test.tsx) renders the real desktop tool run from a projected journal row and asserts the header text, accessible name, no check mark, and no error tint. The mobile test renders the real mobile message from a projected row and asserts the error background is absent for an interrupted call and present for an unverified one.Testing
New tests:
provider-timeline-assembler-tool-ends.test.ts: turn interrupted, turn superseded, session ended on an observed exit, and provider cancel all readinterrupted; a person's Stop written to the journal leaves the call running, and the agent's later turn end (reportinginterrupted, orcompleted) makes it readinterrupted, while a call the agent completed after the Stop stays completed; a completed turn and a lost child still readfailed; a restart with no proof leaves the call failed withendedAs: 'unverifiable'; after a person's Stop, a session end the host could not verify and a restart with no proof both make the call readinterrupted, like its turn, while a call that failed before the Stop stays failed; a call that finished before the stop stays completed.structured-agent-session-late-proof-tool-ends.test.ts(real journal and database): a restart settles with no proof, then a proof naming the owner arrives: the turn and its cut-short call both read interrupted, a call that failed on its own stays failed, a completed call stays completed, and a second settle writes nothing. A proof naming another agent process, or an older build's proof naming no owner, changes nothing; a call another owner wrote stays as it was. Also covers a call the restart-eviction settlement closed.endedAs: 'unverifiable'.interruptedcuts its active tool short; a session end does too; a completed turn still fails it.unverifiableand any unknown ending read as the failed state beside it.mobile/src/session/MobileNativeChatMessage.test.ts): output of an interrupted call has no error background; output of an unverified one keeps it.tests/e2e/cross-version-wire/agent-session-interrupted-tool-downgrade.unit.test.ts): a real older release's reducer, schema, projection and run outcome read this build's interrupted and unverifiable calls as 2 failed calls, not a success, both as a host folding the rows after a downgrade and as a client reading this host's items.Ablations for the turn-row rule (at
a46e8b8a4eb): letting a stopped turn's calls take the provider's later end fails thecompletedStop test; letting a settlement with no ending (the Stop's) end calls fails 6 tests across the tool-ends and Stop suites. Earlier ablations (measured before the 2026-10-05 merges): making the shared rule always writefailedfails 9 settlement tests across the assembler, restart and Codex paths; making the reader ignoreendedAsfails 6 reader and renderer tests. Removing the late-proof correction fails all 3 late-proof tests (the review's reproduction failed the same way before this fix and passes now); dropping its owner check fails the other-owner test.Local runs (vitest, explicit files): the review's reproduction passes; every
src/main/native-chattest file plus the shared native-chat / agent-session and Codex structured-journal suites (293 files, 2,681 tests) pass, as do the renderer and cross-version tests. The mobile test could not run locally: this worktree shares the root install, and the mobile config needs mobile's own install (expo/tsconfig.base.json), which was not installed here;react-test-rendereris declared inmobile/package.json. CI's Mobile Checksverifyjob runs it: on an earlier push its log showedMobileNativeChatMessage.test.ts (23 tests | 2 failed)(my first assertion walked to the wrong element), and ate690534b986MobileNativeChatMessage.test.ts (23 tests)passes with the whole mobile suite (916 files, 9,936 tests). On one earlier push,mobile web app bundlefailed on a Chromium "Unable to capture screenshot" error in an HTML preview test this PR does not touch; it passed on the next run. Known unrelated: the cross-versionqoder-history-search-downgradetest fails on unrelated PRs too, and main's typecheck error atsrc/main/claude/claude-child-work-evidence-retention.test.ts:88is fixed separately in #25063.What I verified / didn't
Notes
docs/reference/remote-wire-compatibility.md); older hosts and clients read the existing failed fallback for both endings, proven by the cross-version test. No capability is needed because nothing old has to interpret the field. The late-proof correction runs on the host that owns the journal, from evidence that host recorded.interruptedCount,interruptedCallsLabel) added to English, the hand-maintained es/fr/ja/ko/zh catalogs (every shipped UI language), and the runtime-required English catalog. The repo's catalog verifiers pass, and all five translated catalogs now miss the same 49 keys as before this PR.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)Merged the rebuilt assembler (2026-10-05)
#25064 was rebuilt: the assembler now settles from the journal at write time, and one shared function,
terminalAgentJournalBody, gives the terminal row for both its settlement and the restart settlement. Merging it conflicted in exactly those two places, where this PR had changed two separate copies of that rule. Resolution: the ending is now a parameter of the one shared function (terminalAgentJournalBody(body, end)), and both settlements pass how the turn or session ended. The restart settlement's own copy is gone, and its "is anything unfinished" check now uses the existingrequiresTerminalSettlement, which is exactly that question. The rebuilt assembler also added a path for a turn another writer already ended (a person's Stop); the next section replaces how this PR handled it. Behaviour everywhere else is unchanged, including the late-proof correction and the Codex lane.The rebuilt assembler removed "a new provider session replaces this one" (session reset), and its test rig's restart now runs the restart settlement, as production does. So the tool-ends tests drop the session-reset case, and the restart case now checks that a restart with no proof leaves the call unverified (the late-proof test covers the later correction).
Local runs at the merge (vitest, explicit files): every assembler test file, the restart (dead-generation), crash turn-end, crash mid-start, stale-turn verdict and open, stop-event (four files), unexpected-exit, late-proof and journal crash-boundary suites, the Codex settlement and command-retention suites, the shared run-outcome and projection suites, and the renderer test: 31 files, 332 tests pass; four more settlement-adjacent files (Claude context-usage restart and subagent permission request, recovered turn clock, compaction) pass, 48 tests; the cross-version downgrade test passes; the earlier review's late-proof reproduction passes. Typecheck (node, then web) was run locally through the shared-machine gate: the only errors are
stream-json/stream-chainmodules missing from this worktree's stale shared install (package.jsonmatches main), none in this PR's files. The mobile test still can't run locally (no mobile install); CI's Mobile Checks runs it.Second merge of the rebuilt assembler and the turn-row rule (2026-10-05)
#25064's next round changed what a person's Stop does in the assembler: it now stops the turn's text and cancels its pending prompts at once, but leaves the turn's running tool calls to the agent, which settles them at its own turn end (or a newer turn, or the session's end settles them). The merge (of #25064 at
9fcb95406ea) was textually clean, but it left this PR's old way of ending calls under a Stop dead: that path read the Stop row when no ending was passed, and no call is ever settled without one now. That path is removed, and replaced by the single rule under Mechanism: a call still running when its turn ends takes that turn's journal row state, so a stopped turn's calls read interrupted even if the agent later reports the turncompleted. ThepromptsOnlyflag #25064 added to the settlement scope is folded into that rule: a settlement with no ending (the Stop's) ends no calls. Spanish (es.json) gained the two strings it was missing.Local runs at
a46e8b8a4eb(vitest, explicit files): every assembler test file, the restart (dead-generation), crash, stale-turn, Stop (nine files), unexpected-exit, late-proof, journal crash-boundary, subagent-liveness, store, write-queue and Stop/resume-row suites, the Codex settlement and command-retention suites, the shared run-outcome and projection suites, and the renderer test: 41 files, 469 tests pass; the cross-version downgrade test passes. Typecheck (node, then web) through the shared-machine gate: only the knownstream-json/stream-chainerrors from this worktree's stale shared install (5 node, 4 web), none in this PR's files. The localization coverage audit currently fails onworktree-creation-hook-preparation.ts, a file identical tomainthat this PR does not touch.The same turn-row rule in the restart settlement (2026-10-05)
What review found. After the second merge, the turn-row rule lived only in the assembler. The restart settlement still ended every running call from the death evidence alone, without reading the call's turn row. So after a person's Stop, if the agent never ended the turn and Orca then restarted with no proof the agent died, the call read "1 failed" in red under a turn that read "interrupted after". The same situation closed by the assembler's own session end read "1 interrupted". This needs Stop, an agent that never answers it, and a restart that cannot prove the old process died (an unverifiable process identity, or a lost remote host). It affects assembler-lane agents such as Grok. Codex writes its turn row and its calls in one batch, so it never reaches this state.
Fix.
runningCallEndmoved out of the assembler intojournal-terminal-settlement.ts, besideterminalAgentJournalBody. The assembler, the restart settlement and the live exit settlement all call it. There is no second copy. A call whose turn row is already settled ends as that row ended. Only a call in a turn still running when the settlement runs keeps that settlement's own end; for a restart, that is the evidence-based end.Side effect, stated for completeness. A call left running under a turn the provider had already completed now reads plain failed in the restart and exit settlements too, as it already did in the assembler (the "completed turn" difference above). Before this commit, those two settlements wrote interrupted or unverified for it.
mainwrites plain failed there, so againstmainnothing changes. The case needs a lane that completes a turn without settling its calls, as the Claude lane can (see the Claude note under Mechanism).Tests and ablations. New tool-ends tests cover two paths after a Stop: a session end the host could not verify, and a restart with no proof. Both make the call read
interruptedwhile the turn keeps the Stop's row. A call that failed before the Stop stays failed across the restart, with noendedAs. Two ablations:Local runs. At
70fd4bac5bd(vitest, explicit files), the same 42 files as the previous section pass, plus the five settlement-adjacent files: Claude context-usage restart, Claude subagent permission request, failure text, recovered turn clock and compaction. That is 47 files and 531 tests, including the cross-version downgrade test.oxfmtandoxlintare clean on the changed files. The node typecheck through the shared-machine gate shows only the 5 knownstream-json/stream-chainerrors from this worktree's stale shared install, none in this PR's files.Up to date with main (2026-10-05)
Merged
mainatd17351401d7and #25064 atad8ebbd1f31(which already contained thatmain). Both merges were clean, and the lockfile equalsmain's. The six translation catalogs still parse, keep this PR's two strings, and pass the repo's three localization checks. The same 47 test files (531 tests, including the cross-version downgrade test) pass at the merged head. The node and renderer typechecks through the shared-machine gate show only the knownstream-json/stream-chainerrors from this worktree's stale shared install.Current main refresh (2026-10-06)
Merged main at
4e64fa9940f5a4a9bfb7b021a3c405fd76643ca5; head98e165184fbab3d2ac976c2b4965ae32e50f03bb. C1a’s landed transition code now comes from main. The interrupted-call change and inherited assembler patch are preserved; the one header conflict uses main’s typography and separator while keeping the failed/interrupted counts. All 24 explicit test files / 277 tests and scoped format/oxlint on 56 code files pass. The lockfile and submission-position test exactly match merged main, with exactly one used provider-handle import. Fresh CI is pending; CI is the typecheck authority.Prior-head CI result (98e1651)
Head
98e165184fbab3d2ac976c2b4965ae32e50f03bb. Current-head CI has finished: static analysis/typecheck, mobile, packaging and the other unit shards passed. The only test failures are main’s unchanged release-checkout parser-dependency assertion (#25731) and file-explorer cache-scan assertion (expected 2001 visits, got 4002); final verification failed from those two results. The cache failure reproduces locally and its sidebar/store code matches merged main byte for byte. No branch-owned failure remains; no local typecheck was run.Refresh after the shared assembler landed
A Stop or session ending could leave a running tool call looking failed; this PR shows it as interrupted while preserving calls that already failed. Merged main at
eaaae0196f556032c64c665f853d46af97461cec, including #25064 and #25747; current head5ca9bc56ff8c1f681d2395b20158103a3146c42e. All 36 feature sections and 1212 added/deleted lines are preserved exactly; no transition/assembler dependency copies remain in the diff. All 13 explicit suites / 199 tests and scoped format/oxlint on 29 code files passed. Main’s lockfile and complete submission-position test are unchanged, with one used provider-handle import. Current-head CI is fully terminal and green, including typecheck; details below.Current-head CI finished
At
5ca9bc56ff8c1f681d2395b20158103a3146c42e, all current-head runs are complete: 16 checks passed and 16 were skipped. Static analysis/typecheck, all five unit shards, compatibility, mobile and both packaging jobs passed. Main’s two corrected test assertions are green in this run.