Skip to content

fix(desktop): use host-owned agent presence (step 2 of 3) - #24175

Closed
brennanb2025 wants to merge 46 commits into
mainfrom
brennanb2025/agent-presence-step2
Closed

brennanb2025 wants to merge 46 commits into
mainfrom
brennanb2025/agent-presence-step2

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 40 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2209 $\color{#cf222e}{\Huge{\mathbf{−}}}$​30 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2179
Prod 105 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2262 $\color{#cf222e}{\Huge{\mathbf{−}}}$​733 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1529

What this fixes

Problem. On desktop, every view that shows a terminal agent (Claude, and other agents whose hooks report their process) decides for itself whether that agent is still running. These views are the tab icon, the tab strip's Chat offer and the Chat shortcut, the terminal-backed Chat view, the command palette, sidebar activity and its sort order, workspace dots, dashboard rows and "finished" notifications. They each guess from the tab title, from what the shell shows in the foreground, or from a lost connection, so they can contradict each other. For example, the tab icon still shows Claude after Chat has returned to the terminal, or a dropped connection makes a running agent look finished.

Why it happened. Step 1 (#23947) taught the machine that runs the agent (the "host": your computer, or the remote machine over SSH) to record which process owns each terminal and to check that exact process. The desktop views never read that record, and several cleanup paths threw it away, so each view kept its own guess.

Who hits it. Anyone running Claude (or another agent whose hooks report its process) in a local terminal on macOS or Linux. Over SSH the new behaviour is ready but inactive until a follow-up fixes one forwarding gap in step 1's code (see Known weaknesses).

What changes for you.

  • All desktop views now agree on whether the agent is running, because they read the host's record.
  • When the agent exits, the icon, Chat, sidebar and dashboard clear together, without waiting for a shell prompt. You get one "finished" notification if the agent was mid-turn or its last turn had not been announced; an agent that never ran a turn exits silently, as on main.
  • The tab icon and the palette stop showing an agent once it exits, instead of keeping its logo because of the title, finished row or saved session it left behind.
  • An unanswered check or a dropped connection no longer makes a running agent look finished.
  • If an agent dies silently and you start a new one in the same terminal, the terminal belongs to the new agent instead of showing "finished".
  • Closing a tab or removing a workspace or repo clears the agent's record everywhere. Sleeping a workspace releases the record instead of treating it as an exit, so a Chat-view tab stays in Chat, its icon stays, and no notification fires, as on main.
  • The app no longer polls agents on a timer while it knows which process owns the terminal.

Not affected. Structured (non-terminal) Chat sessions, turn cancellation, mobile, the paired web client and orca command-line views are unchanged. So are agents with no recorded process: hookless agents, Windows, and WSL. They keep main's behaviour until step 4 (#24578); WSL until a follow-up after that.

The plan

This PR is step 2 of 4. Step 1 (#23947) has merged, and this PR targets main. Merge in order: this PR, then #24184, then #24578.

  1. Step 1 (fix(agent-status): preserve hook presence when process checks cannot answer (step 1 of 3) #23947): hooks record which process owns the terminal, and the host checks that exact process. A check that gets no answer means "can't tell", never "exited".
  2. Step 2 (this PR): desktop views read the host's record and stop guessing; the host keeps the record correct through cleanup, connection loss, restarts and agent replacement.
  3. Step 3 (fix(agent-presence): keep remote, mobile and CLI views on host ownership (step 3/3) #24184): move mobile, paired web, orca command-line views and SSH reconnects onto the same record.
  4. Step 4 (feat(agent-presence): identify terminal owners on the execution host (step 4B) #24578): Orca records the owning process itself, from the program in front of the terminal, for every agent with or without hooks, on macOS, Linux and SSH hosts. Hooks then only report turn state, and the Claude-only process capture is removed. Windows stays on main's rules until its exit check is proven; WSL stays on main's rules until a follow-up links Orca's tabs to the shell inside the WSL guest.

ELI5

Imagine a meeting room with one sign-in sheet at the door, kept by the person who runs the building. Before, each person in the room guessed who was in the room by looking at nametags and empty chairs, and they often disagreed. Now everyone reads the sheet at the door. Only the building keeper crosses a name off, and only once they know the person has left, not because they lost sight of them for a moment.

What Changed

Before

  • The host recorded the owning process, but the desktop views never saw it.
  • Each view had its own rules: a changed title, a shell in the foreground, a closed connection, or the terminal's cleanup could each count as "the agent exited".
  • Several host cleanup paths deleted or blanked the owner record, so a later hook or check had nothing to compare against.
  • While an agent was working, the completion checker polled on a timer to see whether it had stopped.
  • After an agent exited, the tab icon fell back to rules that read the agent's own leftover title and finished row, so it kept showing the agent.

After

  • The host publishes the owner. Every status update and snapshot carries the owner record: which agent it is, which process runs it, and whether it has ended. The desktop keeps an in-memory copy per terminal pane. The copy is never saved to disk and it ignores repeats that change nothing.
  • Cleanup states what it knows. Every host cleanup names one of three outcomes, and one function applies it:
    • "can't tell" (connection lost, remote terminal) keeps the owner;
    • "agent exited" (the agent's own exit hook, or a check that proves the process is gone while the terminal stays open) keeps the ended owner and the information needed to resume the session;
    • "released" (local terminal exit or sleep, tab close, stale-pane cleanup, workspace or repo removal) deletes the record.
  • Releases are published from one place. Every write that drops an owner goes through one point, which tells the desktop. The desktop only removes the matching process, never a replacement.
  • One rule per pane on the desktop. Every desktop view listed above uses the same function to decide a pane's agent from the host record. Title and shell guesses apply only to panes without a recorded process, through main's rules kept word-for-word in separate files.
  • Exits are facts, not guesses. For a pane with a recorded process, the desktop no longer polls the terminal. The "finished" notification fires once, when the host's record changes from running to ended, and only for a turn that had not been announced. The host checks the exact recorded process when the shell prompt comes back, the title changes or the agent goes idle, and also every 2 seconds on the machine running the agent while that agent is alive. It never probes a process ID supplied by the desktop.
  • A dead owner hands over to its replacement. When a hook arrives from a different process, the host checks the recorded owner. If that owner is proven dead, the first process that raised the doubt takes over the pane. The desktop host and the SSH relay host (the helper Orca runs on the remote machine) share this rule. The relay sends the handover as a restatement of the current turn, so the turn's start time stays the same and no second "finished" notification fires.
  • An exited agent's leftovers stay hidden until the pane moves on. The ended record is kept until the shell is confirmed back after a command, the next command starts, or the pane is reattached, so the tab icon and palette ignore the exited agent's leftover title, finished row and saved session in the meantime. The agent's own exit no longer counts as the pane moving on.
  • Orca's own sleep is not an agent exit. When Orca stops a terminal for sleep, Claude's session-end hook arrives before the terminal closes. The host already records that Orca started that stop, so a session-end inside it releases the owner instead of ending it. A session-end while the terminal lives on is still an exit.
  • Restart rechecks every restored owner, four checks at a time, instead of only the ones restored from queued hook events.

Why

The bug class this removes. The bug was never one wrong rule. It was many readers each keeping their own answer to a question the host already answers. Adding another guard to each reader would keep that disagreement. With the host as the only source, a view can no longer contradict it.

Alternatives I rejected:

  • Fix each reader's rule where it disagreed. That patches symptoms and leaves the disagreement for the next reader to reintroduce.
  • Poll the process on a timer. That costs work on every pane and is still a second opinion. The exit already arrives as a host fact.
  • "A hook from a different process never inherits the owner." An agent started by the running agent (a nested agent) also sends hooks. Under this rule it would take the pane from a live outer agent, and its exit would then end the pane while the outer agent kept running. A handover only after the old owner is proven dead avoids both.
  • Keep an "ended" record for a torn-down terminal. Snapshots, replays and the saved file would then bring back a dead pane. Deleting the record matches what main does.
  • A new message type for the relay handover. Not needed. The handover is an ordinary status message using the existing "replay" flag, which every desktop build already reads and checks.

How this differs from the usual design:

  • Intended: a check that gets no answer, or a lost connection, keeps the owner. This is safe because only a positive fact ends an identified owner, and closing the tab or removing the workspace still clears it.
  • Intended: the first process to doubt a dead owner takes over, not the latest. The first one started after the death; a later one may be an agent it launched.
  • Intended: every 2 seconds, the machine running the agent re-checks each live agent whose process it recorded (the same exact check: process ID plus start time; a zombie or a missing process means gone; a check that cannot answer is never treated as an exit). Why: Ctrl+C and silent crashes must clear even when no hook fires and the title does not change. It runs only while such an agent is alive, costs nothing for other panes, and is far cheaper than main's per-pane terminal polling every 0.75 to 15 seconds. Cost: on Linux two small file reads per agent; on macOS one short ps -p per agent every 2 seconds.
  • Temporary (WSL follow-up after step 4): WSL owners cannot be checked from Windows yet. The host publishes them without their process, and they follow main's exit rules.
  • Temporary (removed in step 4, feat(agent-presence): identify terminal owners on the execution host (step 4B) #24578): panes with no recorded process (hookless agents, Windows, older hosts) keep main's title and shell rules, including step 1's hookless process read. The rules live in their own files, main's tab-icon logic is restored unchanged, and they never apply to a pane with a recorded process.
  • Intended: a mixed-version relay handover is not handled. The connection handshake pins the relay to the desktop's own build, so this mix does not happen in normal use. If it did:
    • an old relay with a new desktop behaves like the base branch;
    • a new relay with an old desktop can keep a stale row until the terminal exits or the tab closes.
  • Temporary (follow-up, no ticket yet): removing every repo for an SSH host (repos:removeForHost) does not drop that host's status rows. repos:remove and workspace removal do.

When an exited agent clears

Which agents. Claude and OpenClaude on macOS and Linux: agents whose hooks report their process. Every other agent (Codex, Gemini and the rest) keeps main's timer-based detection until step 4 (#24578), which records the process for every agent. Windows and WSL keep main's detection after that too, until their own follow-ups.

What counts as an exit signal. Either Claude's session-end hook, which Orca installs only for Claude 2.1.261 or later (OpenClaude never gets it), or Orca's check of the recorded process. That check runs when the shell prompt comes back, when the terminal title changes, when the agent goes idle, and every 2 seconds while the agent is alive.

What happens on Ctrl+C (measured on macOS against main, with an agent that does and does not send the session-end hook):

  • Double Ctrl+C at the idle prompt: tab icon, Chat view and sidebar row clear in 0.15 to 0.41 s. On main the sidebar row clears in about 1.1 s, but the tab icon keeps showing Claude and Chat stays open.
  • Ctrl+C during a turn, then again to exit: 0.12 to 0.31 s. Main: row about 1.2 to 1.3 s, icon and Chat stay.
  • claude; sleep 30, Ctrl+C exits Claude while sleep keeps the terminal busy: 0.21 to 1.15 s. Main: about 31 s, when the sleep ends.
  • A plain sh without Orca's shell integration: 0.27 to 0.54 s. Main: still showing Claude after 60 s.
  • No "finished" notification fired for these exits in either build.

The remaining gap. A kill -9 or a crash, while another program keeps the terminal busy and nothing else changes, is now noticed by the 2-second check: measured 0.66 s for a plain kill -9 and 0.86 s for kill -9 followed by sleep 30 (main: about 31 s, when the sleep ends). The worst case is therefore about 2 seconds. Intended: a shorter interval would cost more for little gain.

Where it does not apply yet (temporary). Over SSH the remote host's process record does not reach the desktop yet (a follow-up to step 1), so SSH terminals behave exactly like main. WSL agents cannot be checked from Windows and keep main's rules until a follow-up after step 4. If the process check cannot answer at all, the agent keeps showing until one of the other signals arrives; it is never cleared on a guess.

Known weaknesses

  • SSH owners are inactive until step 1's code is fixed. Step 1's SSH hook handler copies named fields and leaves out the process owner, so over real SSH the owner never reaches the desktop. SSH panes therefore behave like main today, and this PR's SSH paths (adopting the relay's owner, the handover) do nothing yet. WSL forwards the whole message and is unaffected. Once a follow-up forwards the owner, SSH needs its own live QA.
  • A second, identity-less hook silently gives main's behaviour. Some users have an older Orca Claude hook in another settings file, so each event arrives twice. If the copy without a process ID lands first, the pane is recorded without a process for that Claude session and follows main's rules (no flapping; the exit clears when the shell returns). The cause is in step 1's shared rule. The fix is for a hook without process identity to record status only and never claim the owner.
  • Ended-agent history waits for a pane event. After an agent exits, its leftover title is ignored until the shell returns or the next command starts. Two cases stay hidden until then:
    • claude; ssh box on one line, followed by another Claude on the remote box: the second Claude is hidden until ssh exits;
    • a hookless Claude started in a pane that is not on screen: it is hidden until the pane is shown;
    • in a shell without command marks, a later hookless run of the same agent in that pane: it is hidden until the shell is next confirmed in the foreground or the pane is reattached, because that shell never reports a command start.
    • These narrow cases are worse than main. The fix direction is to compare process identity instead.
  • A dismissed but still running agent in a plain-titled, hand-typed terminal is not offered Chat by the tab strip or the shortcut. The pane itself offers it. This is the same as main.
  • Sidebar dot for a tab not yet opened. The dot's tab-title fallback does not see the owner record, so for a tab not opened this session the dot and the sidebar sort order can disagree.
  • Owners are grouped by the workspace recorded on the host row, not the tab's current workspace. No path where these differ has been found.
  • Not demonstrated either way: whether the startup snapshot can show a "finished" notice for an agent that died while Orca was closed, when the restart check finishes after the desktop has already loaded the owner as running.

Linked Issue

Related to #23638 / STA-8819: this fixes verified identity-loss paths, but the reported Chat freeze was not reproduced, so this PR does not close that issue. STA-8862 belongs to the restart work (#23785); STA-7949 belongs to #22727; STA-8902 is unrelated.

The actual diffs of #22708 and #18557 were reviewed. The startup-pending Chat guard is reused from @lurunzi's #22708, with commit credit. This PR does not claim to fully supersede either community PR. #22727's overlapping status-identity change is kept to one additive owner field.

Visual Proof

Live macOS QA with screenshots: first run (before/after, partial) #24175 (comment), and the re-checks of the tab-icon, sleep and exit-notification fixes against main #24175 (comment).

Testing

  • I manually tested these changes locally
  • Automated tests added/updated

Regression tests, each shown failing without its fix:

  • Every fix has a test that fails with its production change removed and passes with it restored. There are 13 renderer failures across 5 files, plus 6 host and 6 renderer failures for the second round, 3 relay-handover failures for the third, 2 for the tab-icon fix, and 2 for the sleep and exit-notification fixes.
  • Owner lifecycle: release on local terminal exit, checked in every projection including the saved file; owner kept through a lost connection; dead-owner handover to the first doubter, on both hosts; relay handover sent as a restatement; WSL owners left to legacy rules; workspace and repo removal; a session-end during Orca's sleep stop releases, while one with the terminal still alive is an exit, and the sleep marker expires after a failed, finished or stale stop.
  • Desktop: views agree per pane; no polling over 60 s, then exactly one completion; a replacement owner is never credited with a late verdict; no "finished" notice on teardown; Chat through sleep and kill matches main; read state kept on exit; the tab icon clears after the agent exits and leaves its title, finished row and saved session behind, while a pane that never had a recorded process keeps main's icon; the exit of an agent that never ran a turn posts no notification, and a turn cut short by /exit posts exactly one.

Suites after merging main, named files only, with real-CLI, real-binary and integration suites excluded:

  • host: 352 files, 4612 passed, 20 skipped, at the final commit;
  • relay: 189 files, 1822 passed, 3 skipped;
  • renderer: 1053 files (every file that mocks or reads the app store, renders the terminal pane or its connection code, or pins a listener budget), 9734 passed, 3 skipped, at the tab-icon fix; the final commit only retypes one test fixture;
  • runtime harness: 13 files, 1393 passed, 1 skipped;
  • every test main added or changed since step 1 in the renderer, host, relay, shared and end-to-end areas: 247 files, 3380 passed, 19 skipped;
  • end-to-end unit and cross-version files: 54 files, all passing except four that fail only on the local machine. Three pass in a clean checkout of the same commit, and one needs a cloud package that is not installed locally and fails the same way on main.

Earlier CI red, now fixed. CI failed 20 tests in 4 files. Three files used test stores that lacked the new owner map, a fixture gap only. The fourth pins how many store listeners each terminal pane may add, and this PR had added one; that read is now folded into an existing subscription, so the pinned budget is unchanged. A wider sweep of similar test stores found and fixed one more fixture.

Live Ctrl+C QA (macOS, a separate machine, against main): idle and mid-turn Ctrl+C, claude; sleep 30, kill -9 with and without a following sleep, a shell without Orca's integration, and a reattached pane, each with an agent that does and does not send the session-end hook. Every case clears the tab icon, Chat view and sidebar row within about 1.5 s on this PR; the numbers are in the section above.

Typecheck. Node, web and CLI projects pass at the final commit.

CI at the final commit: static analysis fails on structured-agent-session-adapter.ts exceeding the line limit. This PR does not touch that file; CI lints the PR merged into current main, and main's own copy trips the limit. The previous head passed every CI check.

Static checks. Every locally runnable static-analysis step from the PR workflow passes, including lint, the low-evidence pattern check, changed-code quality, React Doctor and the no-new-violations checks. No dependencies or lockfiles changed.

Not run: a packaged build, and live Linux, Windows or SSH QA. Live macOS QA on a separate machine is reported in a PR comment.

Review

Host publication and the desktop migration have to land together. Moving the views without keeping the owner through cleanup would strand icons. Keeping the old view rules would still contradict the host. Known weaknesses above are listed with whose code causes them; the two caused by step 1's code belong in a follow-up to step 1.

Agent skill upstream boundary

  • Not applicable; no agent skill installer code or data is copied.

Notes

  • Security: the host only checks the process it recorded itself; it never probes a process ID supplied by the desktop.
  • Cross-platform: no new platform-specific subprocesses, paths or shortcuts. Windows and WSL keep main's behaviour (see step 4, feat(agent-presence): identify terminal owners on the execution host (step 4B) #24578, for what changes and what stays).
  • Remote SSH: the host that runs the agent supplies every verdict, and loss of contact is never treated as an exit. The desktop and the relay use one shared handover rule.
  • Mobile: unchanged here; moved in step 3.
  • Backwards compatibility: only optional fields, one desktop-only event channel and one desktop-only check call were added. The relay uses the existing replay flag, and no new message type was added.
  • Performance: the desktop no longer polls identified agents; the host re-checks each live recorded agent every 2 seconds (macOS: one ps -p per agent; Linux: two file reads); repeated identical records cause no store writes; the restart recheck runs four at a time.
  • Folder workspaces and Git worktrees take the same path.

Author: @BrennanKB5.

Checklist

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

brennanb2025 and others added 17 commits September 29, 2026 16:17
…osing real ones

- Pin the macOS process start time to UTC on both the hook and the host so a
  shell TZ or a time-zone change cannot turn a live Claude into an exit.
- An unanswered process check falls back to the foreground confirmation, so
  Codex, SSH and Windows panes still leave the agent state on a real exit and
  the Codex late-completion recovery still runs.
- A nested agent that inherits the pane key cannot take over the pane's
  presence; a retired session is replaced by the next session even when its
  SessionStart was lost.
- Drop the unused Windows process read (no Windows hook captures an identity
  yet) so shared code no longer imports main-process modules.
- Skip the capture outside Orca panes, gate relay re-checks to real title
  changes, and list the capture module in the CLI project.
… its session

Presence now exists only when a hook carries the agent's process identity,
and only that process's evidence changes it: its SessionEnd ends the pane,
its /clear and /resume keep it, and hooks from any other process (a nested
agent inheriting the pane key) update status without taking ownership.
Hooks without an identity (Windows, sessions started before the capture)
behave exactly as before, so a nested agent can no longer end a pane it
does not own. An ended owner stops answering 'exited', and the host probes
the owner only when another process reports in the pane.
- Relay retries and transcript polls schedule against the row the relay
  cached, and identity-less events pass through the transition unchanged,
  so SSH Grok replies and Codex transcript polls deliver again.
- A live process check restores the runtime's agent status and releases
  queued orchestration mail, like a foreground read that finds the agent.
- An answered foreground read naming a non-agent (wsl.exe, tmux) is still
  an exit; only silence is not.
- A suspended (Ctrl-Z) agent is unverifiable, not live, so the foreground
  read decides as before.
- An agent of another type started mid-turn cannot own or end the pane.
- Replayed spool hooks check each pane once, and SessionEnd ends presence
  only for reasons that end the process.
… process id

Ownership is now decided only by comparing the recorded owner with the
sender, never from the row's agent type or turn state (which identity
resolution rewrites). The first agent hook in an empty pane claims it; a
live owner keeps it against any other agent (a nested claude -p, a Codex
started inside Claude or the reverse); only the owner's own proven process
can end it. An owner no hook identified never ends from a hook and is not
probed, so those panes behave as before.
… again

Only an admitted exit (Claude's process-ending SessionEnd, or a host-proved
exit) is marked ended on the event, and ownership keys on that marker, not
the hook name. Devin, Qoder, CodeBuddy and Copilot SessionEnd hooks are
ordinary status updates again, locally and through the relay.
…nd-finished cleanup

The renderer's command-finished cleanup kept every row when its shell check
could not answer, which left Codex, hookless-agent and old-relay rows over
SSH showing done after a real exit. It now asks the host whether the pane's
agent process can be checked: only a pane with an identified, running owner
keeps its row on an unanswered check; every other pane drops it exactly as
before. The drop stays armed while the host answers, so a new command still
cancels it, and a missing or failing answer (web client, older host) keeps
today's behaviour.
…t confirmation

The title-driven exit confirmation applied 'silence is never an exit' to
every pane. It now applies only when the pane has an identified owner whose
process cannot be checked right now; a pane with no process identity
(Codex, hookless agents, Windows, old relays, sessions started before the
update, or an owner that already ended) confirms exits exactly as before.
…esence-pid

# Conflicts:
#	src/relay/pty-handler.ts
Publish the recorded owner and positive exits to desktop readers, retain owner metadata through generic cleanup, and fence unidentified compatibility from process-backed owners. Keep hookless behavior until step 3.

Reuse the pending Chat startup guard from #22708.

Co-authored-by: lurunzi <lurunzi@gmail.com>
…evidence

Pane cleanup now states what it knows about the recorded owner: a lost
transport or remote handle keeps it unverified, a torn-down local terminal
(PTY exit, restored-pane reaper) ends it, and a closed tab, retirement or
removed workspace releases it. The row write publishes any dropped owner to
the renderer, a proven-dead owner hands its pane to the process whose hook
raised the doubt, a relay's live owner stamp is adopted, and every hydrated
owner is rechecked once at startup. Shell-foreground reconciliation is now
legacy-only and refuses identified rows.
…fied owners

A live identified owner decides only its own pane; the tab icon and palette
read the focused pane first, and siblings keep their row signals. An owner
that exited is history: it hides evidence naming itself (its title, process
read, launch intent) while a later agent in the same pane still shows on the
tab, Chat, sidebar, dots and sort. Panes with no live identified owner keep
the pre-presence rules exactly, including the tab icon's own resolver.

The renderer drops a mirrored owner when the host releases it, and terminal
exits clear rows as before. Identified owners are no longer polled: their
completion fires once from the mirrored exit, and terminal signals only ask
the host's exact-owner check.
…ts terminal

A local terminal teardown also ends its identified owner, and main sends
that PTY exit right behind the ended owner. The mirrored-exit completion now
waits one task so the pane's exit disposes it first; only an owner that
exits while its terminal lives reports a finished task.
… down

The host still marks the owner ended when its local terminal goes away, but
renderers now drop the record as they dropped the row before presence, so
sleep, hibernate or a kill keeps the tab in the view the user chose and no
finished-task notice fires. Only an owner that exits while its terminal
lives publishes an ended record. The one-task completion deferral this
replaces is removed. Also narrows the owner check instead of assuming a
sender always has an agent type.
…rifiable owners

The runtime again records an agent exit when the foreground read answers
with a non-agent process, even if the host could not check the owner; only
silence keeps the agent. That read is the execution host's own answer, not
lost contact, and it restores main's runtime title state. Identified owners
are still ended only by the host: the renderer turns this signal into an
exact-owner check, and remote owners end through the relay's recheck.
…wn owners

- A torn-down terminal releases its owner in every projection (live
  events, snapshots, replay, persistence), so a reload never shows a slept
  Claude as exited.
- The dead-owner handover now lives in the shared transition module and
  both the desktop hook server and the relay use it: when a probe proves the
  owner dead, the process whose hook doubted it takes the pane with its own
  status instead of the pane showing an exit. One probe per owner, keyed by
  pane and owner rather than by the row object.
- An owner no host can check (WSL today) is published without its process
  and left to the legacy exit rules until step 3 forwards the check.
- Runtime-initiated workspace removal and repo removal drop host rows, and
  the boot recheck runs a few probes at a time.
…d-state rules

- A republished owner (every Claude hook carries it) no longer writes the
  store, bumps the epochs or wakes the pane listeners.
- An owner exiting in a live terminal no longer removes the pane's row: the
  host pane clear that follows applies main's rule, so a finished turn keeps
  its read state and cleared-event cutoff.
- An exited owner's history ends when the shell is confirmed back or the next
  command starts, so a later run of the same agent shows again.
- The tab strip and the Chat shortcut use the pane's rule for an exited
  owner's evidence, and an ineligible Chat request stays unbound only while
  an Orca launch is pending; otherwise the tab returns to the terminal as before.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live QA follow-up: re-checks at 47bd3e7 and 574b46f against main 07e9fdf (M4Air, real Claude Code 2.1.285, mock API)

These replace the earlier comment's open items. Same rig as before: hidden dev Orca, isolated home and profile, Claude started only through the rig's guarded launcher (proven in the store, the settings getter and the saved profile before every start, wake and reload), and the process guard quiet throughout. BEFORE is origin/main at 07e9fdfd1369, the main parent of the branch's merge.

574b46f (current head), all PASS

  • Sleep with the tab in Chat view: the view stays Chat, and the tab icon stays Claude for the whole 15 s with no flicker. The owner goes straight to released (no "ended" step seen in 200 ms sampling) at +1.5 s. No notification. This now matches main; on 47bd3e7 the view flipped to terminal and the icon went Claude → terminal → Claude.
  • Wake: the view stays Chat. Orca resumed Claude through the launcher with the isolated home, the new process owns the pane at +2.8 s, and Chat shows the session. No notification. Main also keeps Chat, but its woken Chat view was empty until the resumed Claude got past its startup dialog (a rig-home difference, not this PR).
  • /exit of the resumed Claude, never prompted: no notification (on 47bd3e7 one empty "task complete" fired; main sends none). Chat view and presence clear at +2.0 s. One note: the tab icon and sidebar row clear only at +5.4 s here, about 3.4 s after presence, slower than the other exits.
  • Regression guard (claude; sleep 30, one turn, Chat view, /exit): the tab icon leaves Claude at +1.84 s while the shell is still sleeping, and Chat view and presence clear in the same instant. The finished row stays until the prompt returns (matches main). One notification for the turn and none for the exit.

47bd3e7 (icon fix), all PASS

  • Exit after a finished turn: the icon leaves Claude at +1.66 s while sleep 30 still runs. On c71a327 it stayed Claude through the whole sleep.
  • Exit during a running turn: the working row, presence and icon clear at +1.19 s. One "task complete" fires at +2.5 s.
  • Relaunch in the same pane after the prompt returns: the icon comes back, presence carries the new process, and the row and Chat show the new session.
  • Reload while the workspace sleeps: Orca did not resume Claude by itself on main (two reloads) or on 47bd3e7. The self-resume seen once on c71a327 did not reproduce.

Screenshots (574b46f)

Before sleep, the tab is in Chat view:

574b46f: Chat view before sleep

After wake, still in Chat view, with the resumed session:

574b46f: Chat view after wake

Tab icon with Claude live, before /exit:

574b46f: Claude icon before /exit

Tab icon 10 s after /exit, while the shell still runs sleep 30: no Claude icon, only the finished badge. The finished row in the sidebar is expected until the prompt returns.

574b46f: icon after /exit

Not covered: SSH (no keys in the isolated home), hookless agents, and split tabs (both unchanged by these fixes and not re-run). Every recorded rig process was stopped and verified afterwards, and the worktrees are clean.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status (head 574b46f, draft, targets main)

The problem. On desktop, the tab icon, the terminal-backed Chat view, sidebar activity, workspace dots and the dashboard each guessed on their own whether a terminal agent was still running (from the title, the foreground process, a shell prompt or a lost connection). They disagreed: Claude's icon could flip to a terminal while it was idle, and after /exit from claude; sleep 30 the sidebar kept Claude until the shell prompt came back.

What changes for you. For local Claude on macOS and Linux, every desktop view now reads the one record the host keeps of which process owns the terminal. After /exit the icon, Chat view, sidebar row and agent record clear within about 2 seconds, even while the shell is still running sleep 30. Sleeping a workspace no longer counts as the agent exiting. Agents without a recorded process (hookless agents, Windows, WSL) keep main's behaviour until step 3.

What the review fixed (three review rounds, a readiness check and two live re-checks):

  • Owner records that could never end: kept "live" after Orca proved the terminal dead (restart cleanup, removed worktrees), deleted without telling the desktop, or kept "ended" in a way that hid the next agent in the same terminal. Every cleanup now states what it knows (contact lost, process ended, or released) in one place, and every removal is published to the desktop from one point.
  • A new Claude started in the same terminal after a silent death was erased by the old owner's exit (row, Chat view and icon gone, a false "finished" notice). The host now hands the terminal to the new process once the old one is proven dead, with the same rule on the desktop and the SSH relay.
  • Split tabs showed a sibling pane's agent instead of the focused pane's.
  • Per-pane process polling on a timer (one ps per Claude pane every 750 ms on macOS) is gone; exits arrive as host facts, and repeated identical records no longer re-render or re-sort.
  • After /exit the tab icon kept showing Claude from the leftover title; it now clears with the exit (live: +1.2 to 1.8 s).
  • Sleeping a workspace with a Claude tab in Chat view switched it to the terminal view and made the icon flicker, because Claude's session-end hook arrives during Orca's own sleep stop. The host now uses Orca's existing record that it started that stop, so a session-end inside it is a release, not an exit; a session-end while the terminal lives on is still an exit. Live: Chat view and icon kept through sleep and wake, no notification, matching main.
  • An empty "finished" notification fired when a resumed Claude that never ran a turn was exited. The exit notification now fires only for a turn that has not been announced yet. Live: no notification, matching main.
  • An exited agent no longer wipes the read state of its finished turn; WSL owners (which nothing can check yet) keep main's exit rules; worktree and repo removal drop the host's records; restart rechecks restored owners four at a time.
  • CI: test fixtures missing the new owner map, a pinned per-pane listener budget (now met by folding the read into an existing subscription), a naming rule, and a runtime test whose exit rule the original diff had changed.

Deferred (minor, listed in the PR body): after an agent exits, its leftover evidence stays hidden until the terminal moves on, so a later hookless run of the same agent in that terminal (for example claude; ssh box, an off-screen pane, or a shell without command marks) can stay hidden until then; the sidebar dot for a tab not yet opened can disagree with the sort order; repos:removeForHost does not drop that host's records; after /exit of a resumed, never-prompted Claude, the tab icon and sidebar row cleared about 3 seconds after the Chat view did (still within about 5 seconds).

Not this PR's (follow-ups to step 1): the SSH hook handler does not forward the process owner, so this PR's SSH paths are inactive and SSH panes behave like main; a duplicated older Claude hook without a process ID can make a session fall back to main's behaviour.

Verified:

Not verified:

  • CI at 574b46f: static analysis fails on structured-agent-session-adapter.ts exceeding the line limit. This PR does not touch that file; CI lints the PR merged into current main and main's own copy trips it, so the test jobs were skipped on this run.
  • Live SSH (blocked: no SSH keys in the isolated test home), Windows, Linux and a packaged build. Several lower-priority live scenarios from the first QA plan were not run; the first QA comment lists them.

This PR stays a draft; it has not been merged or marked ready.

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

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[High risk] Refactors agent presence tracking across server and relay.

The PR should not merge until replayed SSH exits and startup snapshot releases converge on the host’s current owner state.

Findings

  1. P1 Replayed replacement exit is lost ▶
  2. P1 Snapshot restores released owner ▶

Summary

The PR publishes host-owned terminal-agent presence to desktop views and changes cleanup, relay handover, and completion handling to follow process ownership.

  • Desktop selectors and notifications now use a per-pane owner record while unidentified panes retain legacy behavior.
  • Host cleanup distinguishes an unverified owner, a proven exit, and a released terminal.
  • SSH replay admission and startup snapshot ordering still have owner-convergence gaps.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  H[Local host or SSH relay] -->|owner status| M[Main hook server]
  M -->|live update or snapshot| R[Renderer presence store]
  M -->|process release| R
  R --> V[Tab, Chat, sidebar and dashboard]
Loading

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

Comment on lines +33 to +37
const relayOwner = incoming.connectionId !== null ? incoming.agentPresence : undefined
const transitioned =
relayOwner?.process && !relayOwner.ended
? incoming
: transitionHookPresence(incoming, this.state.lastStatusByPaneKey.get(incoming.paneKey))

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 Replayed replacement exit is lost

If an SSH connection drops, this PR keeps the old owner on the desktop. If the relay then replaces that agent and the replacement exits before reconnection, the replayed exit goes through transitionHookPresence, which rejects it because its process does not match the old owner. The desktop keeps showing the old agent instead of the relay’s settled state.

Comment on lines +100 to +104
// Why: preserve set→release FIFO so a queued owner publication cannot outlive its release.
if (liveAgentStatusBurstQueue.some((queued) => queued.paneKey === data.paneKey)) {
drainQueuedLiveAgentStatusesForPane(data.paneKey)
}
useAppStore.getState().releaseAgentPresence(data.paneKey, process)

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 Snapshot restores released owner

If a startup snapshot is captured before an owner is released but arrives afterward, this handler removes the owner and the snapshot then records it again. Draining queued live updates does not guard against the pending snapshot. A pane that remains present, such as a sleeping terminal, can therefore show the released agent until another host update arrives.

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

Important

Two behavioral gaps remain in the new host-owned presence paths: a completed turn can lose its "finished" notification when the owner exits inside the hook-done quiet window, and the desktop's owner-release handler can be out-run by a queued status event or miss a pane-keyed authority alias. Both are anchored inline.

Reviewed changes

  • Host publishes the owner — status updates and snapshots now carry agentPresence; one abstract checkAgentPresence fences a probe on the recorded process, and a dead owner's pane is handed to the first doubting process (server-agent-presence.ts, server-status-update.ts).
  • Cleanup states one of three outcomes — released / agent-exited / unverified is applied through a single commitPaneRowAfterCleanup, and every owner-dropping row write publishes a release from commitStatusRowMutation (server-cleanup.ts, server-row-ownership.ts, server-tab-cleanup.ts).
  • Desktop mirror + one per-pane rule — a non-persisted agentPresenceByPaneKey store slice, an IPC release channel, and agent-presence-selectors.ts; main's title/shell rules are kept word-for-word for panes with no recorded process (legacy-unidentified-*.ts).
  • Exits are facts, not polls — polling is disabled for identified owners and a new agent-completion-host-presence.ts observer dispatches the mirrored exit (agent-completion-process-monitor.ts, agent-completion-poll-scheduler.ts).
  • Relay + sleep teardown — relay handover restates the turn via the replay flag, session-end during an Orca sleep stop releases rather than ends, and startup rechecks every restored owner four at a time.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment on lines +75 to +89
if (agent && state.hasAgentRunEvidence && options.isLive()) {
dispatchCompletion('process-exit', agent, {
terminalIdleConfirmed: true,
// Why: a resumed owner that exits without ever working has nothing to announce.
requiresUnnotifiedTurn: true,
completionIdentity: {
source: 'process-exit',
identity: `${exited.platform}:${exited.pid}:${exited.startTime}`,
agentIdentity: agent
}
})
options.onForegroundAgentExited?.({ agent, processName: agent })
}
clearAgentRunEvidence()
scheduleNextPoll()

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.

A process exit that lands inside the hook-done quiet window drops the finished notification for a completed turn entirely. dispatchCompletion refuses any non-hook source while pendingHookDoneTimer is set (agent-completion-notification-controller.ts:158), then this observer unconditionally calls clearAgentRunEvidence(), which clears hasAgentRunEvidence but not the pending hook-done timer (agent-completion-coordinator.ts:78-85); when that timer later fires with source: 'hook' it is refused by the evidence gate (agent-completion-notification-controller.ts:167). The legacy inspection path this replaces defers instead — agent-completion-inspection-result.ts:144-147 returns on hasPendingHookDone() without clearing evidence — so this is a regression for identified owners. The new suite cannot catch it because agent-completion-host-presence.test.ts:194-227 advances the quiet timer before ending the owner.

Technical details
# Owner exit during the hook-done quiet window yields zero notifications

## Affected sites
- src/renderer/src/components/terminal-pane/agent-completion-host-presence.ts:75-89 — dispatch is blocked, then `clearAgentRunEvidence()` runs unconditionally
- src/renderer/src/components/terminal-pane/agent-completion-coordinator.ts:78-85 — `clearAgentRunEvidence` does not clear `pendingHookDoneTimer` (only `clearPendingHookDone` does)
- src/renderer/src/components/terminal-pane/agent-completion-notification-controller.ts:158-160, 167 — guards that suppress both the process-exit and the later hook completion

## Reproduced path
1. `observeHookStatus({ state: 'working' })` then `observeHookStatus({ state: 'done' })` → `doneShouldUseQuietWindow` is true (`state.workingStatusObserved`) → `scheduleHookDoneCompletion` sets `pendingHookDoneTimer`.
2. Before `HOOK_DONE_QUIET_MS` elapses, the owner ends → observer dispatch is refused, then `clearAgentRunEvidence()`.
3. Quiet timer fires → hook dispatch refused by `!processState.hasAgentRunEvidence`. Outward count: 0.

## Required outcome
A completed turn that is followed by the owner exiting within the quiet window still posts exactly one finished notification, matching the legacy path and the PR's stated behavior.

## Suggested approach
Mirror the legacy deferral in `createAgentOwnerExitObserver`: when `hasPendingHookDone()` is true, do not clear run evidence (or clear the pending hook-done before clearing evidence) so the quiet timer's hook completion can still dispatch. Add a test that ends the owner before advancing the quiet timer.

Comment on lines +100 to +104
// Why: preserve set→release FIFO so a queued owner publication cannot outlive its release.
if (liveAgentStatusBurstQueue.some((queued) => queued.paneKey === data.paneKey)) {
drainQueuedLiveAgentStatusesForPane(data.paneKey)
}
useAppStore.getState().releaseAgentPresence(data.paneKey, process)

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.

The release handler mirrors only part of the pane-clear handler it sits beside. It drains the burst queue but never purges pendingAgentStatusEvents (the clear handler does at lines 80-84), and it calls releaseAgentPresence(data.paneKey, …) with the raw key while the set path records under the resolved authority key (resolveAgentPaneAuthorityKey, agent-status-event-applicator.ts:61). Either gap leaves a released owner recoverable.

Technical details
# Owner release can be undone by a queued set, and can miss a transferred pane

## Affected sites
- src/renderer/src/hooks/ipc-events/agent-status-listeners.ts:100-104 — release handler
- src/renderer/src/hooks/ipc-events/agent-status-listeners.ts:77-84 — clear handler purges both queues
- src/renderer/src/hooks/ipc-events/agent-status-ipc-bridge.ts:47-60, 62-100 — `enqueuePendingAgentStatus` / `flushPendingAgentStatuses` re-apply with `retry: true`, which bypasses the set path's own pending purge (`agent-status-event-applicator.ts:121`)
- src/renderer/src/hooks/ipc-events/agent-status-event-applicator.ts:61,152 — set records under `resolveAgentPaneAuthorityKey(data.paneKey)`
- src/renderer/src/store/slices/agent-status-authority-actions.ts:194 — `transferAgentPaneAuthority` moves the record to the owner key

## Required outcome
After a release, the released process owner cannot be re-recorded from queued events, and a release for a pane with an authority alias drops the owner record wherever it is actually stored.

## Suggested approach
In the release handler, purge `pendingAgentStatusEvents` for `data.paneKey` exactly as the clear handler does, and resolve the alias before releasing (`resolveAgentPaneAuthorityKey(data.paneKey)`). Add coverage for release-after-enqueue and release-after-transfer.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (1)
AGENTS.md — auto-discovered
📝 Walkthrough

Walkthrough

The change adds host-owned agent process presence across server, relay, IPC, and renderer state. Hosts check process identities, record owner handovers and exits, and report releases. Renderer consumers use presence to resolve agent identity, completion, chat eligibility, worktree activity, and status projections. Cleanup paths distinguish released, unverified, and agent-exited owners. Tests cover reconciliation, state updates, and UI projections.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 93fea

Agent status views may fail on an invalid pane layout, presence checks may stop early after an error, and a new agent may appear under the previous agent’s status. Resolve these issues before merging.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 93fea

The ownership change merits review because delayed status recovery can restore presence after it was released. The demonstrated effects concern terminal ownership and activity state; broader security consequences and remote forwarding behavior remain unverified.

Retained concerns

  • Low · reliability · inferred: Presence release is not terminal across pending-event recovery. The release listener drains live queued statuses but leaves pending statuses intact. If an earlier status becomes admissible later, retry can recreate the released process record because release removes the record without retaining a release fence. This undermines host-owned lifecycle revocation at the renderer boundary. Exact-process matching protects an existing replacement owner, but does not prevent delayed reinstatement when no record remains. Security consequences beyond ownership and activity projections were not established.
Security review details

Security Blast Radius

  • inferred — Direct HTTP influence requires local reachability and possession of the relay hook token. The inspected envelope parser validates pane syntax and tab consistency rather than a sender-specific pane entitlement, so potential input scope is the token-bearing server's pane namespace, subject to downstream admission. Cross-tenant, credential, or data-store compromise was not established.

Security Findings and Attack Paths

  • observed — A differing hook process can nominate a successor. The inspected handover path probes the recorded owner and adopts the supplied successor after an exited verdict; it does not independently probe the successor in that operation. This is a trust limitation, not a verified PR-introduced vulnerability: upstream identity binding, effective sensitive outcomes, and the base-to-head exposure comparison remain unresolved.

Trust Boundaries and Controls

  • observed — Ordinary mismatched-process hooks retain the recorded owner, and mismatched exit hooks are rejected. Async reconciliation checks that the current row still identifies the probed owner before applying its result. Central row mutation emits release only when the previous process is no longer carried forward.

Resilience and Maintainability Implications

  • observed — Periodic owner checks stop when no live-owner panes remain, and check failures are contained. Renderer transient clearing removes qualifying events from both live and pending queues, providing a stronger recovery fence than the new process-release listener currently supplies.

Hardening Proposals

  • proposed — Carry process-specific release fencing through pending-event retries, without clearing replacement owners or unrelated pane status. A release tombstone or equivalent queue admission rule could preserve revocation until all older queued publications have expired.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.48% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 31 functions across 57 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the desktop host-owned agent presence change and its place in the implementation sequence.
Description check ✅ Passed The description is detailed and covers the change, rationale, linked issues, visual proof, testing, review, and notes. It also documents limitations and unrun checks. The manual-testing and screenshot…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch brennanb2025/agent-presence-step2
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 4f878d01-4bf9-440f-bdbc-20919658a671

📥 Commits

Reviewing files that changed from the base of the PR and between 3a6148e and 85f4d2c.

📒 Files selected for processing (142)
  • src/main/agent-hooks/ended-process-reconciliation.test.ts
  • src/main/agent-hooks/manual-compact-status-cleanup.test.ts
  • src/main/agent-hooks/server-agent-presence-lifecycle.test.ts
  • src/main/agent-hooks/server-agent-presence-publication.test.ts
  • src/main/agent-hooks/server-agent-presence-relay-handover.test.ts
  • src/main/agent-hooks/server-agent-presence-sleep-teardown.test.ts
  • src/main/agent-hooks/server-agent-presence.test.ts
  • src/main/agent-hooks/server-authority-evidence.test.ts
  • src/main/agent-hooks/server-claude-permission-visibility.test.ts
  • src/main/agent-hooks/server-last-status-hydrate-validation.test.ts
  • src/main/agent-hooks/server-opencode-binder.test.ts
  • src/main/agent-hooks/server-replay-evidence-clock.test.ts
  • src/main/agent-hooks/server-status-listener-fanout.test.ts
  • src/main/agent-hooks/server/server-agent-presence.ts
  • src/main/agent-hooks/server/server-authority-fences.ts
  • src/main/agent-hooks/server/server-cleanup.ts
  • src/main/agent-hooks/server/server-lifecycle.ts
  • src/main/agent-hooks/server/server-listeners.ts
  • src/main/agent-hooks/server/server-persistence-validation.ts
  • src/main/agent-hooks/server/server-reaping.ts
  • src/main/agent-hooks/server/server-row-ownership.ts
  • src/main/agent-hooks/server/server-state.ts
  • src/main/agent-hooks/server/server-status-identity.ts
  • src/main/agent-hooks/server/server-status-update.ts
  • src/main/agent-hooks/server/server-tab-cleanup.ts
  • src/main/agent-hooks/server/server-types.ts
  • src/main/agent-hooks/terminal-handle-row-identity.test.ts
  • src/main/ipc/agent-hooks.ts
  • src/main/ipc/agent-status-row-teardown-ipc.ts
  • src/main/ipc/pty-restore-record-seeding.test.ts
  • src/main/ipc/pty/provider/state-cleanup.ts
  • src/main/ipc/repos/repo-catalog-handlers.ts
  • src/main/ipc/worktrees/removal/worktree-removal-ownership.ts
  • src/main/orcad/orcad-entry.ts
  • src/main/persistence/loading-store/profile-state-import-lifetime.test.ts
  • src/main/plugins/plugin-agent-status-event.ts
  • src/main/runtime/agent-status-store-wiring.test-fixture.ts
  • src/main/runtime/orca-runtime-get-orchestration-dispatch-authority.ts
  • src/main/runtime/orca-runtime-preserved-branch-cleanup.ts
  • src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts
  • src/main/runtime/orca-runtime-state-fields.ts
  • src/main/runtime/pty-exit-agent-status-reconciliation.test.ts
  • src/main/runtime/terminal-intentional-stops.test.ts
  • src/main/runtime/terminal-intentional-stops.ts
  • src/main/startup/main-process-runtime-service.ts
  • src/main/startup/main-window-agent-status.ts
  • src/main/startup/main-window-structured-status-filter.test.ts
  • src/preload/api/agent-status-api.ts
  • src/preload/api/agent-status-bridge.ts
  • src/relay/agent-hook-server.ts
  • src/relay/relay-agent-presence.ts
  • src/renderer/src/components/cmd-j/palette-live-status.tsx
  • src/renderer/src/components/dashboard/build-dashboard-snapshot.rows-cache.test.ts
  • src/renderer/src/components/dashboard/build-dashboard-snapshot.ts
  • src/renderer/src/components/dashboard/useAgentBucketCounts.ts
  • src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts
  • src/renderer/src/components/dashboard/useLiveDashboardSnapshot.ts
  • src/renderer/src/components/dashboard/worktree-agent-rows-cache.ts
  • src/renderer/src/components/native-chat/native-chat-leaf-routing.test.ts
  • src/renderer/src/components/native-chat/native-chat-leaf-routing.ts
  • src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts
  • src/renderer/src/components/sidebar/smart-attention-attribution.ts
  • src/renderer/src/components/sidebar/smart-attention.ts
  • src/renderer/src/components/sidebar/smart-sort.ts
  • src/renderer/src/components/sidebar/use-worktree-activity-status.ts
  • src/renderer/src/components/sidebar/use-worktree-activity-statuses.ts
  • src/renderer/src/components/sidebar/useWorktreeAgentRows.ts
  • src/renderer/src/components/sidebar/visible-worktrees.ts
  • src/renderer/src/components/sidebar/worktree-agent-presence-selector.ts
  • src/renderer/src/components/sidebar/worktree-agent-rows.ts
  • src/renderer/src/components/sidebar/worktree-host-presence.test.ts
  • src/renderer/src/components/sidebar/worktree-list/listing/use-sort-order.ts
  • src/renderer/src/components/tab-bar/SortableTab.tsx
  • src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts
  • src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts
  • src/renderer/src/components/tab-bar/tab-bar-item-surface.client-hosted-active-state.test.tsx
  • src/renderer/src/components/tab-bar/tab-bar-item-surface.tsx
  • src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts
  • src/renderer/src/components/tab-bar/use-tab-bar-runtime-model-worktree-write-probe.ts
  • src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts
  • src/renderer/src/components/terminal-pane/agent-completion-coordinator-types.ts
  • src/renderer/src/components/terminal-pane/agent-completion-coordinator.ts
  • src/renderer/src/components/terminal-pane/agent-completion-host-presence.test.ts
  • src/renderer/src/components/terminal-pane/agent-completion-host-presence.ts
  • src/renderer/src/components/terminal-pane/agent-completion-notification-controller.ts
  • src/renderer/src/components/terminal-pane/agent-completion-poll-scheduler.ts
  • src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts
  • src/renderer/src/components/terminal-pane/agent-completion-process-types.ts
  • src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts
  • src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.ts
  • src/renderer/src/components/terminal-pane/parked-terminal-command-status.test.ts
  • src/renderer/src/components/terminal-pane/parked-terminal-command-status.ts
  • src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection-test-store-fixtures.ts
  • src/renderer/src/components/terminal-pane/pty-connection-test-store-state.ts
  • src/renderer/src/components/terminal-pane/pty-connection/agent-idle-working-handlers.ts
  • src/renderer/src/components/terminal-pane/pty-connection/interrupt-input-intent.ts
  • src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts
  • src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts
  • src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts
  • src/renderer/src/components/terminal-pane/remote-hidden-output-restore-unavailable-banner.repro.test.ts
  • src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index.test.ts
  • src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index.ts
  • src/renderer/src/components/terminal-pane/use-agent-owner-exit.test.ts
  • src/renderer/src/components/terminal-pane/use-agent-owner-exit.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts
  • src/renderer/src/components/use-worktree-jump-palette-open-tabs.ts
  • src/renderer/src/components/use-worktree-jump-palette-store-state.ts
  • src/renderer/src/components/use-worktree-jump-palette-worktrees.ts
  • src/renderer/src/hooks/ipc-events-agent-status-store-test-fixtures.ts
  • src/renderer/src/hooks/ipc-events/agent-status-event-applicator.ts
  • src/renderer/src/hooks/ipc-events/agent-status-listeners.ts
  • src/renderer/src/hooks/ipc-events/agent-status-presence-event.ts
  • src/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.ts
  • src/renderer/src/hooks/useIpcEvents-lifecycle.test.ts
  • src/renderer/src/lib/agent-owner-check.ts
  • src/renderer/src/lib/agent-presence-selectors.ts
  • src/renderer/src/lib/agent-presence-transitions.ts
  • src/renderer/src/lib/legacy-unidentified-agent-presence.ts
  • src/renderer/src/lib/legacy-unidentified-tab-icon-agent.ts
  • src/renderer/src/lib/open-tab-occupant-agent.ts
  • src/renderer/src/lib/tab-agent-from-signals.ts
  • src/renderer/src/lib/tab-agent-host-presence.test.ts
  • src/renderer/src/lib/use-tab-agent-host-presence.test.ts
  • src/renderer/src/lib/use-tab-agent.ts
  • src/renderer/src/lib/workspace-tab-palette-entry-builder.ts
  • src/renderer/src/lib/workspace-tab-palette-search.ts
  • src/renderer/src/lib/workspace-tab-palette-selection.ts
  • src/renderer/src/lib/worktree-status.ts
  • src/renderer/src/store/slices/agent-presence.test.ts
  • src/renderer/src/store/slices/agent-presence.ts
  • src/renderer/src/store/slices/agent-status-authority-actions.ts
  • src/renderer/src/store/slices/agent-status-drop-actions.ts
  • src/renderer/src/store/slices/agent-status-drop-reducer.ts
  • src/renderer/src/store/slices/agent-status-slice-contract.ts
  • src/renderer/src/store/slices/agent-status-worktree-drop-actions.ts
  • src/renderer/src/store/slices/agent-status.ts
  • src/shared/agent-hook-presence-transition.ts
  • src/shared/agent-status-ipc-payload.ts
  • src/shared/agent-status-types.ts
  • tests/e2e/paired-runtime-rejected-input-remount.unit.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

check: Promise<AgentProcessVerdict | null>
successor?: AgentProcessPresence
} = { owner, check: Promise.resolve(null), successor }
entry.check = probeAgentProcessPresence(owner)

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
fd agent-process-presence-probe.ts --exec sed -n '1,200p' {}

Repository: stablyai/orca

Length of output: 3391


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- server-agent-presence.ts ---'
sed -n '1,180p' src/main/agent-hooks/server/server-agent-presence.ts
printf '%s\n' '--- relay-agent-presence.ts ---'
sed -n '1,130p' src/relay/relay-agent-presence.ts
printf '%s\n' '--- server-lifecycle.ts ---'
sed -n '1,130p' src/main/agent-hooks/server/server-lifecycle.ts
printf '%s\n' '--- checkAgentPresence call sites ---'
rg -n -C 3 'checkAgentPresence|probeAgentProcessPresence' src/main/agent-hooks/server src/relay

Repository: stablyai/orca

Length of output: 25880


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- server callback method definitions ---'
rg -n -C 5 'adoptPaneOwner|reconcileEndedProcessForPaneKeys|writeLegacyStatusRow|commitStatusRowMutation|scheduleStatusPersist|emitEnrichedStatus' src/main/agent-hooks/server
printf '%s\n' '--- relay check and publication bindings ---'
sed -n '200,245p' src/relay/agent-hook-server.ts
rg -n -C 5 'function handOverDeadOwner|export function handOverDeadOwner|publish\(' src/relay src/shared/agent-hook-presence-transition.ts

Repository: stablyai/orca

Length of output: 42471


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- relay check binding ---'
sed -n '220,242p' src/relay/agent-hook-server.ts
sed -n '310,338p' src/relay/agent-hook-server.ts
printf '%s\n' '--- server emission method ---'
sed -n '24,48p' src/main/agent-hooks/server/server-listeners.ts

Repository: stablyai/orca

Length of output: 3297


Add rejection handling to the complete presence-check chains.

probeAgentProcessPresence catches process-reader failures. The .then callbacks can still throw during state mutation or publication. The current chains have no rejection handler, and callers use void. A rejected checkAgentPresence also stops recheckNext before it processes the remaining restored panes.

The server catch must be terminal. A catch before .then would not handle a rejection from that callback.

Suggested fix
       .finally(() => {
         if (this.presenceChecks.get(resolved) === entry) {
           this.presenceChecks.delete(resolved)
         }
       })
+      .catch(() => 'unverifiable' as const)
       .finally(() => {
         if (this.pending.get(row.paneKey) === entry) {
           this.pending.delete(row.paneKey)
         }
       })
+      .catch(() => undefined)
       const recheckNext = async (): Promise<void> => {
         for (let paneKey = restored.shift(); paneKey; paneKey = restored.shift()) {
-          await this.checkAgentPresence(paneKey)
+          try {
+            await this.checkAgentPresence(paneKey)
+          } catch {
+            // Continue checking the remaining restored panes.
+          }
         }
       }
📍 Affects 3 files
  • src/main/agent-hooks/server/server-agent-presence.ts#L85-L85 (this comment)
  • src/relay/relay-agent-presence.ts#L39-L39
  • src/main/agent-hooks/server/server-lifecycle.ts#L59-L62

if (options?.emitStatusRowMutation !== false) {
this.commitStatusRowMutation(previousStatus, undefined)
}
this.commitPaneRowAfterCleanup(previousStatus, owner)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
fd -e ts agent-status-listeners.ts agent-status-presence-event.ts agent-presence.ts agent-status-drop-reducer.ts src/renderer --exec rg -n -C4 'clear|onClear|agentPresence|presence' {}

Repository: stablyai/orca

Length of output: 3557


🏁 Script executed:

#!/bin/bash
set -o pipefail
printf '%s\n' '--- server-tab-cleanup ---'
sed -n '1,230p' src/main/agent-hooks/server/server-tab-cleanup.ts
printf '%s\n' '--- server-authority-fences ---'
sed -n '150,235p' src/main/agent-hooks/server/server-authority-fences.ts
printf '%s\n' '--- server-cleanup ---'
sed -n '205,280p' src/main/agent-hooks/server/server-cleanup.ts
printf '%s\n' '--- renderer listener ---'
rg -n -C12 'onClear|clearTransientAgentStatuses|clearAgentStatus|agentStatus' src/renderer
printf '%s\n' '--- related store declarations/implementations ---'
rg -n -C15 'clearTransientAgentStatuses|clear.*Agent.*Status|agentPresence|transientClearWatermark' src
printf '%s\n' '--- exact PR diff for cited files ---'
git diff --no-ext-diff --unified=12 3a6148e6fe6ef6afe9c1c6b7aea8c09a9c6e81e3 85f4d2c52e65e95cdbde874feffc8c67c8ebd0aa -- src/main/agent-hooks/server/server-tab-cleanup.ts src/main/agent-hooks/server/server-authority-fences.ts src/main/agent-hooks/server/server-cleanup.ts

Repository: stablyai/orca

Length of output: 45654


🏁 Script executed:

set -o pipefail
sed -n '1,230p' src/main/agent-hooks/server/server-tab-cleanup.ts
sed -n '150,235p' src/main/agent-hooks/server/server-authority-fences.ts
sed -n '205,280p' src/main/agent-hooks/server/server-cleanup.ts
rg -n -C12 'onClear|clearTransientAgentStatuses|clearAgentStatus|agentStatus' src/renderer
rg -n -C15 'clearTransientAgentStatuses|clear.*Agent.*Status|agentPresence|transientClearWatermark' src

Repository: stablyai/orca

Length of output: 45612


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- candidate files ---'
fd -i -t f 'agent-status|status.*listener|store' src/renderer/src | head -200
printf '%s\n' '--- exact symbols ---'
rg -l --glob '*.ts' --glob '*.tsx' 'clearTransientAgentStatuses|transientClearWatermarkByConnectionId|emitPaneStatusCleared|onClear' src/renderer/src src/main
printf '%s\n' '--- renderer clear listener ---'
rg -n -C18 'onClear|clearTransientAgentStatuses|clearAgentStatus|transientClearWatermarkByConnectionId' src/renderer/src
printf '%s\n' '--- pane clear emitter ---'
rg -n -C12 'emitPaneStatusCleared' src/main/agent-hooks
printf '%s\n' '--- status store clear implementations ---'
rg -n -C25 'clearTransientAgentStatuses|agentStatusByPaneKey.*delete|delete.*agentStatusByPaneKey|agentStatusByPaneKey' src/renderer/src/store src/renderer/src 2>/dev/null | head -1200

Repository: stablyai/orca

Length of output: 45660


🏁 Script executed:

set -o pipefail
rg -l --glob '*.ts' --glob '*.tsx' 'clearTransientAgentStatuses|transientClearWatermarkByConnectionId|emitPaneStatusCleared|onClear' src/renderer/src src/main
rg -n -C18 'onClear|clearTransientAgentStatuses|clearAgentStatus|transientClearWatermarkByConnectionId' src/renderer/src
rg -n -C12 'emitPaneStatusCleared' src/main/agent-hooks
rg -n -C25 'clearTransientAgentStatuses|agentStatusByPaneKey.*delete|delete.*agentStatusByPaneKey' src/renderer/src

Repository: stablyai/orca

Length of output: 45438


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- renderer listener ---'
sed -n '1,115p' src/renderer/src/hooks/ipc-events/agent-status-listeners.ts
printf '%s\n' '--- event applicator ---'
sed -n '1,280p' src/renderer/src/hooks/ipc-events/agent-status-event-applicator.ts
printf '%s\n' '--- cleanup actions ---'
sed -n '120,225p' src/renderer/src/store/slices/agent-status-cleanup-actions.ts
printf '%s\n' '--- pane clear emitter ---'
rg -n -C15 'emitPaneStatusCleared|setPaneStatusClearListener' src/main/agent-hooks/server src/main/startup
printf '%s\n' '--- pane teardown test ---'
sed -n '450,545p' src/renderer/src/hooks/useIpcEvents-agent-status-pane-teardown.test.ts

Repository: stablyai/orca

Length of output: 42414


🏁 Script executed:

set -o pipefail
sed -n '1,115p' src/renderer/src/hooks/ipc-events/agent-status-listeners.ts
sed -n '1,240p' src/renderer/src/hooks/ipc-events/agent-status-event-applicator.ts
sed -n '130,220p' src/renderer/src/store/slices/agent-status-cleanup-actions.ts
rg -n -C8 'emitPaneStatusCleared|setPaneStatusClearListener' src/main/agent-hooks/server/server-listeners.ts src/main/startup/main-window-agent-status.ts
sed -n '450,535p' src/renderer/src/hooks/useIpcEvents-agent-status-pane-teardown.test.ts

Repository: stablyai/orca

Length of output: 25776


Preserve retained rows across cleanup clears.

commitPaneRowAfterCleanup publishes the retained row before the clear. A pane clear can then remove that row when its state is not done. The renderer preserves only an existing done row.

The transient connection clear removes rows with updatedAt <= clearedAt. The retained unverified row keeps its old receivedAt, which becomes its updatedAt, so the clear can remove the row that cleanup just published. Restamp that row above the connection watermark.

Suggested fix
diff --git a/src/main/agent-hooks/server/server-tab-cleanup.ts b/src/main/agent-hooks/server/server-tab-cleanup.ts
@@
-    this.commitPaneRowAfterCleanup(previousStatus, owner)
     if (hadStatus || authorityChanged) {
+      this.emitPaneStatusCleared({ paneKey: resolvedPaneKey })
+    }
+    this.commitPaneRowAfterCleanup(previousStatus, owner)
+    if (hadStatus || authorityChanged) {
       this.runtimeObservedStatusPaneKeys.delete(resolvedPaneKey)
       this.scheduleStatusPersist()
       this.notifyStatusChangeListeners()
-      this.emitPaneStatusCleared({ paneKey: resolvedPaneKey })
     }
diff --git a/src/main/agent-hooks/server/server-authority-fences.ts b/src/main/agent-hooks/server/server-authority-fences.ts
@@
+    if (statusChanged) {
+      for (const paneKey of clearedStatusPaneKeys) {
+        this.emitPaneStatusCleared({ paneKey })
+      }
+    }
     for (const row of clearedStatusRows.values()) {
       this.commitPaneRowAfterCleanup(row, owner)
     }
     if (statusChanged) {
       this.scheduleStatusPersist()
       this.notifyStatusChangeListeners()
-      for (const paneKey of clearedStatusPaneKeys) {
-        this.emitPaneStatusCleared({ paneKey })
-      }
     }
diff --git a/src/main/agent-hooks/server/server-cleanup.ts b/src/main/agent-hooks/server/server-cleanup.ts
@@
   protected commitPaneRowAfterCleanup(
     previous: EnrichedAgentHookEventPayload | undefined,
-    disposition: PaneOwnerDisposition
+    disposition: PaneOwnerDisposition,
+    options?: { minimumReceivedAt?: number }
   ): void {
-    const retained = rowAfterPaneCleanup(previous, disposition)
+    const retainedRow = rowAfterPaneCleanup(previous, disposition)
+    const retained =
+      retainedRow &&
+      options?.minimumReceivedAt !== undefined &&
+      retainedRow.receivedAt < options.minimumReceivedAt
+        ? { ...retainedRow, receivedAt: options.minimumReceivedAt }
+        : retainedRow
@@
-        this.commitPaneRowAfterCleanup(deleted, 'unverified')
+        this.commitPaneRowAfterCleanup(deleted, 'unverified', {
+          minimumReceivedAt: clearedAt + 1
+        })
📍 Affects 3 files
  • src/main/agent-hooks/server/server-tab-cleanup.ts#L164-L164 (this comment)
  • src/main/agent-hooks/server/server-authority-fences.ts#L208-L208
  • src/main/agent-hooks/server/server-cleanup.ts#L252-L252

Comment on lines +132 to +135
const activeLeafId = options.terminalLayoutsByTabId?.[tab.id]?.activeLeafId
const presence = activeLeafId
? options.agentPresenceByPaneKey?.[makePaneKey(tab.id, activeLeafId)]?.presence
: undefined

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
fd -t f stable-pane-id.ts --exec sed -n '1,60p' {}
rg -nP -C2 'makePaneKey\(\s*tab\.id' src/renderer/src
rg -nP -C2 'isTerminalLeafId\(\w*[Ll]eafId\)\s*\?\s*makePaneKey' src/renderer/src

Repository: stablyai/orca

Length of output: 8269


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- worktree-status imports and affected functions ---'
sed -n '1,175p' src/renderer/src/lib/worktree-status.ts
printf '%s\n' '--- smart-attention imports and affected functions ---'
sed -n '1,380p' src/renderer/src/components/sidebar/smart-attention.ts
printf '%s\n' '--- layout and presence references ---'
rg -n -P -C3 'agentPresenceByPaneKey|activeLeafId|type .*Terminal.*Layout|interface .*Terminal.*Layout|terminalLayoutsByTabId' src/renderer/src src/shared -g '*.{ts,d.ts}' | head -n 500

Repository: stablyai/orca

Length of output: 41717


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- presence map declarations and producers ---'
rg -n -P -C4 'AgentPresenceByPaneKey|agentPresenceByPaneKey\s*[:=]|makePaneKey\(' src/renderer/src/store src/renderer/src/lib src/renderer/src/components/sidebar src/shared -g '*.{ts,d.ts}' | head -n 500
printf '%s\n' '--- exact smart-attention affected region ---'
sed -n '285,365p' src/renderer/src/components/sidebar/smart-attention.ts

Repository: stablyai/orca

Length of output: 41610


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- runtime leaf resolver ---'
fd -t f runtime-pane-title-leaf-id.ts --exec sed -n '1,180p' {}
printf '%s\n' '--- terminal layout types ---'
fd -t f terminal-tab-types.ts --exec rg -n -C4 'TerminalLayoutSnapshot|TerminalPaneLayoutNode|activeLeafId|leafId' {}
printf '%s\n' '--- presence slice declaration ---'
fd -t f agent-presence.ts --exec sed -n '1,120p' {}

Repository: stablyai/orca

Length of output: 27024


Avoid makePaneKey for non-UUID layout leaf IDs.

TerminalPaneLayoutNode and activeLeafId can contain legacy leaf IDs such as legacy-leaf. makePaneKey throws for these values. Use the key format directly for these presence-only lookups, or guard each call with isTerminalLeafId.

Suggested fix
-          : options.agentPresenceByPaneKey?.[makePaneKey(tab.id, leafId)]?.presence
+          : options.agentPresenceByPaneKey?.[`${tab.id}:${leafId}`]?.presence
-    ? options.agentPresenceByPaneKey?.[makePaneKey(tab.id, activeLeafId)]?.presence
+    ? options.agentPresenceByPaneKey?.[`${tab.id}:${activeLeafId}`]?.presence

Apply the same replacements in src/renderer/src/components/sidebar/smart-attention.ts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const activeLeafId = options.terminalLayoutsByTabId?.[tab.id]?.activeLeafId
const presence = activeLeafId
? options.agentPresenceByPaneKey?.[makePaneKey(tab.id, activeLeafId)]?.presence
: undefined
const activeLeafId = options.terminalLayoutsByTabId?.[tab.id]?.activeLeafId
const presence = activeLeafId
? options.agentPresenceByPaneKey?.[`${tab.id}:${activeLeafId}`]?.presence
: undefined
📍 Affects 2 files
  • src/renderer/src/lib/worktree-status.ts#L132-L135 (this comment)
  • src/renderer/src/lib/worktree-status.ts#L111-L114
  • src/renderer/src/components/sidebar/smart-attention.ts#L313-L316
  • src/renderer/src/components/sidebar/smart-attention.ts#L344-L347

@brennanb2025
brennanb2025 marked this pull request as draft October 2, 2026 04:46
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live QA: Ctrl+C and silent exit, 3b7566f vs main 444f195 (M4Air)

Question: when Claude exits on Ctrl+C or dies without a goodbye, how fast do the tab icon, the Chat view and the sidebar row stop showing it? Below: this PR at 3b7566f, main at 444f195, and the previous PR head 0ff278b.

Setup: a hidden dev Orca per arm, with an isolated profile and home, driven only through the DevTools protocol.

  • The agent under test is a stub Claude: a small script that runs Orca's installed Claude hooks as its own child, so Orca records its pid as the pane's owner. It titles the tab like Claude and handles Ctrl+C like Claude: a Ctrl+C during a turn cancels it, and a double Ctrl+C at the prompt exits.
  • Variant A sends the session-end hook before exiting. Variant B exits silently.
  • On the PR, every run confirmed the presence record carried the stub's own pid.
  • Ctrl+C was sent as terminal input over the DevTools protocol. Times are seconds after the exiting keystroke or kill.

Results (3b7566f | main 444f195 | 0ff278b)

  • a-A, idle double Ctrl+C: icon, Chat and row clear at +0.41 | main clears only the row (+1.15); its icon and Chat keep Claude | all clear at +1.82.
  • a-B, the same without session-end: all clear at +0.15 | row clears at +1.07; icon and Chat keep Claude | row stayed for more than 10 s.
  • b-A, Ctrl+C during a turn, then exit: all clear at +0.31 | row clears at +1.33 | all clear at +1.43.
  • b-B, the same without session-end: all clear at +0.12 | row clears at +1.19 | row stayed for more than 5 s.
  • c-A, claude; sleep 30, then exit: all clear at +0.21 | row stays until the sleep ends (+31.8) | row stayed through the sleep.
  • c-B, the same without session-end: all clear at +1.15 | row clears at +31.2 | row stayed for more than 5 s.
  • d1, kill -9 of the stub: all clear at +0.66 | row clears at +0.61; icon and Chat keep Claude | row stayed for more than 12 s.
  • d2, kill -9 with ; sleep 30: all clear at +0.86 | row clears at +30.9 | all clear at +31.0.
  • e-B, a plain sh root shell with no Orca command marks: all clear at +0.54 | icon and row still showed Claude after 60 s | same, more than 60 s.
  • f-A / f-B, a window reload while the stub runs, then exit: all clear at +0.27 / +1.54 | +27.1 / +49.1 | +8.35 / +7.15.
  • No exit notifications in any case on this PR.

Verdict: on 3b7566f, every Ctrl+C case clears the icon, Chat and sidebar row within 1.2 s, in both variants. 0ff278b's retained-row failures (a-B, b-B, c-A, c-B) are fixed. The 2 s host check also clears silent deaths (c-B, kill -9 with and without a trailing sleep) in under 1.2 s, where main waits for the sleep. In every case this PR is as fast as main or faster.

Ctrl+C to a background tab: I sent the pair to a hidden tab's terminal by writing straight to its pty. On both this PR and main, both SIGINTs reached the stub and it exited, so the earlier failure seen on 0ff278b doesn't reproduce. Typing Ctrl+C into the hidden tab's xterm delivered nothing on either arm, so that isn't caused by this PR.

Caveat: the M4Air ran out of terminal devices (pty exhaustion) during the PR run. So e-B on the PR ran in the existing tab after exec /bin/sh, with plain sh and no command marks verified, instead of a new tab. And the background-tab check's second tab on the PR had no pty of its own, although the tab under test was still hidden.

Screenshots, 3 s after the exit (tab bar + sidebar)

a-B (silent exit) on this PR: plain terminal tab, no agent row.

3b7566f a-B +3 s

a-B on main: the tab is still Claude and in Chat view, and the sidebar still shows "Claude Code - Idle".

main a-B +3 s

c-B (claude; sleep 30, silent exit) on this PR: cleared while the shell still sleeps.

3b7566f c-B +3 s

c-B on main: Claude icon, Chat view and agent row are all kept while sleep 30 runs.

main c-B +3 s

e-B (plain sh root shell) on this PR: no Claude icon or row (the bell is the unread-output marker).

3b7566f e-B +3 s

e-B on main at +10 s: Claude icons and Chat are still shown.

main e-B +10 s

Every rig process was stopped afterwards and verified gone, and the worktrees are clean.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 3, 2026 02:14

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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Keep the published status identity aligned with the successor. · server-status-update.ts:31-37

src/main/agent-hooks/server/server-status-update.ts:31-37
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Keep the published status identity aligned with the successor.

When pane A is active and a hook from successor B arrives, the host can retain A as the status identity while publishing B as the pane owner. On the local path, the host can first accept B’s status with identity A; when the presence check later proves A dead, adoption changes only the owner presence. On the relay path, the relay can forward B’s live-owner event, but the host skips the local presence transition and still resolves B’s status against active identity A. Both paths can therefore publish a row whose owner is B and whose status names A. Resolve the identity consistently at the host publication boundary for both paths.


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 75b029be-20dc-4f39-93b2-991a78e43c01
📥 Commits

Reviewing files that changed from the base of the PR and between 85f4d2c and 93fea62.

📒 Files selected for processing (14)
  • src/main/agent-hooks/server-agent-owner-liveness-recheck.test.ts
  • src/main/agent-hooks/server/server-agent-presence.ts
  • src/main/agent-hooks/server/server-lifecycle.ts
  • src/main/agent-hooks/server/server-row-ownership.ts
  • src/main/orcad/orcad-entry.ts
  • src/main/runtime/orca-runtime-state-fields.ts
  • src/main/startup/main-process-runtime-service.ts
  • src/relay/agent-hook-server.ts
  • src/relay/relay-agent-owner-liveness-recheck.test.ts
  • src/relay/relay-agent-presence.ts
  • src/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.ts
  • src/renderer/src/store/slices/agent-presence.test.ts
  • src/renderer/src/store/slices/agent-presence.ts
  • src/shared/agent-owner-liveness-recheck.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

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

Important

Two behavioral gaps carried from the earlier review are still open at 93fea627: a completed turn can lose its "finished" notification when the owner exits inside the hook-done quiet window, and the owner-release handler can be out-run by a queued status event or miss a pane-keyed authority alias. Both are anchored below. The delta since that review — the 2 s owner recheck and the exited-owner row drop — reads sound on inspection.

Reviewed changes

  • Exited owner's row drops like a shell return — on a proven exit, agent-presence.ts now calls dropAgentStatus(paneKey) when the live row's agentType matches the ended owner, instead of keeping the finished row (ab97c9e).
  • 2 s owner liveness recheck — new shared/agent-owner-liveness-recheck.ts runs one unref'd interval per execution host and re-probes every live identified owner; the server arms it from commitStatusRowMutation and after startup hydration, and it stops itself when no live owner remains (3b7566f).
  • Relay owns its doubt and recheck — RelayAgentPresence now takes the row source and check callback, applies ownerDoubtFromHook itself, arms the same recheck, and stops it with the relay.
  • Still-open findings from review 5377542109 — re-verified at this head; neither was addressed by the delta.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment on lines +75 to +89
if (agent && state.hasAgentRunEvidence && options.isLive()) {
dispatchCompletion('process-exit', agent, {
terminalIdleConfirmed: true,
// Why: a resumed owner that exits without ever working has nothing to announce.
requiresUnnotifiedTurn: true,
completionIdentity: {
source: 'process-exit',
identity: `${exited.platform}:${exited.pid}:${exited.startTime}`,
agentIdentity: agent
}
})
options.onForegroundAgentExited?.({ agent, processName: agent })
}
clearAgentRunEvidence()
scheduleNextPoll()

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.

A process exit that lands inside the hook-done quiet window drops the finished notification for a completed turn entirely. dispatchCompletion refuses any non-hook source while pendingHookDoneTimer is set (agent-completion-notification-controller.ts:158), then this observer unconditionally calls clearAgentRunEvidence(), which clears hasAgentRunEvidence but not the pending hook-done timer (agent-completion-coordinator.ts:78-85); when that timer later fires with source: 'hook' it is refused by the evidence gate (agent-completion-notification-controller.ts:167). The legacy inspection path this replaces defers instead — agent-completion-inspection-result.ts:144-147 returns on hasPendingHookDone() without clearing evidence — so this is a regression for identified owners. This is the finding from the earlier review, still open; the suite cannot catch it because agent-completion-host-presence.test.ts:194-227 advances the quiet timer before ending the owner.

Technical details
# Owner exit during the hook-done quiet window yields zero notifications

## Affected sites
- src/renderer/src/components/terminal-pane/agent-completion-host-presence.ts:75-89 — dispatch is blocked, then `clearAgentRunEvidence()` runs unconditionally
- src/renderer/src/components/terminal-pane/agent-completion-coordinator.ts:78-85 — `clearAgentRunEvidence` does not clear `pendingHookDoneTimer` (only `clearPendingHookDone` does)
- src/renderer/src/components/terminal-pane/agent-completion-notification-controller.ts:158-160, 167 — guards that suppress both the process-exit and the later hook completion

## Reproduced path
1. `observeHookStatus({ state: 'working' })` then `observeHookStatus({ state: 'done' })` → `doneShouldUseQuietWindow` is true (`state.workingStatusObserved`) → `scheduleHookDoneCompletion` sets `pendingHookDoneTimer`.
2. Before `HOOK_DONE_QUIET_MS` elapses, the owner ends → observer dispatch is refused, then `clearAgentRunEvidence()`.
3. Quiet timer fires → hook dispatch refused by `!processState.hasAgentRunEvidence`. Outward count: 0.

## Required outcome
- A completed turn followed by the owner exiting within the quiet window still posts exactly one finished notification, matching the legacy path and the PR's stated behavior.

## Suggested approach
Mirror the legacy deferral in `createAgentOwnerExitObserver`: when a pending hook-done exists, do not clear run evidence (or clear the pending hook-done before clearing evidence) so the quiet timer's hook completion can still dispatch. Add a test that ends the owner before advancing the quiet timer.

Comment on lines +100 to +104
// Why: preserve set→release FIFO so a queued owner publication cannot outlive its release.
if (liveAgentStatusBurstQueue.some((queued) => queued.paneKey === data.paneKey)) {
drainQueuedLiveAgentStatusesForPane(data.paneKey)
}
useAppStore.getState().releaseAgentPresence(data.paneKey, process)

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.

The release handler drops an owner without the two cleanups the pane-clear handler beside it performs. It drains liveAgentStatusBurstQueue but never purges pendingAgentStatusEvents (the clear handler does at lines 80-84), and it releases under the raw data.paneKey while the set path records under resolveAgentPaneAuthorityKey(data.paneKey) (agent-status-event-applicator.ts:61). Either gap leaves a released owner recoverable — a queued set re-applied later, or a transferred pane's record never dropped. Carried from the earlier review and independently raised by CodeRabbit; still open.

Technical details
# Owner release can be undone by a queued set, and can miss a transferred pane

## Affected sites
- src/renderer/src/hooks/ipc-events/agent-status-listeners.ts:95-105 — release handler
- src/renderer/src/hooks/ipc-events/agent-status-listeners.ts:77-84 — clear handler purges both queues
- src/renderer/src/hooks/ipc-events/agent-status-event-applicator.ts:61,152 — set records under `resolveAgentPaneAuthorityKey(data.paneKey)`
- src/renderer/src/store/slices/agent-status-authority-actions.ts:194 — `transferAgentPaneAuthority` moves the record to the owner key

## Required outcome
- After a release, the released process owner cannot be re-recorded from queued events, and a release for a pane with an authority alias drops the owner record wherever it is actually stored.

## Suggested approach
In the release handler, purge `pendingAgentStatusEvents` for `data.paneKey` exactly as the clear handler does, and resolve the alias before releasing (`resolveAgentPaneAuthorityKey(data.paneKey)`). Add coverage for release-after-enqueue and release-after-transfer.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing this older desktop integration and rebuilding consistent agent presence on current main. The request remains for every desktop view to agree when an agent is running, exits, or becomes unreachable.

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