Repository navigation
Conversation
…nd ran When a command finishes, main (local and WSL panes) and the relay (SSH panes) apply one rule, commandEndEndsRow: an owner with a process is left to the host's process check; a process-less owner, or a row with no owner record, ends when it was that command's foreground agent, read from the host's process table (CommandForegroundTracker), or when nothing named the foreground and it reported during the command. The renderer's command-finished drop, its deferred confirm path, and the reconcileEndedProcess and hasVerifiableAgentProcess IPC are gone. The launch-specific owner end and its hook-agent map are folded into this rule; the launch keeps only its authority and token fence. A guest replayed after its owner's exit keeps its turn's prompt.
…-command-end # Conflicts: # src/main/agent-hooks/server-launch-retirement.test.ts # src/main/agent-hooks/server/server-persistence.ts
…r on main Main's command end skips only hook rows a relay holds, so an SSH row painted only from terminal output ends there. The command foreground tracker reads on live agent reports only, plus the shared start ladder when a consumer needs it: main's opencode run producer now consumes the tracker's samples instead of running its own ladder, and only on PTYs main can read. Relay replays are not activity.
…and end output-painted rows under a program The leaked-end guard read the pane's cached foreground at the command end. That cache still named an agent that had just exited (Codex Ctrl-C, Codex normal exit) and, on the relay, a job Ctrl-Z had stopped, so real ends were dropped. The end now asks a fresh read which process group holds the terminal: main uses the controller's fresh confirmForegroundProcess (local and daemon panes), the relay a fresh process table. Only a non-shell there is a leaked end; the read runs only when a row would end. A row painted from terminal output has no process of its own, so a command whose foreground read as an unrecognized program (`printf <OSC>; sleep`) is that row's agent and now ends it.
…end, not a process-table scan A fresh process-table capture costs one to several seconds on a loaded host, so a Ctrl-C'd agent's row stayed working past the end. The command end now reads node-pty's own name for the terminal's foreground process group (tcgetpgrp), which costs nothing and never lists a stopped job: local panes from the local provider, daemon panes through a new readTerminalForeground daemon request, SSH panes on the relay. Windows names only the spawned shell, and an older daemon does not know the request; both fall back to the fresh confirmForegroundProcess.
… runtime too orcad built the runtime without the hook server's launch and command ends, so its panes never ran the host command-end rule the desktop runs.
This was referenced Oct 17, 2026
Closed
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ELI5
When a command finishes in a terminal, Orca decides whether the agent it ran has stopped. Until now the Orca window made that decision. The window cannot see the processes behind a pane, so it sometimes hid the wrong agent. Now the machine that runs the terminal decides: the desktop for local and WSL terminals, the remote relay for SSH terminals. The window only shows the result.
What Changed
Before:
kill -9) while a Codex it started in the background waits behind it. Codex takes the pane, then the window hides Codex's row as soon as the shell prompt returns (over SSH, about 7 seconds later). Codex reappears on its next tool event, and locally that row has lost its prompt.orca serveand the phone had no such cleanup at all.After:
printf <status>; sleep 6).codex &): a later command such aslsno longer ends it.Mechanism: one shared rule,
commandEndEndsRow(src/shared/agent-command-end.ts).endCommand): local, WSL, and SSH rows painted only from terminal output, which no relay holds. Main cannot read an SSH foreground, so for those rows the "no read named it" branch applies.What counts as "the command's foreground agent" comes from the host's process table (
CommandForegroundTracker, src/shared/command-foreground-tracker.ts):opencode runproducer, which has no reports to wait for. It runs only on PTYs main can read (local, not SSH or WSL), and it retries only while a shell or launcher still holds the foreground.opencode runproducer no longer reads the foreground itself; it consumes the tracker's samples. The window's own foreground tracker now serves only presentation (routing, the pane's agent icon, title and mode cleanup, its launch registry), so it stays.tcgetpgrp): instant, never cached, and a job stopped with Ctrl-Z is not in it. Local panes read it from the local provider, daemon panes through a newreadTerminalForegrounddaemon request, SSH panes on the relay. Windows names only the spawned shell, and an older daemon does not know the request; both fall back to the existing freshconfirmForegroundProcess. The read runs only when a row would end.One rule, not two: #26434's launch-specific owner end is deleted, along with the relay's per-terminal launch agent and the launch-to-hook-name map. A launched agent is simply its command's foreground agent. The launch keeps only what is launch-scoped: its orchestration authority and the token fence. The fence now also stops a surviving row from persisting the ended token as a restart commitment.
Removed from the window:
agentStatus:reconcileEndedProcessandagentStatus:hasVerifiableAgentProcessIPC.The window keeps only presentation: routing, title and mode cleanup, and clearing its launch registry.
Prompt loss: when an owner's exit replays the guest held behind it, the guest's prompt now seeds the pane's prompt cache. The owner's exit had cleared that cache.
Why
The execution host owns agent status (docs/reference/agent-status-store.md), and it is the only side that can tell which process held a terminal. The window's drop was a reader making a status decision, and it could not tell a detached guest from the agent that exited.
Alternatives considered:
codex &and for Ctrl-Z thenfg.Linked Issue
STA-9582 (stacked on #26434).
Visual Proof
N/A. No new UI; this changes which status rows stay visible after a command finishes. It is covered by the tests below; the e2e scenario (i) evidence is in the review thread.
Testing
server-command-end.test.ts(main):codex &: kept when another program or another agent held the command, or when it reported only before the command.relay-agent-command-end.test.ts: (b) andcodex &over SSH, and (i) over SSH.pty-handler-agent-command-end.test.ts: the relay reports the command's foreground agent, and confirms the end from node-pty's terminal group: a leaked end under Codex is not confirmed, and an exited Codex the cached sampler still names is.command-foreground-tracker.test.ts: reads only on reports unless the ladder is asked for, guest event order, an agent winning over an earlier program, unknown; the end is confirmed from the terminal read, never the cached sampler, read once, and an unreadable terminal trusts the end.local-pty-provider-foreground-process.test.ts,daemon-server.test.ts: the terminal read is node-pty's name, not the cached identity, and reads nothing on Windows.terminal-creation-and-readiness.spec.ts: the runtime confirms the end throughreadTerminalForeground.Main and relay: a leaked end keeps the row; a row painted from output ends under a program (e2e viii).
opencode-run-lifetime-status.test.ts: all 24 run-producer cases now go through the tracker, wired as the runtime wires them.An SSH row painted only from terminal output clears on main. The relay reads nothing on a cached replay.
agent-command-end.test.ts: the rule's table.Updated the window tests that asserted the drop: they now assert the row is left to the host.
Ran, all under node where needed:
src/main/agent-hooks,src/main/runtime,src/relay,src/shared,src/renderer,src/main/ipc,src/preload,src/main/startup,src/main/daemon,src/main/providers,src/main/orcad.omp-draft-launch-shell's fish case: the local~/.local/bin/env.fisherrors while being sourced. It fails on the base branch too.pnpm tc, oxlint, and the changed-code quality gate.I manually tested these changes locally (the owner matrix e2e below)
Automated tests added/updated, or explained why not below
Review
Owner matrix e2e (control, S1-S6, (i)-(ix); Electron in the background, SSH against a Docker host), on this branch merged with #26434 and #26391 plus the one-line seed resolution above: all checks pass, locally (32) and over SSH (33). Root causes found with it:
sleep, and the rule kept every row under a program. Fixed by the output-painted row rule.Agent skill upstream boundary
Notes
Where the foreground cannot be read, the rule behaves as the window's drop did: it ends the agent that reported during the command.
In those cases (i) behaves as before.
macOS: Claude's foreground name can be hidden there, but Claude has a process id, so only the process check decides for it.
SSH: the relay always runs the desktop's exact build, so there is no mixed-version gap. No wire change; the relay sends the ended row it already sends for a proven exit.
Mobile and
orca serve: they now get the same command-end behaviour, since it lives on the host. The headless runtime (orcad) now wires the launch and command ends the desktop wires.Merging with #26391: that PR keys the prompt cache by producer (
lastPromptByProducerKey). Whichever of the two merges second must change this PR's guest-replay prompt seed inserver-status-update.tstolastPromptByProducerKey.set(producerCacheKey(paneKey, source), prompt). Without it the merged tree does not typecheck, and at runtime the replay throws, so a killed owner's held guest is never shown.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)