Repository navigation
fix(agent-presence): keep remote, mobile and CLI views on host ownership (step 3/3) - #24184
brennanb2025 wants to merge 36 commits into
Conversation
…2' into brennanb2025/agent-presence-step3 # Conflicts: # src/main/agent-hooks/server/server-agent-presence.ts # src/renderer/src/lib/tab-agent-from-signals.ts
…ep 3 Step 3 now only carries owners that hooks already identify (Claude on macOS/Linux, local and over SSH) to mobile, paired clients and the CLI. Hookless and native-Windows panes stay uncovered and keep step 2's behaviour; their capture moves to a follow-up with its own design. Removes process discovery on the host and relay, its title/command triggers and wiring, the Windows single-process reader, and the capture upgrade in the presence transition. The relay keeps stamping presence with its epoch/sequence. The remote replay tests now establish owners through Claude hooks carrying their process. The offline "replacement exits too" replay case is red after the step-2 merge (its cleanup rebuilds the ended owner from the stale mirror row); the host-frame replacement fix that follows makes it green.
…ost presence A pane's identified owner now answers only whether an agent is there. The live permission title, the approval-prompt screen text and the `claude agents` management screen keep their order ahead of it, so a Cursor approval menu still reads as waiting for permission (sends are refused instead of pressing Enter on "Run (once)") and the management screen is never treated as a task agent. A live owner still stands in for the shell-title and foreground reads; an ended owner still reads as no agent.
A paired desktop/web client latched an ended owner record: once Claude exited in a pane, a later agent the host cannot identify (for example Codex) stayed hidden there until the pane closed. A newer host row without a process now drops the mirrored record, so that pane falls back to its row signals as it does on the host; an older snapshot still cannot revive an ended owner. A host retraction also sweeps the mirrored presence record with the tab's other per-pane state instead of leaking it.
… the stale mirror row After an SSH reconnect, the desktop kept the row it held for the old agent when the execution host reported a different owner process: the dead agent's permission prompt held the new agent's turn, and an owner that started and exited while disconnected never replaced the old one. A host-stamped frame whose owner process differs from the stored row's now replaces that row instead of inheriting its turn, identity or exit fence from it. Terminal join and ordering watermarks still come from the stored row.
…ead projection default `worktree ps` rows no longer carry the owner record (no reader used it, and it published process ids and relay ordering on --json); their agent type still comes from an identified owner. The mobile status builder's default presence branch was never reached in production, so it and the reformatted caller go back to step 2; its tests now run the real snapshot projection and client negotiation instead.
…d CLI After an identified Claude exited, the phone, `terminal show`, the send guard, dispatch and terminal summaries read its pane as "no agent" until another hooked agent ran there, so a hookless agent started next was hidden (worse than before presence). They now use step 2's rule: a live identified owner decides its pane; once it exits, only evidence naming that owner (its hook row, launch intent, title or foreground process) is set aside, and any other agent shows exactly as it did before. The rule moves unchanged from the desktop renderer to src/shared so the phone, main and renderer share one implementation; the renderer module re-exports it.
…s presence fixture
…ard routing A live identified owner proves the agent exists, not that it holds the terminal. After Ctrl-Z (the probe reads a stopped process as unverifiable, so the owner stays live), the running-agent check and the status query said Claude was running, so guarded sends, dispatch --inject, agent-launch reuse and worker-start typed into the shell. The foreground read now decides again: a confirmed shell in front says no, and the owner stands in only when the read cannot answer (unavailable, or a process name the recogniser does not know). A live owner still overrides a shell-like title, and the management, permission and exited-owner rules are unchanged.
…2' into brennanb2025/agent-presence-step3 # Conflicts: # src/renderer/src/lib/agent-presence-selectors.ts
After step 2's relay-owner adoption, the transition's host-owned branch was reachable only for replayed exits. That acceptance now sits beside the adoption in applyNormalizedStatus (a host-stamped replay of the relay's own settled exit), so the shared transition is back to step 2's host-independent rule and live exits keep the exact-owner fence.
The remote-ingress tag that says "the execution host decided this owner" describes one admission, but it was spread onto the stored row and so into the last-status file and retained rows, where nothing reads it. It is now taken out before the row is written and used only for that admission's owner-replacement decision.
…stead of replacing it Capable clients received the host's hook row in place of the tab's published status, so a paired client lost the pane's activity history, unread count, prompt search and orchestration labels, and the phone lost the title correction for a stale working turn. The host's owner record is now added to the published status; the hook row stands in only when the pane published no status at all (Claude idle under a shell title). Old clients still get the exact legacy row.
The mirror had started turning a host row's null connection into the environment id, which made the tab palette drop every mirrored agent's prompt and task metadata, and it attributed any row without a worktree to the snapshot's worktree. It is back to main's rule (only a missing connection takes the environment id); only rows carrying a host-identified owner get the snapshot's worktree, so their exit and retraction can still clear them.
When a pane has no published status, the host's own row stands in for capable clients. For an owner that has exited it now carries only the owner record and clocks, not the exited agent's provider session, model or type, so a later agent in that pane can never open the old agent's transcript in Chat or inherit its model or paste format.
… the pane without status The paired client kept a mirrored owner record when a newer snapshot still listed the pane but carried no status for it. Snapshots arrive in version order and capable hosts always publish a status for an owned pane, so such a pane no longer has an owner and its record is dropped.
… changes Every status ping from a covered pane rewrote the paired client's owner record and bumped its status and sort epochs. The record is now written only when the owner's agent, process or exit changes, so pings no longer re-sort the sidebar or invalidate the per-tab presence index.
…n another host A live owner recorded from another host (a WSL guest relay or an SSH relay) cannot be checked by this host, so it no longer stands in for an unrecognised foreground such as wsl.exe or overrides a shell title in the running-agent and status checks. Such panes read exactly as on main: a WSL Claude that died without its exit hook no longer reads as present with sends going to the shell. Local owners this host can check keep the stand-in; exited owners still hide only their own evidence.
…2' into brennanb2025/agent-presence-step3
…it is in front After Ctrl-Z the owner stays live, so a user who then ran ssh, vim, less, python or tmux in that pane still read as "agent running", and guarded sends and dispatch typed into that program. Presence now only proves the agent exists: the running-agent check follows main's foreground rules (null, failed, shell or any other program all answer as on main), and a fresh hook status overrides a shell-like title only when the program in front is recognised as the owner's own agent.
…rent identified owner A host-stamped replay of an old owner's exit could delete a later agent's row that carries no process identity (for example a hookless agent in the same SSH pane), where step 2 dropped that frame. The replay now bypasses the transition only when it replaces a different stored owner that has a process; otherwise the transition decides, and it drops an exit that has no owner.
… the keyboard answer
…er agent's status The paired client ordered owner records by the published status clock, which the host does not keep monotonic (a shell-title correction can stamp an older time), so a relaunched Claude or an exit could be ignored and the pane hidden until a later status. Snapshots already arrive in delivery/version order and the host re-derives presence for each one, so the mirror now takes each snapshot's owner as published. It also uses the shared ended-owner rule: after an owner exits, only statuses naming that agent are set aside, so a later agent's status (for example a hookless agent) still shows, as on the phone and the host desktop.
…t read it The capability that asks a host for the owner record on session tabs was in the shared remote list, so the CLI advertised it without reading session tabs, while the browser web client, which mirrors host tabs like the paired desktop, never did. It now comes from the paired desktop's Electron remote list, the browser web client's list and the mobile list, not the CLI's. The desktop renderer's own-main list records it as a remote-only decision: main's renderer reads presence over agent-status IPC. A test pins that the paired desktop connection negotiates it and the CLI does not.
…ing host presence Removing the host's owner record from renderer-published statuses built a fresh object for every status on every republish, so the per-worktree mobile snapshot cache missed and rebuilt unchanged worktrees. A status without an owner record is now published as is, and one with it maps to a single stripped copy per source entry.
…dule (frees protocol-version line budget)
…2' into brennanb2025/agent-presence-step3 # Conflicts: # src/main/ipc/desktop-renderer-runtime-capabilities.test.ts # src/main/runtime/orca-runtime-stop-requested-pty-ids.ts # src/renderer/src/lib/agent-presence-selectors.ts # src/renderer/src/web/web-runtime-client.test.ts # src/renderer/src/web/web-runtime-connection-frame-router.ts # src/shared/protocol-version.ts
The SSH notification handler copied a fixed list of envelope fields and left out the owner record (agentPresence with its process and relay ordering), so no identified SSH owner ever reached main: the step 1/2 owner paths and this PR's reconnect handling never ran over SSH. It now forwards the envelope like the WSL link, with ingestRemote re-validating every field; the host-set run-capability field is applied last so a relay cannot override it, and host provenance is still decided only at remote ingress.
Review status — PR #24184 (step 3 of 3), head 351af5aThe problem: the phone app, a paired desktop/web client and the CLI each guessed whether an agent was in a terminal from its title. A running Claude idling under a shell-like title disappeared from them; an exited Claude could keep its icon and Chat on the phone; and after an SSH reconnect a pane could keep showing the agent that ran before the disconnect. What changes for users: for Claude on macOS/Linux (local or SSH), the phone, paired clients, Scope change during review: the first draft also captured process identity for hookless agents and on native Windows. Review proved that made things worse than main (a working hookless agent showed Done; every shell command started a machine-wide process scan; on Windows an exited agent never left the tab), so that part was removed and redesigned as a separate follow-up. Fixed in review (each with a test that fails without the fix):
Deferred (P3, no user-visible harm proven):
Verified:
Not verified:
Seen in QA but not caused by this PR: after Base update: step 2's sleep/teardown fixes and its merge of current main (step 2 @85f4d2c52e6) are merged in as 351af5a; neither touches this PR's code. Node and web typechecks and all named suites pass, and CI on 351af5a is fully green (unit shards, typecheck, static analysis, packages, cross-version wire, Mobile Checks, computer-use smoke on mac/Ubuntu/Windows). Live QA ran on e6353da, before those merges. The PR stays a draft. |
…2' into brennanb2025/agent-presence-step3
…2' into brennanb2025/agent-presence-step3
|
| agentStatus: legacy.agentStatus | ||
| ? { ...legacy.agentStatus, agentPresence: hostStatus.agentPresence } | ||
| : hostStatus |
There was a problem hiding this comment.
New owner inherits old conversation When the host records a new live agent before the renderer replaces the previous agent's published status, this merge puts the new owner's presence on a status that can still contain the old agent's session ID and transcript path. Mobile Chat selects the new agent but uses that old conversation identity, so it can open or attempt to open the wrong conversation. Keep the published session only when it belongs to the recorded owner.
Knowledge Base Used: Agent integrations and hooks
Comments Outside DiffThese findings could not be posted inline.
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — read the complete diff against brennanb2025/agent-presence-step2, traced the owner-record flow end to end, and ran the new/changed tests for the presence, projection, remote-replay, and paired-client paths (all pass).
- One shared owner/evidence rule — new
src/shared/ended-agent-owner-evidence.tsextracts the ended-owner helpers out ofrenderer/.../agent-presence-selectors.ts(which now re-exports them); mobile, the CLI status query, and the desktop dispatch/terminal paths all call the shared functions so a live identified owner wins, an exited owner's own title/launch/process evidence is history, and other agents still show. - Keyboard routing separated from presence —
runtime-terminal-agent-status-query.tsandruntime-terminal-agent-presence.tsonly let the owner answer for keyboard input when it is the program in front, keep permission/management checks ahead, and leave main's rules untouched for owners recorded on another host (selectKeyboardAgentPresencedownsamples a remote live owner to{agent}). - Relay ordering + admission — new
relay-agent-presence-observation.tsstamps each forwarded owner with an epoch/sequence; newserver-remote-agent-presence.tsfences stale deliveries and sets the internalagentPresenceFromExecutionHostflag, whichserver-status-update.tsuses to adopt host-stamped owner replacements/replayed exits and then strips before storing.ssh-relay-session.tsnow forwards the whole envelope (WSL-style) so presence reaches main, with host-set capabilities applied last. - Capability-gated client projection —
session-tab-presence-projection.tsattaches the owner record to the published tab only foragent-process-presence.v1clients, via a module-private Symbol so it cannot arrive over JSON;web-session-tabs-syncre-derives the paired-client mirror per snapshot, keeps a later agent's status after an exit, releases on retraction, and does not republish presence for a same-owner ping.
No inline comments. The base is the stacked agent-presence-step2 branch rather than main (the author documents the 3-step plan), so merge ordering is a reviewer/process decision, not a code concern here.
Default format / metadata
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…2' into brennanb2025/agent-presence-step3 # Conflicts: # src/main/ipc/desktop-renderer-runtime-capabilities.test.ts # src/main/runtime/rpc/methods/session-tabs-inventory.ts # src/relay/agent-hook-server.ts # src/shared/protocol-version.ts
…2' into brennanb2025/agent-presence-step3 # Conflicts: # src/main/ipc/orcad-runtime-maintenance-handlers.ts # src/main/runtime/runtime-terminal-agent-presence.ts # src/main/runtime/runtime-terminal-agent-status-query.ts # src/main/ssh/orcad-runtime-decommission.ts # src/main/ssh/orcad-runtime-maintenance.ts
…splay-clear accessor
…-session-projection.ts to stay under max-lines
|
Close this older stacked implementation while preserving the work to make phone, desktop and command-line agent identity agree. Build it around the computer that now owns each terminal. |

What this fixes
The phone app, a paired desktop/web client, and the command line (
orca terminal show,worktree ps, orchestration dispatch) could each disagree with the computer that actually runs the agent. A running Claude disappeared from them when its terminal title looked like a plain shell (for example while Claude sat idle), and an agent that had provably exited kept its icon and its Chat button on the phone. After a reconnect to a remote host, a pane could keep showing the agent that ran before the disconnect instead of the one running now.Who hits it: anyone watching or driving Claude from the phone, a paired client or the CLI, when Claude runs on macOS or Linux, either locally or on an SSH host. Those are the panes whose owner a hook already identifies by its process (steps 1 and 2).
What is NOT affected: hookless agents (aider, goose, crush and similar), agents whose hooks carry no process identity (Codex, Cursor and others), and everything on native Windows keep today's behaviour on every surface, including when one of them starts in a pane right after an identified Claude exited there. Capturing their process was part of an earlier draft of this PR and has moved to a separate follow-up, because it needs its own design (see Notes). The host desktop's own tab icon, sidebar and Chat already read this record since step 2 and are unchanged here.
ELI5
A terminal title is a label, not proof that an agent is there. The computer running the agent keeps one record per terminal pane of which agent process owns it and whether that process has exited. Until now only the desktop app on that computer read the record; the phone, paired clients and the command line still guessed from titles. After this PR they all read the same record, so they agree with each other.
What Changed
This PR is step 3 of 3, stacked on #24175 (
brennanb2025/agent-presence-step2, step 2), which now targets main; step 1 (#23947) has already merged into main. The whole plan: (1) the execution host records which agent process owns each pane and checks that exact process; (2) the desktop's icons, Chat and sidebar read that record; (3) the phone, paired clients, the CLI and SSH reconnects read the same record, and mixed versions keep working. This PR is step 3 of 3.Before, as the user sees it: on the phone and on a paired client, an idle Claude under a shell-looking title showed as a plain shell with no Chat; an exited Claude could keep its icon and Chat; after an SSH reconnect the pane could still show the old agent; if that agent was waiting for permission when you disconnected, then exited and a new Claude started, the pane kept showing the dead agent's permission prompt.
orca terminal showand dispatch decided "is an agent running" from the title and the foreground process, so an idle Claude under a shell title could read as "no agent".After: those surfaces show the owner the host recorded. A live identified owner counts as present under any title for display, while sends and dispatch only go to it when it is the program in front. Once it has provably exited, its own leftovers (its hook row, launch intent, title and process name) no longer bring it back, while any other agent started in that pane shows exactly as it does today. After a reconnect the pane shows the agent the remote host says is running now (see the SSH note below).
Mechanism, phone and paired clients: for clients that advertise the new
agent-process-presence.v1capability (negotiated per connection; the paired desktop, the browser web client and the phone advertise it, the CLI does not, and the desktop's own renderer keeps reading presence from its main process), the host adds its owner record to the status each terminal tab already publishes, so the tab keeps its activity history, unread count, prompt search, orchestration labels and title corrections. Only when a pane published no status at all (Claude idle under a shell title) does the host's own row stand in, and for an exited owner that row carries identity only, never the exited agent's session, model or type. Older clients receive exactly the previous row, and advertising the capability alone never marks a row as identified. The paired client takes the record from each host snapshot as published, in snapshot order (never by the status clock, which the host does not keep monotonic): a row without a process identity, or the pane published without status, drops the record instead of hiding the next agent; an older snapshot is never applied, so it cannot bring an exited owner back; closing the tab on the host clears it; routine status pings do not rewrite it or re-sort the sidebar. After an owner exits, the paired client sets aside only statuses that name that agent, so a later agent's status in the pane still shows, the same rule as the phone and the host desktop. Host-local rows keep their local attribution, as today.Mechanism, command line: presence proves the agent exists; the owner answers for the keyboard only when the program in front is that agent.
terminal show, the send guard and dispatch's "is an agent running" check therefore keep main's foreground rules: a shell, any other program (for examplessh,vimorlessafter Ctrl-Z suspends Claude), or a foreground read that fails all answer exactly as on main, so nothing is typed into that program. The one change is that a fresh hook status is no longer hidden by a shell-like title when the process in front is recognised as the owner's own agent. An owner recorded on another host (a WSL guest or an SSH relay) cannot be checked here, so those panes keep main's rules entirely. The permission checks (a live permission title, an approval menu on screen) and theclaude agentsmanagement screen stay ahead of it in their previous order, so a Cursor-style approval menu still reads as "waiting for permission" and the management screen never reads as a task agent. Once the owner has exited, these checks fall back to today's title and foreground rules with only the evidence naming that exited agent set aside; this is the same rule step 2 applies on the desktop, now moved to shared code so the desktop, phone and CLI use one implementation.worktree psand terminal summaries take the agent type from the identified owner.Mechanism, remote hosts: the relay on the execution host stamps every owner it publishes with an ordering marker (a random id per relay process plus an increasing number). The desktop accepts that stamp only on its authenticated remote channel, so local hooks, client publications and terminal output cannot claim it, and it rejects older deliveries. The provenance this earns ("the execution host decided this owner") is used only for that admission and is never stored on the row; the marker itself is stored with the owner record (see Known weaknesses). When a stamped frame names an owner process different from the one the desktop remembered, it replaces that stale row outright: the new agent's state is not held by the old agent's permission prompt, and an agent that started and exited while you were disconnected replaces the old one. Deciding that "the relay chose the owner" now happens in one place on the desktop. Relays from older builds send no marker and keep the previous behaviour. This PR also makes the SSH session forward the relay's whole hook envelope to the desktop, the way the WSL link already does (the desktop re-validates every field). Before, the SSH path copied a fixed field list that left out the owner record, so this turns the step 1/2 owner paths on for SSH for the first time.
Why
The bug is several readers guessing presence from different signals (titles, launch intent, the foreground process) and disagreeing. The fix gives every reader the one record the execution host already keeps, reusing its storage, cleanup and publication rather than adding a second store. That removes the class of "surfaces disagree about whether an agent is there" for identified owners, instead of patching each title rule.
Alternatives rejected: sending the whole host store to clients (larger wire change, and older clients would need a second path anyway); letting each client recompute ownership from its own evidence (that is the bug); and capturing every agent's process in this PR. The last one was built and reviewed, and it made things worse than today in three ways: a working hookless agent showed as Done, every command in a plain shell pane started a machine-wide process scan, and on native Windows an exited agent never left the tab because Windows cannot yet prove a process has gone. So it moved to a follow-up with a design that avoids all three.
The paired-client fix removes a latch rather than adding a guard: presence is re-derived from each host snapshot, so no stale record can outlive what the host says. The reconnect fix makes the remote host's word final for its own panes instead of having the desktop re-decide it from a copy that belonged to a dead process.
How this differs from the reference designs we compared against:
Linked Issue
Partly addresses the inconsistent icon/Chat presence reported in #23638 (STA-8819); the reported freeze was not reproduced and this PR does not claim to close it. STA-8862/#23753 restart rebinding and STA-7949/#22727 turn cancellation remain separate. No Linear ticket is claimed closed.
Visual Proof
Live QA ran on remote machines from dev builds of this branch (M4Air macOS: desktop, CLI, phone and a real SSH host; Windows high-spec: native Windows), with isolated homes and an unauthenticated Claude so only its hooks and process mattered.
Phone (round 1, head 20c3343): Claude idle under a
zshtitle keeps its icon and the "Switch to chat view" entry; after/exitboth clear within 5 s. The final-head phone run was blocked because another session held the shared Android emulator for the whole window; the phone-facing code changed after round 1 (published status kept, presence added), and that path is covered by the unit tests listed below.SSH (final head): the desktop now receives the remote Claude's process identity. Claude replaced on the remote while the client was disconnected: after reconnect the pane shows the new Claude live and a terminal send lands in its input. Claude killed while disconnected: after reconnect
worktree ps, the CLI and the sidebar clear.CLI (final head):
worktree psandterminal show/listshow Claude while it idles under azshtitle and are empty within 3 s of/exit.Native Windows (final head): unchanged from main, as intended: the sidebar row and
worktree psclear within about a second of Claude exiting (3/3), and a plain PowerShell pane shows no agent.Not this PR's (reproduced identically on step 2 and on main): after
/exitthe host desktop's tab icon can stay on Claude and the macOS sidebar can keep a "shell - Claude" row, because the desktop's sleeping-session and retained-row records survive the exit; on the final head the macOS tab sparkle clears but the sidebar row stays. These are desktop readers outside this PR's diff.Testing
On macOS, at the merged head: 63 named root Vitest files (this PR's own plus neighbours that import changed modules, including
orca-runtime.test.ts, step 2's desktop presence and relay handover tests, the send and dispatch tests, the capability-list tests and the mobile publication-cost test) passed, 2206 tests; the SSH and remote-ingress set (everyssh-relay-session*test including the integration file run by name, remote replay, relay handover, remote ingest and the WSL relay tests) passed 23 files, 237 tests; mobile's own Vitest passed 4 files, 36 tests; the 12 cross-version wire fixtures passed 78 tests. New cases cover: an idle owner under a shell title, a proven exit on the phone, per-connection capability negotiation, client-published presence rejected as host evidence, the Cursor approval menu and theclaude agentsscreen with an identified owner, a hookless agent started after the owner exited (phone and CLI) next to the exited Claude's own title, process and launch hint staying hidden, a suspended (Ctrl-Z) owner with a shell,ssh,vim,python3,lessortmuxin front, or a failed foreground read, answering exactly as main (the send guard refuses), the owner itself in front under a shell title still reading as running, a WSL-style owner behindwsl.exereading exactly as main, a replayed old exit leaving a later unidentified row alone, a newer snapshot's owner winning over an older status clock (exit and relaunch), a later agent's status kept after the owner exited on a paired client, the SSH session forwarding the relay's owner record (agent, process and ordering) to the desktop row while keeping the host-set run capability, a later unidentified agent after an exit on a paired client, the host retracting a tab, the published status keeping its history and labels for capable clients, an exited owner projected as identity only, host-local rows keeping their local attribution, a pane published without status, a status ping that must not republish presence, host provenance kept off stored rows, offline exit plus replacement plus replay, and a dead owner's permission prompt after reconnect.Red proof: with each of the eleven main review fixes temporarily undone, its new tests fail on assertions (approval menu read as working and management screen read as an agent; the exited record kept and leaked on the paired client; the dead Claude shown waiting on permission after reconnect; a hookless successor hidden on the phone and read as not running by the CLI; a suspended owner read as running with a shell in front, in both the running check and the status query; the published status replaced and its history lost; a host-local row rewritten to the paired environment; a WSL owner read as running behind
wsl.exe; a live owner answering for a program that is not the agent; a relaunch or exit ignored on an older status clock and a later agent's status dropped on the paired client; the SSH owner record dropped by the old field-by-field copy), and pass again once restored. The changed-code-quality gate against step 2 reports no new findings and oxlint is clean on every changed file. The node, web and CLI typechecks and the mobile test-typecheck ratchet passed on the merged head (run through the gated tsc); an app build is left to CI.Review
The only protected hook-server file edited is
server-status-update.ts: a host-stamped frame for a different owner process replaces the stored row, a host-stamped replay of the relay's own exit is accepted next to step 2's relay-owner adoption (so the shared transition is back to step 2's version), and the host-provenance tag is removed before the row is written. Remote ingress, the internal host-provenance tag, the relay projection and the SSH session's hook forwarding (ssh-relay-session.ts, now forwarding the envelope like the WSL link) are the other approved edits. One separate refactor commit moves three browser capability constants out ofprotocol-version.tsinto their own module, with the same values, only to keep that file within its line limit after merging step 2. No changes to #22727's branch or to turn-state semantics. Step 2 is merged in at its latest head, including main with step 1 (per-pane presence resolution, no polling of identified owners, owner release on terminal teardown, the answered-shell exit confirmation, and one owner-handover rule for every host).Agent skill upstream boundary
Notes
Known weaknesses
Author: @BrennanKB5.
Checklist