Repository navigation
fix(desktop): use host-owned agent presence (step 2 of 3) - #24175
brennanb2025 wants to merge 46 commits into
Conversation
…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.
…cement for identified owners
… 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.
… the pane moves on
Live QA follow-up: re-checks at
|
Review status (head
|
|
| const relayOwner = incoming.connectionId !== null ? incoming.agentPresence : undefined | ||
| const transitioned = | ||
| relayOwner?.process && !relayOwner.ended | ||
| ? incoming | ||
| : transitionHookPresence(incoming, this.state.lastStatusByPaneKey.get(incoming.paneKey)) |
There was a problem hiding this comment.
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.
| // 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 abstractcheckAgentPresencefences 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/unverifiedis applied through a singlecommitPaneRowAfterCleanup, and every owner-dropping row write publishes a release fromcommitStatusRowMutation(server-cleanup.ts,server-row-ownership.ts,server-tab-cleanup.ts). - Desktop mirror + one per-pane rule — a non-persisted
agentPresenceByPaneKeystore slice, an IPC release channel, andagent-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.tsobserver 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.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
| 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() |
There was a problem hiding this comment.
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.| // 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) |
There was a problem hiding this comment.
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.|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 🧰 Additional context used📚 Code guidelines (1)📝 WalkthroughWalkthroughThe 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 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 ReviewSecurity architecture risk: 🔵 Low · up to 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
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
⚔️ Resolve merge conflicts 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 4f878d01-4bf9-440f-bdbc-20919658a671
📒 Files selected for processing (142)
src/main/agent-hooks/ended-process-reconciliation.test.tssrc/main/agent-hooks/manual-compact-status-cleanup.test.tssrc/main/agent-hooks/server-agent-presence-lifecycle.test.tssrc/main/agent-hooks/server-agent-presence-publication.test.tssrc/main/agent-hooks/server-agent-presence-relay-handover.test.tssrc/main/agent-hooks/server-agent-presence-sleep-teardown.test.tssrc/main/agent-hooks/server-agent-presence.test.tssrc/main/agent-hooks/server-authority-evidence.test.tssrc/main/agent-hooks/server-claude-permission-visibility.test.tssrc/main/agent-hooks/server-last-status-hydrate-validation.test.tssrc/main/agent-hooks/server-opencode-binder.test.tssrc/main/agent-hooks/server-replay-evidence-clock.test.tssrc/main/agent-hooks/server-status-listener-fanout.test.tssrc/main/agent-hooks/server/server-agent-presence.tssrc/main/agent-hooks/server/server-authority-fences.tssrc/main/agent-hooks/server/server-cleanup.tssrc/main/agent-hooks/server/server-lifecycle.tssrc/main/agent-hooks/server/server-listeners.tssrc/main/agent-hooks/server/server-persistence-validation.tssrc/main/agent-hooks/server/server-reaping.tssrc/main/agent-hooks/server/server-row-ownership.tssrc/main/agent-hooks/server/server-state.tssrc/main/agent-hooks/server/server-status-identity.tssrc/main/agent-hooks/server/server-status-update.tssrc/main/agent-hooks/server/server-tab-cleanup.tssrc/main/agent-hooks/server/server-types.tssrc/main/agent-hooks/terminal-handle-row-identity.test.tssrc/main/ipc/agent-hooks.tssrc/main/ipc/agent-status-row-teardown-ipc.tssrc/main/ipc/pty-restore-record-seeding.test.tssrc/main/ipc/pty/provider/state-cleanup.tssrc/main/ipc/repos/repo-catalog-handlers.tssrc/main/ipc/worktrees/removal/worktree-removal-ownership.tssrc/main/orcad/orcad-entry.tssrc/main/persistence/loading-store/profile-state-import-lifetime.test.tssrc/main/plugins/plugin-agent-status-event.tssrc/main/runtime/agent-status-store-wiring.test-fixture.tssrc/main/runtime/orca-runtime-get-orchestration-dispatch-authority.tssrc/main/runtime/orca-runtime-preserved-branch-cleanup.tssrc/main/runtime/orca-runtime-resolve-worktree-removal-target.tssrc/main/runtime/orca-runtime-state-fields.tssrc/main/runtime/pty-exit-agent-status-reconciliation.test.tssrc/main/runtime/terminal-intentional-stops.test.tssrc/main/runtime/terminal-intentional-stops.tssrc/main/startup/main-process-runtime-service.tssrc/main/startup/main-window-agent-status.tssrc/main/startup/main-window-structured-status-filter.test.tssrc/preload/api/agent-status-api.tssrc/preload/api/agent-status-bridge.tssrc/relay/agent-hook-server.tssrc/relay/relay-agent-presence.tssrc/renderer/src/components/cmd-j/palette-live-status.tsxsrc/renderer/src/components/dashboard/build-dashboard-snapshot.rows-cache.test.tssrc/renderer/src/components/dashboard/build-dashboard-snapshot.tssrc/renderer/src/components/dashboard/useAgentBucketCounts.tssrc/renderer/src/components/dashboard/useDashboardPopoutBridge.tssrc/renderer/src/components/dashboard/useLiveDashboardSnapshot.tssrc/renderer/src/components/dashboard/worktree-agent-rows-cache.tssrc/renderer/src/components/native-chat/native-chat-leaf-routing.test.tssrc/renderer/src/components/native-chat/native-chat-leaf-routing.tssrc/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.tssrc/renderer/src/components/sidebar/smart-attention-attribution.tssrc/renderer/src/components/sidebar/smart-attention.tssrc/renderer/src/components/sidebar/smart-sort.tssrc/renderer/src/components/sidebar/use-worktree-activity-status.tssrc/renderer/src/components/sidebar/use-worktree-activity-statuses.tssrc/renderer/src/components/sidebar/useWorktreeAgentRows.tssrc/renderer/src/components/sidebar/visible-worktrees.tssrc/renderer/src/components/sidebar/worktree-agent-presence-selector.tssrc/renderer/src/components/sidebar/worktree-agent-rows.tssrc/renderer/src/components/sidebar/worktree-host-presence.test.tssrc/renderer/src/components/sidebar/worktree-list/listing/use-sort-order.tssrc/renderer/src/components/tab-bar/SortableTab.tsxsrc/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.tssrc/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.tssrc/renderer/src/components/tab-bar/tab-bar-item-surface.client-hosted-active-state.test.tsxsrc/renderer/src/components/tab-bar/tab-bar-item-surface.tsxsrc/renderer/src/components/tab-bar/terminal-tab-activity-status.tssrc/renderer/src/components/tab-bar/use-tab-bar-runtime-model-worktree-write-probe.tssrc/renderer/src/components/tab-bar/use-tab-bar-runtime-model.tssrc/renderer/src/components/terminal-pane/agent-completion-coordinator-types.tssrc/renderer/src/components/terminal-pane/agent-completion-coordinator.tssrc/renderer/src/components/terminal-pane/agent-completion-host-presence.test.tssrc/renderer/src/components/terminal-pane/agent-completion-host-presence.tssrc/renderer/src/components/terminal-pane/agent-completion-notification-controller.tssrc/renderer/src/components/terminal-pane/agent-completion-poll-scheduler.tssrc/renderer/src/components/terminal-pane/agent-completion-process-monitor.tssrc/renderer/src/components/terminal-pane/agent-completion-process-types.tssrc/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.tssrc/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.tssrc/renderer/src/components/terminal-pane/parked-terminal-command-status.test.tssrc/renderer/src/components/terminal-pane/parked-terminal-command-status.tssrc/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.test.tssrc/renderer/src/components/terminal-pane/pty-connection-test-store-fixtures.tssrc/renderer/src/components/terminal-pane/pty-connection-test-store-state.tssrc/renderer/src/components/terminal-pane/pty-connection/agent-idle-working-handlers.tssrc/renderer/src/components/terminal-pane/pty-connection/interrupt-input-intent.tssrc/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.tssrc/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.tssrc/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.tssrc/renderer/src/components/terminal-pane/remote-hidden-output-restore-unavailable-banner.repro.test.tssrc/renderer/src/components/terminal-pane/terminal-tab-agent-type-index.test.tssrc/renderer/src/components/terminal-pane/terminal-tab-agent-type-index.tssrc/renderer/src/components/terminal-pane/use-agent-owner-exit.test.tssrc/renderer/src/components/terminal-pane/use-agent-owner-exit.tssrc/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.tssrc/renderer/src/components/terminal-pane/use-terminal-pane-projection.tssrc/renderer/src/components/use-worktree-jump-palette-open-tabs.tssrc/renderer/src/components/use-worktree-jump-palette-store-state.tssrc/renderer/src/components/use-worktree-jump-palette-worktrees.tssrc/renderer/src/hooks/ipc-events-agent-status-store-test-fixtures.tssrc/renderer/src/hooks/ipc-events/agent-status-event-applicator.tssrc/renderer/src/hooks/ipc-events/agent-status-listeners.tssrc/renderer/src/hooks/ipc-events/agent-status-presence-event.tssrc/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.tssrc/renderer/src/hooks/useIpcEvents-lifecycle.test.tssrc/renderer/src/lib/agent-owner-check.tssrc/renderer/src/lib/agent-presence-selectors.tssrc/renderer/src/lib/agent-presence-transitions.tssrc/renderer/src/lib/legacy-unidentified-agent-presence.tssrc/renderer/src/lib/legacy-unidentified-tab-icon-agent.tssrc/renderer/src/lib/open-tab-occupant-agent.tssrc/renderer/src/lib/tab-agent-from-signals.tssrc/renderer/src/lib/tab-agent-host-presence.test.tssrc/renderer/src/lib/use-tab-agent-host-presence.test.tssrc/renderer/src/lib/use-tab-agent.tssrc/renderer/src/lib/workspace-tab-palette-entry-builder.tssrc/renderer/src/lib/workspace-tab-palette-search.tssrc/renderer/src/lib/workspace-tab-palette-selection.tssrc/renderer/src/lib/worktree-status.tssrc/renderer/src/store/slices/agent-presence.test.tssrc/renderer/src/store/slices/agent-presence.tssrc/renderer/src/store/slices/agent-status-authority-actions.tssrc/renderer/src/store/slices/agent-status-drop-actions.tssrc/renderer/src/store/slices/agent-status-drop-reducer.tssrc/renderer/src/store/slices/agent-status-slice-contract.tssrc/renderer/src/store/slices/agent-status-worktree-drop-actions.tssrc/renderer/src/store/slices/agent-status.tssrc/shared/agent-hook-presence-transition.tssrc/shared/agent-status-ipc-payload.tssrc/shared/agent-status-types.tstests/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) |
There was a problem hiding this comment.
🩺 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/relayRepository: 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.tsRepository: 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.tsRepository: 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-L39src/main/agent-hooks/server/server-lifecycle.ts#L59-L62
| if (options?.emitStatusRowMutation !== false) { | ||
| this.commitStatusRowMutation(previousStatus, undefined) | ||
| } | ||
| this.commitPaneRowAfterCleanup(previousStatus, owner) |
There was a problem hiding this comment.
🎯 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.tsRepository: 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' srcRepository: 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 -1200Repository: 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/srcRepository: 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.tsRepository: 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.tsRepository: 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-L208src/main/agent-hooks/server/server-cleanup.ts#L252-L252
| const activeLeafId = options.terminalLayoutsByTabId?.[tab.id]?.activeLeafId | ||
| const presence = activeLeafId | ||
| ? options.agentPresenceByPaneKey?.[makePaneKey(tab.id, activeLeafId)]?.presence | ||
| : undefined |
There was a problem hiding this comment.
🩺 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/srcRepository: 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 500Repository: 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.tsRepository: 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}`]?.presenceApply 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.
| 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-L114src/renderer/src/components/sidebar/smart-attention.ts#L313-L316src/renderer/src/components/sidebar/smart-attention.ts#L344-L347
Live QA: Ctrl+C and silent exit,
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 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 winKeep 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
📒 Files selected for processing (14)
src/main/agent-hooks/server-agent-owner-liveness-recheck.test.tssrc/main/agent-hooks/server/server-agent-presence.tssrc/main/agent-hooks/server/server-lifecycle.tssrc/main/agent-hooks/server/server-row-ownership.tssrc/main/orcad/orcad-entry.tssrc/main/runtime/orca-runtime-state-fields.tssrc/main/startup/main-process-runtime-service.tssrc/relay/agent-hook-server.tssrc/relay/relay-agent-owner-liveness-recheck.test.tssrc/relay/relay-agent-presence.tssrc/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.tssrc/renderer/src/store/slices/agent-presence.test.tssrc/renderer/src/store/slices/agent-presence.tssrc/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.
There was a problem hiding this comment.
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.tsnow callsdropAgentStatus(paneKey)when the live row'sagentTypematches the ended owner, instead of keeping the finished row (ab97c9e). - 2 s owner liveness recheck — new
shared/agent-owner-liveness-recheck.tsruns one unref'd interval per execution host and re-probes every live identified owner; the server arms it fromcommitStatusRowMutationand after startup hydration, and it stops itself when no live owner remains (3b7566f). - Relay owns its doubt and recheck —
RelayAgentPresencenow takes the row source and check callback, appliesownerDoubtFromHookitself, 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.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
| 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() |
There was a problem hiding this comment.
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.| // 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) |
There was a problem hiding this comment.
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.|
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. |











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.
Not affected. Structured (non-terminal) Chat sessions, turn cancellation, mobile, the paired web client and
orcacommand-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.
orcacommand-line views and SSH reconnects onto the same record.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
After
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:
How this differs from the usual design:
ps -pper agent every 2 seconds.repos:removeForHost) does not drop that host's status rows.repos:removeand 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):
claude; sleep 30, Ctrl+C exits Claude whilesleepkeeps the terminal busy: 0.21 to 1.15 s. Main: about 31 s, when the sleep ends.shwithout Orca's shell integration: 0.27 to 0.54 s. Main: still showing Claude after 60 s.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
claude; ssh boxon one line, followed by another Claude on the remote box: the second Claude is hidden until ssh exits;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
Regression tests, each shown failing without its fix:
Suites after merging main, named files only, with real-CLI, real-binary and integration suites excluded:
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 followingsleep, 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.tsexceeding 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
Notes
ps -pper agent; Linux: two file reads); repeated identical records cause no store writes; the restart recheck runs four at a time.Author: @BrennanKB5.
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)