Skip to content

fix(agent-status): ending a launched command retires only that launch - #26434

Open
Jinwoo-H wants to merge 10 commits into
jinwoo/sta-9582-pane-owner-corefrom
jinwoo/sta-9582-retire-owner
Open

Jinwoo-H wants to merge 10 commits into
jinwoo/sta-9582-pane-owner-corefrom
jinwoo/sta-9582-retire-owner

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​610 $\color{#cf222e}{\Huge{\mathbf{−}}}$​13 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​597
Prod 29 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​313 $\color{#cf222e}{\Huge{\mathbf{−}}}$​109 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​204

ELI5

When an agent that Orca started in a terminal exits, Orca resets that terminal's status. Until now the reset deleted everything on the pane: the status of an agent that had just taken the pane over, and the record Orca uses to resume the exited agent. Now Orca treats the command ending as that agent's exit, the same way it treats a confirmed process exit. The pane keeps the exited agent's resume record and passes to the agent waiting behind it.

What Changed

Before:

  • Claude, started by Orca, crashes while a detached Codex is still running in the same pane. Orca hands the pane to Codex (fix(agent-status): one owner per pane status row, so a nested agent can't relabel it (STA-9582) #26408), then the shell prompt comes back and Orca deletes Codex's row. Codex's next tool event does not bring it back; only its next prompt does. Locally the shell prompt often arrives before Orca's own exit check, so Codex never gets the pane at all.
  • Claude exits normally (/exit). Orca briefly keeps a resume record for that Claude session, then deletes it about 2 seconds later when the process ends.

After:

  • Pane passes to the waiting agent: Codex gets the pane whichever comes first, the shell prompt or the exit check, and its later events (tool use, Stop) update the row.
  • Resume record kept: the exited agent's resume record stays. Late hooks from it do not bring its row back. A new run does: a new Claude process (any event), or an event that starts a new run, as the old reset allowed.
  • Pane with no owner record, or a launch with no session to resume: reset as before.
  • SSH panes: the same happens on the remote machine. A Codex launched over SSH and stopped with Ctrl-C, or killed mid-turn, leaves its pane when the command ends, instead of showing "working" until the 30-minute fallback.

Mechanism: the machine that runs the pane decides: the desktop for local panes, the relay for SSH panes. Both use one shared rule, ownerEndedByLaunch: when the launched command finishes, the pane's owner ends if it is the launched agent, whether or not its process can be checked.

On the desktop: when a launched agent's command finishes, the runtime now calls endLaunchAuthority(paneKey, launchAgent) instead of retirePaneAuthority. PTY exit keeps retirePaneAuthority; the two have separate runtime entry points. endLaunchAuthority reads the pane's current owner (the agentPresence record from #26408):

  • The launch still owns the pane: its ownership ends through the existing proven-exit path, the same one a process check that finds the owner exited uses. That path leaves an ended resume record, then the guest event held behind that owner is replayed through the owner rule (new PaneOwnerProbes.ownerEnded, also used by the process check).
  • The launched agent is matched by the name its hooks report: Claude Agent Teams and OpenClaude report claude (getTuiAgentHookAgent).
  • WSL panes run on this machine: the desktop ends them. It uses isLocalHookConnectionId, now shared with worktree cleanup.
  • The owner is a different agent, the row comes from SSH, or only a resume record is left: the row is left as it is.
  • The launch agent is unknown: after an app restart, a restored terminal no longer knows which agent was launched. Its first command finish resets the pane as before.
  • The launch had no session to resume: ending it leaves no record, so the pane is then reset and fenced as before.
  • Every case: the launch's orchestration authority (its current and restored attestations) ends. Every process its shell starts inherits the launch token, so the desktop records that token as ended for the pane. A later event carrying it, whether a late hook or a new run in the same shell, is applied without the token and can never win back authority. The old reset's restart did the same.
  • Why a recorded token, not "record authority only for events the owner rule writes": that ordering alone still lets a new run in the same shell write its row, and with it the inherited token.
  • On the relay: each PTY now remembers the agent Orca launched in it and forgets it at the first command finish, just as the desktop's launch record does. That first finish calls the new RelayAgentHookServer.endLaunch. It ends the owner through the same exit write the relay's process check uses (now one function, endRelayPaneOwner), then replays the guest held behind it. The process check still runs on every command finish.
  • Late hooks from an ended owner: the owner rule drops an event from the ended process. When neither side reports a process, it drops a same-type event unless it starts a new run. That check is startsNewAgentRun, one function now shared with the old reset's fence (including the prompt MessagePart of OpenCode, OpenCode 2 and mimo-code). Any other type, or a different process, is admitted.
  • No owner record: the pane is reset with the existing retirePaneAuthority, which is unchanged for pane, tab and worktree close.

Why

The command ending is evidence that the launched agent is gone, which is exactly what the owner rule's exit path handles. Routing it there makes the order of signals irrelevant and removes the delete-then-re-add of the resume record from the first version of this PR.

Alternatives considered:

  • Keep the old reset and spare the new owner: this still undid the handover whenever the shell prompt arrived first.
  • Teach the renderer to keep rows: that would be reader-side policy.

Why only launched commands, and not any command that finishes: a pane where the user typed an agent was never reset before. Ending on any command finish would be worse than before in two cases. After Ctrl-Z, the agent's events would be dropped once fg resumes it. An agent started with & would be ended by the next unrelated command. Scoping to the launch matches the old reset in every case: the user quitting a typed agent, Ctrl-Z, &, and panes Orca did not launch an agent in.

Why the relay and not the desktop for SSH panes: only the relay knows the remote rows' owners. The SSH relay always runs the desktop's exact build (its handshake compares build hashes), so there is no mixed-version gap.

Known limits:

  • Agents with no new-run event (command-code): after the reset, a new run is admitted only once the terminal sees its process, as before.
  • Same-type takeover: the launch is identified by agent type, because the terminal does not know the launched agent's process or session. If the agent that takes over is the same type (a detached Claude after a launched Claude), it is treated as the launch.

Linked Issue

STA-9582 (follow-up to #26408).

Visual Proof

N/A. No UI change; this only changes which status rows survive the pane reset.

Testing

  • New server-launch-retirement.test.ts:

    • Handover, both orders: the command ends before any exit check, and the held Codex takes the pane and its next tool event is admitted; the reverse order is also covered.
    • Launch still owns the pane: its ownership ends into a resume record, a late hook from the ended process does not revive it, and a new Claude process's PreToolUse does.
    • Claude Agent Teams and OpenClaude launches: they end the claude owner. Without the mapping, these tests fail.
    • WSL row: it ends. With the old local-only check, this test fails.
    • Unknown launch agent after a restart: the pane is reset and fenced as before.
    • SSH row, with a known or unknown launch agent: the row is untouched.
    • Session-less command-code launch: the pane is fenced, a late PostToolUse is dropped, and its next run is admitted once its process shows in the terminal.
    • Already-exited launch: its resume record is left untouched.
    • Late hooks after a no-process launch ends: a late Codex Stop or PostToolUse is dropped, and a new Codex prompt revives the pane.
    • Pane with no owner record: it is reset and fenced as before.
  • Swapping the old reset back in fails the first-order handover test and both resume-record tests.

  • New relay tests:

    • Process-less owner killed mid-turn: a Codex owner ends at the launch's end, its late Stop and PostToolUse are not forwarded, and a new prompt revives the pane.
    • Other owners: an owner that is not the launched agent is left alone.
    • Handover: the held guest takes the pane.
    • Claude Agent Teams launch: it ends the claude owner.
    • Relay PTY: the PTY reports the launch end only once, on its first command finish, and never in a pane with no launch. Without the stored launch agent, this test fails.
  • Ended token: neither a late hook nor a new run attests the ended launch's token. The STA-4557 suite opencode-finished-session-authority.test.ts is now wired to the launch end, and all 5 pass. Without the recorded token, its "later OpenCode session" test fails.

  • Owner rule unit tests: after an ended row, a mimo-code or OpenCode prompt MessagePart, a droid prompt and a Codex SessionStart are admitted; other types and other processes are admitted; a same-type process-less Stop is dropped.

  • Updated the runtime specs: command finish calls endLaunchAuthority, PTY exit calls retirePaneAuthority.

  • Ran:

    • src/main/agent-hooks, src/shared/agent-hook* and src/shared/agent-pane-owner*: plus the relay, parity and SSH hook suites: 185 files, 1949 passed; relay PTY suites: 33 files, 419 passed;
    • src/main/runtime/orca-runtime.test.ts under node: 1336 passed;
    • pnpm tc, oxlint, and the changed-code quality gate.
  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Cross-platform: this is main-process logic only, the same on every platform.
  • WSL: WSL panes are this machine's, so the desktop ends their launches.
  • SSH: the relay ends the launched owner on its own panes. The desktop revokes only the launch's authority and leaves relayed rows to the relay. There is no wire change; the relay forwards the ended row it already sends for a proven exit.
  • Compatibility: no wire change.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

When an agent Orca launched exits, the pane's command-finished retirement
wiped every row on the pane: the row of an agent the launch had already
handed the pane to (a detached Codex promoted after Claude crashed), and
the launch's own resume remnant. endLaunchAuthority keeps a row whose owner
is a different agent than the launch, without fencing its later events, and
keeps the resume remnant; the launch's own live row is wiped and fenced as
before.
endLaunchAuthority no longer wipes and re-admits rows. When the launch still
owns the pane, its command ending ends that ownership through the owner
rule's exit path (ended resume remnant, then the guest held behind it takes
the pane), so the handover works whichever of the exit check and the
command-finished signal arrives first. A remnant or an owner the launch
already handed the pane to is left in place; only the launch's token and
restored authority are revoked. A pane with no owner record is reset as
before.
An ended owner record now admits a same-type event only when it starts a new
turn (the isNewTurnEvent set the retirement fence used); a matching process
stays blocked. This keeps base's suppression of a launched agent's late hooks
without a pane fence, including for agents that report no process.
…split launch end from PTY exit

The ended-owner rule now drops only the ended process's hooks, or a same-type
process-less hook that does not start a new run (the retired-pane gate's own
predicate, startsNewAgentRun). Command finish ends the launch; PTY exit keeps
retirement. Relayed rows and an unknown launch agent only lose launch authority.
An owner with no session to resume is fenced as before.
… launch command finishes

Main and the relay now apply one rule, ownerEndedByLaunch, to the panes each
executes: a launched agent command finishing ends its owner when that owner is
the launched agent, whether or not its process can be checked. The relay tracks
the launch per PTY and clears it on the first command finish, as main's receipt
does; its presence check and launch end share one owner-exit write.
…esets as before

WSL rows are this machine's (isLocalHookConnectionId, shared with worktree
cleanup), so their launch ends here. A launch maps to the agent its hooks name
(claude-agent-teams and OpenClaude report claude). A restored PTY whose launch
agent is unknown resets the pane as before. One main owner-exit helper,
endPaneOwner; opencode2 prompt MessageParts start a new run.
hookAgent is an optional catalog field (claude-agent-teams and OpenClaude name
claude), read through getTuiAgentHookAgent; the separate map is gone.
Ending a launch drops its current authority and records its token hash for the
pane; a later event carrying that token (a late hook, or a new run in the same
shell) is applied without it, as the old retirement's restart did. The hook
agent map moves back out of the catalog into tui-agent-hook-agent.ts.
…ce is minted

toAuthorityEvidence refuses a pane's ended launch token, so no path records or
persists it; the tmux path now publishes the event recordCurrentAuthorityObservation
returns, and pane-authority transfer moves the fence with the other maps.

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