Repository navigation
fix: a terminal pane no longer resumes a conversation a native chat owns - #28288
brennanb2025 wants to merge 10 commits into
Conversation
d7eab01 to
9636645
Compare
… owns A pane that saved a native chat's conversation before the previous fix (or a CLI conversation the user later moved into a chat) still resumed it on every restart, as a second writer on the chat's transcript and in the wrong mode. The rule already exists: a conversation any native-chat record names is never resumed in a terminal (typed resume commands, Session History). The pane's own restore resumes now meet it too, at the one place every automatic resume passes: the PTY spawn, which already receives the resume's provider session and already has a "resume dropped" outcome. When a chat owns the conversation the pane starts its plain shell, shows the existing "previous session unavailable" line, and its saved record is cleared, so a stale record dies after one restart. Ownership is read from the durable session records each time, never copied; a journal that cannot be read leaves resumes as they were.
…e pane's saved session The refusal now drops the whole agent launch, so the pane is an ordinary shell (no agent registration, unattended Git guard or Claude auth prep). The spawn reports agentResumeOwnedByChat and the renderer clears both recovery sources, the saved record and the status row. WSL spawns skip the check; the remote runtime path is left for a stacked change that can carry the result back.
…ands The renderer now retires a refused plan before anything registers it (no agent hint over the plain shell), on both the fresh-spawn and reattach paths, including a fresh result with no scrollback. The status row is dropped with no resumable remnant on the host, so a Pi pane cannot rebuild the record and refuse again.
…before attaching The early-exit result kept only the id, and the reattach handler returned on it before settling, so a chat-owned record stayed retryable. The result now keeps the ownership flag and the handler settles before that return.
9636645 to
c55aa94
Compare
Each pane used to retire a refused resume after receiving the spawn result, so a pane that remounted or whose shell exited mid-spawn never forgot it, and an exited shell was counted as a resumed agent. The spawn now asks the hook server to forget the refused conversation for the pane as soon as the pane is known; the server drops a matching row with no resumable copy and sends one identity-scoped clear, which the window applies everywhere the conversation is saved. Panes only show the notice. When the chat records can't be read, an automatic restore opens a plain shell and keeps its saved session; a resume the user asked for still runs.
The host now publishes the moved pane's own key and, when it really deleted the row, tells every subscriber; the window drops queued events that would restore the conversation and forgets it only within the pane's own workspace, leaving a pane that moved on to another conversation alone. Every spawn result, including an exit during registration and an exit before attaching, says the resume was refused and why. An owner that can't be checked refuses any resume the user didn't ask for, and the pane keeps the saved session for the next try even when it was consumed before the spawn.
… runs Clearing the last saved agent session sent the field as undefined, which the per-host session split drops, so the record stayed on disk until the quit snapshot and a crash brought it back. The patch now sends an empty map, which the split already keeps. Also loosens the refused-resume matcher to the status types' optional fields.
Clearing every saved copy of the conversation in the workspace reached panes in another namespace (a WSL tab in the same folder), missed the pane's own record after its workspace was re-keyed, and let one pane's refusal undo another's kept session. The window now forgets the conversation only for the refusing pane and its older keys in the same tab; any other pane holding it is refused and forgotten at its own restore, since the host checks every time. Queued events are matched by the pane's current key, as they are applied. An unknown owner again refuses only what restore admission already calls automatic, so reopening a terminal is never blocked by bookkeeping.
…session The same-tab match meant for a pane's older numeric keys also matched a split sibling's current key, so one pane's refusal could clear another's saved session and let that pane's late result save the chat's conversation again. Only legacy numeric keys of the tab now count as the pane's own.
|
Review loop status: converged at 9035ba4 (synced with main once), ready for a human look (still draft, not merged). What this PR does: on restart, a terminal pane whose saved conversation belongs to a native chat opens a plain shell with the "previous session unavailable, started fresh" notice, and Orca forgets that conversation as the pane's, instead of resuming the chat's conversation as a CLI in the pane's folder (#26417). Since the last update, per the agreed design, cleanup moved from the panes to the host that refuses the resume: it drops its own copy right away and sends one message the window applies, so a pane that remounts or exits mid-spawn can no longer skip it. Three more review rounds on that change found and fixed: queued status events restoring the forgotten row, cleanup reaching other panes, split siblings and other hosts (now limited to the refusing pane and its old numeric keys), every spawn result now carrying the refusal, the deleted row reaching every listener, and the "unknown owner" rule following the existing restore classification. Live check on a remote Mac (hidden windows, same saved state on both sides): main resumes the chat's conversation in the floating terminal; this PR shows the notice and a plain shell, then a clean shell on the next restart, with the chat untouched. That run also exposed that clearing the last saved session was only written at quit (a pre-existing session-write bug); fixed here, and the record now leaves disk within 20 s while Orca runs. Screenshots are in the PR body. The final head differs from the live-tested 8686ba6 in narrowing that cleanup, which the tested single-pane scenario doesn't exercise differently, and in the end-of-review sync with main. Sync with main: the updated #28267 (synced with main) merged into this branch with no conflicts, and this PR's own diff is unchanged. Main's AI-button launch change (#28210) edits the same terminal-spawn files; a fresh independent review of how the two fit together found no defects. Verified at 9035ba4: node and web typechecks, lint and code-quality checks, and the targeted tests (spawn, agent hooks, terminal pane, the refusal and forget tests: 6,733 passed). With the refusal disabled, its spawn test fails. CI at 9035ba4: static analysis, typecheck, packaging and test shards 1 and 5 pass. Four test failures in shards 2-4, none in files this PR touches. Main broke three of them ( Not verified: Windows, WSL and SSH machines, forced crashes, the unreadable-chat-records case, and a pane remounting mid-spawn (all covered only by tests or source review). Known gaps are listed in the body (SSH not checked yet; the old naming-process leak isn't cleaned up here). Decision for you: the session-write fix (an emptied saved-session map now reaches disk) is a separate pre-existing bug. I kept it here because this repair depends on it; I can split it into its own PR if you prefer. |
|
Closing as superseded by merged #30930, whose description explicitly includes the saved-pane repair from this draft. Its merge commit I reviewed this draft’s complete production and six-file test diff at The successor deliberately refines the policy: saved starts are refused for an open chat or a chat that may still be writing its current conversation; a closed, stopped chat may resume in a terminal. Cleanup is fenced to captured status/saved-record versions so a late refusal cannot erase newer work. These replace this draft’s broader historical ownership and cleanup rules. Unreadable ownership retains the saved recipe; typed starts are left alone. Focused verification on the checked-out main-derived tree: saved-resume spawn admission, saved-startup cancellation, and workspace-session patch suites passed, 31 tests across 3 files. Relevant implementations have no difference from fetched main. No live UI or cross-platform QA claim, and no branch deletion. |
ELI5
Before the fix in #28267, a chat agent could be mistaken for a terminal pane, and that pane saved the chat's conversation as its own. After a restart the pane "resumed" the chat's conversation as a terminal agent, in the pane's workspace instead of the chat's. #28267 stops new mix-ups, but panes that already saved a chat's conversation keep doing this on every restart. Now, before a pane resumes anything, Orca checks its own chat records. If a chat owns that conversation, the pane opens a plain shell, says the old session isn't available, and Orca forgets that conversation as the pane's to resume.
What Changed
What the user saw (#26417). A background Claude chat worker started in worktree A came back after every Orca restart as a
claude --resume <id>terminal tab in a floating terminal, with the home directory as its working folder, while Orca's own session record still placed the chat in worktree A.Why it kept happening after the leak was fixed. Terminal restore resumes whatever conversation the pane last saved: its saved restore record, or its last status row (kept for up to 7 days). Neither is checked against anything else, so a pane that once saved a chat's conversation resumes it forever. Orca already has the durable answer to "who owns this conversation": the chat session records, which native chat already uses to refuse a terminal resume command for a conversation a chat owns.
After.
Mechanism.
nativeChatOwnsTerminalResumereuses the rule native chat already applies to terminal resume commands (same record matching, same per-agent session-file comparison), so terminal restore and native chat can't disagree.agentResumeRefusedalongside the existingagentResumeUnavailable.agentStatus:clearcarrying the refused conversation; if a row was deleted, every listener hears it like any pane clear, otherwise only the window does. One listener in the window forgets that conversation as that pane's, whether or not the pane is mounted: status events still queued for it (matched by the pane's current key, as they are applied), its live and retained rows, its saved restore record and copies under its older numeric pane keys in the same tab, the launch config, a launch-derived agent hint, and the pane tab's conversation title. A pane already on a different conversation is left alone, and another pane holding the same conversation is refused and forgotten at its own restore, because the host checks every time. Nothing is sent back to the host.agentResumeRefusedand why (ownedorunverifiable). The pane drops its resume plan (nothing of it is registered over the shell) and shows the notice. Forunverifiableit keeps a resume handle the same way it already does when the host postpones a restore, because a background workspace wake removes the saved record before it spawns. Panes own no cleanup, so a pane that remounts or exits mid-spawn can't skip it.Why
Known limits.
refusedResumeis a new optional field on the window's existing status-clear message andagentResumeRefuseda new optional field on the local spawn result. Both are local (main process to window), with no stored format change.Builds on #28267. Supersedes #26426 by @petersindex together with #28267. Thank you.
Linked Issue
Fixes #26417
Visual Proof
Hidden-window restart on a remote QA Mac, with the same saved state on both sides: a profile from main where a floating terminal had saved a native Claude chat's conversation (the chat lives in worktree
qa-W).Before (main): after restart the floating terminal, titled with the chat's prompt, runs
claude --resume <chat's id>in the home folder.After, first restart (this PR at 8686ba6): the floating terminal is a plain home shell with the "previous session unavailable, started fresh" notice and its own title; the chat is untouched in
qa-W. The saved record was gone from disk 20 s after restore while Orca was still running.After, second restart: plain shell, no notice, nothing re-created.
The final head 9035ba4 differs from the tested 8686ba6 in narrowing the window cleanup to the refusing pane (and how a queued event's pane is matched), which this single-pane scenario doesn't exercise differently, and in the end-of-review sync with main, which merged without conflicts in this PR's files.
Testing
pty-spawn-native-chat-owned-resume.test.ts(real spawn handler and ownership rule): a chat-owned resume starts a plain shell, never types the resume, reports both result fields, carries no unattended-agent Git guard, and asks the host to forget it for the pane; an unowned conversation still resumes; with unreadable chat records, an automatic restore starts the plain shell and forgets nothing, while a resume the user asked for still runs.server-ai-vault-liveness.test.ts: the hook server forgets only the refused conversation's row, leaves no resumable copy, and tells the window for each refusal.forget-refused-pane-resume.test.ts(real store): the pane's saved record, its copy under an older numeric key of the same tab, its launch config and its tab title are forgotten; a split sibling's copy, another tab's copy and the chat's own tab title are kept.native-chat-owned-resume.test.ts: SSH and WSL spawns never consult the host's chat records.pty-transport-recycled-pty-incarnation.test.ts: a shell that replaced a resume and exited first still reports the resume unavailable.Ablation: with the refusal disabled, or with the host's forget step removed, the plain-shell test fails.
pnpm tc:node,pnpm tc:weband the repo's static checks pass.I manually tested these changes locally
Automated tests added/updated, or explained why not below
Review
Agent skill upstream boundary
Notes
Cross-platform: no platform-specific code; Windows and macOS/Linux take the same path, WSL is skipped as above. SSH: unchanged in this PR (see limits). Folder workspaces: ownership is by conversation, not workspace kind. Performance: the chat records are read only when a pane asks to resume a conversation, through the store native chat already opens. Backwards compatibility: optional local fields, no stored format change.
X: @BrennanKB5