Repository navigation
fix(native-chat): show a reply cut off by an Orca crash or quit like a finished turn, with one explanation - #25043
Conversation
…ne explanation A reply that an Orca crash or restart cut off said it failed three times: the turn bar read "Failed after N", the chat's notice row said Claude stopped, and the sidebar dot stayed red after the chat was read. The turn bar now reads "Worked for N" for a proven crash/restart cut, the notice row stays the one explanation, and the sidebar card's dot and agent rows read failed only until the user has visited the chat (the same acknowledgement that un-bolds the row), then read done. A failure, a user's Stop, a replaced turn and an unconfirmed end keep their labels. The stored outcome is unchanged.
A turn cut off by quitting Orca, an idle eviction or a teardown recorded the same outcome as a crash-cut turn but wrote no notice row, so with the turn bar now reading "Worked for N" nothing in the chat said it stopped. The host stop's settle now writes the existing providerExited notice for the latest turn when it ends as news (no person's Stop decided it): in the same write as the host's own turn end, or right after the adapter's. It is keyed by the turn, and skipped when an error row already explains that turn, so a retry or an earlier exit row never leaves two.
…riting it at stop time A turn cut short when the agent stopped without anyone asking now reads "Worked for N", so it needs a row saying it stopped. Writing that row only on the quit and eviction paths missed journals written before this change, a quit that died between its drain and its settle, and any future stop cause. The transcript now derives it from the journal on desktop and phone alike: a root turn that ended interrupted with no verdict and no row about the stop gets one notice in the provider-exit row's words. A stored exit row, matched by its exit fact or its writer's identity rather than its tone, stays the explanation, and so does the restart continuation's own outcome note, so a refused resume says it once. The host's stop path is back to what it was.
…nt on every surface A turn cut short with nobody asking reads failed only until the user has seen it. The previous revision passed an optional "seen" flag to each caller, so any surface that did not pass it, the chat's own tab dot among them, stayed red beside a sidebar that read Done. The verdict's display now takes the acknowledgement as part of what it reads: the entry's stateStartedAt beside the time the user last acknowledged it, both required, judged by one shared rule. Each surface joins it once where it builds its rows: the sidebar's agent rows (and so the notes send menu), the workspace card summary, the terminal tab bar, Cmd-J recent rows, and Activity threads. Failures, Stops, replaced turns and unproven ends keep their marks as before; the phone, which has no acknowledgement record, keeps the unseen reading.
…pied Auto-acknowledgement, the Activity unread count, dashboard row buckets and notification acknowledgement each spelled the same "acknowledged at or after the current state began" comparison; they now call the shared rule.
…' acknowledgement join
…ies on A resume after a quit writes its "asked this agent to continue" note after its own message, so counting that note as the cut's explanation removed the notice once the resume went on, and made it flash away under automatic resume. Only the notes that say the chat was not carried on (refused, not connected, not confirmed) stand in for the notice now. A row about the whole conversation now explains a cut turn only when no other turn lies between them, and a failed start's row, which is about a start, never does. The host writers and the rule share the row identity prefixes, with the contract that a new row explaining a stop carries the provider-exited fact or one of them.
…bagent holds the row A subagent can keep a row working after its main agent was cut, and the row's clock then predates the cut, so a look at the working chat counted as having seen the cut and no red mark showed. The mark now compares the acknowledgement with the later of the row's and the main agent's clocks; auto-acknowledgement of the chat on screen reads the same clock, and its stamp covers it, so looking at the chat still clears the mark. Bold rows, dashboard buckets, notifications and unread counts keep the row's clock.
… no message sent since
A send after a quit's cut whose new agent died before its turn opened leaves a
provider-exit row about the whole conversation. That row is about the send,
not the earlier cut, so the cut kept no explanation. An exit row now explains
a preceding cut only when no message was sent in between; the restart notes,
which follow the continuation's own message, still need only no turn between.
The notes that say a resume did not carry the chat on are now told apart from
the continued note by their tone ('error' or 'warning'), which every host that
wrote them has set, instead of by their words.
… cut after a send A chat read before the startup reconcile settles its cut turn unverifiable; if the user then sends a message, the reconcile proves the old agent dead and writes its row about the conversation after that message. The row names the old owner's death, never the send, so a message since no longer detaches it from the cut. Only a provider-exit row, which a later start can write, still needs no message sent since.
…t map The card summary now reads acknowledgedAgentsByPaneKey; this test's hand-built store state lacked it, so every summary read threw.
Review status: ready for review (part 1 of 2)This PR was split. Part 1 (this PR) changes how a reply cut off by an Orca crash, quit or idle close reads in the chat. Part 2, #25293, makes the red Failed mark in the sidebar and the other status surfaces clear once you have seen the chat. Each part lands safely on its own, in either order. The problem. When Orca crashed, quit, or closed an idle chat while Claude or Codex was mid-reply, the reopened chat showed a red "Failed after N" on the reply. A crash added a notice row; a quit or an idle close often left no explanation at all. What changes for you (this part).
How it works. The notice is worked out from the saved chat when it is shown, not written when the host stops the agent. Review found, by reading the stop path, that a stored notice would miss chats saved before this change and a quit that dies before writing it. Nothing new is stored or sent between devices. Fixed during review (this part)
Deferred (listed in the PR body)
Seen in live testing, not caused by this PR (needs a decision)
Verified
Not verified: a real person's click at the desktop, the phone in an emulator, SSH hosts, and idle-close or close-it-yourself stops run live. |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run read the complete raw diff: all 42 production files plus the surrounding call sites, and re-derived the key behaviors against the repo (not just the new tests).
- Turn label:
settledTurnStatusKeymapsinterruptiontoworkedFor, so a turn cut by a crash/quit/eviction reads "Worked for N" on desktop and mobile (formatNativeChatTurnStatusLabelshared by both). Stop/replaced stay "Interrupted"; real failures stay "Failed". - Derived notice: new pure rule
withNativeChatCutTurnNotices(src/shared/native-chat-cut-turn-notice.ts) inserts one provider-exit status row after a cut root turn that no existing row explains; wired into the desktop and mobile structured-session hooks. No storage/wire change; the host writer prefixes were extracted to constants with byte-identical values (verified). - Seen-ness:
isAgentTurnAcknowledged/agentAttentionStartedAtadded,agentVerdictDisplayMarknow requiresacknowledgedAt, and every surface joins the acknowledgement at row-build time (agent-entry-acknowledgement.tsidentity-preserving WeakMap join, card/tab/Cmd+J/Activity/review-notes).latestAgentTurnTimestampalso maxesmainAgent.stateStartedAtso a cut that postdates the row's combined clock (subagent holding the row open) is still cleared on view.
I traced the derive rule's attribution boundaries (turn-scoped vs conversation-wide rows, exit vs owner-death vs restart-note, root-only, interruption-only, old-host no-scope placement), the ack cache-key changes across the card/tab/Activity caches, and the acknowledgement type propagation into every agentVerdictDisplayMark / agentVerdictStatusLine consumer; none produced an actionable defect. The two documented known limits (pre-2026-09-29 untagged-host duplicate, and old journals that cannot distinguish a real cut from an early Stop/replaced turn) are deliberate scope decisions, not regressions.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 🧰 Additional context used📚 Code guidelines (1)No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughInterrupted turns now use worked-time labels, while cancellations remain interrupted and failures remain failed. A shared transcript transformation adds an error notice when an interrupted root turn lacks an existing stop explanation. Mobile and renderer session hooks use the enriched transcript items for message projection. Tests cover notice rules and behavior across session lifecycle cases. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to No actionable issue is established for this change; it is mergeable after normal checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change affects how interrupted replies are displayed and explained. No new privileged operation or access-control bypass was identified, but incomplete baseline coverage limits confidence. 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)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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 |
This change now keeps only the cut turn's label and its one derived notice. The red mark that clears once the user has seen a cut turn moves to its own change, so each can land alone; until it lands, the sidebar, tab bar, Cmd-J, Activity and the notes send menu read a cut turn as failed, as before.
…n leaves the cut's notice
There was a problem hiding this comment.
ℹ️ No critical issues — the production change reads correct. Two new tests assert something they do not actually pin, plus one small coverage gap.
Reviewed changes
This run read the complete raw diff (all 20 files) and re-derived the new rule against the host writers that produce the rows it matches.
- Turn label:
settledTurnStatusKeymapsinterruptiontoworkedFor, so a reply cut by a crash, quit or eviction reads "Worked for N" on desktop and mobile through the sharedformatNativeChatTurnStatusLabel. A person's Stop and a replaced turn stay "Interrupted after N"; a real failure stays "Failed after N". - One notice, derived on read: new pure rule
withNativeChatCutTurnNotices(src/shared/native-chat-cut-turn-notice.ts) inserts oneproviderExitedstatus row after each cut root turn that no existing row explains, and returnsitemsunchanged otherwise. Wired into the desktop (use-structured-agent-session.ts) and mobile (use-mobile-structured-agent-session.ts) hooks. Nothing stored, no wire change; the host writer prefixes moved tosrc/shared/agent-session-stop-row-identity.tswith byte-identical string values. - Scope reduced since the prior review: the seen-gated red mark moved to #25293, so this diff keeps only the label and the derived notice; the sidebar, tab bar, Cmd+J and Activity still mark a cut turn red-Failed for now.
I traced the rule's matching against settleStructuredAgentSessionDeadGeneration and settleStaleStructuredAgentSessionState (exit row and death row, turn-scoped via exitedRootTurnScope / runningRootTurnScope, else thread-scoped), the unexpected-exit settlement id, and the restart-continuation note (prefix plus error/warning tone). The reducer's creation-sequence rule keeps the revised turn record ahead of the settlement row, so both the scope path and the preceding path populate as intended. No double- or missing-notice case found for the real host shapes.
ℹ️ Nitpicks
src/shared/native-chat-cut-turn-notice.test.ts:139—expect(notice.sequenceIndex).toBeGreaterThan(0)only proves a positive index; a fixed1would also pass. Pin the(after.sequenceIndex ?? 0) + 0.5placement relative to the following row instead.- No test exercises a thread-scoped
provider-exitrow directly after the cut with no message sent since — thecutNoSendSincebranch of the'exit'explanation. The negative (a send since) is covered at line 292; the positive is not.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
| ] | ||
| }) | ||
| // The list places rows by the same items, so the notice joins the cut turn. | ||
| expect(result.current.journalItems.at(-1)).toMatchObject({ turnScope: SCOPE }) |
There was a problem hiding this comment.
The last raw item in items (reply-1) already carries turnScope: SCOPE, so this matches whether journalItems is the derived transcriptItems or the raw transportState.journalItems — a regression that reverts only the journalItems: transcriptItems return (hook line 211) would still pass. Pin the notice's identity instead. The mobile test (mobile/src/session/use-mobile-structured-agent-session.cut-turn-notice.test.tsx:130) has the same gap for turnJournal.
| expect(result.current.journalItems.at(-1)).toMatchObject({ turnScope: SCOPE }) | |
| expect(result.current.journalItems.at(-1)?.itemId).toContain('cut-turn-notice') |
…2025/crash-cut-turn-label # Conflicts: # src/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-filing.test.ts
…2025/crash-cut-turn-label # Conflicts: # mobile/src/session/use-mobile-structured-agent-session.ts

