Repository navigation
feat(native-chat): say in the chat why Orca stopped a reply, and offer Continue - #25675
Conversation
…r Continue When the Orca that runs a structured chat (this computer or a paired server) quits, updates or crashes mid-reply, the chat's stopped row now names the cause and the machine, and a Continue button sends the existing restart continuation for that cut turn, with or without a restart offer. - Host: a quit/update writes one turn-scoped row for the turn its stop cut, in today's words, with an optional `orcaStop` cause on the providerExited fact; restart adjudication stamps how the previous runtime ended on the deaths it proves (crash, or the quit/update it began), so the crash row names it too. Older clients keep their single row. - Host: agentSession.continueInterrupted, capability-gated, rechecks under the session lock that the chat still sits on that cut, so a second click or a retry sends nothing. - Client: the row's copy names the cause and machine; Continue sits above the composer.
…the agent; neutral row, Continue explains itself - The cause now rides on the host's row itself (`orcaStop` on the status row, beside today's words), the same row family and id scheme as the reopen's death row. - Each recorded owner is stamped with the Orca runtime that holds it; a death proven later (at restart, or when recovery stops a survivor) names how that runtime ended: the quit or update it began, else a crash. Owners an older build recorded, a terminal's claim, an agent that died while its Orca ran, and unreadable quit records all keep the generic words. - The quitting runtime's word is written first in teardown, before the recovery wait, through a bounded asynchronous writer apart from the chat database. - Row copy: one neutral sentence naming the machine and cause; it drops "You can continue in this conversation." while Continue is offered, and Continue's tooltip says what it does.
…me-in-chat # Conflicts: # src/main/native-chat/agent-session-wire/structured-agent-session-host-lifetime.ts # src/main/native-chat/agent-session-wire/structured-agent-session-host-stop-event.ts # src/main/runtime/structured-agent-session-runtime-teardown.ts
…passes The cut turn's outcome takes the journal's outcome type, and the stand-in close reads the provider sink through a checked lookup instead of an index that may be absent.
… and never ended A chat said "Orca stopped unexpectedly" whenever its runtime left no quit record, and only the desktop quit wrote one, so a headless server's restart or update, the Settings relaunch, and a Windows logoff all read as crashes. Each runtime now records its own start when its chat store opens, and every graceful exit records its end through one synchronous entry point: the desktop quit's teardown, the headless server's stop, the in-app relaunch, the GPU-fallback restarts, the update-install watchdog, and Windows session end. A crash is a runtime that started and never ended; a runtime with no readable record (never written, pruned, unreadable) names no cause, so the chat keeps its generic words. One file per runtime, written durably and only by that runtime, so a damaged file never blocks a later write and two processes never lose each other's record.
… agent has started On a paired server, Continue waited for the agent to start before answering, and the client gives a paired call 15 s. A slow start (account switch, login shell, a long resume) showed "Couldn't continue this chat" while the agent was in fact continuing. Continue now answers when Orca has accepted the message, as a send does. The agent's start and answer settle afterwards, and a start that fails is the chat's own note, as before. The restart dialog's batch still waits for the handover, which is where it counts a start as done. The verdict helpers move to their own module to keep the continuation file within its size limit.
…e cut, still offers Continue The cut detector stopped at the first user message after the cut turn, so a steer the turn had taken, or a conversation command such as /context run after the cut, removed Continue while the row still named the cause. The rule for what is no request of its own (a conversation command, a row its turn produced, a send handed into a running turn) moves out of the latest-request reader into one shared predicate, which both that reader and the cut detector use. The host's "still wanted?" check reads the same detector, so the client and host agree.
… turn it ends When a chat was read before the restart proved its old agent dead, the read could only call the turn unverifiable. The proof then revised the turn to interrupted, but the death row was scoped by the turn still marked running, and none was, so it landed on the conversation instead of the turn. That cut never offered Continue, and the row did not name it as the turn's explanation. The row is now scoped to the newest root turn the settle actually ends, running or revised.
…out a resume already running The row's "You can continue in this conversation." came and went with the button: it showed while a paired host's answer was still on its way, vanished when the button appeared, and came back the moment Continue was clicked. Continue also appeared on chats the restart prompt or the launch's own resume was already carrying on. The row now drops that sentence wherever the chat's host can continue a cut, counting a host that has not answered yet as able (a host that writes cause rows has Continue), so its words never change on screen. Continue is hidden while a resume is carrying that chat on.
A Continue the host refused before accepting anything wrote a red note into the chat and brought the button back, so each retry added another identical note; a chat the host had no record of was refused with no word at all. Continue now reports every refusal the same way as a failed request: the existing composer line "Couldn't continue this chat. Try again, or send a message.", which a retry replaces rather than repeats. A refusal before acceptance writes no note. A failure after the message was accepted (the agent could not start) is still the chat's own note, as for any send.
…tion and never folds A client that re-words host rows it cannot name (the draft that makes these cuts read as interruptions) treated the cause row as an older red row and replaced its words, so the cause never showed there. The row was also folded away under its collapsed turn once shown muted. The host's cause row now names the presentation 'orca-stop' beside today's words, failure fact and red tone, so a client that predates both changes still prints exactly today's row, red and on screen, and a client that re-words unnamed rows passes it through. This build shows it muted, counts it as no failure (the reply it cut stays the turn's answer), and never folds it; the fold field becomes `explainsTurn`, as the other change names it.
…r re-presented it Pins the rendered tone on this build: the stored red row, and the same row after a reader re-presents it in the neutral tone with its presentation and cause kept, both render muted and never fold. The phone draws chat rows without tone styling, so it needs no change.
…continued The composer line a failed or refused Continue set stayed on screen while the agent carried on: after an answer lost in transit, or once another client or the restart prompt continued the chat. Only the next Continue click or the user's own send cleared it, and a click also wiped an unrelated composer error. The line is now derived: shown only while the chat still sits on the cut that Continue failed on, so it goes as soon as the journal shows the chat continued, from anywhere. A Continue click clears only its own line, and a retry answered "already continued" leaves none.
…esume the chat With "resume automatically" on, Continue showed on a quit or update cut while the launch was still waiting for its settings and reading the restart offer, then vanished when the launch's own resume began; a click in between sent a competing continuation. The launch's one decision (nothing offered, ask, or resume) is now published, and the chats it resumes are named the moment it decides, with no gap. Until it decides, and while the setting has not loaded or is on, Continue stays hidden on this machine's chats; a paired server's chats are not the launch's to resume and keep it.
… write and any clean exit Three ways the runtime record could still read a graceful stop as a crash: - A chat host reinstalled during the quit (a request landing after teardown began) recorded the runtime's start again and erased the end it had just written. A second start of the same runtime now keeps that end. - When the end could not be written (a full disk), the start alone stayed and read as a crash. The runtime now removes its record, so its chats name no cause. - Each `app.exit(0)` had to remember to record the end. A process 'exit' with code 0 now records a quit when nothing else did: Electron emits it on every quit and exit once its loop runs (`app.exit` -> Browser::Shutdown -> the app's 'quit' -> process 'exit'), and Node on every `process.exit`. The relaunch and GPU-fallback calls it covers are dropped; the quit teardown, the headless server's stop, the update watchdog and Windows session end keep theirs, which run earlier or say more.
…me-in-chat # Conflicts: # src/main/runtime/agent-session-record-store-file.ts
…o its own failure shows Since the "Couldn't continue" line became derived, an older composer error (such as "Remove attachments before using a chat-session command.") outranked it: a failed Continue showed the old error instead, and a Continue that went through left the old error on screen. A Continue click is the user's newer action, so it clears the composer's error again, as before; the line then shows the Continue's own failure, if any. That failure still goes away by itself once the chat is continued, and nothing but the user's own Continue click clears an unrelated composer error.
…ntime stamped on it A chat start checks that the process it just started is the one the record names, by a deep comparison of the stored owner with the adapter's process. The store stamps that owner with the Orca runtime holding it, so the check passed only because the store happened to return the record from before the stamp; returning the published record would have refused every chat start with agent_session_ownership_unknown. The start now compares the process identity without the runtime stamp, which says who holds the process rather than which process it is.
…tructured methods
…me-in-chat # Conflicts: # src/shared/agent-session-journal-schemas.ts
Review status — ready for reviewHead: The problem. When Orca on this computer or on a paired Orca server quits, installs an update, or crashes in the middle of a structured-chat reply, the chat showed one generic red row ("{agent} stopped while this response was in progress…"). It never said that Orca stopped, why, or on which machine. The only one-click way to continue was the local-only resume dialog, which never appears after a crash or for a paired server. What changes for the user.
Fixed during review (all on this branch, each with a test that fails when the fix is reverted):
Deferred (none block release):
Verified.
Not verified.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
⛔ Files ignored due to path filters (1)
📒 Files selected for processing (13)
🚧 Files skipped from review as they are similar to previous changes (6)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughThe changes add runtime-end records and attribute runtime endings to agent-session death evidence. They add turn-scoped Orca stop rows and a host RPC for continuing interrupted turns. Native chat now displays cause-specific stop notices and offers continuation when the host supports it and the cut remains eligible. The changes also update launch-resume tracking, transcript folding, and localized messages. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to No concrete issue in the reviewed changes currently calls for a merge hold. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to Continue can restart interrupted work on a local or paired computer. The implementation checks that the interrupted turn is still current and limits duplicate requests. No introduced security vulnerability was established, but paired-computer authorization and complete shutdown recovery were not fully verified. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 47.95% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 73 functions across 52 files. (6 skipped: 6 unsupported.) ✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Host records why Orca stopped, in the chat's history: the cut turn's existing
stale-session:status row gains an optionalorcaStop: { cause }andpresentation: 'orca-stop'(structured-agent-session-orca-stop-row.ts,dead-generation-settlement.ts), so every client reads the cause the same way it reads the chat. Older clients keep the stored red generic row — the journal schema is non-strict and strips the unknown key. - Crash distinguished from quit/update by per-process start/end records: new
agent-session-runtimes/<runtime>.jsonfiles (agent-session-runtime-end-record.ts) are written on store open and on every graceful exit, andattributeAgentSessionRuntimestampsdeathEvidence.runtimeEndwhen a proven death is held by an earlier runtime.crashis concluded only from a recorded start with no end. - New
agentSession.continueInterruptedRPC, capability-gated behindagent-session.continue-interrupted.v1, reusing the restart continuation and re-checking the cut under the session lock (structured-agent-session-interrupted-continuation.ts). - Renderer Continue + muted cause row:
useNativeChatInterruptedContinuation,NativeChatInterruptedContinue.tsx, the orca-stop context and words, the machine label hook, and launch-resume gating that suppresses Continue while an opted-in launch may still resume local chats. - Shared helpers and refactors:
latestNativeChatOrcaStopCut,agent-session-orca-stop.ts,isStructuredAgentSessionNonRequestRow,reportsCompaction→explainsTurn, and the fold/projection changes that keep anorca-stoprow on screen and out of the turn's failure count.
Verified the send-once and crash-attribution mechanisms with independent review passes; both are sound for the single-live-runtime model, and the guards are pinned by tests (double click, two clients, lost-answer retry, user-stop/idle-eviction/older-host staying generic).
ℹ️ Two-runtime attribution limit (documented, non-blocking)
The rule "a runtime record with no end reads as crash" conflates still running with crashed. It is only reachable when two live Orca processes share one state directory (single-instance lock bypassed): the second process snapshots runtimeEnds once at load, sees the still-running first runtime as crash, and could stamp that onto a death it attributes to it. This is already called out in the PR body's residual limits, so it is not new — noting it only to confirm it is the one residual the independent passes found and that it is accepted.
Technical details
# Two live runtimes on one state directory read as a crash
## Affected sites
- `src/main/runtime/agent-session-runtime-end-record.ts:139-140` — a record lacking `end` maps to `'crash'`
- `src/main/runtime/agent-session-runtime-attribution.ts:36-54` — consumes the map with no liveness check on `heldBy` (only `heldBy === incarnation` is guarded)
- `src/main/runtime/agent-session-store-transactions.ts:90-93` — `runtimeEnds` is snapshotted once and never refreshed
## Required outcome
None required for merge. Behavior is the documented residual limit ("two Orca processes forced
onto one profile ... would read the still-running one as started-never-ended"). If that limit is
ever lifted, a liveness check on the held-by runtime is the missing guard.
## Open questions for the human
- Is bypassing the single-instance lock considered unsupported enough that no further guard is wanted?deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Delta since the prior review (
2264121→4c91058): a merge oforigin/maininto the branch. The PR's own changed files are unchanged — the diff TOC line ranges and per-file region hashes are identical to the previously reviewed revision — so no PR-authored code moved and the earlier review stands.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…me-in-chat # Conflicts: # src/renderer/src/i18n/locales/en.json # src/renderer/src/i18n/locales/es.json # src/renderer/src/i18n/locales/fr.json # src/renderer/src/i18n/locales/ja.json # src/renderer/src/i18n/locales/ko.json # src/renderer/src/i18n/locales/zh.json
…me-in-chat # Conflicts: # src/shared/protocol-version.ts
…me-in-chat # Conflicts: # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
…me-in-chat # Conflicts: # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx # src/renderer/src/i18n/locales/en.json # src/renderer/src/i18n/locales/es.json # src/renderer/src/i18n/locales/fr.json # src/renderer/src/i18n/locales/ja.json # src/renderer/src/i18n/locales/ko.json # src/renderer/src/i18n/locales/zh.json
…me-in-chat # Conflicts: # src/main/runtime/agent-session-record-store-file.ts # src/renderer/src/components/native-chat/native-chat-transcript-slots.ts
… live Main's #26038 dropped stateDirectory from JournalHostDatabase; this PR's runtime end records are read from and written beside it.
…me-in-chat # Conflicts: # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx # src/renderer/src/i18n/locales/en.json # src/renderer/src/i18n/locales/es.json # src/renderer/src/i18n/locales/fr.json # src/renderer/src/i18n/locales/ja.json # src/renderer/src/i18n/locales/ko.json # src/renderer/src/i18n/locales/zh.json
…me-in-chat # Conflicts: # src/main/native-chat/agent-session-wire/structured-agent-session-journal-corruption.test.ts # src/main/orcad/orcad-lifecycle.ts # src/shared/protocol-version.ts
…nto brennanb2025/rsc-resume-reconnect #25652: each workspace is a bordered box drawn in the content area, a nested child's box sits inside its parent's, and rows have no dividers. Under a machine row everything a machine holds moves in one level while every checkbox stays in the outside column. The machine row is the header over its boxes, never boxed: the project's band in the content area, set off from the previous machine's boxes by twice the list's gap. Main via the base: the in-chat "why Orca stopped" row and Continue (#25675) and the desktop send layer (#25959). - Host: main's restart-resume contract module now also declares this branch's listed dismissal; the branch's own copy of that type is gone. continueInterrupted and the listed dismissal both stay. - Client: one options argument carries both the pairing fence and the compatibility skip; restartContinue keeps its 195 s. - Continue is hidden while a resume carries the chat on its own machine (resumes are per machine here), and the launch's resume decision is marked where main marks it: when this computer's resume names its chats, and when the launch read ends.
Main's #25675 now writes the quit's own row about a cut turn, and these restart tests read it twice: as stored and as the transcript shows it. The host stores it red so an older client keeps it on screen, and this branch's reader shows it muted, so the two views differ only in that tone. The stored-row assertions now expect the red row; the reader assertions keep the muted notice.

ELI5
When Orca quits, installs an update, or crashes while an agent is in the middle of a reply, that reply gets cut off. Today the chat shows a generic red line ("Claude stopped while this response was in progress. You can continue in this conversation.") that doesn't say why it stopped or on which machine, and there is no button to pick up where it left off. With this PR the chat says what happened ("Orca on QA server was closed while this response was in progress.") and offers a Continue button that asks the agent to check its last step and carry on. Chats on this computer and chats on a paired Orca server work the same way.
What Changed
Before (what a user sees):
After:
{machine}is the name Orca already shows for that host: the user's rename, the paired server's name, or "Local Mac" / "Local Windows" / "Local Linux". A paired server this client has no name for gets today's generic words, never a raw id./contextafter the cut does not remove it.Mechanism:
The cause is recorded on the host, in the chat's history, so every client reads it the same way it reads the rest of the chat, including after a reconnect. It is the host's existing
stale-session:row about the cut turn, plus:orcaStop: { cause }field, andpresentation: 'orca-stop'.The stored row keeps today's words, failure fact and red tone, so older clients still draw one visible row. New clients see the presentation and render it muted, keep it on screen under a collapsed turn, and don't count it as the turn's failure.
Quit / update. After a quitting Orca proves a session's agent exited and the exit handler has settled the turn, the host writes the row if that root turn reads "cut off with nobody asking". It never writes one for a finished turn, a person's Stop, or an idle eviction. The row id is keyed by the stopped child, so a retry adds nothing.
Crash, or a quit that did not get that far. The existing reopen row gets the cause from two facts:
exithook for any other clean exit.A death of an agent held by an earlier Orca reads "crash" only if that Orca has a start record and no end. If the record is missing, pruned (16 kept), unreadable, or the end write failed (the file is then removed), the chat keeps the generic words.
Continue is one host-side operation: new RPC
agentSession.continueInterrupted { sessionId, turnItemId }, advertised by the runtime capabilityagent-session.continue-interrupted.v1. It reuses the existing restart continuation end to end: same message, same Orca caller (it leaves a queue pause alone), same delivery verdicts and notes.Client. The button is routed to the chat's owning target (local or paired) and shown only when the host advertises the capability.
Why
Differences from the common pattern (labelled):
Not in this PR (follow-ups / known limits):
orca-stoprow with its neutral words but keeps the presentation and cause, so this PR's client still names the cause. Whichever lands second addsorca-stopto the never-fold list.Linked Issue
N/A (maintainer change)
Visual Proof
Live QA: hidden Electron on a separate Mac, isolated profiles, real Claude, a client and a paired server on the same box. Before: today's generic red row (main), not re-captured.
Testing
Live QA (macOS, real Claude, local + paired server):
Unit tests, each ablated (fails with its fix reverted):
/contextkeep Continue;Targeted vitest across the touched areas: 101 files / 1,334 tests at the last full targeted run. The cross-version
agentSession.*wire suite includes the new method. Localization verify scripts, RPC catalog check, changed-code quality gate, React Doctor and oxlint pass.Typecheck:
tc:node,tc:webandtc:clipass. CI is fully green ate7ec734118b, including the cross-version wire suite with the new method.I manually tested these changes locally
Automated tests added/updated, or explained why not below
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
method_not_found(asserted by the cross-version suite).session-end; the record write reuses the existing durable write with its locked-file retry.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)