Repository navigation
Tell you when a paired server's restart interrupted your chats, and resume them from one dialog - #25676
Tell you when a paired server's restart interrupted your chats, and resume them from one dialog#25676brennanb2025 wants to merge 50 commits into
Conversation
…-only sidebar card The resume-on-restart dialog listed workspaces as a bare name and glyph, so users could not tell their sessions apart. It now renders each workspace with the sidebar's own WorktreeCard in a new read-only mode, with the offered chats in place of its live agent rows, and nests child workspaces under a listed parent the way the sidebar does. The dialog is wider and the chat rows use the sidebar's compact agent row layout. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The dialog's cards stand in for the chats before the restart, so the sidebar's live status (Failed, now Interrupted) read as a verdict on chats the dialog offers to resume. A read-only card shows no status, and in the new card style its PR joins the badges because the status lane that normally holds it is gone. Co-Authored-By: Claude <noreply@anthropic.com>
…ever its caller passes A read-only card stayed inert only because each control checked `readOnly` on its own and the caller left out the handlers that would turn the rest on: passing `onLineageToggle` put a live child-workspaces toggle on it. It also still painted live state over the resume dialog's chat rows: the sleep dim, the "Queued for deletion" overlay (which blocked unticking a pre-ticked chat) and the prompt-cache countdown. WorktreeCard now reduces a read-only card's props to what it pictures (every handler, selection and active flag dropped), and the controller resolves `readOnly` once into what the views already read: card properties without status, ports or live agents (and with the host), no sleep or delete state, no cache countdown, and one `interactive` flag for its own controls. A contract test renders a read-only card with every feature and handler on in all three card styles and fails on any control outside the caller's rows or any live state.
…s as the sidebar does A child workspace whose parent has older metadata with no host id stayed at the top of the dialog, though the sidebar nests it: the dialog copied a strict same-host filter, while the sidebar treats a hostless row as belonging to the host. The nesting rule now lives in one sidebar lineage helper, getWorktreeLineageAncestorsOnHost, which scopes by host the way the sidebar's lineage inputs do. In the legacy card style a nested child was inset for its parent's depth instead of its own, so it sat one sidebar step too far left (0px instead of 14px), outdented into the parent's padding. Each child is now inset for its own depth, as a sidebar row is. A groups-level test renders real sidebar cards from the store, with the child nested under a hostless parent, and checks every offered chat's checkbox renders and the legacy inset.
…neage loops Nesting followed each workspace's parent chain, so two workspaces listed as each other's ancestors (possible only when two hosts report opposite lineage for the same ids) were placed under no top-level entry and vanished, while their pre-ticked chats would still be resumed. The loop is now cut where the walk first comes back, so every workspace renders.
…ttons when narrow Below the small breakpoint the footer stacks bottom-up, so the option moved into it rendered under Resume and Dismiss all. It now reads top to bottom: the option, then the actions; wider windows keep the single row.
Its dynamic import ran in a beforeAll with a 20 s timeout, below the suite's 60 s hook timeout, and timed out under parallel load, which skipped all six contract tests.
… lane Legacy cards outdent their nested child list by 1.125rem to pull it back over the 22px status lane. A card without that lane (a read-only card in the resume dialog, or the sidebar with the Status property off) kept the outdent, so a child's title sat 3px right of its parent's instead of 25px. Without a lane the list now sits at ml-1, which gives the same 25px step (39px at depth 2). Cards with a status lane, the default sidebar, render exactly as before.
…w rule Round one nested a child under a parent whenever either had no host id, but the sidebar's row builder looks a parent up on the child's own host, so a row with no host id nests only under a parent with none either. The dialog showed a parent/child pair nested that the sidebar shows side by side. The row builder's parent lookup is now one function, getSidebarLineageParent, and the dialog walks ancestors with it (getSidebarLineageAncestors) over the same non-archived rows and cycle set. The chat's workspace is still found on its host, falling back to a row with no host id. A test checks the four host pairings against the real row builder, and the dialog test renders each pairing with every chat's checkbox.
Covers legacy, compact and new card styles with the Status property on and off. With Status on, the legacy and compact sidebar keep their outdent unchanged; with it off they now step children in by the same amount; the new card style is identical either way. The file now imports the card statically, since its 15 s per-test dynamic import timed out under load.
…bar's do The legacy outdent on a card's child list cancels its status lane, which in the sidebar is the unread button plus the row gap: 1.125rem, exactly the outdent, not the 22px the previous fix assumed. Its ml-1 for a card with no lane put the resume dialog's children 4px deeper per level than the sidebar's (25px against 21px). With no lane there is now nothing to cancel and no margin, which gives the sidebar's 21px (35px at depth 2). Status is a fixed card property, so every live card has the lane: only a read-only card takes this branch, and the live sidebar is unchanged. The tests now pin the outdent for live legacy and compact cards and no margin for a read-only one, instead of an unreachable Status-off state.
Conflict in worktree-card-surface.tsx: main (#25486) moved the multi-selected look to a data-worktree-card-selected attribute; kept that, with the read-only card's `interactive` flag still gating the hover class, cursor and click.
…arker Main now styles a multi-selected card through data-worktree-card-selected. The contract test passes isMultiSelected, so it now asserts the marker stays off a read-only card, and drops the context-menu event constant main removed from that module.
…odal-sidebar-cards
Every row of the resume list (Select all, workspace, chat) now keeps its checkbox in one left column; nesting indents only the content after it, and a divider separates every row. A workspace checkbox is tri-state over its own chats and those of the workspaces nested under it; Select all heads the list with an "N of M selected" count. Failures a retry cannot fix stay out of both. Up/Down move between the list's checkboxes. The dialog no longer renders the sidebar's WorktreeCard, so the read-only card mode and the sidebar changes that only served it are reverted to main. The sidebar lineage helpers stay: the dialog still nests child workspaces by the sidebar's own rule.
…odal-sidebar-cards
…ialog The workspace row's host chip spelled an SSH host with its raw target id (e.g. ssh-1728291234567-abc12d) because it never got the sidebar's host labels. It now takes them from useSidebarHostScopeOptions, as the delete dialog does, so the chip reads "devbox" like the sidebar. Also translates the dialog's five new strings in es, fr, ja, ko and zh.
…odal-sidebar-cards
…lect its chats The resume list now follows the common grouped-list pattern: each workspace heads its chats with a full-width tinted band (lighter for a nested child, at its own indent), chats are split by plain hairlines, and groups meet without gaps or rounded row corners. The project row gets a checkbox in the same left column, tri-state over every eligible chat in the project (nested workspaces included, failures a retry cannot fix left out) with an "x of y" count, on a band a step stronger than a workspace's. The list takes arrow keys, so it is now a named group (role="group").
…odal-sidebar-cards
Brennan chose the nested-box layout without row dividers. A project is a tinted band; each workspace is a bordered, rounded box one step in from its project, holding its header band and its chats, and a child workspace's box sits inside its parent's, after the parent's chats, at the parent's chat indent. The boxes are drawn in the content area only, so every checkbox stays in the one left column outside them. Rows carry no dividers; the box border is the only line.
…odal-sidebar-cards
…ct toast, one status entry, one dialog grouped by machine When a paired Orca server restarts while chats on it are mid-reply, this desktop now finds out: each verified connection to a server reads its restart offers, a toast names the server and offers Resume or Show chats, the status-bar entry counts the user's own interrupted chats on every machine, and the resume dialog lists every machine with its own row, tri-state box and workspaces. A server that predates the offer capability shows nothing. Rebuilt on #25652 alone: this branch no longer depends on #25674. The dialog opens through main's request/consume/subscribe API; a request carries only the machine it was opened for, and a request while one is open keeps the open dialog (its focus and the user's ticks). The offer-store, actions, toast, status entry, machine rows, Select all, own-only Dismiss, outcome toast, listed dismissal, origin, per-machine fence and the 195 s continue budget are this branch's own work, carried over unchanged.
…w; toasts follow what is drawn Clicking the "Resuming" status entry while a read racing that resume had listed nothing created a dialog request with nothing to draw. Nothing ended it, a later server's restart toast was held back for it, and the stale request then opened the dialog by itself over the new rows. The opener now asks only when some machine has rows to list (a running resume keeps its machine's rows listed until it answers), and an emptied listing still ends any request. The dialog reports when it is actually drawn, and the reconnect toast is held back only then.
Brennan chose the Resume Tree. The list is now a tree: machine, then project, then workspace (a child workspace is a node inside its parent, after the parent's chats), then the chats. Every row keeps its checkbox in one left column, then an 18px indent per level, a disclosure arrow (an empty slot on a leaf), the icon and label, and its count. Levels read apart by icon and weight; there are no boxes, bands or row dividers, only the divider under Select all. The machine level shows only when the machine is not obvious (a remote host, or chats on more than one), named with the sidebar's host labels and an SSH chip; it replaces the per-workspace host chip. Chats are grouped per machine first, so one workspace id on two hosts stays two nodes. Every node is tri-state over the eligible chats under it, through the existing per-chat onToggle. Nodes start expanded; collapsing is local to this opening and keeps the selection. The list is a role="tree" of leveled treeitems: Up/Down move between checkboxes, Left collapses and Right expands the focused node.
…odal-sidebar-cards
…not from an effect's copy The reconnect toast was held back by a flag the dialog set in an effect. When this computer's last rows went and a server's restart landed in the same tick, the request was already gone but the flag still said "on screen": the server's interruption was recorded as shown, its toast was skipped, and no dialog ever showed it. The toast guard now reads, at the moment it asks, the state the dialog draws from: a pending request, rows listed in the offer store, and a mounted dialog (a mount count, set once per mount).
784df58 to
2bdcd7c
Compare
Every per-chat read in the resume tree (its tick, its toggle, its failure, and each group's coverage) now goes through chatState, so a later change of chat key stays in one function. Tests pin the machine level's host rules: a runtime host gets a machine node but no SSH chip.
…and after a click Tab lands on the tree itself, where the arrow keys did nothing. Down and Home now enter at the first checkbox, Up and End at the last; Home and End also jump to the ends from a row. Clicking an arrow with the mouse left focus on the arrow, where no tree key works. After a collapse or expand click, focus now moves to that row's checkbox, or to the tree when the checkbox is disabled.
…nnanb2025/rsc-resume-reconnect #25652 shows the resume list as a tree: machine, project, workspace, chat, every checkbox in one left column, role="tree" with flat treeitems, Up/Down between checkboxes and Left/Right to close and open. Its machine level names each host from the sidebar's labels. This branch's own machine wrapper (NativeChatResumeOnRestartMachineSection, the machineHostId and depth props) goes: one tree lists every machine's rows. What the tree gains for several machines, all optional props: - rowKey: chats are keyed by their machine's listing (machine and pairing revision) and session id, since two servers may hold one id; selected, onToggle, failureFor, row actions and origin labels take it. - busyFor: a machine mid-resume locks only its own nodes and rows. - defaultExpanded: a machine starts open when the dialog was opened for it, when it is the only one, or when nothing on it starts ticked; the user's own opening and closing wins, also across later answers. - machineSubtitle: "Installed an update / Was quit · <age>" beside a machine's name. - originLabelFor: the origin chip on a chat that is not the user's, and that row's own dismiss beside it, outside the row's checkbox label.
…ts node key carries
… after a click) into brennanb2025/rsc-resume-reconnect #25652 reads every chat through chatState and builds group coverage through coveredKeys, and makes the tree's keys work from its own Tab stop and after an arrow is pressed. This branch's additions move onto those plug-in points: the row key (machine listing plus session id) is read in chatState, which now also names a row's own actions and origin; busy per machine is one helper beside it; the machine props live with the tree's props. The branch's separate tree-props module is gone. Also: each locale catalog had two "selectMachine" keys in one object after the first tree merge (this branch's "Resume every chat on …" and the tree's "Select all chats on …"); the branch's one goes, the tree owns the string.
…ting it was made in The resume tree kept what the user opened or closed for the whole dialog, keyed by host only. A server the user had closed, that then left the list and came back (re-paired, or listed again), stayed closed over its chats although its own default was open. A choice is now stored under its host's listing identity (machine and pairing revision) and goes when that listing is no longer shown; later answers under the same listing keep it. The tree's one expansion state takes the listing as an optional second argument, and its callers that pass none keep host-only keys exactly as before.
… one end-of-review sync) #25652 landed on main squashed (4bd4f8c), so its branch commits are not on main. The merge result is main's tree with only #25676's own change re-applied: the diff from #25652's final head 25cefbc to this branch's 996e884, applied 3-way onto main, with no conflicts. The same 93 files differ from main as differed from #25652's head, and in them the merge adds exactly main's own lines since 25cefbc.
…e Continue, outside protocol-version Main grew protocol-version.ts to its 300-line limit; this branch's capability put it at 303. Like main's Continue capability, it now lives in its own module, and the two restart capabilities are spread into RUNTIME_CAPABILITIES together, so protocol-version.ts is exactly main's length. No capability is added or removed.
Review status (ready)The problem. If a paired Orca server restarted while your chats on it were mid-reply (it installed an update, or someone quit it), your desktop never told you. The server already recorded which chats it cut off, but no paired desktop ever read that record: the "Resume interrupted chats?" dialog and the "N chats to resume" status-bar entry only asked this computer, once, at launch. You found out only by opening each chat. What changes for you.
How the review shaped it.
Deferred (known, not fixed here).
Verified.
Not verified.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Merge Risk: 🔵 Low · up to This change shows restart-interrupted chats from paired servers in one resume dialog. The new code looks sound, but three minor earlier concerns are still open: the dismiss button reads as "Close" in some locales, a dismissed offer may reappear, and the capability probe could hang a dismissal. None of these blocks merging, but the owner should review them. Pre-merge checks |
|
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
9938ad01-2ef8-44e0-84b8-0bcf9c5a16ab
⛔ Files ignored due to path filters (1)
src/shared/rpc-contract/rpc-params-catalog.generated.tsis excluded by!**/*.generated.*
📒 Files selected for processing (93)
src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-test-recovery-capsule.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-audience.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-ledger.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-ownership.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-contract.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.tssrc/main/runtime/agent-session-recovery-capsule-entries.tssrc/main/runtime/agent-session-recovery-capsule-read.tssrc/main/runtime/agent-session-recovery-capsule.test.tssrc/main/runtime/agent-session-recovery-capsule.tssrc/main/runtime/orca-runtime-get-structured-agent-session-create-support.tssrc/main/runtime/restart-offer-workspace-provenance.test.tssrc/main/runtime/restart-offer-workspace-provenance.tssrc/main/runtime/rpc/methods/structured-agent-session-restart-offer-origin.test.tssrc/main/runtime/rpc/methods/structured-agent-session-restart-offer-origin.tssrc/main/runtime/rpc/methods/structured-agent-session-restart-offer-read.test.tssrc/main/runtime/rpc/methods/structured-agent-session-restart-offer-read.tssrc/main/runtime/rpc/methods/structured-agent-session-restart-resumable-empty.test.tssrc/main/runtime/rpc/methods/structured-agent-session-restart-resume.test.tssrc/main/runtime/rpc/methods/structured-agent-session-restart-resume.tssrc/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.tssrc/main/runtime/rpc/methods/structured-agent-session-schemas.tssrc/renderer/src/components/NativeChatResumeFailureDetails.tsxsrc/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsxsrc/renderer/src/components/NativeChatResumeOnRestartGroups.tsxsrc/renderer/src/components/NativeChatResumeOnRestartModal.layout.test.tsxsrc/renderer/src/components/NativeChatResumeOnRestartModal.list.test.tsxsrc/renderer/src/components/NativeChatResumeOnRestartModal.machine-column.test.tsxsrc/renderer/src/components/NativeChatResumeOnRestartModal.machines.test.tsxsrc/renderer/src/components/NativeChatResumeOnRestartModal.test.tsxsrc/renderer/src/components/NativeChatResumeOnRestartModal.tsxsrc/renderer/src/components/native-chat-launch-resume-decision.test.tsxsrc/renderer/src/components/native-chat-restart-action-notifications.test.tssrc/renderer/src/components/native-chat-restart-action-notifications.tssrc/renderer/src/components/native-chat-restart-decided.tssrc/renderer/src/components/native-chat-restart-machine-fence.tssrc/renderer/src/components/native-chat-restart-machine-name.tssrc/renderer/src/components/native-chat-restart-machines.tssrc/renderer/src/components/native-chat-restart-offer-actions.tssrc/renderer/src/components/native-chat-restart-offer-activity-watch.tssrc/renderer/src/components/native-chat-restart-offer-payload.tssrc/renderer/src/components/native-chat-restart-offer-reopen.tssrc/renderer/src/components/native-chat-restart-offer-test-support.tssrc/renderer/src/components/native-chat-restart-offer-triggers.test.tssrc/renderer/src/components/native-chat-restart-offer-triggers.tssrc/renderer/src/components/native-chat-restart-reconnect-toast.tssrc/renderer/src/components/native-chat-resume-dialog-opening.tssrc/renderer/src/components/native-chat-resume-failed-chat-watch.test.tssrc/renderer/src/components/native-chat-resume-failure-action.tssrc/renderer/src/components/native-chat-resume-machine-views.tssrc/renderer/src/components/native-chat-resume-machines.test-support.tssrc/renderer/src/components/native-chat-resume-on-restart-dialog.tssrc/renderer/src/components/native-chat-resume-on-restart-grouping.tssrc/renderer/src/components/native-chat-resume-on-restart-launch-prompt.tssrc/renderer/src/components/native-chat-resume-on-restart-modal.test-support.tssrc/renderer/src/components/native-chat-resume-on-restart-outcome-toast.test.tssrc/renderer/src/components/native-chat-resume-on-restart-store.tssrc/renderer/src/components/native-chat-resume-ownership.test.tssrc/renderer/src/components/native-chat-resume-ownership.tssrc/renderer/src/components/native-chat-resume-selection.tssrc/renderer/src/components/native-chat-resume-tree-machines.tssrc/renderer/src/components/native-chat-resume-tree-state.tssrc/renderer/src/components/native-chat-resume-unsent-requests.tssrc/renderer/src/components/native-chat/NativeChatInterruptedContinue.test.tsxsrc/renderer/src/components/native-chat/NativeChatInterruptedContinue.tsxsrc/renderer/src/components/sidebar/workspace-creator-visibility.tssrc/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsxsrc/renderer/src/components/status-bar/NativeChatResumeStatusSegment.tsxsrc/renderer/src/components/status-bar/native-chat-resume-status-text.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/lib/activate-ai-vault-structured-session-reveal.test.tssrc/renderer/src/lib/activate-ai-vault-structured-session.test.tssrc/renderer/src/lib/activate-ai-vault-structured-session.tssrc/renderer/src/lib/structured-agent-session-tab-activation.test.tssrc/renderer/src/lib/structured-agent-session-tab-activation.tssrc/renderer/src/runtime/structured-agent-session-client.test.tssrc/renderer/src/runtime/structured-agent-session-client.tssrc/shared/agent-session-restart-capabilities.tssrc/shared/protocol-version.tssrc/shared/restart-offer-origin.test.tssrc/shared/restart-offer-origin.tssrc/shared/rpc-contract/structured-agent-session-params.tssrc/shared/rpc-contract/structured-agent-session-restart-params.tssrc/shared/workspace-creator-provenance.tstests/e2e/cross-version-wire/cross-version-restart-offers.unit.test.tstests/e2e/cross-version-wire/structured-agent-session-host-fixture.ts
💤 Files with no reviewable changes (1)
- src/shared/rpc-contract/structured-agent-session-params.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.
| "resumeSelected": "{{value0}} チャットを再開", | ||
| "resumeSelectedOne": "1 チャットを再開", | ||
| "dismissAll": "すべて閉じる", | ||
| "dismiss": "閉じる", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Translate the destructive dismiss label so it does not mean "Close".
All three locales translate the new NativeChatResumeOnRestartModal.dismiss key as "Close". The footer button that uses this key deletes the user's durable restart offers. Closing the dialog is a snooze that keeps them. A user who wants to close the dialog can delete their own offers by mistake.
src/renderer/src/i18n/locales/ja.json#L17531-L17531: replace閉じるwith a dismiss/discard term such as破棄.src/renderer/src/i18n/locales/ko.json#L17531-L17531: replace닫기with a dismiss term such as무시.src/renderer/src/i18n/locales/zh.json#L17531-L17531: replace关闭with a dismiss term such as忽略.
📍 Affects 3 files
src/renderer/src/i18n/locales/ja.json#L17531-L17531(this comment)src/renderer/src/i18n/locales/ko.json#L17531-L17531src/renderer/src/i18n/locales/zh.json#L17531-L17531
Live QA r7: successful resume on the final headThe one row the earlier QA couldn't run (a resume that succeeds, which needs a signed-in Claude on the server) now passes on the final head
Screenshots are in the PR description. This replaces the "Not verified: a successful resume" note in the status comment above. The openclaw Linux box was tried first; its Claude never answered a test prompt there (cause unknown), so the round moved to Windows. Stat-only checks of the real |
Resolves the one conflict in structured-agent-session-restart-resume.ts: keeps main's permission: 'workspace' on all five restart methods (#26337 deny-by-default) and PR B's listed dismiss, capability check and per-row origins.
|
Synced with main again ( |
Fits the paired-server resume work onto #26361's live per-chat resume progress: each machine keeps its own resume run (main's run model, now per machine), the dialog's run panel and the status bar's done/total count sum across machines, main's offered-chat watch replaces the PR's copy, and main's target-based chat opening carries the PR's pairing fence.
…alog - Unpairing a server whose finished resume is all the open dialog shows now retires the dialog request, so a later read never opens it unasked. - A new resume retires an earlier resume's finished runs on other machines, and the status bar counts the whole resume, so the dialog and the status bar agree. - Reopening skips only the re-read of a machine that is mid-resume.
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
dcbf2bb2-b4b1-44ae-9d11-1b33f08202e5
⛔ Files ignored due to path filters (1)
src/shared/rpc-contract/rpc-params-catalog.generated.tsis excluded by!**/*.generated.*
📒 Files selected for processing (54)
src/main/native-chat/agent-session-wire/structured-agent-session-host.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-ownership.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-contract.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.tssrc/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.tssrc/main/runtime/rpc/methods/structured-agent-session-schemas.tssrc/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsxsrc/renderer/src/components/NativeChatResumeOnRestartGroups.tsxsrc/renderer/src/components/NativeChatResumeOnRestartModal.layout.test.tsxsrc/renderer/src/components/NativeChatResumeOnRestartModal.machine-column.test.tsxsrc/renderer/src/components/NativeChatResumeOnRestartModal.machines.test.tsxsrc/renderer/src/components/NativeChatResumeOnRestartModal.test.tsxsrc/renderer/src/components/NativeChatResumeOnRestartModal.tsxsrc/renderer/src/components/NativeChatResumeRunPanel.tsxsrc/renderer/src/components/native-chat-restart-action-notifications.test.tssrc/renderer/src/components/native-chat-restart-action-notifications.tssrc/renderer/src/components/native-chat-restart-offer-actions.tssrc/renderer/src/components/native-chat-restart-offer-activity-watch.tssrc/renderer/src/components/native-chat-restart-offer-reopen.tssrc/renderer/src/components/native-chat-restart-offer-triggers.test.tssrc/renderer/src/components/native-chat-restart-runs.tssrc/renderer/src/components/native-chat-resume-bulk-progress.test.tsxsrc/renderer/src/components/native-chat-resume-dialog-opening.tssrc/renderer/src/components/native-chat-resume-failed-chat-watch.test.tssrc/renderer/src/components/native-chat-resume-failure-action.tssrc/renderer/src/components/native-chat-resume-finished-run.test.tsxsrc/renderer/src/components/native-chat-resume-machine-runs.test.tsxsrc/renderer/src/components/native-chat-resume-machine-views.tssrc/renderer/src/components/native-chat-resume-modal.test-support.tsxsrc/renderer/src/components/native-chat-resume-on-restart-outcome-toast.test.tssrc/renderer/src/components/native-chat-resume-on-restart-store.tssrc/renderer/src/components/native-chat-resume-run-status-watch.tssrc/renderer/src/components/native-chat-resume-skipped-progress.test.tsxsrc/renderer/src/components/native-chat-resume-tree-keyboard.tssrc/renderer/src/components/native-chat-resume-tree-machines.tssrc/renderer/src/components/native-chat-resume-tree-state.tssrc/renderer/src/components/native-chat/NativeChatInterruptedContinue.test.tsxsrc/renderer/src/components/status-bar/NativeChatResumeStatusSegment.tsxsrc/renderer/src/components/status-bar/native-chat-resume-status-text.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/lib/activate-ai-vault-structured-session-reveal.test.tssrc/renderer/src/lib/activate-ai-vault-structured-session.test.tssrc/renderer/src/lib/activate-ai-vault-structured-session.tssrc/renderer/src/lib/structured-agent-session-tab-activation.test.tssrc/renderer/src/lib/structured-agent-session-tab-activation.tssrc/renderer/src/runtime/structured-agent-session-client.tssrc/shared/protocol-version.tssrc/shared/rpc-contract/structured-agent-session-params.tstests/e2e/cross-version-wire/structured-agent-session-host-fixture.ts
💤 Files with no reviewable changes (1)
- src/shared/rpc-contract/structured-agent-session-params.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- src/renderer/src/i18n/locales/en.json
- src/renderer/src/i18n/locales/zh.json
- src/renderer/src/i18n/locales/ko.json
- src/renderer/src/i18n/locales/fr.json
- src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
- src/renderer/src/i18n/locales/es.json
- src/renderer/src/i18n/locales/ja.json
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.
| }, | ||
| audience | ||
| ), | ||
| dismissListed: failures.dismissListed, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
# Inspect the listed-dismissal path and the teardown witness publication path.
ast-grep outline src/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-ledger.ts --items all
rg -n -C 8 'dismissListed|witnesses\.clear|witnesses\.record|enqueueRecoveryOperation' \
src/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-ledger.ts \
src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.tsRepository: stablyai/orca
Length of output: 28142
🏁 Script executed:
set -e
printf '%s\n' '--- witness references ---'
rg -n -F --glob '*.ts' -- 'createStructuredAgentSessionRestartWitnesses' src/main/native-chat/agent-session-wire
rg -n -F --glob '*.ts' -- 'dismissListed' src/main/native-chat/agent-session-wire src/main/runtime
printf '%s\n' '--- witness and queue files ---'
rg --files src/main/native-chat/agent-session-wire src/main/runtime | rg 'restart-(witness|operation|resume|failure)|recovery-capsule'
printf '%s\n' '--- witness implementation ---'
w=$(rg -l -F --glob '*.ts' -- 'export function createStructuredAgentSessionRestartWitnesses' src/main/native-chat/agent-session-wire)
sed -n '1,280p' "$w"
printf '%s\n' '--- operation queue implementation ---'
q=$(rg -l -F --glob '*.ts' -- 'createStructuredAgentSessionRestartOperationQueue' src/main/native-chat/agent-session-wire)
sed -n '1,220p' "$q"
printf '%s\n' '--- recovery capsule definitions and listed dismissal ---'
rg -n -C 12 -F -- 'dismissListed' src/main/runtime src/main/shared src/main/native-chat/agent-session-wireRepository: stablyai/orca
Length of output: 9646
🏁 Script executed:
set -e
printf '%s\n' '--- operation queue ---'
sed -n '1,220p' src/main/native-chat/agent-session-wire/structured-agent-session-restart-operation-queue.ts
printf '%s\n' '--- recovery capsule API and methods ---'
sed -n '1,340p' src/main/runtime/agent-session-recovery-capsule.ts
printf '%s\n' '--- related tests ---'
sed -n '1,360p' src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts
sed -n '240,315p' src/main/runtime/agent-session-recovery-capsule.test.tsRepository: stablyai/orca
Length of output: 20722
Clear teardown witnesses during listed dismissal.
If dismissListed runs before a queued witness record, the witness remains in confirmed and can be recorded after the listed offer is removed. Clear the applicable witnesses before delegating the dismissal.
🐛 Suggested fix
--- "a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts"
+++ "b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts"
@@ -319,7 +319,10 @@
},
audience
),
- dismissListed: failures.dismissListed,
+ dismissListed: (listed, audience) => {
+ witnesses.clear(audience)
+ return failures.dismissListed(listed, audience)
+ },
continueAfterRestart,
onAgentStarted: withdrawal.onAgentStarted,
continueInterrupted: createInterruptedContinuation(continuationHost, readMarkers)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| dismissListed: failures.dismissListed, | |
| dismissListed: (listed, audience) => { | |
| witnesses.clear(audience) | |
| return failures.dismissListed(listed, audience) | |
| }, |
| export async function pairedRestartOffersSupport( | ||
| environmentId: string | ||
| ): Promise<'supported' | 'unsupported' | 'unknown'> { | ||
| try { | ||
| return (await runtimeEnvironmentSupportsCapability( | ||
| environmentId, | ||
| AGENT_SESSION_PAIRED_RESTART_OFFERS_RUNTIME_CAPABILITY | ||
| )) | ||
| ? 'supported' | ||
| : 'unsupported' | ||
| } catch { | ||
| return 'unknown' | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Bound the paired capability probe with a deadline.
pairedRestartOffersSupport awaits runtimeEnvironmentSupportsCapability without a timeout. On a cache hit, that function awaits cached.check. cached.check is a promise that an earlier probe created, and it may have no deadline. revealStructuredSession in src/renderer/src/lib/activate-ai-vault-structured-session.ts documents the same risk and races its probe for this reason.
Two callers depend on this probe:
dismissNativeChatRestartOfferawaits the probe before it sends anything. A probe that never settles means a row's dismiss never completes. The row stays listed with no feedback.readNativeChatRestartMachineholds its read on the probe. The 1.5 s budget inreadForClickonly hides this from the user.
Race the probe against a timeout. Return 'unknown' when the timeout wins. 'unknown' already means "nothing proven" and keeps the last listing.
Proposed fix
--- "a/src/renderer/src/runtime/structured-agent-session-client.ts"
+++ "b/src/renderer/src/runtime/structured-agent-session-client.ts"
@@ -67,20 +67,33 @@
/** Whether a paired server takes the restart-offer read on connect and named dismissal. A probe
* that failed proves nothing either way, so it is `unknown`, never `unsupported`. */
export async function pairedRestartOffersSupport(
environmentId: string
): Promise<'supported' | 'unsupported' | 'unknown'> {
+ let timer: ReturnType<typeof setTimeout> | undefined
try {
- return (await runtimeEnvironmentSupportsCapability(
- environmentId,
- AGENT_SESSION_PAIRED_RESTART_OFFERS_RUNTIME_CAPABILITY
- ))
+ const supported = await Promise.race([
+ runtimeEnvironmentSupportsCapability(
+ environmentId,
+ AGENT_SESSION_PAIRED_RESTART_OFFERS_RUNTIME_CAPABILITY,
+ PAIRED_RESTART_OFFERS_PROBE_TIMEOUT_MS
+ ),
+ new Promise<never>((_resolve, reject) => {
+ timer = setTimeout(
+ () => reject(new Error('paired_restart_offers_probe_timeout')),
+ PAIRED_RESTART_OFFERS_PROBE_TIMEOUT_MS
+ )
+ })
+ ])
+ return supported
? 'supported'
: 'unsupported'
} catch {
return 'unknown'
+ } finally {
+ clearTimeout(timer)
}
}
export function supportsStructuredAgentSessionQuestionAnswers(
target: RuntimeClientTarget…g shows A finished resume keeps showing chats their host has dropped from its list, so their origin must come from the run's own row, not the current listing (which labelled the user's own resumed chat "Unknown origin").
|
Final sync with main, and final QA, at
Not merged; ready for review. |
ELI5
If a paired Orca server restarts while your chats on it are in the middle of a reply (it installed an update, or someone quit it), your desktop never tells you. Today "Resume interrupted chats?" and the "N chats to resume" status-bar entry only ever ask this computer, and only once, when Orca starts. After this change the desktop asks every paired server each time it connects. A toast tells you when a server's restart cut off your own chats. The status-bar entry and the resume dialog cover every machine, grouped by machine, and every resume ends with one toast saying what it did, as it already does for this computer.
What Changed
The problem. The server already keeps a record of every chat it cut off when it shuts down gracefully. But nothing on a paired desktop ever read that record. The desktop read only its own record, once at launch. So a server restart was silent until you happened to open each chat.
What you see now (new desktop + new server)
After the final sync with main (#26361's live resume progress). While this PR was in review, main's #26361 ("show each chat's resume as it happens, and start more at once") landed. It added live per-chat progress, but only for this computer. This PR now uses #26361's mechanism for every machine, rather than keeping a second one beside it:
How it is built:
native-chat-resume-run.ts) is now kept once per machine, innative-chat-restart-runs.ts. It replaces this PR's own "chats being resumed" list.Differences from #26361's single-computer behaviour
How it works
agent-session.paired-restart-offers.v1:offers: [{sessionId, recordedAt}]). The server then forgets only records that still match. It never deletes a chat that another action is resuming right now. "Clear everything" spares such a chat too.originfor the asking device: own, other-device, automation or server-made. The server computes it from the workspace's creator and automation records and the caller's paired device. It uses the same rule as the sidebar's "hide workspaces from other devices" filter, now moved tosrc/sharedand shared by both. A workspace with no creator record (for example a repo's main checkout) counts as yours, as the sidebar shows it.native-chat-restart-action-notifications.tsis main's one-toast builder, fed by one result per machine and summed into one toast.native-chat-resume-unsent-requests.ts, keyed by machine and then chat. A chat id on one server never marks another machine's row. A mark ends on that machine's next answer that no longer offers the chat, Retry, a successful Dismiss, a re-pair, the server being removed, or a relaunch.reopenNativeChatRestartOffer, main's opener taking the machines to read and focus. The status-bar entry, the outcome toast's Show and the reconnect toast's Show chats all go through it.Why
Differences from the common pattern
Mixed versions
{}andsessionIds. The newoffersandoriginfields are optional, and older desktops never read a paired server's offers.offers,origin) changes freely now. The longer continue timeout is client-side only.Stacking. #25652 (the resume list this builds on) is merged; this PR is based on main and merged main once at the end of review (the final sync also carries #26361's live resume progress, above). It does not depend on #25674 (dialogs taking turns), which is not merged. #25674 adapts after this PR lands: it registers this PR's status-bar and toast open paths as dialogs the user opened.
Follow-ups (out of scope)
From the final live QA, display details (P3, not fixed here): while a resume runs, the status bar can show "Resuming chats x/y" beside "N chats to resume" and "N chats failed to resume", which are separate facts. Mid-run the footer can read "Dismiss all" while a chat that isn't yours is listed on the machine that is resuming. A finished summary keeps the offer's body copy and an empty "Select all · 0 of 0" row.
The tooltip on a running chat's spinner was not read live (three tries; the spinner itself and its steps in the progress line were). Unit tests cover its text.
Restart offers after a crash ("stopped unexpectedly"). These need their own record, retirement and compatibility design.
The in-app relaunch records no offers, on this computer too.
Device names ("Sam's laptop") would need a new optional host field. Until then the label reads "Another device".
What to say when every chat a resume named had already moved on (shared with fix(native-chat): say each chat outcome once and drop internal-only surfaces #24595's follow-up).
With "Don't ask again" on, a launch that finds your chats on this computer and on K servers shows 1 + K outcome toasts, because each machine's automatic resume is its own action. Whether one launch should be one toast is an open product question.
Linked Issue
N/A (maintainer follow-up to #25652).
Visual Proof
Final head, after the merge with #26361 — live run at
d86e4ca35f4on a Mac: a hidden desktop paired to an Orca server on the same machine, real Claude chats, the server restarted gracefully mid-reply. An AI agent (Codexgpt-5.6-terra, medium effort) drove every row through the app's DevTools protocol; the QA lead checked each claim against the screenshots, a toast recorder running from page load, and the agent log. All seven rows passed. One bug it found, a resumed chat of your own labelled "Unknown origin" in the finished summary, is fixed in60aa16fb679with a test (not re-run live); the shots below don't show it.Earlier rounds (before the merge with #26361).
Before the final sync — live run r6 at
996e88466ddon a Mac test machine: a desktop paired to an Orca server on the same box, the server restarted gracefully mid-reply, driven through the app's DevTools protocol by a script. The head before the final sync differed from it only by main's changes in shared files plus one moved constant.Successful resume before the final sync — live run r7 at
863e1384012on the Windows test machine (low spec): a desktop paired to an Orca server on the same box, a signed-in Claude on the server, the server restarted gracefully mid-reply, driven by a script through the app's DevTools protocol.Testing
Unit tests (explicit file lists through
vitest --config config/vitest.config.ts): 90 files, 1099 tests passed at6546c3ce683, plus the 10 renderer files (179 tests) touched again atd2b7631969b. They cover the offer store, the reconnect triggers, actions and the one outcome toast, the dialog and its machine rows, the status bar, feat(native-chat): show each interrupted chat's workspace as its read-only sidebar card #25652's groups, the recovery record, the restart-resume server methods and the cross-version restart-offer checks.New behaviours under test include:
originon every reply;Ablations: each guard was removed and its test turned red: 58 of the first 60 behaviours (the other two are each protected by more than one guard), plus all 15 added this round (lost-request marks per machine and their end on forget, the automatic resume's toast, one toast across machines, the listed dismiss's client filter on the server and the method, the opener reading first, skipping a server out of contact or whose runtime is not answering, opening on the last listing after a failed read and after 1.5 seconds, the lost paired answer as "Couldn't confirm" with and without Show, and the continue timeout).
check-changed-code-quality, oxlint on every changed file, and the localization checks pass. Every string is in en, es, fr, ja, ko and zh. Strings this change no longer uses were removed from every catalog.Typecheck: tc:web and tc:node are clean at
6546c3ce683and at851a907f541.React Doctor (changed lines): against main's merge-base, which the check uses once the base is main, it passes. This PR's own findings were fixed: Zod 4 strict schemas, the status entry's text in its own module, and the dialog split into a hook and helpers. The warnings left are on feat(native-chat): show each interrupted chat's workspace as its read-only sidebar card #25652's and main's lines. In CI the check diffs against feat(native-chat): show each interrupted chat's workspace as its read-only sidebar card #25652's base, which can show main's newer lines until the base is main.
Head
863e1384012, before the final sync (based on main; feat(native-chat): show each interrupted chat's workspace as its read-only sidebar card #25652 merged; one main sync at the end of review): tc:web and tc:node clean; 115 test files / 1,217 tests pass (every resume/restart test, the restart host and RPC tests, main's in-chat Continue tests, capability/protocol and cross-version tests); the full static-analysis check set passes 22/22; no duplicate locale keys. New tests cover the machine checkbox and one machine row per server in feat(native-chat): show each interrupted chat's workspace as its read-only sidebar card #25652's tree, one Select all across machines, machine names from the sidebar's host names, the per-machine row key (same chat id on two servers), per-machine busy, expand state tied to the server's pairing, Continue hidden only while a resume on the same machine runs, and the dialog request always having something to draw. Each guard was checked by removing it.Final sync with main (feat(native-chat): show each chat's resume as it happens, and start more at once #26361) at
60aa16fb679: tc:web and tc:node clean; 29 renderer test files / 358 tests and 20 restart-host, RPC and cross-version files / 199 tests pass, including feat(native-chat): show each chat's resume as it happens, and start more at once #26361's own progress, finished-run and skipped-chat tests ported to the per-machine store; the static-analysis check set passes (24/24). An adversarial review of the merge found three integration bugs, all fixed with tests: a dialog request left open after a server whose finished resume was all it showed got unpaired; the dialog and status bar counting different resumes; reopening skipping every machine's re-read while one resumed. The live QA found the "Unknown origin" label (fixed, test proven red without the fix).CI on
d86e4ca35f4: everything passed except main's ownpreflight-workspace-scoped-downgradecross-version test, which main broke in fix(agents): an old Windows or unknown-platform host asks to update instead of answering its default list #27164 and fixed in test(wire): give the preflight downgrade journey a non-Windows old host #27177 right after this run's merge base, and a timer flake in an untouched test that passed on re-run. CI on60aa16fb679(merged onto current main): all checks pass (17 passed, 15 skipped by change detection, 0 failed), after re-running one shard for an unrelated filesystem-timing flake inlocal-downloaded-folder-promotionthat passes locally.Live QA, final round at
d86e4ca35f4(after the merge with feat(native-chat): show each chat's resume as it happens, and start more at once #26361; a Mac, hidden windows, isolated profiles, real Claude through its own login; an AI driver, Codexgpt-5.6-terraat medium effort, drove every row; toasts counted by a recorder from page load):Live QA (earlier heads, before the merge with fix(native-chat): say each chat outcome once and drop internal-only surfaces #24595; macOS, a client and a paired server on one machine, real Claude, driven through the app's DevTools protocol by a non-Claude agent): passed — toast for own chats only; Resume from the toast (one notice, exactly one continuation); Show chats (server row expanded, partial-selection dash); server-made chat listed unticked with its label; no second toast after a window reload or a relaunch; this computer's restart still opens the dialog by itself with no toast; two machines in one dialog with the per-machine tooltip and one notice; a paired server's interruption arriving while the launch dialog is open joins the dialog and keeps the user's unticks; the per-row dismiss and own-only Dismiss (and Dismiss disabled when nothing listed is yours); an older server built from main shows nothing and logs no errors; a server's restart toast closes when the resume dialog opens. Not exercised live: a second client paired to the same server (the test server publishes one pairing), a dropped connection without a restart, the "restarted for an update" wording (only graceful quits were run).
Live QA at
6546c3ce683(the merge with fix(native-chat): say each chat outcome once and drop internal-only surfaces #24595 and fix(dialogs): dialogs that open by themselves take turns #25674's final head; same Mac test machine and setup as above). A recorder in the page logged every toast with its text and time, so "exactly one toast" means none appeared within 10 seconds after it either. Results:Live QA r6 at
996e88466dd(the final design; Mac test machine, paired server on the same box, scripted through the app's DevTools protocol with no AI driver after the AI drivers were blocked: Codex had no login in an isolated home and credentials may not be copied; the opencode models were over their limits). The final head differs from it only by main's own changes in shared files plus one moved capability constant, so the results carry over.863e1384012, before the final sync, (Windows test machine, low spec; signed-in Claude on the server; scripted, no AI driver): PASS on all four rows: Resume from the reconnect toast and from the dialog each gave exactly one "Resumed 1 chat on " and one continuation; chats on this computer and the server resumed together gave exactly one "Resumed 2 chats" and one continuation in each; with "Don't ask again" the chat resumed by itself with one toast and no offer toast. Zero other toasts within 10 seconds each time, and the status entry cleared. (Claude restarted its counting from 1 after continuing; that is the agent's reply, not this change.)I manually tested these changes (live QA on a remote Mac; see above)
Automated tests added/updated
Notes
Checklist