Skip to content

Refuse chat resume until terminal writers have exited - #29242

Merged
brennanb2025 merged 97 commits into
mainfrom
brennanb2025/flip-pr1-stop-before-adopt
Oct 19, 2026
Merged

brennanb2025 merged 97 commits into
mainfrom
brennanb2025/flip-pr1-stop-before-adopt

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 15 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​830 $\color{#cf222e}{\Huge{\mathbf{−}}}$​46 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​784
Prod 46 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​999 $\color{#cf222e}{\Huge{\mathbf{−}}}$​144 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​855

ELI5

Before this change, "Resume in New Native Chat" could take over a conversation while the terminal agent that started it was still running there, for example paused with Ctrl+Z or running in the background. Resuming that agent later meant two programs writing the same conversation. Now Orca refuses while the agent may still be in the terminal it ran in, and adopts the conversation with its full history once the agent has exited.

What Changed

What the user sees:

  • From Session History, or from a terminal's header or tab menu, a conversation whose agent may still be in that terminal (running, paused with Ctrl+Z, backgrounded with & or bg, nohup, or disowned) is refused. Nothing is stopped or closed.
  • Session History shows one sentence: "Orca can't confirm Claude has exited in its terminal. Stop what's running there or close the terminal, then try again." Its failed tab keeps Retry.
  • The terminal header and tab menu end the same sentence with "…then open this conversation from Session History.", because that action disappears once the agent exits.
  • After the agent exits and the terminal is back at an idle shell prompt, the chat opens the same conversation with its full history. A prompt theme's background helper that runs in its own session (for example the async git worker in the Pure prompt) doesn't block this; checked in the real app on macOS on the current head.
  • If the conversation is already held by a native chat, "Resume in New Native Chat" from the pane header, the tab menu or Session History first checks the terminals that ran it. While one of them is busy or can't be confirmed, it refuses with the same sentence and opens no tab and no chat. Once they are all at rest, it behaves as before for a conversation another chat holds.
  • On Windows, a Git Bash pane whose ~/.bashrc starts ssh-agent no longer blocks the switch after the agent exits. Closing that pane still asks for confirmation, as on main, because closing really does end ssh-agent.

How it works:

  • A record per terminal process. Each time an agent's status hook reports a conversation from a local terminal, Orca notes "this terminal process ran conversation C". The note lives on the terminal's in-memory process record, tagged with that process's incarnation, so it lasts exactly as long as that terminal process. Moving the pane to another tab, or a later run overwriting the status row, cannot hide it.
  • Each check looks at that terminal's own process tree. Adoption inspects every live terminal process that ran the conversation, plus the terminal the header action came from. On macOS and Linux it admits only when the terminal's shell is in the foreground and no process the shell started is still in the terminal's session. A process that started its own session below the shell (the zpty worker an async prompt uses) has left the terminal and is not counted; suspended, background, nohup and disowned jobs are counted. On Windows the check uses the terminal's process job, with the Git for Windows bash launcher chain treated as the shell; for adoption only, a job member that is both off the shell's live parent chain and detached from the pane's console (an rc-started ssh-agent) is not counted.
  • Windows background daemons, for adoption only. When the adoption check reads a Windows terminal's job, it sets a member aside only if that member is both unreachable from the shell through live parent processes and detached from the pane's console. An rc-started ssh-agent is both, so it no longer blocks. Jobs started with &, nohup, Ctrl+Z, and agents launched from a script stay attached to the console, so they still count. Only the adoption read asks for this, through one new optional request field, ignoreDetached. Closing a pane, tab or window, and the cleanup of finished run terminals, keep the full job census and its confirm dialog, because closing a Windows pane ends its whole job.
  • A conversation another chat already holds. When adoption finds the conversation owned by a native chat, it runs the same terminal check before reporting that owner. A live or unconfirmed terminal returns the normal refusal, so nothing opens; a terminal at rest gets the existing owner behaviour.
  • Escaped agents. While the terminal the conversation ran in is still open, if the agent's hooks reported its process, a link also refuses while that process is still alive or suspended, even when the terminal itself looks idle (detached tmux, nohup, disown).
  • Restarted app. After an app restart, a reloaded status row or saved checkpoint is checked against the pane's current terminal, so a conversation whose agent already exited is adopted once that terminal is idle.
  • Windows steady-state status polls of local terminals read the 500 ms cached process table; every read that decides adoption or close stays fresh. A paired Windows Orca server still reads fresh for its clients' status polls (temporary, see below).

Why

Chat ownership alone could not see a terminal writer, so two writers on one conversation were possible. The common pattern keeps a registry of the executions that host a conversation, alive until the execution ends, and asks it before attaching a new writer. Orca's execution unit here is the terminal, because Orca does not own a process the user started, so the registry lives on the terminal's process record and dies with it. Refusing with a clear next step avoids building stop operations that can outlive their recovery surface. Counting jobs in the terminal's session, rather than every process the shell ever started, keeps prompt helpers from blocking a switch without a fragile list of helper names. On Windows, adoption and close ask different questions: adoption asks "could something here still write this conversation?", while close asks "would closing end something the user cares about?". That is why only adoption sets detached daemons aside.

Linked Issue

Part of STA-10705. The base PR #29150 has merged; this PR now targets main.

Visual Proof

Real Electron QA screenshots are in the review status comment. macOS QA passed on the current head 4f956b5; Windows QA passed on 7caa594 (one test-only commit and the owned-conversation commit before the current head; that commit is platform-neutral).

Testing

  • Automated, on the real terminal host: the header path with no records; a restored row with no in-memory identity; Ctrl+Z followed by a second run that overwrote the row, then a pane move; a live, suspended and exited agent process; an async prompt worker with its own git child (admits) beside a suspended or background job (refuses); an OpenCode ownership proof that survives an unmarked copy; an uninspectable terminal; a tmux-published event; and a new terminal process dropping the old links.
  • Windows, on a real Git Bash pane: a detached ssh-agent is set aside for the adoption read, never reads as idle for close, and a later sleep 60 & still counts for adoption.
  • Also covered: the adoption replay through the RPC path with a real shell-only source terminal, both refusal copies, and the owned-conversation case (a live source terminal returns the writer refusal with no owner; at rest the owner comes back).
  • Real-app QA on macOS, on the exact current head 4f956b5: after Claude exits, History adopts the conversation with the same history as the terminal; a live Claude and a Claude paused with Ctrl+Z refuse; with the Pure prompt, an idle terminal adopts, a background sleep refuses, and the conversation is adopted once it exits; for a conversation an earlier chat adopted, the header's "Resume in New Native Chat" refuses while Claude runs in the terminal and the chat opens after Claude exits.
  • Real-app QA on Windows, on 7caa594: an rc-started ssh-agent pane is adopted after the agent exits, with the console helper running from the terminal daemon; a live Claude launched from a script refuses from History and from the header; sleep & and nohup sleep & refuse; closing still asks for confirmation; reopening a conversation an earlier chat holds from the pane header refuses while Claude runs in that terminal. Since 7caa594 only platform-neutral code in the chat-create path changed (the owned-conversation check). Ctrl+Z and a bare nohup claude & couldn't be tested on Windows, because native Windows Claude doesn't suspend and exits by itself under a bare nohup; the macOS run covers both.
  • Not run on Linux: the session-leader rule is shared POSIX code that reads s from ps stat, verified on macOS. The light Linux ps check was blocked (no zsh on the shared Linux box).
  • Full static analysis, typecheck and tests run in CI.

Review

Six review rounds, then three bounded reviews of the later fixes (the adoption-only Windows rule, the header reopen check, and the owned-conversation check on the current head). Every finding is fixed or labelled below. The earlier automatic-stop design is removed from this PR; #29367 is parked.

Agent skill upstream boundary

  • Not applicable; no upstream skill-installer material is copied.

Notes

Reliability class: agent-session.provider-ownership. The adoption check runs only when the user acts and adds no saved data. It adds one optional request field to the terminal daemon, ignoreDetached, sent only by the Windows adoption read. Older Orca clients never send it. An older or relocated terminal daemon ignores it and keeps the full job census until it restarts, so the ssh-agent over-refusal stays with that daemon until then (temporary, see below). On Windows, status polls of a Git Bash terminal now read the process table to judge the terminal's process job (see the temporary list).

SSH and remote: remote and WSL terminals refuse while open, and loss of contact is never treated as exit. No conversation links are recorded for SSH terminals; status rows and saved records that belong to an SSH connection are checked by their own host, and an older host keeps its existing behaviour. SSH terminals, including on Windows hosts, refuse while open.

Intended differences

  • Only the terminal the conversation came from is protected, while it is open. If you run the same conversation yourself in another terminal that never reported it to Orca, Orca can't see it.
  • "Resume in New Native Chat" never stops or closes your terminal; there is no stop dialog.
  • One message covers every terminal refusal ("the agent is still running", "something else is running there", "Orca can't inspect that terminal"), because the host's refusal doesn't distinguish them today. Another native chat holding the conversation still shows "Another chat is already holding this conversation."
  • A terminal that ran the conversation and now runs something else (a dev server, another agent, a suspended or background job) refuses until that work ends or the terminal closes.
  • A conversation another chat already holds uses the same terminal check as a first adoption. So if a terminal that once ran it is now busy (for example a dev server started after the switch), "Resume in New Native Chat" refuses even though a chat holds the conversation. Open that chat from its tab instead.
  • The exit check counts the processes the shell started that are still in the terminal's session, not every process the shell ever started. A helper that left the session, such as an async prompt worker, doesn't block switching; background, suspended, nohup and disowned jobs still refuse. The common pattern judges only the terminal's foreground or ignores helpers by name; Orca also refuses background jobs because it prevents two writers, and it uses no name list.
  • A helper that stays in the terminal's session (for example a coproc from rc files) refuses until it exits, because it can't be told apart from a job.
  • A process the shell itself starts in a new session (for example zsh's zpty) isn't seen by the terminal check, because the check sets aside only a session leader below the shell, together with its subtree. Claude is still caught by its process check while the terminal the conversation ran in is still open. Wrappers such as script, expect, sudo with use_pty and setsid -w stay in the terminal's session, so they are counted and refuse. Launches that detach to run on their own (setsid without -w, detached tmux) were never in the terminal's process tree; they fall under the escaped-agent limit in the temporary list.
  • On Windows, with no backstop: an agent hosted in MSYS tmux, screen or setsid, or launched with cmd /c start claude, is detached from the console and has lost its parent chain, so the terminal check doesn't see it. Windows hooks report no agent process to probe, so the escaped-agent check can't catch it either.
  • An adoption click on an affected Windows terminal starts a short helper to list the processes attached to the pane's console. Closing a terminal never starts it.
  • WSL terminals refuse until closed, because Orca can't inspect their processes from Windows.
  • After an app restart, the pane's current terminal decides: a saved or reloaded record is cleared once that terminal is idle at a shell prompt.
  • History waits for the check before switching to a workspace with sleeping agents, and a refusal leaves that workspace asleep.
  • Windows "has running work" comes from the terminal's process job everywhere, with the Git Bash launcher chain treated as the shell: an idle Git Bash prompt no longer asks for close confirmation, finished headless runs become eligible for normal cleanup, and real background work stays protected. A pane whose rc starts ssh-agent, or that launched a detached window (for example gitk & or cmd /c start), still asks before closing, as on main.
  • Windows launch-readiness polls still read the process table fresh; they are short bursts at agent launch.
  • Terminal links are kept in memory, following the common pattern of a registry that lives until the execution ends.

Temporary differences

  • Sends from a chat that already holds the conversation (an open tab, a native Session History row, a restart, or auto-resume on launch) aren't checked against a terminal where the user later resumed the same conversation, so the next send can start a second writer while that terminal agent runs. Only "Resume in New Native Chat" checks today. Follow-up: a check before every agent start in a chat (step 3, fix: a terminal pane no longer resumes a conversation a native chat owns #28288).
  • The escaped-agent check (detached tmux, nohup, disown) works only while the terminal the conversation ran in is still open; once that terminal closes, Orca treats its work as finished. It covers Claude on macOS and Linux only, because today only Claude's hooks report its process. Codex in a detached tmux is still admitted, as before. Extending process capture belongs to the agent-presence work.
  • Unknown or shared OpenCode (OpenCode 1.x shared servers, or OpenCode whose ownership of the terminal Orca can't prove) refuses until its terminal closes.
  • Restarting the app loses the in-memory links: a suspended agent in a terminal that survived the restart, whose status row and saved record are gone, isn't seen. Follow-up: the session-ownership work (step 3, fix: a terminal pane no longer resumes a conversation a native chat owns #28288).
  • A writer started in the terminal between Orca's check and the chat starting isn't caught. Follow-up: step 3.
  • With agent status hooks turned off, nothing is checked. Follow-up: step 5.
  • An older or relocated Windows terminal daemon keeps the full job census for adoption until it restarts after the update, so an rc-started ssh-agent pane on it still refuses until the terminal closes.
  • Windows Git Bash terminals now read the process table on status polls. Polls of several panes share one cached read every 500 ms, but a single pane still reads about once per poll while an agent is in the foreground. Follow-up: measure the cost on Windows and trim it if needed.
  • On those polls, a job member started in the last 500 ms can be missing from the cached table. That only delays the completion indicator; it never allows an adoption, because the adoption check always reads fresh.
  • A paired Windows Orca server answers its clients' status polls with a fresh process-table read; only local panes use the cached table.
  • An adoption click can start the short console-list helper up to three times for the same Windows terminal (once per check of that terminal: as the source, as a status row and as a link). Follow-up: reuse one reading per click.
  • Older Orca clients talking to a newer host show "Another chat is already holding this conversation." for a terminal refusal, because no new refusal reason was added.
  • Pre-existing on main, tracked in STA-11125: after a native chat tab is closed, Session History's "Resume in New Native Chat" shows a generic failure instead of opening that chat, because History handles only one of the two refusal codes that name an owning chat.
  • Pre-existing upstream, tracked in [Bug]: Cleanly exited Claude session is auto-resumed with claude --resume after an app restart (local macOS, hibernation off) #14228 (by code trace; base not rerun): after a cold restart, Orca can relaunch a Claude that had already exited.

Checklist

  • Scope and user-facing before/after described.
  • Current-head review, CI and real-app QA complete.
  • Before/after screenshots attached.

…2025/rcc-b-source-admission

# Conflicts:
#	src/main/native-chat/agent-session-journal/journal-legacy-import.ts
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/rcc-c-cli-to-native to main October 18, 2026 22:22
@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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: 1aa641d2-fa5c-4e15-9769-bf0e7c059ab0
📥 Commits

Reviewing files that changed from the base of the PR and between 14b5b55 and 7caa594.

📒 Files selected for processing: 2
  • src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts
  • src/main/runtime/session-tabs/structured-session-create-support.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

The change adds CLI-writer topology and observation tracking to hook status processing. Structured-session adoption now checks hook observations and terminal process evidence before adopting a conversation. Windows PTY inspection uses shell and process identity data, and rejects results when the inspected session or PTY changes. Renderer resume handling defers worktree activation for sleeping sessions and displays CLI exit guidance for applicable refusals.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 7caa5

The Session History resume flow can show a generic failure instead of useful refusal guidance. That feedback should be corrected, but the inspected adoption paths do not show a writer-safety failure. A separate tab-order defect remains unchanged by this PR.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 37.80% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 82 functions across 79 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed 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 and concisely describes the primary change: blocking chat resume until terminal writers exit.
Description check Passed The description is detailed and covers the required explanation, behavior changes, rationale, testing, limitations, review notes, and cross-platform considerations. Visual proof and some checklist ite…
Full details: Description check

Explanation

The description is detailed and covers the required explanation, behavior changes, rationale, testing, limitations, review notes, and cross-platform considerations. Visual proof and some checklist items remain pending, but the description is otherwise substantially complete.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · 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: 2

🧹 Nitpick comments: 1
src/renderer/src/components/terminal-pane/TerminalPaneResumeInChatButton.tsx: 1

19-26: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Avoid resolving the full resume source in the reactive selector.

useShallow prevents unnecessary re-renders, but the selector still runs on store updates. resolveCliChatResumeSource allocates pane state and performs host and launch-route checks. Keep the reactive selector cheap, or resolve the full source from current state in the click handler, as CliChatResumeMenuItems does.


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1372160a-221d-42fa-bfe6-61ffebaa35dc
📥 Commits

Reviewing files that changed from the base of the PR and between ecd83b7 and 5fa7777.

📒 Files selected for processing: 160
  • config/scripts/vitest-node-runtime-files.mjs
  • src/main/agent-hooks/agent-hook-runtime-deps.ts
  • src/main/agent-hooks/server-agent-presence.test.ts
  • src/main/agent-hooks/server-opencode-writer-observation.test.ts
  • src/main/agent-hooks/server/cli-writer-observation.ts
  • src/main/agent-hooks/server/server-agent-presence.ts
  • src/main/agent-hooks/server/server-authority-aliases.ts
  • src/main/agent-hooks/server/server-ingest-normalization.ts
  • src/main/agent-hooks/server/server-ingest-terminal.ts
  • src/main/agent-hooks/server/server-listeners.ts
  • src/main/agent-hooks/server/server-status-application.ts
  • src/main/agent-hooks/server/server-status-update.ts
  • src/main/agent-hooks/server/server-tmux-status.ts
  • src/main/ai-vault/session-scanner-discovery-wsl-gate.test.ts
  • src/main/ai-vault/session-scanner-discovery.ts
  • src/main/claude/claude-cli-conversations.ts
  • src/main/codex/codex-ai-vault-session-resume.ts
  • src/main/codex/codex-cli-conversations.ts
  • src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts
  • src/main/daemon/pty-subprocess/spawn-file-child-processes.ts
  • src/main/daemon/pty-subprocess/subprocess-handle.ts
  • src/main/daemon/session-subprocess-handle.ts
  • src/main/daemon/session.ts
  • src/main/daemon/terminal-host-non-agent-foreground.test.ts
  • src/main/daemon/terminal-host-process-inspection.test.ts
  • src/main/daemon/terminal-host-process-inspection.ts
  • src/main/daemon/terminal-host.ts
  • src/main/grok/grok-transcript-import.ts
  • src/main/native-chat/agent-session-journal/adopted-transcript-source.ts
  • src/main/native-chat/agent-session-journal/journal-legacy-import.ts
  • src/main/native-chat/agent-session-journal/journal-newest-item-window.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-attach.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-create-adoption.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-create.ts
  • src/main/native-chat/codex-adoption-preparation.ts
  • src/main/native-chat/session-file-resolver.ts
  • src/main/native-chat/structured-agent-cli-conversation-adoption.test.ts
  • src/main/native-chat/structured-agent-cli-conversations.ts
  • src/main/native-chat/structured-agent-cli-source.ts
  • src/main/native-chat/structured-agent-session-history-adoption.test.ts
  • src/main/native-chat/structured-agent-session-history-adoption.ts
  • src/main/native-chat/structured-agent-session-source-admission.test.ts
  • src/main/native-chat/transcript-opencode-sqlite-query.test.ts
  • src/main/native-chat/transcript-opencode-sqlite-query.ts
  • src/main/native-chat/transcript-opencode.test.ts
  • src/main/native-chat/transcript-opencode.ts
  • src/main/native-chat/transcript-opencode2-sqlite.test.ts
  • src/main/native-chat/transcript-opencode2-sqlite.ts
  • src/main/native-chat/transcript-path-admission.ts
  • src/main/native-chat/transcript-reader.ts
  • src/main/omp/omp-session-cwd-lookup.ts
  • src/main/omp/omp-transcript-import.ts
  • src/main/opencode/opencode-transcript-import.ts
  • src/main/orcad/orcad-entry.ts
  • src/main/pi/pi-transcript-import.ts
  • src/main/providers/local-pty-child-process-verdict.test.ts
  • src/main/providers/local-pty-foreground-inspection.ts
  • src/main/providers/local-pty-provider-foreground-process.test.ts
  • src/main/providers/local-pty-provider.ts
  • src/main/runtime/agent-session-founding-admission.ts
  • src/main/runtime/agent-session-reservation-admission.ts
  • src/main/runtime/agent-status-observed-pane-identity.ts
  • src/main/runtime/construction/runtime-construction-dependencies.ts
  • src/main/runtime/construction/runtime-construction.ts
  • src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts
  • src/main/runtime/rpc/core.ts
  • src/main/runtime/rpc/dispatcher-stream-options.ts
  • src/main/runtime/rpc/methods/orchestration/worker/fleet-status-observed-identity.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-create-handler.ts
  • src/main/runtime/rpc/methods/structured-agent-session-create.ts
  • src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.ts
  • src/main/runtime/rpc/methods/structured-agent-session.test.ts
  • src/main/runtime/runtime-terminal-state-records.ts
  • src/main/runtime/session-tabs/structured-session-account-home.ts
  • src/main/runtime/session-tabs/structured-session-cli-writer-host.ts
  • src/main/runtime/session-tabs/structured-session-create-support.ts
  • src/main/runtime/structured-agent-cli-writers.test.ts
  • src/main/runtime/structured-agent-cli-writers.ts
  • src/main/runtime/structured-agent-session-create-adoption.ts
  • src/main/startup/main-process-runtime-service.ts
  • src/renderer/src/app-shell/use-app-startup-hydration.ts
  • src/renderer/src/components/native-chat/structured-agent-session-status-bridge-unconfirmed-start.test.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionSurfaceSwitchMenuItems.tsx
  • src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.test.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.ts
  • src/renderer/src/components/tab-bar/CliChatResumeMenuItems.tsx
  • src/renderer/src/components/tab-bar/SortableTab.tsx
  • src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx
  • src/renderer/src/components/tab-bar/TabSessionSurfaceSwitchMenuItems.test.tsx
  • src/renderer/src/components/tab-bar/TabSessionSurfaceSwitchMenuItems.tsx
  • src/renderer/src/components/tab-bar/cli-chat-resume-feedback.ts
  • src/renderer/src/components/tab-bar/cli-chat-resume-run.test.ts
  • src/renderer/src/components/tab-bar/cli-chat-resume-run.ts
  • src/renderer/src/components/tab-bar/cli-chat-resume-source.ts
  • src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts
  • src/renderer/src/components/tab-bar/tab-session-history-switch.ts
  • src/renderer/src/components/tab-bar/use-sortable-tab-context-menu.test.ts
  • src/renderer/src/components/tab-bar/use-sortable-tab-context-menu.ts
  • src/renderer/src/components/terminal-pane/TerminalPaneResumeInChatButton.test.tsx
  • src/renderer/src/components/terminal-pane/TerminalPaneResumeInChatButton.tsx
  • src/renderer/src/hooks/ipc-events/session-tab-ipc-bridge.ts
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/lib/agent-session-launch-plan.ts
  • src/renderer/src/lib/ai-vault-tab-title-requests.ts
  • src/renderer/src/lib/cli-conversation-exit-guidance.ts
  • src/renderer/src/lib/launch-structured-agent-session.ts
  • src/renderer/src/lib/structured-agent-launch-settlement.ts
  • src/renderer/src/lib/structured-agent-session-launch-admission.test.ts
  • src/renderer/src/lib/structured-agent-session-launch-callers.ts
  • src/renderer/src/lib/structured-agent-session-launch-cancellation.test.ts
  • src/renderer/src/lib/structured-agent-session-launch-cancellation.ts
  • src/renderer/src/lib/structured-agent-session-launch-create-call.ts
  • src/renderer/src/lib/structured-agent-session-launch-errors.ts
  • src/renderer/src/lib/structured-agent-session-launch-exited-owner.test.ts
  • src/renderer/src/lib/structured-agent-session-launch-failure.ts
  • src/renderer/src/lib/structured-agent-session-launch-outcome-tracking.ts
  • src/renderer/src/lib/structured-agent-session-launch-persistence.test.ts
  • src/renderer/src/lib/structured-agent-session-launch-persistence.ts
  • src/renderer/src/lib/structured-agent-session-launch-record.ts
  • src/renderer/src/lib/structured-agent-session-launch-registry.ts
  • src/renderer/src/lib/structured-agent-session-launch-reload.ts
  • src/renderer/src/lib/structured-agent-session-launch-resume-identity.test.ts
  • src/renderer/src/lib/structured-agent-session-launch-surface.ts
  • src/renderer/src/lib/structured-agent-session-launch.ts
  • src/renderer/src/lib/structured-agent-session-provisional-tab.ts
  • src/renderer/src/lib/workspace-session-hydration-keys.ts
  • src/renderer/src/lib/workspace-session-unified-tabs.ts
  • src/renderer/src/runtime/paired-host-structured-launch-bookkeeping.test.ts
  • src/renderer/src/runtime/structured-agent-session-client.ts
  • src/renderer/src/runtime/structured-agent-session-tab-retirement.test.ts
  • src/renderer/src/runtime/structured-agent-session-tab-retirement.ts
  • src/renderer/src/runtime/web-session-client-owned-tab-placement.ts
  • src/renderer/src/runtime/web-session-tabs-sync/terminal-surfaces.ts
  • src/renderer/src/store/slices/tab-group-state.ts
  • src/renderer/src/store/slices/tabs-hydration.ts
  • src/renderer/src/store/slices/tabs/structured-chat-close-owner.test.ts
  • src/renderer/src/store/slices/tabs/tabs-close-actions.ts
  • src/renderer/src/store/slices/tabs/tabs-create-actions.ts
  • src/renderer/src/store/slices/tabs/tabs-drop-actions.ts
  • src/renderer/src/store/slices/tabs/tabs-group-actions.ts
  • src/renderer/src/store/slices/tabs/tabs-label-actions.ts
  • src/renderer/src/store/slices/tabs/tabs-move-actions.ts
  • src/renderer/src/store/slices/tabs/tabs-reconciliation.ts
  • src/renderer/src/store/slices/tabs/tabs-slice-contract.ts
  • src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts
  • src/shared/agent-session-definitive-refusal.ts
  • src/shared/agent-session-refusal-details.ts
  • src/shared/agent-session-refusal-notice.test.ts
  • src/shared/agent-session-refusal-reason-words.ts
  • src/shared/protocol-version.ts
  • src/shared/rpc-contract/structured-agent-session-params.ts
  • src/shared/structured-agent-session-create.test.ts
  • src/shared/structured-agent-session-create.ts
  • src/shared/structured-agent-session-mutation.ts
  • src/shared/structured-agent-session-surface-capabilities.ts
  • src/shared/workspace-layout/tab-order.ts

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

…check

- The POSIX shell-alone proof skips a session leader below the shell and
  everything under it: a prompt's zpty worker has left the terminal, while
  suspended, background, nohup and disowned jobs still refuse.
- Windows steady-state polls read the cached process table; every read that
  decides adoption or close stays fresh.
- The pane header and tab menu point back to Session History, since their
  action is gone once the agent exits.
…-stop-before-adopt

# Conflicts:
#	src/main/ai-vault/session-scanner-discovery.ts
#	src/main/claude/claude-cli-conversations.ts
#	src/main/codex/codex-cli-conversations.ts
#	src/main/grok/grok-transcript-import.ts
#	src/main/native-chat/structured-agent-cli-conversations.ts
#	src/main/native-chat/structured-agent-cli-source.ts
#	src/main/native-chat/structured-agent-session-source-admission.test.ts
#	src/main/native-chat/transcript-opencode-sqlite-query.ts
#	src/main/omp/omp-transcript-import.ts
#	src/main/opencode/opencode-transcript-import.ts
#	src/main/pi/pi-transcript-import.ts
#	src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts
#	src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts
#	src/main/runtime/session-tabs/structured-session-create-support.ts
#	src/main/runtime/structured-agent-session-create-adoption.ts
#	src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.test.ts
#	src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.ts
#	src/renderer/src/components/tab-bar/cli-chat-resume-feedback.ts
#	src/renderer/src/components/tab-bar/cli-chat-resume-run.test.ts
#	src/renderer/src/components/tab-bar/cli-chat-resume-run.ts
#	src/renderer/src/i18n/en-runtime-required.json
#	src/renderer/src/lib/launch-cancel-tombstones-compat/structured-agent-session-launch-cancellation.test.ts
#	src/renderer/src/lib/launch-cancel-tombstones-compat/structured-agent-session-launch-cancellation.ts
#	src/renderer/src/lib/structured-agent-session-launch-outcome-tracking.ts
#	src/renderer/src/lib/structured-agent-session-launch-surface.ts
#	src/renderer/src/runtime/paired-host-structured-launch-bookkeeping.test.ts
#	src/renderer/src/runtime/structured-agent-session-tab-retirement.test.ts
#	src/renderer/src/runtime/structured-agent-session-tab-retirement.ts
#	src/renderer/src/store/slices/tabs/tabs-slice-contract.ts
#	src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts
A pane's job keeps every process it ever started, including daemons whose
parent exited (an rc-started ssh-agent), so such panes never read as an idle
shell and the chat switch refused forever. Walk descendants of the shell along
live parent links within the job, for every shell; the Git Bash launcher keeps
its bash-chain allowance and a child older than its parent still refuses.
… the shell"

MSYS exec and fork leave intermediate Win32 parents exited, so a live Claude
started through a script (and 'sleep 60 &') became unreachable and the chat
switch admitted while it ran. Back to the job census until a console-based
design is measured.
…cision

A pane's job keeps every process ever started in it, so an rc-started
ssh-agent made a Git Bash pane read as busy forever and the chat switch
refused. MSYS fork/exec leaves live work under exited Win32 parents, so parent
links alone can't tell the daemon apart; it is the one that also left the
console. On a scanChildProcesses decision read (switch admission, run-terminal
close, close guards), a member neither linked to the shell nor attached to its
console no longer counts. Polls and shell confirmation keep the job census.
The console-list helper now forks through the process-host chokepoint.
Keeps the daemon router and terminal host within their line budget now that
the options carry scanChildProcesses.
…r close

Closing a Windows pane kills its whole job, so a close check that ignored a
detached process (gitk &, cmd /c start ...) would kill it without asking.
Adoption asks whether anything is still in the terminal; close asks whether
closing destroys something. Only the adoption read now sends ignoreDetached;
close guards and the run-terminal sweep keep the job census. The win32 test
also stops the ssh-agent it starts.
…ning its chat

When the conversation already had a native chat, the switch hit the
ownership conflict first and the header opened that chat, even while the
terminal's agent (a later claude --resume) was still running. The switched-out
terminal is now checked before the conflict, so a live one refuses as usual
and one at rest still opens the owning chat.
A plain read can answer unverifiable while the eval's short-lived processes
exit; either way it must not read as no-children.
… for any switch

Session History's Resume in New Native Chat carries no source pane, so with an
earlier chat still holding the conversation it skipped the terminal check and
the owning chat's first send would start a second writer beside a live
terminal Claude. Every adoption request now runs the writer check before the
owner conflict, as the no-owner path already does.
…-stop-before-adopt

# Conflicts:
#	src/main/startup/main-process-runtime-service.ts
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review loop status: ready for Brennan's review (head 59c72b3)

The problem

"Resume in New Native Chat" (pane header, tab menu, Session History) could open a native chat on a conversation that a Claude CLI in a terminal was still writing. The user saw two writers on one conversation: the most visible case was a Claude paused with Ctrl+Z. The shell prompt was back, so Orca took the terminal as idle and adopted the conversation, and fg then resumed the paused Claude on the same transcript. The cause was that adoption only looked at terminals Orca's status records still named, and those records can be overwritten, cleared or moved while the agent is still alive.

What changes for the user

  • While the agent may still be in the terminal the conversation ran in, Resume in New Native Chat refuses. That covers working, idle, paused with Ctrl+Z, backgrounded, a Claude left in detached tmux or under nohup while that terminal is open, or Orca can't tell. The message is: "Orca can't confirm Claude has exited in its terminal. Stop what's running there or close the terminal, then try again." From the pane header it ends "...then open this conversation from Session History." History keeps its usual failed tab with Retry; the header opens no tab.
  • After the agent exits, the chat opens on the same conversation with full history, including after an Orca restart.
  • If a native chat already holds the conversation and it was later resumed in a terminal, the same check runs first, so the old chat isn't reopened next to a live terminal Claude.
  • Nothing is stopped or killed (Brennan's option 3). Closing a terminal keeps main's behaviour and confirm dialog.

What the loop fixed (8 review rounds plus bounded reviews; design panels on the repeated cases)

  • Ctrl+Z adoption: the check now follows the terminal's running process, not status rows. Each terminal process keeps an in-memory list of the conversations it ran, and adoption inspects that terminal's process tree, the source terminal first.
  • Escaped and moved agents: pane moves, record overwrites and capacity eviction can no longer hide a live terminal. tmux events never count as pane-owned. Claude's own reported process is also checked, so a Claude that left the terminal's process tree but is still alive refuses (macOS/Linux).
  • zsh-async prompts (Pure, spaceship): these no longer refuse forever after exit. A session leader below the shell is ignored; &, nohup, Ctrl+Z and disown jobs still count.
  • Windows Git Bash with an rc ssh-agent: this no longer refuses forever. Adoption ignores a job member only if it is both off the shell's live parent chain and detached from the pane's console. An earlier attempt was reverted because Git Bash breaks Win32 parent links. Close and run cleanup keep the full job census, so detached windows still get the confirm dialog.
  • Already-adopted conversation resumed in a terminal: the header and Session History both refuse while that terminal is live.
  • Smaller fixes: truthful refusal copy; restored panes adopt after exit; Windows status polls use the cached process table; CI fixture fixes; the PR's tests added to the provider-ownership reliability gate.

Deferred or labelled (the full list is in the PR body; the new refusal copy is English-only for now, and other locales fall back to English as before)

  • Temporary: sends from a chat that already holds the conversation (open tab, native History row, restart, auto-resume) aren't checked against terminals where the user resumed it later. Follow-up: check before every provider start (step 3 / fix: a terminal pane no longer resumes a conversation a native chat owns #28288). Also the inspect-to-start race (step 3), hooks off (step 5), the in-memory list lost on app restart, old or relocated terminal daemons keeping the full census until they restart, paired Windows servers reading fresh on polls, and up to three console-helper starts per adoption click on Windows.
  • Intended:
    • WSL and SSH terminals refuse while open.
    • A busy terminal that once ran the conversation refuses even when a chat already holds it.
    • On Windows, an agent detached from the console with no live parent chain (MSYS tmux, screen or setsid, or cmd /c start claude) isn't detected, because Windows hooks report no process.
    • Unknown or shared OpenCode refuses.
  • Pre-existing, outside this PR:

Verified

  • CI: the full PR workflow is green on 59c72b3 (run 39194980084; 39 passed, 15 skipped, none failed), and GitHub reports it mergeable with no conflicts. Two outdated automated review threads were resolved: one about the Session History notifier is the pre-existing STA-11125, and the other is in a file that left this PR when the base merged.
  • macOS real-app QA on 4f956b5: that head's code is identical to 59c72b3 apart from the two main merges and the gate config. Results: exit then History adopts with the same history; live and Ctrl+Z refuse; Pure idle adopts, background sleep refuses, then adopts; an adopted conversation resumed in a terminal refuses from the header while live and reopens after exit. Earlier Mac rounds also covered pane move, overwrite, bg/disown, OpenCode 2 and restart.
  • Windows real-app QA on 7caa594: only platform-neutral create code changed since. Results: rc ssh-agent adopts after exit; a script-launched live Claude refuses from History and the header; sleep & and nohup sleep & refuse; closing panes with background or detached work asks for confirmation; header reopen refuses.
  • Raw evidence: process tables, traces and history comparisons were checked first-hand against the QA reports, with manifests hash-verified.
  • Not verified:
    • Windows Ctrl+Z and bare nohup claude & can't be exercised, because native Windows Claude doesn't suspend and exits on its own under bare nohup. macOS covers both.
    • Linux was not run in the app. The session-leader rule is shared POSIX code that reads s from ps stat; it was verified on macOS. The light Linux ps check was blocked because the shared Linux box has no zsh, and installing it there isn't ours to do.
    • The original H8 handler trace from the first failure was lost, so the source-verified paths are what's closed.

For Brennan's review

macOS, base (ccdfcd6) vs branch: Pure prompt, /exit, then Session History. Both adopt; the branch must not regress this.
Base ccdfcd6: Pure, exit, History adopts
Branch ccc73c6: Pure, exit, History adopts

macOS, base vs branch: Claude exited and sleep running in that terminal. Base adopts (the sleep terminal is out of frame; the process table shows it running). The branch refuses with the new message and History's usual failed tab.
Base ccdfcd6: adopts while sleep runs
Branch ccc73c6: refuses while sleep runs

macOS on 4f956b5: adopt after exit; header refusal while Claude is live (no tab opened); header refusal for an already-adopted conversation resumed in the terminal.
4f956b5: adopt after exit
4f956b5: header refuses while live
4f956b5: adopted conversation resumed in terminal, header refuses

Windows: Git Bash with an rc ssh-agent, adopted after /exit (14b5b55); script-launched live Claude refused from Session History (14b5b55).
Windows 14b5b55: rc ssh-agent, adopted after exit
Windows 14b5b55: script-launched live Claude, History refuses

Three places where a human judgment matters:

  1. structured-agent-cli-writers.ts:49: what "the terminal is free" means on macOS/Linux. A foreground shell with no descendants still in its session; a session leader below the shell (an async prompt worker) is ignored. Over-refusal is the deliberate direction: a terminal that ran the conversation and now runs a dev server refuses until it's stopped.
  2. windows-shell-alone-in-job.ts:79: the Windows rule, adoption only. A member is ignored only if it is both off the shell's live parent chain and off the pane's console. The accepted gap is agents that detach from the console and lose their parents (MSYS tmux, cmd /c start).
  3. structured-session-create-support.ts:130: the terminal check runs before the "another chat holds this" path for every adoption request. The wider fix, a check before every provider start, is deferred to step 3 / fix: a terminal pane no longer resumes a conversation a native chat owns #28288.

…-stop-before-adopt

# Conflicts:
#	src/main/agent-hooks/server/server-ingest-terminal.ts
#	src/main/agent-hooks/server/server-listeners.ts
#	src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts
#	src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.ts
#	src/renderer/src/lib/structured-agent-session-launch-failure.ts
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