Skip to content

fix(agent-presence): keep remote, mobile and CLI views on host ownership (step 3/3) - #24184

Closed
brennanb2025 wants to merge 36 commits into
brennanb2025/agent-presence-step2from
brennanb2025/agent-presence-step3
Closed

brennanb2025 wants to merge 36 commits into
brennanb2025/agent-presence-step2from
brennanb2025/agent-presence-step3

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 12 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1232 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1232
Prod 36 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​754 $\color{#cf222e}{\Huge{\mathbf{−}}}$​276 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​478

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 show and 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.v1 capability (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 example ssh, vim or less after 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 the claude agents management 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 ps and 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:

  • Intended: readers on the phone and the CLI project the host's record only for identified rows; unidentified rows keep today's title and launch-hint rules. Safe because a row is identified only by a process the host captured, and the approved compatibility rule is that unidentified rows keep existing behaviour.
  • Temporary: hookless agents, agents without process capture and native Windows are not identified yet. Follow-up: the planned hookless/Windows capture PR (not yet opened), which records identity without inventing a turn state, runs only on agent evidence, and leaves Windows uncovered until an exit can be proven on a real Windows machine.
  • Temporary: the relay's ordering marker is a stale-delivery fence on its per-pane replay, not a resume cursor with a full snapshot and removals, and in practice nothing reorders these frames, so its real job is marking a new relay. It costs a field persisted with the owner record and published to every client. Follow-up: replace it with a relay capability marker and drop the stored field (not yet opened).
  • Intended: for a live (not replayed) exit, the desktop still checks the exit comes from the owner process it holds, instead of adopting the host's exit outright. Safe because the relay forwards in order, so a connected exit always names the owner the desktop holds; live owners, replayed exits and owner replacements are adopted from the host.
  • Intended: the host carries its record to the per-client projection on a private field that JSON cannot carry, instead of passing capabilities into the projection. Safe because no client publication can supply it; it is an extra moving part, not a behaviour difference.
  • Intended: the paired client and the phone both take each host snapshot's owner as published, in snapshot order; neither re-orders owners by a clock.

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 zsh title keeps its icon and the "Switch to chat view" entry; after /exit both 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.

Phone: Claude idle under a zsh title shows the Claude icon
Phone: long-press menu offers Switch to chat view
Phone after /exit: no Claude icon
Phone after /exit: no Switch to chat view

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.

SSH: connected, remote Claude shown with its remote process
SSH: before disconnect, old Claude in the pane
SSH: after reconnect, the new Claude shown live in the same pane
SSH: terminal send landed in the new Claude's input

CLI (final head): worktree ps and terminal show/list show Claude while it idles under a zsh title and are empty within 3 s of /exit.

Native Windows (final head): unchanged from main, as intended: the sidebar row and worktree ps clear within about a second of Claude exiting (3/3), and a plain PowerShell pane shows no agent.

Windows: Claude running
Windows: plain PowerShell pane shows no agent

Not this PR's (reproduced identically on step 2 and on main): after /exit the 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.

Main, same steps: tab sparkle, bell and sidebar row stay after /exit
This PR, final head: tab sparkle gone, sidebar row still present

Testing

  • Manually tested on remote QA machines (M4Air macOS + SSH, Windows high-spec)
  • Automated tests added/updated

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 (every ssh-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 the claude agents screen 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, less or tmux in 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 behind wsl.exe reading 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 of protocol-version.ts into 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

  • Not applicable; no upstream skill content copied or translated.

Notes

Known weaknesses

  • Right after Claude exits, relaunching Claude in the same pane reads as "no agent" on the phone and in the CLI until the new Claude's first hook arrives (usually at startup), because its title and process name name the exited owner. The desktop behaves the same since step 2. It fails closed: nothing is typed into a shell.
  • A live owner is trusted until an event proves otherwise; there are no sweeps. A silent kill with no later title change, command end or hook stays "present" until the next such event.
  • Temporary: the relay's ordering marker (a relay id plus a counter) is stored with the owner record in the last-status file and published to every client. It is optional and dies with the row, but it is delivery metadata, not state, and does not fence across relay restarts. Follow-up: a relay capability marker instead (not yet opened).
  • Not exercised live: a paired web client, mixed-version apps across a real connection (covered by unit tests and the cross-version wire fixtures), Codex rows on Windows (real Codex is not allowed in a QA rig pane), and the phone on the final head (emulator held by another session; it passed on an earlier head).
  • Windows, WSL, hookless agents and agents without process capture keep today's behaviour and today's limitations, including known false exits from title changes.

Author: @BrennanKB5.

Checklist

  • This PR is small and focused (cross-surface step 3; 46 files)
  • I explained what changed and why
  • Before/after screenshots attached (remote QA)
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered
  • Full lint/typecheck/test/build pass (CI green on e6353da: all unit shards, typecheck, package mac + Windows, cross-version wire, Mobile Checks)

…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.
…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.
…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.
…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.
…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.
@brennanb2025

brennanb2025 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Review status — PR #24184 (step 3 of 3), head 351af5a

The 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, orca worktree ps / terminal show and SSH reconnects now show the agent the execution host recorded: an idle Claude stays shown under any title, an exited one clears, and an agent replaced while you were disconnected shows correctly after reconnect. Sends and dispatch still only go to an agent that is actually in front. Hookless agents, Codex/Cursor and native Windows keep today's behaviour on every surface.

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):

  • Sends to an identified agent skipped the permission and claude agents checks, so a note could be typed into Cursor's "Run this command?" menu; those checks run first again.
  • A Ctrl-Z-suspended Claude counted as ready, so sends could be typed into bash, ssh, vim or python3; the agent now answers for the keyboard only when it is the program in front, otherwise main's rules apply.
  • WSL/SSH-recorded agents can't be checked locally, so those panes keep main's send rules.
  • After an agent exited, a different agent started in the same pane was hidden on the phone, the CLI and paired clients; only the exited agent's own leftovers are set aside now.
  • Paired clients lost Activity history, unread counts, prompt search and orchestration labels because the host's status was replaced instead of extended; host presence is now added to the published status.
  • Paired clients dropped host-local rows from the palette, could keep a stale exited record, ignored a relaunch with an older clock, and re-sorted the sidebar on every status ping.
  • SSH reconnect: an agent replaced while disconnected kept the old agent shown; a replayed old exit could delete a later agent's row.
  • SSH never forwarded the agent's process record to the desktop (a field omission present since step 1); the SSH link now forwards the relay's message like WSL, which turns the step 1/2 owner handling on for SSH for the first time.
  • CI: two capability-list tests, the browser web client not advertising the capability, and a publication-cost regression that rebuilt every worktree on each republish.

Deferred (P3, no user-visible harm proven):

  • The relay's ordering marker is stored and published though only its "new relay" meaning is needed; follow-up: a relay capability marker.
  • "Owner is in front" is decided by process name, not pid (a different claude process in front of a suspended one counts); same looseness main already has without a fresh hook status.
  • One extra foreground read on explicit status requests for a live local owner.
  • A hostile relay sending a non-string version makes that frame throw and be dropped (WSL already behaves so).
  • worktree ps reports the owner's agent type in a pane with a nested agent.

Verified:

  • Node, web and CLI typechecks and the mobile test-typecheck ratchet pass on the merged head; CI is green on 351af5a (all unit shards, typecheck, mac + Windows packages, cross-version wire, Mobile Checks ran and passed).
  • 63 root test files (2206 tests), the SSH/remote set (23 files, 237 tests), mobile (36 tests) and 12 cross-version wire fixtures pass; old phone + new host gets byte-identical rows, new phone + old host keeps today's behaviour.
  • Live on the M4Air: CLI shows idle Claude under a zsh title and clears within 3 s of /exit; over a real SSH host the desktop receives the remote Claude's process, a replaced agent shows live after reconnect and a send lands in it, and a killed agent clears from worktree ps, CLI and sidebar.
  • Live phone (earlier head 20c3343): Claude icon and "Switch to chat view" while idle under a shell title, both cleared within 5 s of /exit.
  • Live native Windows: unchanged from main (sidebar and worktree ps clear within about a second, plain PowerShell pane shows no agent).

Not verified:

  • The phone on the final head (the shared emulator was held by another session for the whole window).
  • A live paired web client and mixed-version apps over a real connection (unit tests and wire fixtures only).
  • Codex rows on Windows (real Codex is not allowed in a QA rig).

Seen in QA but not caused by this PR: after /exit, the native-Windows tab icon can stay on Claude and the macOS sidebar keeps a "shell - Claude" row; both reproduce identically on step 2 and on main, because the desktop's sleeping-session and retained-row records survive the exit (on this PR's final head the macOS tab sparkle clears; the sidebar row remains). The same sleeping-session record also kept the tab icon on an SSH pane after a disconnected kill; that case was not re-run on main.

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.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 1, 2026 10:24
@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[High risk] Adds agent process presence tracking across remote, mobile, and CLI views.

The PR should not merge until terminal status stops reporting an exited owner from its stale permission title and mobile Chat keeps provider sessions aligned with the host owner.

Findings

  1. P1 New owner inherits old conversation ▶

Summary

The PR carries execution-host agent ownership to mobile and paired-client tab snapshots, uses it in terminal and CLI readers, and forwards SSH relay presence through remote ingress.

  • Capability-gated projections preserve the legacy view for clients that do not opt in.
  • Two reader paths still need attention: an exited owner's permission title can remain authoritative in the CLI, and a new owner's mobile Chat identity can be paired with an old provider session.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  H[Execution-host hook owner] --> R[Host status row]
  R --> P[Capability-gated tab projection]
  P --> M[Mobile icon and Chat]
  P --> W[Paired-client mirror]
  R --> C[CLI terminal status]
  S[SSH relay] --> R
Loading

Reviews (1) · Last reviewed commit: "Merge remote-tracking branch 'origin/bre..."

Comment on lines +31 to +33
agentStatus: legacy.agentStatus
? { ...legacy.agentStatus, agentPresence: hostStatus.agentPresence }
: hostStatus

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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

@greptile-apps

greptile-apps Bot commented Oct 1, 2026

Copy link
Copy Markdown

Comments Outside Diff

These findings could not be posted inline.

  • P1 Exited agent still appears waiting src/main/runtime/runtime-terminal-agent-status-query.ts:93 ▶

    When the host has proved an agent exited but its last terminal title still says it is waiting for permission, this return reports it as running before checking the exit record. terminal show can therefore keep showing the dead agent as waiting. Filter the exited owner's title before accepting it as permission evidence.

@pullfrog pullfrog 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.

✅ 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.ts extracts the ended-owner helpers out of renderer/.../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.ts and runtime-terminal-agent-presence.ts only 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 (selectKeyboardAgentPresence downsamples a remote live owner to {agent}).
  • Relay ordering + admission — new relay-agent-presence-observation.ts stamps each forwarded owner with an epoch/sequence; new server-remote-agent-presence.ts fences stale deliveries and sets the internal agentPresenceFromExecutionHost flag, which server-status-update.ts uses to adopt host-stamped owner replacements/replayed exits and then strips before storing. ssh-relay-session.ts now forwards the whole envelope (WSL-style) so presence reaches main, with host-set capabilities applied last.
  • Capability-gated client projection — session-tab-presence-projection.ts attaches the owner record to the published tab only for agent-process-presence.v1 clients, via a module-private Symbol so it cannot arrive over JSON; web-session-tabs-sync re-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

Pullfrog  | View workflow run | Using 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
…-session-projection.ts to stay under max-lines
@brennanb2025

Copy link
Copy Markdown
Contributor Author

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.

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