Skip to content

Tell you when a paired server's restart interrupted your chats, and resume them from one dialog - #25676

Open
brennanb2025 wants to merge 50 commits into
mainfrom
brennanb2025/rsc-resume-reconnect
Open

brennanb2025 wants to merge 50 commits into
mainfrom
brennanb2025/rsc-resume-reconnect

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 28 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3195 $\color{#cf222e}{\Huge{\mathbf{−}}}$​140 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3055
Prod 66 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3633 $\color{#cf222e}{\Huge{\mathbf{−}}}$​940 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2693

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)

  • A toast, for your own chats only. It appears when the desktop reconnects to a paired server whose restart cut off your chats. The title says why: "studio-mac restarted for an update" or "Orca on studio-mac was restarted". The body reads, for example, "2 of your chats there were stopped mid-reply…". There are two buttons:
    • Resume N chats resumes exactly those chats, then shows the one result toast every resume shows (below).
    • Show chats opens the dialog on that server.
    • It closes when a resume dialog opens, since the dialog already lists those chats.
  • One status-bar entry across machines. It is the one summary of what is waiting ("N chats to resume", "N chats failed to resume", "N chats to check"). It counts only your own chats. It names the machine only when one machine has chats ("2 chats to resume on studio-mac"). With several machines, the tooltip breaks the count down by machine.
  • One dialog, grouped by machine.
    • The list is feat(native-chat): show each interrupted chat's workspace as its read-only sidebar card #25652's tree: machine → project → workspace → chat, with one checkbox per row in a single left column and collapse arrows. feat(native-chat): show each interrupted chat's workspace as its read-only sidebar card #25652 shows the machine level whenever a non-local host is listed; this PR lists each paired server's chats under that server, so a paired server is exactly one machine row, named the way your sidebar names it (renames included), with why and when it stopped ("Installed an update · 5m") and no SSH chip. A server's row opens expanded when the dialog was opened for it.
    • This PR adds to the tree: each chat is keyed by its machine and the server's current pairing (so the same chat id on two servers never mixes, and a re-pair never carries ticks over); only a machine whose resume is running is locked; what you collapse is kept across fresh answers from the same pairing and forgotten when the server leaves the list or is re-paired.
    • There is one "Select all" for the whole dialog. It covers every chat that can be resumed on every machine, including other devices' chats, which start unticked (maintainer decision: a tick is an explicit choice, the Resume button's count says what will be sent, and resuming another device's chat only continues it; Dismiss all still clears only your own chats). It leaves out failures a retry can't fix and any machine whose resume is still running; while every machine is resuming it shows that run's ticks, disabled.
    • Your own chats start ticked. Chats in workspaces made by another device, by an automation, or on the server itself start unticked, with a small label saying so.
    • When only this computer has chats, the dialog keeps today's flat list.
  • Opening the dialog (the status-bar entry, a toast's Show, Show chats) re-reads first and opens only if something is still listed or a resume is there to follow, so it never flashes open and shut. This computer is read as before. A paired server is read only while its runtime is answering, and for at most 1.5 seconds; past that, or when the server is out of contact, the dialog opens on its last listing and a late answer updates the open dialog.
  • Every resume ends with one toast, however many machines it reached. That covers Resume in the dialog, a row's Retry, the reconnect toast's Resume, and an automatic resume (at launch or on reconnect, with "Don't ask again" on). It reads "Resumed N chats" with Show, "N chats couldn't be resumed" with Show, "Couldn't confirm N chats were resumed" with Show, or a mixed result in that same toast. It names the machine only when one machine is involved ("Resumed 2 chats on studio-mac"). Each chat's own note in the chat still says what happened to it.
  • No toast for Dismiss or for Orca's own bookkeeping. A dismiss that a server refused or never answered re-reads the list instead, which puts the chats back.
  • When a resume's answer is lost:
    • On this computer, as on main: the chats it named show as failed, with Retry, until a host answer, Retry, a successful Dismiss or a relaunch.
    • On a paired server, the request may well have arrived (the server answers only once each agent has started), so the desktop now waits up to 195 seconds for that answer, as long as it waits for a chat command. If the answer is still lost, chats the server still offers show as failed on that server only, with Retry. Chats it no longer lists, or every chat when the server can't be re-read either, count as "Couldn't confirm" rather than "couldn't be resumed", which would invite a second send.
  • Dismiss all clears only your own chats, on every machine (maintainer decision). Another device's, an automation's or the server's own chat is never cleared by it, and the button reads "Dismiss" while any such chat is listed (disabled when none of the listed chats is yours). Each of those still ends through an existing path: the chat moves on (anyone sends in it, or its agent starts), its tab is closed, or the dismiss control on its own row in the dialog, which forgets exactly that offer. A chat that isn't yours (an automation's, for example) no longer opens the dialog by itself at launch.
  • No repeats. A reconnect without a server restart, a window reload or a relaunch never announces or auto-resumes the same interruption again.
  • Unchanged:
    • A paired server that predates this change shows nothing, as today.
    • This computer's own restart still opens the dialog by itself at launch, and only for your own chats.
    • "Don't ask again (resume automatically)" keeps its meaning. It now also applies to your own chats on paired servers, each interruption at most once.

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:

  • Reopening the dialog mid-resume shows the resume, on every machine.
    • The title reads "Resuming N chats".
    • Each running chat shows a spinner where its checkbox was. Its tooltip says the step: waiting to start, starting, or waiting for a reply.
    • A line above the list counts progress ("2 of 3 done · 1 waiting for a reply"). Filters narrow the list: All / In progress / Resumed / Need you.
    • When the resume finishes, the title reads "Resumed x of y chats". A paired server's progress comes from that server's own status feed. A server built before feat(native-chat): show each chat's resume as it happens, and start more at once #26361 has no progress to send, so its chats show "waiting to start" until its answer arrives.
  • The status bar reads "Resuming chats x/y" while a resume runs. It counts across every machine that resume reached, the same way the dialog counts.
  • The result toast now has Show when chats resumed, too. Show opens that resume's summary. The summary is kept until you have seen it and closed the dialog, or until the next resume starts.
  • Opening the dialog now also opens over a resume in progress, or a finished one you haven't seen, even when nothing is left to choose. In that case the footer has one Done button.
  • A chat the host leaves out of a resume is dropped from that resume's progress and from its toast. This happens when another action (for example another desktop) is already resuming it.

How it is built:

Differences from #26361's single-computer behaviour

  • The dialog's Resume button stays enabled for other machines while one machine resumes. feat(native-chat): show each chat's resume as it happens, and start more at once #26361 disabled it while anything resumed. Intended: a slow server must not lock this computer's chats. This PR already locked only the machine that is resuming.
  • Each machine's progress is summed into one title, one progress line and one status-bar count. Intended: one resume reaches several machines, and the user sees it as one resume.

How it works

  • Server. New capability agent-session.paired-restart-offers.v1:
    • Reading the offers builds no chat host on a server that never ran a chat.
    • Dismiss can name each chat together with the interruption the desktop listed (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.
    • Like every restart-offer method on main, a listed dismiss acts only on chats of agents the asking client can show.
    • Every listed chat carries an optional origin for 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 to src/shared and shared by both. A workspace with no creator record (for example a repo's main checkout) counts as yours, as the sidebar shows it.
  • Desktop.
    • When it reads: each paired server is read on every verified connection change (first connection, new server process, return after lost contact). A failed read is retried after 2 s, 5 s and 15 s, then not again until the next connection change.
    • Which answer wins: every read, resume and dismiss takes a ticket and records the server's pairing when it starts. An answer is shown only if nothing newer was shown first and the server is still paired the same way. A re-paired server's old chats are dropped as soon as the re-pair is saved.
    • Old servers: a server that definitely lacks the capability is cleared. A failed capability check counts as a failed read and decides nothing.
    • Outcome toast: native-chat-restart-action-notifications.ts is main's one-toast builder, fed by one result per machine and summed into one toast.
    • Lost-request marks: main's 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.
    • One way to open the dialog: 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.
    • What it has already announced: the desktop keeps a per-server set of interruptions it has already decided about (toasted, auto-resumed, or shown in the dialog). The set lives in memory for the run and is carried to the next launch in the window's local storage. It is pruned to what the server still offers on every read. It uses local storage rather than the persisted UI state because it is presentation-only and must be loaded before the first paired read; losing it costs one repeated notice, never data.
  • Opening the dialog. It uses main's existing resume-dialog request (no new dialog machinery): every path opens it at once — this computer's launch read (only when it offers your own chats), the status-bar entry, the outcome toast's Show and the reconnect toast's Show chats — each naming the machine to expand. A request while the dialog is already open leaves it as it is, so a launch read landing under a dialog you opened never moves your focus or resets your ticks. A paired server's read never opens the dialog by itself. While a resume dialog is open the reconnect toast stays down, and an already-showing one closes. How the resume dialog takes turns with other dialogs that open by themselves (crash report, feature tip) is fix(dialogs): dialogs that open by themselves take turns #25674's separate fix, not this PR's.
  • Continue in the chat as well. Main's in-chat Continue (feat(native-chat): say in the chat why Orca stopped a reply, and offer Continue #25675) also works on a paired server's interrupted chat. Both ways are kept, as main already does for this computer: the server continues a chat once, the Continue send retires the offer, and Continue hides while a resume on that chat's machine is running.

Why

  • Pull on reconnect, not a push stream. Offers are only created when a server shuts down, and the desktop already sees every reconnect to a new server process. Re-reading then keeps the toast, the status bar and the dialog in agreement with the server's own record. The common pattern also re-reads on reconnect. A push stream would need a new stream type that older clients drop silently, and the chat status feed would build the chat host on servers that never ran a chat.
  • Ownership on the server, not the desktop. The server holds both facts (who made the workspace, and which device is asking). Deciding there removes the desktop's wait for its copy of the server's workspace list and the "unknown" answers while that list was still loading. It also keeps one ownership rule, the sidebar's, everywhere.
  • Decide the toast from the answer, not from a remembered "server restarted" flag. Any successful read decides the user's own interruptions it hasn't decided before. A failed read or failed check therefore never loses or repeats a toast.
  • Paired servers folded into fix(native-chat): say each chat outcome once and drop internal-only surfaces #24595's model rather than kept beside it. One builder for the outcome toast, one set of lost-request marks and one dialog opener mean a resume is reported the same way whichever machine ran it, and nothing says the same outcome twice.
  • "Couldn't confirm" for a lost paired answer. On this computer a failed request provably reached nothing. Across a network it may have arrived and be running, so "couldn't be resumed" would be wrong and would invite a second send. The long wait comes first because the server's answer legitimately waits for each agent to start; the same timeout table already does this for chat commands.
  • A short wait when opening from a click. Reading first avoids opening over rows that are already gone. A bounded wait, and none at all for a server out of contact, keeps a click answered at once even when a server is slow.

Differences from the common pattern

  • Toast, status-bar entry and a per-machine dialog listing interrupted chats. Intended (maintainer product decision). The common pattern shows the interruption only inside the chat and re-reads silently on reconnect. A paired server can restart while you work elsewhere, and without a heads-up the only way to find out is to open each chat. It is safe because the server's record stays the only truth. The surfaces only read it, and every action re-derives eligibility on the server.
  • One outcome toast per resume, including automatic ones. Intended, as fix(native-chat): say each chat outcome once and drop internal-only surfaces #24595 already decided for this computer: a resume acts on chats that are mostly off-screen, so it gets one short toast as feedback.
  • Auto-continue is driven by each desktop's own "Don't ask again" setting and a continue the desktop sends, not by an opt-in held on the server. Intended. The setting is the preference of the person at that desktop, and it resumes only chats in workspaces that are theirs. An opt-in held on the server would continue a device's chats while that device is offline, sending messages nobody at it asked for. It is safe with several desktops: the server reserves each offer under a lock, so a second desktop's continue of the same chat sends nothing (covered by a test).
  • The desktop remembers which interruptions it has already announced. Intended. It is presentation only. The server stays the source of truth, the memory is pruned on every read, and losing it costs one repeated notice.
  • A resume whose chats were all already resumed elsewhere (another desktop got there first) shows no toast. Temporary: this is the gap fix(native-chat): say each chat outcome once and drop internal-only surfaces #24595 lists for this computer, and its follow-up (not yet filed) decides what to say for both.

Mixed versions

  • New desktop, older server: the capability is missing, so the desktop never reads that server or sends it a dismiss. It shows nothing for it, as today.
  • Older desktop, new server: the server's dismiss still accepts {} and sessionIds. The new offers and origin fields are optional, and older desktops never read a paired server's offers.
  • This computer always runs the same build on both sides.
  • The capability has never shipped, so its contract (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 d86e4ca35f4 on 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 (Codex gpt-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 in 60aa16fb679 with a test (not re-run live); the shots below don't show it.

Reconnect toast for your own chat One result toast, with Show
02-final-r1-offer-toast.png 03-final-r1-outcome-toast.png
"Show chats": the server's row, nothing resumed This computer's own restart: the dialog opens by itself, no toast
07-final-r2-dialog-server-row.png 09-final-r7-dialog-opened-itself.png
Two machines: own chats ticked, the server-made one unticked with its label One status-bar entry, broken down by machine
10-final-r4-tree-default.png 12-final-r3-status-tooltip.png
Reopened mid-resume: live progress across both machines, "Resuming chats 0/2" Finished: "Resumed 2 of 2 chats"
15-final-r5-progress-mid-run.png 17-final-r5-finished.png
A resume the server couldn't carry on, under Need you with Open chat and Retry Status entry "1 chat failed to resume"
22-final-r6-need-you.png 24-final-r6-status-failed.png
Dismiss clears only your own chats; no toast
26-final-r4-after-dismiss.png

Earlier rounds (before the merge with #26361).

Before the final sync — live run r6 at 996e88466dd on 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.

The reconnect toast after the server restart (your chat only) and the status-bar entry The dialog from Show chats: the server is one machine row with why/when it stopped, your chat ticked
02-r6-f1-01-offer-and-status.png 03-r6-f2-01-tree-dialog.png
A resume the server could not carry on: exactly one toast "1 chat on … couldn't be resumed" with Show, status "1 chat failed to resume" Launch after a restart with chats here and on the server: the dialog opens by itself with two machine rows, no toast
04-r6-f2-RESUME-FAILED.png 05-r6-f3-01-launch-two-machines.png
A machine collapsed with its arrow A chat made on the server: unticked, labelled, with its own dismiss
06-r6-f3-02-collapsed-by-arrow.png 08-r6-f4-01-dialog-origin-chip-and-x.png
After Dismiss (your chats only): no toast, status entry gone The server's chat is still listed next time, until its own dismiss
09-r6-f4-02-after-dismiss-no-toast.png 10-r6-f4-03-server-made-still-listed.png
Collapsed, then a fresh answer from the same server: still collapsed Reopened: back to the defaults
13-r6-f5-02-still-collapsed-after-reread.png 14-r6-f5-03-reopen-defaults.png
Only this computer listed: today's flat list A window reload: no second toast, the status entry stays
15-r6-f6-01-flat-dialog.png 17-r6-f7-01-after-reload.png

Successful resume before the final sync — live run r7 at 863e1384012 on 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.

Resume from the reconnect toast: exactly one "Resumed 1 chat on …", status entry gone Resume from the dialog: one machine row, the chat ticked
03-r7-g1-one-success-toast.png 06-r7-g2-tree-one-machine-ticked.png
Chats on this computer and on the server: both machines in one dialog One Resume across both: exactly one "Resumed 2 chats" (no machine name)
10-r7-g3-two-machine-launch-tree.png 12-r7-g3-one-combined-success-toast.png
"Don't ask again" on: the chat resumes by itself, one toast, no offer toast After the continuation, Claude replies for real
16-r7-g4-one-auto-success-toast.png 04-r7-g1-transcript-continued.png

Testing

  • Unit tests (explicit file lists through vitest --config config/vitest.config.ts): 90 files, 1099 tests passed at 6546c3ce683, plus the 10 renderer files (179 tests) touched again at d2b7631969b. 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:

    • an answer shown only when it is the newest under the current pairing (including a dismiss outranking an earlier read);
    • the three-way capability check;
    • retries;
    • no re-toast after a reload, at boot, or when storage refuses writes;
    • the reconnect toast held back only while a resume dialog is on screen;
    • own-only counting in one status-bar entry;
    • one toast for a resume across machines, and for an automatic resume on reconnect;
    • no toast for any Dismiss, refused or lost;
    • lost-request marks kept to the machine they were made for, and ended when that server is forgotten;
    • a lost paired answer counted as "Couldn't confirm" when the server no longer lists the chats, or cannot be re-read;
    • the 195-second continue timeout;
    • the dialog opener: nothing opens when the re-read lists nothing; a server out of contact, or whose runtime is not answering, opens on its last listing without a read; a failed read opens on the last listing; a read past 1.5 seconds opens on the last listing and the late answer updates the open dialog;
    • Dismiss all clearing only your own chats, and the per-row dismiss on any other chat (sending exactly that offer);
    • a listed dismiss sparing chats of agents the asking client cannot show;
    • the existing server tests proving the other ways an offer ends: a send in the chat or its agent starting, and closing its tab;
    • the launch dialog opening only for your own chats and staying once shown;
    • the machine row naming its machine;
    • the server's listed-dismiss and its protection of a resume in progress;
    • origin on every reply;
    • two desktops continuing one chat.
  • 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 6546c3ce683 and at 851a907f541.

  • 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 own preflight-workspace-scoped-downgrade cross-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 on 60aa16fb679 (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 in local-downloaded-folder-promotion that 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, Codex gpt-5.6-terra at medium effort, drove every row; toasts counted by a recorder from page load):

    • R1 PASS: the reconnect toast's "Resume 1 chat" gave exactly one "Resumed 1 chat on QA server" with Show; the status bar went "1 chat to resume on QA server" → "Resuming chats 0/1" → "1/1" → gone; one continuation in the chat.
    • R2 PASS: "Show chats" opened the dialog on the server's row, expanded and ticked; nothing was resumed.
    • R3 PASS: one status entry, "4 chats to resume", tooltip "2 on QA server, 2 on Local Mac".
    • R4 PASS: two machine rows with tri-state boxes; the server-made chat unticked with "Made on QA server"; Select all 5 of 5 then 0 of 5; Dismiss cleared only your own chats with no toast, and the server still lists the server-made chat.
    • R5 PASS: resuming chats on both machines and reopening mid-run showed "Resuming 2 chats", "0 of 2 done · 2 waiting to start" stepping through starting and waiting for a reply, filters with the right counts, and a spinner in each running chat's checkbox slot; it ended "Resumed 2 of 2 chats", one toast, one continuation in each chat; closing the finished dialog retired it. The spinner's tooltip was not read (see Follow-ups).
    • R6 PASS: one chat's continuation refused on purpose (the agent wrapper refused it; the real CLI stayed signed in) gave one toast "1 chat on QA server couldn't be resumed / Resumed 1 chat / Show"; the chat was under Need you with Open chat and Retry; the status entry read "1 chat failed to resume".
    • R7 PASS: this computer's own restart opened the dialog by itself, with zero toasts.
  • 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:

    • E1 PASS: the offer toast read "Orca on Remote structured chat QA server was restarted / 1 of your chats there was stopped mid-reply. It shows where it stopped, and nothing was lost." with Show chats and Resume 1 chat. The status entry read "1 chat to resume on Remote structured chat QA server".
    • E2 PASS: the toast's Resume showed exactly one toast, "Resumed 1 chat on Remote structured chat QA server". The chat got exactly one continuation message, and the status entry cleared.
    • E3 PASS: opening from the status entry, the offer toast was gone 444 ms after the dialog opened. Resume in the dialog gave one toast. Opening from the toast's Show chats, "Resume 2 chats" gave one toast, "Resumed 2 chats on …".
    • E4 PASS: with chats on two machines, a relaunch opened the dialog by itself with two machine rows and no toast. The status entry read "2 chats to resume", with the tooltip "1 on Local Mac, 1 on Remote structured chat QA server". One Resume gave one toast, "Resumed 2 chats" (no machine name).
    • E5 PASS: Dismiss (your own chats only) gave no toast within 10 seconds and the status entry disappeared. The server-made chat stayed listed, unticked, with its own dismiss, which removed it.
    • E6 PASS: with "Don't ask again" on, there was no offer toast. Exactly one toast, "Resumed 1 chat on …", and one continuation.
    • E7 PASS: a window reload brought no second toast; the status entry stayed.
    • E8 PASS: this computer's own restart still opened the dialog by itself, with the flat list and no toast.
    • E9 not run: "N chats couldn't be resumed" needs the agent provider itself to refuse a continuation, and there is no live way to make it do that without changing code. Unit tests cover that toast and status wording.
  • 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.

    • PASS: reconnect toast and status entry; the tree with exactly one machine row per server (no SSH chip), keyboard navigation and collapse arrows; the launch dialog listing two machines with no toast; another chat unticked with its label and dismiss, Select all, Dismiss with zero toasts; collapse kept across a fresh answer and reset on reopen; flat list for this computer only; no repeat toast after a reload.
    • PASS, first live run: a resume the server could not carry on gives exactly one "1 chat on couldn't be resumed" toast with Show, and the status entry reads "1 chat failed to resume".
    • Successful resume, live run r7 at 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

  • SSH: structured chats run on runtime hosts; nothing here falls back to SSH.
  • Folder workspaces: ownership looks up folder workspaces by their key, as well as git worktrees.
  • Web client: does not watch paired servers. Its Dismiss names its chats, so it can no longer clear every device's offers on the server it talks to.

Checklist

  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Screenshots attached for UI changes
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered

brennanb2025 and others added 22 commits October 5, 2026 11:19
…-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.
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.
…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.
…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").
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.
…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.
…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).
@brennanb2025
brennanb2025 force-pushed the brennanb2025/rsc-resume-reconnect branch from 784df58 to 2bdcd7c Compare October 8, 2026 01:48
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.
… 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.
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/resume-modal-sidebar-cards to main October 8, 2026 07:25
@brennanb2025

Copy link
Copy Markdown
Contributor Author

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.

  • A toast when you reconnect to a paired server whose restart cut off your own chats: "studio-mac restarted for an update / 2 of your chats there were stopped mid-reply…", with Resume N chats and Show chats. It goes away when the resume dialog opens.
  • One status-bar entry across all machines, counting only your own chats ("2 chats to resume on studio-mac"; with several machines the tooltip breaks it down).
  • One resume dialog for every machine, built on feat(native-chat): show each interrupted chat's workspace as its read-only sidebar card #25652's tree (machine → project → workspace → chat, one checkbox column). A paired server is one machine row named the way your sidebar names it, with why and when it stopped. Your own chats start ticked; chats from another device, an automation or the server itself start unticked, labelled, with their own dismiss button. One "Select all" covers every machine. Only this computer listed = today's flat list.
  • Every resume ends with one toast, the same outcome toast main gives this computer, however many machines it reached. No toast for Dismiss. "Dismiss all" clears only your own chats.
  • No repeats after a reconnect, a reload or a relaunch. Older servers show nothing, as before.

How the review shaped it.

Deferred (known, not fixed here).

  • (P2) After the connection to a paired server drops, the pending read retries (2 s / 5 s / 15 s) still fire, up to 3 wasted reads; they're finite and change nothing on screen.
  • (P3) A server you remove leaves an empty bookkeeping entry in memory until the window closes.
  • A slow server with many chats can take longer than the 195-second wait for its answer; those chats then read "Couldn't confirm" even if they did continue.
  • "N chats failed to resume" in the status bar doesn't name the machine (the toast does, and the entry opens on that server).
  • A resume whose chats were all already resumed elsewhere shows no toast (the same gap main lists for this computer).
  • With "Don't ask again" on, one launch with chats on this computer and on K servers shows 1 + K outcome toasts.
  • Out of scope: crash restart offers, the in-app relaunch not recording offers, device names ("Another device" for now).

Verified.

  • Final head 863e1384012 (based on main, one main sync): tc:web and tc:node clean; 115 test files / 1,217 tests pass; the full static-analysis check set passes 22/22; no duplicate locale keys.
  • Code review rounds on Codex: the decoupled rebuild (carry-over fidelity: every hunk accounted for, nothing lost), the dialog-flow port, the tree swap, and a final readiness review (PASS WITH RISK, no P0/P1; the two deferred items above are its findings).
  • Live QA r6 (M4Air, a desktop paired to an Orca server on the same box, scripted through the app's DevTools protocol, no AI driver) at 996e88466dd. The final head differs from it only by main's own changes in shared files plus moving one capability constant, so the results carry over.
    • PASS: reconnect toast + status entry; the tree with one machine row per server (no SSH chip), keyboard navigation and collapse arrows; the launch dialog listing two machines with no toast; other-device chat unticked with its chip and dismiss, Select all, Dismiss with zero toasts; collapse kept across a fresh answer and reset on reopen; flat list for this computer only; no repeat toast after a reload.
    • PASS (new, first live run): a resume the server could not carry on gives exactly one "1 chat on couldn't be resumed" toast with Show, and the status entry reads "1 chat failed to resume".
  • CI on 863e1384012: all checks pass (20 passed, 17 skipped by change detection, 0 failed).

Not verified.

  • A successful resume end to end on the final design: the test box's Claude account was signed out, and signing in or copying credentials isn't allowed. Resume did reach the server each time (request and answer logged). Successful resumes were verified live on earlier heads of this PR (one outcome toast, one continuation message).
  • A second desktop paired to the same server; a dropped connection without a server restart; Windows, Linux and SSH hosts live.
  • Note for the maintainer: during QA r6 the M4Air's real ~/.claude and ~/.codex top-level files changed modification time (stat-only check; contents never opened). The real Claude CLI ran there through the sanctioned test shim, which likely accounts for ~/.claude; the ~/.codex change is unattributed.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 8, 2026 08:37
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 05fde1ae-cfac-4cb7-84d2-8ee6652544aa


📥 Commits

Reviewing files that changed from the base of the PR and between d86e4ca and 60aa16f.



📒 Files selected for processing (2)
  • src/renderer/src/components/native-chat-resume-machine-runs.test.tsx
  • src/renderer/src/components/native-chat-resume-tree-machines.ts


Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.




📝 Walkthrough
📝 Walkthrough

Walkthrough

The change adds per-machine restart-offer handling across runtime hosts, RPC handlers, and the renderer. It adds timestamp-qualified dismissal and offer-origin metadata. The renderer reads, resumes, and dismisses offers by machine, and presents machine-grouped offers with ownership labels and machine-specific outcomes. Session activation and structured-session calls carry execution-host and pairing-revision data. The changes also add tests for host behavior, RPC handling, renderer interactions, and cross-version wire compatibility.



Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 60aa1

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 | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 54.07% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 209 functions across 76 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly summarizes the primary change: notifying users about paired-server restart interruptions and enabling recovery from one dialog.
Description check Passed The description is substantially complete. It includes the ELI5 summary, user-facing changes, rationale, compatibility details, visual proof, extensive testing results, notes, and checklist items. It …

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR


🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9938ad01-2ef8-44e0-84b8-0bcf9c5a16ab
📥 Commits

Reviewing files that changed from the base of the PR and between 0eee1ce and 863e138.

⛔ Files ignored due to path filters (1)
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (93)
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-test-recovery-capsule.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-audience.test.ts
  • 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-ownership.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-contract.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts
  • src/main/runtime/agent-session-recovery-capsule-entries.ts
  • src/main/runtime/agent-session-recovery-capsule-read.ts
  • src/main/runtime/agent-session-recovery-capsule.test.ts
  • src/main/runtime/agent-session-recovery-capsule.ts
  • src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts
  • src/main/runtime/restart-offer-workspace-provenance.test.ts
  • src/main/runtime/restart-offer-workspace-provenance.ts
  • src/main/runtime/rpc/methods/structured-agent-session-restart-offer-origin.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-restart-offer-origin.ts
  • src/main/runtime/rpc/methods/structured-agent-session-restart-offer-read.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-restart-offer-read.ts
  • src/main/runtime/rpc/methods/structured-agent-session-restart-resumable-empty.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-restart-resume.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-restart-resume.ts
  • src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.ts
  • src/main/runtime/rpc/methods/structured-agent-session-schemas.ts
  • src/renderer/src/components/NativeChatResumeFailureDetails.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.layout.test.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.list.test.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.machine-column.test.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.machines.test.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.tsx
  • src/renderer/src/components/native-chat-launch-resume-decision.test.tsx
  • src/renderer/src/components/native-chat-restart-action-notifications.test.ts
  • src/renderer/src/components/native-chat-restart-action-notifications.ts
  • src/renderer/src/components/native-chat-restart-decided.ts
  • src/renderer/src/components/native-chat-restart-machine-fence.ts
  • src/renderer/src/components/native-chat-restart-machine-name.ts
  • src/renderer/src/components/native-chat-restart-machines.ts
  • src/renderer/src/components/native-chat-restart-offer-actions.ts
  • src/renderer/src/components/native-chat-restart-offer-activity-watch.ts
  • src/renderer/src/components/native-chat-restart-offer-payload.ts
  • src/renderer/src/components/native-chat-restart-offer-reopen.ts
  • src/renderer/src/components/native-chat-restart-offer-test-support.ts
  • src/renderer/src/components/native-chat-restart-offer-triggers.test.ts
  • src/renderer/src/components/native-chat-restart-offer-triggers.ts
  • src/renderer/src/components/native-chat-restart-reconnect-toast.ts
  • src/renderer/src/components/native-chat-resume-dialog-opening.ts
  • src/renderer/src/components/native-chat-resume-failed-chat-watch.test.ts
  • src/renderer/src/components/native-chat-resume-failure-action.ts
  • src/renderer/src/components/native-chat-resume-machine-views.ts
  • src/renderer/src/components/native-chat-resume-machines.test-support.ts
  • src/renderer/src/components/native-chat-resume-on-restart-dialog.ts
  • src/renderer/src/components/native-chat-resume-on-restart-grouping.ts
  • src/renderer/src/components/native-chat-resume-on-restart-launch-prompt.ts
  • src/renderer/src/components/native-chat-resume-on-restart-modal.test-support.ts
  • src/renderer/src/components/native-chat-resume-on-restart-outcome-toast.test.ts
  • src/renderer/src/components/native-chat-resume-on-restart-store.ts
  • src/renderer/src/components/native-chat-resume-ownership.test.ts
  • src/renderer/src/components/native-chat-resume-ownership.ts
  • src/renderer/src/components/native-chat-resume-selection.ts
  • src/renderer/src/components/native-chat-resume-tree-machines.ts
  • src/renderer/src/components/native-chat-resume-tree-state.ts
  • src/renderer/src/components/native-chat-resume-unsent-requests.ts
  • src/renderer/src/components/native-chat/NativeChatInterruptedContinue.test.tsx
  • src/renderer/src/components/native-chat/NativeChatInterruptedContinue.tsx
  • src/renderer/src/components/sidebar/workspace-creator-visibility.ts
  • src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx
  • src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.tsx
  • src/renderer/src/components/status-bar/native-chat-resume-status-text.ts
  • 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
  • src/renderer/src/lib/activate-ai-vault-structured-session-reveal.test.ts
  • src/renderer/src/lib/activate-ai-vault-structured-session.test.ts
  • src/renderer/src/lib/activate-ai-vault-structured-session.ts
  • src/renderer/src/lib/structured-agent-session-tab-activation.test.ts
  • src/renderer/src/lib/structured-agent-session-tab-activation.ts
  • src/renderer/src/runtime/structured-agent-session-client.test.ts
  • src/renderer/src/runtime/structured-agent-session-client.ts
  • src/shared/agent-session-restart-capabilities.ts
  • src/shared/protocol-version.ts
  • src/shared/restart-offer-origin.test.ts
  • src/shared/restart-offer-origin.ts
  • src/shared/rpc-contract/structured-agent-session-params.ts
  • src/shared/rpc-contract/structured-agent-session-restart-params.ts
  • src/shared/workspace-creator-provenance.ts
  • tests/e2e/cross-version-wire/cross-version-restart-offers.unit.test.ts
  • tests/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": "閉じる",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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-L17531
  • src/renderer/src/i18n/locales/zh.json#L17531-L17531

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live QA r7: successful resume on the final head

The 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 863e1384012, on the Windows test machine (low spec): a desktop paired to an Orca server on the same box, the server restarted gracefully mid-reply, scripted through the app's DevTools protocol.

  • Resume from the reconnect toast: exactly one "Resumed 1 chat on ", one continuation in the chat, status entry gone.
  • Resume from the dialog (one machine row, chat ticked): the same single toast and one continuation.
  • Chats on this computer and on the server, one Resume: exactly one "Resumed 2 chats" (no machine name), one continuation in each chat.
  • "Don't ask again" on: no offer toast; the chat resumed by itself with exactly one toast.
  • Each time: no other toast within 10 seconds, and Claude replied for real after continuing. (It restarted its counting from 1, which is the agent's reply, not this change.)

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 ~/.codex found no change; the Windows .claude folder and the Linux .claude/.last-cleanup file changed modification time (contents never opened).

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.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Synced with main again (9bcde43dee1): main's #26483 now requires every RPC method to say who may call it, and it added permission: 'workspace' to the restart-resume methods this PR also edits. The merge keeps those lines and this PR's own changes; nothing else in this PR's files changed except main's new translation strings.
No behaviour change for this PR: paired desktops and this computer hold the workspace permission, so reading, dismissing and resuming a paired server's chats work as before; live QA r6/r7 still applies.
Checks: tc:web + tc:node clean, 119 test files / 1,275 tests pass, the full static-analysis check set passes, no duplicate locale keys, CI green (22 passed, 15 skipped, 0 failed). Still ready; not merged.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: dcbf2bb2-b4b1-44ae-9d11-1b33f08202e5
📥 Commits

Reviewing files that changed from the base of the PR and between 9bcde43 and d86e4ca.

⛔ Files ignored due to path filters (1)
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (54)
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-ownership.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-contract.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts
  • src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.ts
  • src/main/runtime/rpc/methods/structured-agent-session-schemas.ts
  • src/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.layout.test.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.machine-column.test.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.machines.test.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.tsx
  • src/renderer/src/components/NativeChatResumeRunPanel.tsx
  • src/renderer/src/components/native-chat-restart-action-notifications.test.ts
  • src/renderer/src/components/native-chat-restart-action-notifications.ts
  • src/renderer/src/components/native-chat-restart-offer-actions.ts
  • src/renderer/src/components/native-chat-restart-offer-activity-watch.ts
  • src/renderer/src/components/native-chat-restart-offer-reopen.ts
  • src/renderer/src/components/native-chat-restart-offer-triggers.test.ts
  • src/renderer/src/components/native-chat-restart-runs.ts
  • src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx
  • src/renderer/src/components/native-chat-resume-dialog-opening.ts
  • src/renderer/src/components/native-chat-resume-failed-chat-watch.test.ts
  • src/renderer/src/components/native-chat-resume-failure-action.ts
  • src/renderer/src/components/native-chat-resume-finished-run.test.tsx
  • src/renderer/src/components/native-chat-resume-machine-runs.test.tsx
  • src/renderer/src/components/native-chat-resume-machine-views.ts
  • src/renderer/src/components/native-chat-resume-modal.test-support.tsx
  • src/renderer/src/components/native-chat-resume-on-restart-outcome-toast.test.ts
  • src/renderer/src/components/native-chat-resume-on-restart-store.ts
  • src/renderer/src/components/native-chat-resume-run-status-watch.ts
  • src/renderer/src/components/native-chat-resume-skipped-progress.test.tsx
  • src/renderer/src/components/native-chat-resume-tree-keyboard.ts
  • src/renderer/src/components/native-chat-resume-tree-machines.ts
  • src/renderer/src/components/native-chat-resume-tree-state.ts
  • src/renderer/src/components/native-chat/NativeChatInterruptedContinue.test.tsx
  • src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.tsx
  • src/renderer/src/components/status-bar/native-chat-resume-status-text.ts
  • 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
  • src/renderer/src/lib/activate-ai-vault-structured-session-reveal.test.ts
  • src/renderer/src/lib/activate-ai-vault-structured-session.test.ts
  • src/renderer/src/lib/activate-ai-vault-structured-session.ts
  • src/renderer/src/lib/structured-agent-session-tab-activation.test.ts
  • src/renderer/src/lib/structured-agent-session-tab-activation.ts
  • src/renderer/src/runtime/structured-agent-session-client.ts
  • src/shared/protocol-version.ts
  • src/shared/rpc-contract/structured-agent-session-params.ts
  • tests/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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.ts

Repository: 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-wire

Repository: 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.ts

Repository: 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.

Suggested change
dismissListed: failures.dismissListed,
dismissListed: (listed, audience) => {
witnesses.clear(audience)
return failures.dismissListed(listed, audience)
},

Comment on lines +70 to +83
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'
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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:

  • dismissNativeChatRestartOffer awaits 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.
  • readNativeChatRestartMachine holds its read on the probe. The 1.5 s budget in readForClick only 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").
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Final sync with main, and final QA, at 60aa16fb679.

Not merged; ready for review.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant