fix(agent-hooks): bind agent status to the pane its session was spawned into (STA-2069) - #14615
Conversation
📝 WalkthroughWalkthroughThe change adds shell-aware Claude session pinning for eligible PTY launches. It binds pinned sessions to PTYs, panes, tabs, and worktrees, and clears bindings during PTY cleanup. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/shared/tui-agent-startup-shell.ts (1)
162-172: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueShorten the new TypeScript comment blocks.
src/shared/tui-agent-startup-shell.ts#L162-L172: reduce the block to the modelability precondition and remove implementation walkthrough.src/shared/claude-session-pin-launch-command.ts#L28-L44: reduce the block to the attribution purpose and fail-closed behavior.As per coding guidelines: “Comments must be concise, non-obvious, and brief—prefer one line; do not explain obvious behavior or walk through code.”
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: ccbd4667-f485-42ce-908f-d1032fb0133f
📒 Files selected for processing (14)
src/main/agent-hooks/agent-session-pane-bindings.test.tssrc/main/agent-hooks/agent-session-pane-bindings.tssrc/main/agent-hooks/server-agent-session-pane-attribution.test.tssrc/main/agent-hooks/server.tssrc/main/ipc/pty.test.tssrc/main/ipc/pty.tssrc/main/pty/spawn-startup-shell.test.tssrc/main/pty/spawn-startup-shell.tssrc/shared/agent-hook-listener.tssrc/shared/agent-resume-launch-command.tssrc/shared/claude-launch-executable-token.tssrc/shared/claude-session-pin-launch-command.test.tssrc/shared/claude-session-pin-launch-command.tssrc/shared/tui-agent-startup-shell.ts
| const base = | ||
| opts.shellOverride?.trim().replaceAll('\\', '/').split('/').pop()?.toLowerCase() ?? '' | ||
| if (/^(?:ba|z|k|da|)sh(?:\.exe)?$/.test(base)) { | ||
| return 'posix' | ||
| } | ||
| return base.startsWith('cmd') ? 'cmd' : 'powershell' |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Classify Fish as fish.
Line 21 does not recognize fish.exe. The resolver returns powershell for Fish on Windows. pinClaudeLaunchSessionId then tokenizes Fish text as PowerShell text. This can reject eligible launches or produce an invalid rewritten command. Return fish for Fish executables. Add a fish.exe fixture in src/main/pty/spawn-startup-shell.test.ts.
Proposed fix
const base =
opts.shellOverride?.trim().replaceAll('\\', '/').split('/').pop()?.toLowerCase() ?? ''
+ if (/^fish(?:\.exe)?$/.test(base)) {
+ return 'fish'
+ }
if (/^(?:ba|z|k|da|)sh(?:\.exe)?$/.test(base)) {
return 'posix'
}11286b5 to
26a7020
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
26a7020 to
a8b7147
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
a8b7147 to
d9ac901
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
Validation pass —
|
| Hunk | Before | Now |
|---|---|---|
pty.ts — both bindAgentSessionPane call sites + clearAgentSessionPaneBindingsForPty |
deletable with 3009/3009 src/main/ipc tests green |
new spawn test asserts the id on the command line equals the id recorded against the pane |
ingestRemote's boundPane?.worktreeId ?? |
no test failed | relay test binds a worktreeId and asserts it |
resolveBoundPaneOverride's bound.paneKey === postedPaneKey early-return |
no test failed | asserts an already-correct pane is not restamped from the binding |
Each was re-mutated after the fix to confirm it now goes red. Also fixed a vacuous parameterized case — 'refuses to compete with an existing --fork-session selector' used claude --resume abc --fork-session, which the --resume clause already rejected, so it never exercised the flag it was named after; and added the missing =-form selectors (--session-id=, --continue=, -r=, -c=), 5 of 11 clauses previously deletable with all cases green.
Supersede claim vs #9237 (@BrianDai22) — audited
- Spawn pin (kept) — accurate, and this version is a superset on POSIX. fix(agent-status): follow daemon-hosted Claude sessions to their true pane (spawn pin + fork lineage + prompt-time rebind) #9237 appended after claude's
--terminator; this splices before it. - Roster lineage (declined) — accurately described. Note fix(agent-status): follow daemon-hosted Claude sessions to their true pane (spawn pin + fork lineage + prompt-time rebind) #9237's version has its own defect: with the parent pane still alive it routes the fork to the parent's pane, a fresh misattribution. Declining is sound.
- Prompt-time keystroke rebind (declined) — accurate; fix(agent-status): follow daemon-hosted Claude sessions to their true pane (spawn pin + fork lineage + prompt-time rebind) #9237's own body documents the residual mis-bind.
- Nothing fix(agent-status): follow daemon-hosted Claude sessions to their true pane (spawn pin + fork lineage + prompt-time rebind) #9237 fixes is left broken that this PR does not now name. Its
clearProviderSessionPanesForPtyhas a direct equivalent here. Co-authored-by: Brian Daiadded — it was missing.
The body now also discloses a gap it had understated: Orca itself writes --resume via buildClaudeResumeLaunchCommand, so every session-restore pane is unpinned and keeps the daemon's key. Fresh spawns are fixed; restored panes are not.
Live validation
Isolated dev instance (own ORCA_DEV_USER_DATA_PATH, own CDP port, Playwright over CDP — no computer-use), real Claude Code 2.1.233. True before/after across a code revert with main rebuilt each time: identical POST, wrong pane key, fix off → lands on Terminal 1; fix on → lands on the pane that spawned the session. The pane running the agent shows the row, so this does not convert "wrong pane" into "no pane". Screenshots in the PR body.
Not reproduced: the daemon causing the stale key. The inherited-key payload was posted directly — byte-identical to what such a worker sends, but it does not independently confirm #9236's inheritance mechanics.
Gates
oxfmt clean on all 16 files; oxlint clean (gate proven live by injecting a no-debugger violation); tsc --noEmit clean on the node project; 70 test files / 805 tests green on the rebased tree. No max-lines suppression added.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/shared/claude-session-pin-launch-command.ts (1)
28-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShorten the rationale comments.
Keep the non-obvious invariant. Remove release history and test-suite mechanics from inline comments.
src/shared/claude-session-pin-launch-command.ts#L28-L43: reduce the function documentation to the pinning contract and fail-closed condition.src/main/ipc/pty-login-shell-startup-commands.test.ts#L247-L250: reduce the test comment to the production binding invariant.As per coding guidelines, “Comments must be concise, non-obvious, and brief—prefer one line; do not explain obvious behavior or walk through code.”
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2ebda63a-bc08-4e2f-a9dd-c8171410c73a
📒 Files selected for processing (16)
src/main/agent-hooks/agent-session-pane-bindings.test.tssrc/main/agent-hooks/agent-session-pane-bindings.tssrc/main/agent-hooks/server-agent-session-pane-attribution.test.tssrc/main/agent-hooks/server.tssrc/main/ipc/pty-ipc-mock-registry.tssrc/main/ipc/pty-ipc-suite-environment.tssrc/main/ipc/pty-login-shell-startup-commands.test.tssrc/main/ipc/pty.tssrc/main/pty/spawn-startup-shell.test.tssrc/main/pty/spawn-startup-shell.tssrc/shared/agent-hook-listener.tssrc/shared/agent-resume-launch-command.tssrc/shared/claude-launch-executable-token.tssrc/shared/claude-session-pin-launch-command.test.tssrc/shared/claude-session-pin-launch-command.tssrc/shared/tui-agent-startup-shell.ts
🚧 Files skipped from review as they are similar to previous changes (12)
- src/main/ipc/pty-ipc-mock-registry.ts
- src/main/pty/spawn-startup-shell.test.ts
- src/shared/agent-resume-launch-command.ts
- src/main/ipc/pty-ipc-suite-environment.ts
- src/main/pty/spawn-startup-shell.ts
- src/shared/claude-launch-executable-token.ts
- src/shared/tui-agent-startup-shell.ts
- src/main/agent-hooks/agent-session-pane-bindings.test.ts
- src/main/ipc/pty.ts
- src/main/agent-hooks/server-agent-session-pane-attribution.test.ts
- src/shared/agent-hook-listener.ts
- src/main/agent-hooks/server.ts
Included review availability: Your plan includes up to 10 reviews per rolling hour; 0 remain after this review.
…ed into (STA-2069) Claude Code >= 2.1.206 hosts TUI sessions as workers under a shared daemon, and the daemon forwards only its own allowlisted env — so hook posts carry whichever pane first started the daemon, not the pane the user is in. Pin a minted --session-id at spawn where Orca still knows the pane, record sessionId -> pane, and correct the posted key at both hook ingest seams. Co-authored-by: Brian Dai <43929761+BrianDai22@users.noreply.github.com>
…ended Appending `--session-id <uuid>` broke every `claude <subcommand>` launch: `--session-id` is a ROOT option, so `claude mcp list --session-id <uuid>` exits with "error: unknown option '--session-id'". Splice it immediately after the executable token instead, which is valid for both a bare session and a subcommand, and is already before claude's own `--` terminator. Also write the binding-key separator as an escape rather than a raw NUL byte, which made the file a binary blob in git. Close three hunks that no test could fail on: the pty.ts spawn call site that records the binding, the relay seam's worktreeId override, and the already-correct-pane early-return that suppresses a worktree restamp.
d9ac901 to
fae16a9
Compare
…as spawned into (STA-2069) (#14615)" This reverts commit 15efc87. The premise does not reproduce, the fix does not reach the failure that does, and the correction it installs can misattribute status on a path that worked before. 1. PREMISE FALSE. #14615 asserts Claude Code >= 2.1.206 hosts TUI sessions as workers under a shared daemon. On 2.1.233 `claude daemon status` reports "not running" while 69 interactive sessions are live, and all 68 running clients are direct children of their own pane's shell. Measured across the fleet: 68 distinct pane keys, ZERO collisions. The 19 keys with no live pane also have a dead ORCA_TERMINAL_HANDLE (pane closed, process outlived it). Terminals live under a *different* pane key: 0. Foreground attribution was never broken, so the correction has nothing to correct. 2. DOES NOT FIX THE REAL BUG. The failure #9236 describes is real but scoped to BACKGROUNDED sessions: a `--bg` / `/background` worker inherits the dispatching pane's whole ORCA_* set, and the hook script self-heals its endpoint while taking paneKey straight from ambient env, so the POST lands under the wrong pane. #14615 mints a binding only for launches Orca constructs, so a typed `claude --bg` produces none. That needs a `kind: "bg"` guard at ingest instead; #9236 is reopened for it. 3. INTRODUCES A MISATTRIBUTION. Bindings are created at two spawn sites and removed only on PTY death. A pane whose user exits Claude keeps its PTY, so the binding survives; resuming that session in another pane does not rebind (`--resume` is a session selector, so the pin declines), and resolveBoundPaneOverride then rewrites paneKey and tabId onto the ORIGINAL pane despite a correct posted key. That is the reported symptom, on a path that previously worked. Kept #14706's observations.rebind() in the conflicting hunk — it postdates #14615 and is not part of this revert; only #14615's comments were dropped and the surviving rationale reworded to alias resolution, which is what actually makes paneKey differ from envelope.paneKey now.
…as spawned into (STA-2069) (#14615)" (#15295) Reverts #14615. Its premise does not reproduce, it does not reach the failure that does, and the correction it installs can misattribute status on a path that worked before. 1. PREMISE FALSE. #14615 asserts Claude Code >= 2.1.206 hosts TUI sessions under a shared daemon. On 2.1.233 `claude daemon status` reports "not running" with 69 live interactive sessions, and every client is a direct child of its own pane's shell. Measured across the fleet: 68 distinct pane keys, zero collisions. Foreground attribution was never broken. 2. DOES NOT FIX THE REAL BUG. The failure in #9236 is real but scoped to BACKGROUNDED sessions, whose workers inherit the dispatching pane's whole ORCA_* set. #14615 mints a binding only for launches Orca constructs, so a typed `claude --bg` produces none. Fixed properly in #15304. 3. INTRODUCES A MISATTRIBUTION. Bindings are removed only on PTY death, and a user who exits Claude keeps the pane's PTY. Resuming that session in another pane does not rebind (`--resume` is a session selector, so the pin declines), and resolveBoundPaneOverride then rewrites paneKey and tabId onto the ORIGINAL pane despite a correct posted key. Demonstrated with a failing test against main; causation isolated to resolveBoundPaneOverride. Kept #14706's observations.rebind() in the conflicting hunk — it postdates #14615 and is not part of this revert.
|
This change was reverted in #15295 because the foreground attribution path needed a narrower scope. Please reland the intended background-session fix with the corrected behavior; tracking: https://linear.app/stably/issue/STA-4769. |
…ed into (STA-2069) (stablyai#14615) * fix(agent-hooks): bind agent status to the pane the session was spawned into (STA-2069) Claude Code >= 2.1.206 hosts TUI sessions as workers under a shared daemon, and the daemon forwards only its own allowlisted env — so hook posts carry whichever pane first started the daemon, not the pane the user is in. Pin a minted --session-id at spawn where Orca still knows the pane, record sessionId -> pane, and correct the posted key at both hook ingest seams. Co-authored-by: Brian Dai <43929761+BrianDai22@users.noreply.github.com> * fix(agent-hooks): pin the session id in root-option position, not appended Appending `--session-id <uuid>` broke every `claude <subcommand>` launch: `--session-id` is a ROOT option, so `claude mcp list --session-id <uuid>` exits with "error: unknown option '--session-id'". Splice it immediately after the executable token instead, which is valid for both a bare session and a subcommand, and is already before claude's own `--` terminator. Also write the binding-key separator as an escape rather than a raw NUL byte, which made the file a binary blob in git. Close three hunks that no test could fail on: the pty.ts spawn call site that records the binding, the relay seam's worktreeId override, and the already-correct-pane early-return that suppresses a worktree restamp. --------- Co-authored-by: Brian Dai <43929761+BrianDai22@users.noreply.github.com>
…as spawned into (STA-2069) (stablyai#14615)" (stablyai#15295) Reverts stablyai#14615. Its premise does not reproduce, it does not reach the failure that does, and the correction it installs can misattribute status on a path that worked before. 1. PREMISE FALSE. stablyai#14615 asserts Claude Code >= 2.1.206 hosts TUI sessions under a shared daemon. On 2.1.233 `claude daemon status` reports "not running" with 69 live interactive sessions, and every client is a direct child of its own pane's shell. Measured across the fleet: 68 distinct pane keys, zero collisions. Foreground attribution was never broken. 2. DOES NOT FIX THE REAL BUG. The failure in stablyai#9236 is real but scoped to BACKGROUNDED sessions, whose workers inherit the dispatching pane's whole ORCA_* set. stablyai#14615 mints a binding only for launches Orca constructs, so a typed `claude --bg` produces none. Fixed properly in stablyai#15304. 3. INTRODUCES A MISATTRIBUTION. Bindings are removed only on PTY death, and a user who exits Claude keeps the pane's PTY. Resuming that session in another pane does not rebind (`--resume` is a session selector, so the pin declines), and resolveBoundPaneOverride then rewrites paneKey and tabId onto the ORIGINAL pane despite a correct posted key. Demonstrated with a failing test against main; causation isolated to resolveBoundPaneOverride. Kept stablyai#14706's observations.rebind() in the conflicting hunk — it postdates stablyai#14615 and is not part of this revert.
…as spawned into (STA-2069) (stablyai#14615)" (stablyai#15295) Reverts stablyai#14615. Its premise does not reproduce, it does not reach the failure that does, and the correction it installs can misattribute status on a path that worked before. 1. PREMISE FALSE. stablyai#14615 asserts Claude Code >= 2.1.206 hosts TUI sessions under a shared daemon. On 2.1.233 `claude daemon status` reports "not running" with 69 live interactive sessions, and every client is a direct child of its own pane's shell. Measured across the fleet: 68 distinct pane keys, zero collisions. Foreground attribution was never broken. 2. DOES NOT FIX THE REAL BUG. The failure in stablyai#9236 is real but scoped to BACKGROUNDED sessions, whose workers inherit the dispatching pane's whole ORCA_* set. stablyai#14615 mints a binding only for launches Orca constructs, so a typed `claude --bg` produces none. Fixed properly in stablyai#15304. 3. INTRODUCES A MISATTRIBUTION. Bindings are removed only on PTY death, and a user who exits Claude keeps the pane's PTY. Resuming that session in another pane does not rebind (`--resume` is a session selector, so the pin declines), and resolveBoundPaneOverride then rewrites paneKey and tabId onto the ORIGINAL pane despite a correct posted key. Demonstrated with a failing test against main; causation isolated to resolveBoundPaneOverride. Kept stablyai#14706's observations.rebind() in the conflicting hunk — it postdates stablyai#14615 and is not part of this revert.
What you see, before and after
Before this fix: you open Claude in a terminal and start working. The spinner shows up on some other terminal — frequently in a different workspace — while the pane you are actually typing in sits blank. Your sidebar tells you an unrelated old terminal is busy and that your real work is idle. Nothing you do in the pane fixes it, because the status was never addressed to that pane in the first place.
After this fix: the spinner appears on the terminal that is actually running Claude.
Why it happens. Newer Claude Code stopped giving each terminal session its own process. It hands them all to one shared background helper, and that helper is started by whichever pane opens Claude first. The helper then reports every later session under that first pane's identity — so pane #1 gets the status for work happening in pane #4.
That also explains the shape of the bug: it only appears once you have used Claude in more than one pane, it survives restarts of the individual panes, and the wrongly-labelled row is often somewhere you are not even looking.
How it is fixed. At the moment Orca launches Claude it is the only moment when both facts are known together — which pane this is, and which conversation is starting. Orca now mints a session id, passes it to Claude, and records "this conversation belongs to this pane." When a status arrives later carrying the wrong pane, that note is used to redirect it to the right one.
What Changed
A session→pane binding, established at spawn (
src/main/agent-hooks/agent-session-pane-bindings.ts)A bounded (512, least-recently-bound eviction) registry keyed by
(hook source, session id)holding{ paneKey, ptyId, worktreeId }. Bindings are dropped when the spawning PTY dies, so a dead pane can never claim a later session.Spawn-side pin (
src/shared/claude-session-pin-launch-command.ts,src/main/ipc/pty.ts)Claude launches get a minted
--session-id <uuid>spliced in, and the resulting binding is registered against the pane's key. The splice reuses the repo's existing shell-aware machinery rather than a new regex:tokenizeStartupCommand+findClaudeExecutableIndex(lifted verbatim out ofagent-resume-launch-command.tsintoclaude-launch-executable-token.ts) +isFullyModelableStartupCommand(the modelability guard that was inline inbuildClaudeResumeLaunchCommand, now shared with it).It fails closed — this is an attribution improvement, never a launch requirement, so any doubt leaves the command byte-for-byte alone:
claude,claude "fix it",FOO=1 claude,/usr/local/bin/claude --model opus,& claude(pwsh),claude -- --rawclaude mcp list,claude doctor,claude updateand every other subcommand--session-id/--resume/--continue/--fork-session/-r/-cclaude && deploy,claude; x,claude | tee,claude > out(operators tokenize as shell-divergent)nvm exec claude(not in command position),ssh -i ~/.ssh/claude host(merely ends in/claude)--%stop-parsingThe pin is spliced immediately after the executable token, not appended.
--session-idis aroot option, so
claude mcp list --session-id <uuid>exits witherror: unknown option '--session-id'— a trailing pin would break subcommand launches that work today. Root position isalso, by construction, ahead of claude's own
--terminator. Verified against the real binary(Claude Code 2.1.233).
Ingest-side correction (
src/main/agent-hooks/server.ts)Both seams resolve the binding before pane-migration aliasing, so aliasing still applies on top:
normalizeHookBodyAgentSessionPaneruns ahead ofnormalizeHookBodyPaneKeyAlias.ingestRemoteresolves from the envelope'sproviderSession.A corrected event has its
tabIdrewritten too (normalizeHookPayloadrejects atabId/paneKeydisagreement outright) and itsworktreeId, since the same inherited env named the daemon's workspace.The session id is read by a new
readHookBodyProviderSessionIdinagent-hook-listener.tsrather than a second parser — it reusesparseAgentHookJsonandextractAgentProviderSession, the exact pathnormalizeHookPayloadtakes, including the Codex-childagent_idcarve-out. Correction must run beforenormalizeHookPayload, whose prompt/compaction state machine is keyed on the posted pane key.Not touched deliberately: an unbound session (hand-typed
claude, anything Orca didn't launch) passes through unchanged. This mechanism can only ever move status onto a pane Orca itself started.A known gap, stated plainly: the "already carries a selector" row above is not only the user's own flag — Orca itself writes
--resume <id>viabuildClaudeResumeLaunchCommand, so every session-restore / cold-restore Claude pane is unpinned and keeps the daemon's inherited key. Fresh spawns are fixed here; restored panes are not. Binding the fork at the point Orca issues the resume is the follow-up, and is the same change the declined roster-lineage mechanism was reaching for.Why
The pane's identity reaches an agent's hooks only through
ORCA_PANE_KEYin the process environment. A worker under a prewarmed daemon inherited that env from a different pane, so the posted key is simply wrong — andnormalizeHookPayloadrequires a valid postedpaneKeyand has no other pane coordinate in the payload to fall back on.That settles the design question this cluster raises. Two shapes were on the table:
(a) is the root-cause fix; (b) can only ever convert "status on the wrong pane" into "status on no pane." The pane that is actually running the agent still shows nothing under (b) alone. So (a) ships here, and (b) remains worth having as a bounded fail-safe layered on top — see the superseded-PR notes below.
Session-scoped rather than process-scoped on purpose: the daemon's worker is not the pane's PTY and outlives it, so process ancestry proves nothing. Transport-agnostic for the same reason — a remote host's daemon inherits a stale key exactly like a local one, which is why the relay seam is covered too and why the pin is not gated on
!connectionId(that flag guards local Claude account switching, not attribution).Binding a host-side pane to a process-side reporter by injecting a session id at launch, with a guard for the user's own session flags, is the conventional approach across comparable agent-workspace tools; this follows it rather than inventing a mechanism.
Linked Issue
Fixes #9236 (STA-2069)
Supersedes #9237 (@BrianDai22), which diagnosed this correctly and independently. That PR is
CONFLICTINGagainst main and layers three mechanisms; this one keeps the first and declines the other two:APPENDABLE_CLAUDE_COMMAND_RE, and fail-closed instead of best-effort. That matters on Windows, where the command may be parsed by PowerShell, cmd, or Git Bash.~/.claude/daemon/roster.json— declined. It reverse-engineers an undocumented internal file of a third-party CLI, at a path derived from a hook-suppliedtranscript_path.Both declined mechanisms address real follow-on gaps (resume forks the session id; the fleet view attaches without a spawn). They are better handled by binding the fork explicitly at the point Orca issues the resume, which is a separate change.
Related, not superseded:
Explicitly NOT claimed fixed by this PR, despite being in the same symptom cluster — each is a distinct root cause:
$$<hash>:L$$pane key) is contradicted by its own evidence dump, which shows a valid<tabId>:<leafUuid>key, and no such key format exists anywhere in this repo. The real cause is the OMP lifecycle gap in OMP terminal agent_end can leave status stuck as working #14278, addressed by fix(agents): settle and surface the OMP terminal lifecycle (#14278) #14605. A test here pins the actual contract: a key of that shape failsisValidPaneKey, and a binding cannot introduce one.omplabeled Pi) — owner-precedence inside thepi-compatibletitle group; addressed by fix(agents): settle and surface the OMP terminal lifecycle (#14278) #14605.cd) — rows derived from the tab map, not the pane's live cwd.Visual Proof
Validated live in an isolated Orca dev instance (own
ORCA_DEV_USER_DATA_PATH, own CDP port,Playwright over CDP — never computer-use), against real Claude Code 2.1.233.
The spawn-side pin is real, not just unit-tested. Orca's own Claude launcher writes the minted
id into the pty:
$ claude '--dangerously-skip-permissions' --session-id 05f70dcc-…Before / after, same app, same persisted workspace, one line of code different. In both runs an
identical
UserPromptSubmitwas POSTed to the local hook listener carrying a different pane's key(what a daemon worker sends when it inherited another pane's
ORCA_PANE_KEY) plus the session idOrca pinned at spawn. Only
server.ts's ingest seam differed; main was rebuilt for each run.6083dd99…:dce8f439…— Terminal 1, the wrong paneba8c89f2…:26b518cb…— the pane that spawned the sessionHow to read these two screenshots. Look at the sidebar agent rows, and specifically which terminal name the working spinner sits next to. Claude is running in the Claude tab in both runs; only the ingest seam differs.
Before — the spinner is beside Terminal 1, which is not running Claude. The Claude tab (Terminal 3) has no row at all. This is the user-visible bug: the wrong terminal looks busy and the right one looks idle.
After — the spinner is beside Terminal 4, the tab actually running Claude, and Terminal 1 is clean.
One thing to be clear about, because it is why these images look synthetic: the daemon-inheritance condition is impractical to provoke on demand, so the hook POST that a daemon worker would send — an identical
UserPromptSubmitcarrying a different pane's key alongside the real pinned session id — was injected directly into the local hook listener. Everything downstream of that POST is production code. The spawn-side pin above is genuine, unforced Orca behaviour. So: real pin, real ingest path, real rows; a hand-delivered envelope.Split panes, and "no pane" ruled out. With a Claude pane and a plain sibling in one tab, the
corrected row resolves to the Claude pane's key — it carries
data-focused-agent-pane="true"whilethat pane is focused, the sibling never gets a row, and the tab-level chip is not what is being
matched. A bound session and an unbound one posting the same wrong key land on two different rows
simultaneously, which is the passthrough path (pre-fix behaviour) and the corrected path side by side:
Testing
What is and is not verified live. Verified in the running app (see Visual Proof): the pin lands on the command Orca writes, the spawn records the binding, and an event posted with another pane's key is re-filed onto the spawning pane — with a true before/after across a code revert. Not reproduced: the daemon causing the wrong key in the first place. I did not stand up a Claude daemon already holding another workspace's environment; the inherited-key payload was posted directly, which is byte-identical to what such a worker sends but does not independently confirm #9236's inheritance mechanics (
ps eww, the roster's"source": "spare"dispatch, the env allowlist), which are taken from that report.New tests (
vitest --config config/vitest.config.ts):src/main/agent-hooks/server-agent-session-pane-attribution.test.ts— real routing outcomes over the live HTTP listener andingestRemote, asserting the resulting status rows, not mock calls:tabId, different leaf) posting the same inherited key keep separate rows with separate prompts — without the binding both collapse and the second prompt clobbers the first;claudeis unaffected);codexbinding cannot capture aclaudepost reusing the same id;$$<hash>:L$$pane key failsisValidPaneKeyand a binding cannot introduce one;readHookBodyProviderSessionIdacross the object and JSON-string payload forms, the Codex-child carve-out, and malformed payloads.src/main/agent-hooks/agent-session-pane-bindings.test.ts— source scoping, re-bind follows the pane, PTY-exit cleanup hits only that PTY, cap eviction is least-recently-bound (a re-bound entry survives), incomplete bindings are refused.src/shared/claude-session-pin-launch-command.test.ts— 21 cases covering the pin/no-pin table above across posix, cmd and PowerShell.src/main/pty/spawn-startup-shell.test.ts— dialect resolution: SSH and WSL panes are posix even on a Windows host; cmd and Git Bash overrides recognized.src/main/ipc/pty.test.ts— the existing startup-command timing test now asserts the written command matchesclaude --session-id <uuid>, making the spawn-side pin covered end to end.Red/green oracle. With both ingest seams reverted to their previous single line, exactly the 3 routing assertions fail and the remaining 8 passthrough/negative assertions still pass (they assert preserved behavior). Restored: 11/11.
Regression scope.
src/main/agent-hooks,src/main/pty,src/main/ipc/pty,src/relay,src/shared/agent-hook-listener,src/shared/agent-resume-launch-command,src/shared/claude-session-pin-launch-command,src/main/ssh— 3055 passed, 12 failed. All 12 failures are pre-existing: I ran the same files on a clean stash of this branch's base and got the identical 12 (10 inpty.test.tsaround MiMo/Pi/OMP overlay env and WSL handles, 2 in the SSH/Codex-prefill suites). Nothing this PR touches fails.The extraction into
isFullyModelableStartupCommand/claude-launch-executable-token.tsis behavior-preserving:agent-resume-launch-command.test.tsis 104/104 before and after, untouched.typecheckpasses on all three projects (node,cli,web) run individually.oxlint, both--type-awareand native code-quality configs,oxfmt, and the max-lines ratchet pass on every changed file. Nomax-linessuppression added. I did not run the fullpnpm test/pnpm build; CI covers those.Platforms. Written on macOS. Cross-platform handling is in the code rather than in my test matrix: the splice dialect is resolved explicitly (
resolveSpawnStartupShell) instead of read offprocess.platform, so an SSH pane and a WSL pane are treated as POSIX even when Orca runs on Windows, and a Windows pane pointed at cmd or Git Bash is not treated as PowerShell. Folder workspaces are unaffected — nothing here reads git state. I did not exercise Windows or a live SSH host.Review
Mapkey for lookup — never as a path, glob, or command fragment. No new filesystem access, no third-party internal files read. The minted id israndomUUID(), so no user input is interpolated into the command string, and it is only ever spliced into a command that fully tokenizes. Existingx-orca-agent-hook-tokenauth is unchanged. The registry is bounded at 512 with PTY-exit cleanup.paneKey/worktreeIdis the same field shape an old client already handles.Maplookup per hook event, on a parse the ingest path already performs.Author