ELI5
Claude or Codex sometimes stops in the middle of a reply without you asking: Orca crashed, you quit Orca, Orca closed an idle chat, or the agent's own process died. When that happened, the cut-off reply said "Failed after 3m 42s" in red. Some of those cuts (a quit, an idle close) had no word in the chat about what happened, only that red label. A crash showed the label and also a notice saying the agent stopped.
Now the reply reads "Worked for 3m 42s" like any other finished reply. Every reply cut off this way shows exactly one notice saying the agent stopped.
What Changed
This is part 1 of 2. Part 2 (#25293) makes the red Failed mark in the sidebar and the other agent-status surfaces clear once you have seen the chat. The two parts land independently, in either order. Until part 2 lands, those surfaces keep marking a cut turn Failed, as they do today.
The problem. When the agent's process goes away mid-reply and nobody pressed Stop, Orca records the turn as ended with no verdict, which it reads as the outcome "interruption". In the chat:
src/shared/native-chat-turn-status.ts).Before (as you see it): the cut reply reads "Failed after 3m 42s" in red, with a notice row or none, depending on what cut it.
After (as you see it):
What did not change:
Mechanism:
settledTurnStatusKeymaps "interruption" to the finished-turn label. The phone renders its turn bar through the same shared function.withNativeChatCutTurnNotices(src/shared/native-chat-cut-turn-notice.ts), takes the journal items the transcript reads.providerExitedwording and error tone, and is scoped to that turn.use-structured-agent-session.ts) and the phone chat (use-mobile-structured-agent-session.ts) read the journal through this rule. Rows, turn grouping and folding therefore treat the notice exactly like the stored crash row.src/shared/agent-session-stop-row-identity.ts). The contract: any new row that explains a stop carries the provider-exited fact or one of those prefixes.Why
Turn bar. Where the common pattern ends a turn because a host or process went away, the turn reads like a finished one ("Worked for N"). Stop wording is kept for a person's own Stop. Saying "Failed" on the turn repeated the notice.
One explanation, derived.
Quit and resume.
Alternatives considered:
Differences from the common pattern
mainthe same turns read "Failed after N", which was also wrong.main. Where the common pattern shows a red mark for this, it lasts only until the chat is read. Follow-up: part 2, fix(sidebar): clear the red Failed mark on a crash-cut turn once the chat is seen #25293.Linked Issue
None. Found during live testing of #24233.
Visual Proof
Live check on a second Mac with a dev build carrying both parts and a real Claude chat (isolated profile, app hidden, driven over the DevTools protocol). These are the transcript's screenshots. The sidebar's are in part 2.
Orca killed mid-reply, then relaunched. The reply reads "Worked for 39s" with one notice, "Claude stopped while this response was in progress. You can continue in this conversation.":
Quit mid-reply, then relaunch with "Resume interrupted chats?".
Unchanged cases. A Stop still reads "Interrupted after 53s" with no notice. A real failure (bad model) still reads "Failed after 0s" with its error:
Testing
New tests, one per rule:
src/shared/native-chat-cut-turn-notice.test.ts):Ablations. I removed each rule in turn, and its tests went red (16 rules):
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
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)