Skip to content

fix(agent-status): preserve hook presence when process checks cannot answer (step 1 of 3) - #23947

Merged
brennanb2025 merged 13 commits into
mainfrom
brennanb2025/agent-presence-pid
Oct 1, 2026
Merged

brennanb2025 merged 13 commits into
mainfrom
brennanb2025/agent-presence-pid

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​964 $\color{#cf222e}{\Huge{\mathbf{−}}}$​23 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​941
Prod 43 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​779 $\color{#cf222e}{\Huge{\mathbf{−}}}$​109 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​670

What this fixes

Orca sometimes decided a terminal agent had exited when the real answer was "Orca couldn't check." When a Claude or Codex tab's title changed to something that did not look like an agent, Orca asked the terminal which program was in front. If that question failed or timed out (a slow or reconnecting SSH host, a busy machine, a process read that errored), Orca treated the silence as "the agent is gone": the terminal-backed Chat view dropped to a bare terminal while the agent was still running, and the sidebar row could be deleted by the same unanswered check.

The opposite also happened: when Claude really did exit with /exit, Orca ignored Claude's own "session ended" hook, so the sidebar row stayed on the last state until a desktop window happened to confirm a shell prompt. With no window open (headless host, or the tab not mounted) the row could stay indefinitely.

Who hits it: anyone running Claude or Codex in Orca terminals, most often over SSH, under heavy machine load, or while a remote host reconnects.

What users see change in this PR: for a pane whose Claude reported its own process (Claude on macOS and Linux, locally or through an updated SSH relay), a check that cannot answer no longer ends the agent, so Chat stays open and the row stays while Claude is running. When Claude exits for real (/exit and the other quit paths Claude reports with an exit reason, /logout, or a crash or kill that the process check detects), the machine running Claude marks it ended in its status store immediately, even with no Orca window open, so headless hosts and every reader of that store see the exit. The desktop sidebar display still clears through its existing path, when the shell prompt returns (measured: about 0.4 s after /exit normally, same as today); reading the store directly is step 2. This covers local macOS and Linux Claude, Claude over SSH once the remote relay is updated, and Claude inside WSL on Windows (code path only; not yet live-tested).

What is NOT affected in this PR: /clear and /resume inside Claude keep the session present exactly as today. Any pane without a recorded process identity keeps today's exit behaviour unchanged, including exiting when a check cannot answer: Codex, native Windows Claude, agents without hooks, panes behind an older SSH relay, and Claude sessions started before this update. The tab icon, Chat entry/exit, dashboard and mobile still use their existing logic. Restart/tab re-binding is owned by a separate change.

This PR is step 1 of 3. Step 1 (this PR): the machine running the agent records which agent owns each terminal pane and, for Claude on macOS/Linux, that agent's own process id and start time; it decides live / can't tell / exited from that one process, and an unanswered check is never an exit. Step 2: the desktop tab icon, terminal-backed Chat, sidebar and dashboard read that single record, and the old renderer-side exit guesses (title and foreground-process based) are deleted. Step 3: paired web, mobile and CLI views read the same record, with reconnect coverage and cleanup of the old-version compatibility paths. Hookless agents get a launch-time process record in a later step.

ELI5

"I can't tell whether Claude is there" must not mean "Claude left." Each terminal pane now has one owner record kept on the machine where the agent runs: which agent owns the pane and, when Claude's hook reported it, Claude's own process number plus the time that process started. To answer "is it still there?", Orca looks at that one process: same number and same start time means it is running; missing, reused number or zombie means it exited; anything Orca cannot read means "can't tell", which keeps things as they are. Claude saying "my session ended" because the user quit also ends the record right away.

Before / After

  • Before: a title that stopped looking like an agent made Orca ask which program owns the terminal; no answer, or a failed read, published "agent exited". After: if the pane has a Claude owner with a known process, Orca checks that process instead; running means the exit is cancelled, gone means exited, and can't-tell falls back to the foreground check, which for that pane exits only when it actually names a non-agent program (a shell, tmux, wsl.exe), never on silence. Panes without a known process keep exactly today's foreground check, including exiting on silence.
  • Before: the desktop's command-finished cleanup deleted the agent row even when it could not confirm a shell prompt. After: if it cannot confirm a shell prompt, it asks the host whether the pane's agent process can be checked; only then does it keep the row. Every other pane (Codex, hookless agents, older relays, the web client) deletes the row exactly as today. The renderer gets one yes/no answer, not the owner record; reading the record is step 2.
  • Before: Claude's SessionEnd hook was ignored. After: SessionEnd with an exit reason (prompt_input_exit, logout, other, bypass_permissions_disabled) from the pane's owning Claude process ends the record and clears the row on the host; clear, resume, subagent and unknown reasons do not.
  • Before: any hook posting with a pane's key could change who that pane belonged to. After: the first agent hook in an empty pane claims it; a running owner keeps the pane against any other agent (for example a claude -p started inside it), those other hooks still update status, and only the owner's own proven process can end it. An owner whose process was never reported is never ended by a hook and never process-checked.
  • Before: nothing checked whether the agent process was still alive. After: the check runs only on real triggers: a hook from a different process than the owner, a real title change (spinner frames are ignored), a command-finished mark, a host reconnect, and once per pane after replaying hooks saved while Orca was down. Terminal exit keeps its existing cleanup. There is no timer or sweep.
  • How the process is recorded: Claude's outer hook command saves its parent process id (Claude itself) before Orca's hook script starts, then reads that process's start time (Linux /proc/<pid>/stat plus boot id; macOS ps -o lstart in UTC). It travels as an optional field through the local hook request, the hook spool file, saved status rows and the SSH relay message.
  • Over SSH: the remote relay records and checks the process on the remote machine and forwards "exited"; the desktop never checks a remote process id locally. An older relay sends no process field, so its panes behave as today.

Why this design

The bug class removed is "treat not knowing as knowing the agent left", plus "any hook in the pane can end the pane". The fact that decides presence is now the agent's own process on the machine running it, recorded in the one existing status store, instead of whichever program happens to be in front of the terminal. A foreground program can change (a tool command, tmux, a nested shell) while the agent is still running, so it was never the right identity.

Rejected: keeping foreground and title checks as the exit signal and only softening their failure case (the closed #23786 approach). It kept two exit deciders, made a normal local /exit slower, and added a second renderer publisher.

Rejected: keying presence on the Claude session id. /clear and /resume switch sessions inside the same running process, and nested claude -p runs inherit the pane key with their own session ids. Earlier rounds of this PR tried session-keyed transitions and each fix produced a new trap: a nested session taking over the pane and ending it with its own SessionEnd, a lost SessionStart after /clear freezing all later updates, and late events from a retired session re-pointing the pane. Keying on the owning process removes that whole family.

Rejected for step 1: a periodic sweep (no observed failure that real triggers miss yet), a native module for sub-second macOS start times (not observed to matter), guessing Codex's process from its hook's parent (not provable for current Codex), and checking remote process ids from the desktop (the SSH boundary rule is that the machine running the agent owns liveness).

Differences from the usual process-identity approach used elsewhere: keeping an explicit "can't tell" verdict across permission errors and disconnected hosts, instead of treating a failed read as absent, is intended because that failure is exactly this bug. Second-resolution macOS start time is an intended step-1 trade-off. The remaining foreground/title readers are temporary and are removed in step 2 (desktop) and step 3 (web, mobile, CLI).

Known weaknesses

  • macOS start time has one-second resolution; if a process id were reused by a new process within the same second, Orca could mistake it for the old agent. Not observed; no native module added.
  • Codex's process id is not recorded, so Codex presence still ends only through the existing paths (terminal exit, the desktop's confirmed shell prompt, old foreground check).
  • Panes without a recorded process identity (Codex, hookless agents, Windows, older relays, sessions started before this update) still get today's false "agent exited" when a check cannot answer; only identified Claude panes are protected in step 1. The later steps give every agent an owner record.
  • Native Windows Claude records no process id yet, so its SessionEnd is still ignored, exactly as today, and it relies on terminal exit and the old checks. The planned single-process Windows read was not added because nothing on Windows produces an id to check; Windows capture comes in a later step (accepted for step 1).
  • A Claude session started before this update, or any pane whose first hook had no process id, has an owner Orca cannot check; that pane keeps today's behaviour until the terminal or row is torn down, even if a later Claude in it reports a process id.
  • If Claude is killed without a SessionEnd and a new Claude starts in the same pane before any trigger ran, the old owner is checked when the new Claude's first hook arrives; that clears the row, and the new idle Claude is hidden until its first prompt.
  • A silent hard kill with no title change, no command-finished mark and no hook leaves the row until the next trigger; there is no sweep by design.
  • If Claude's hooks run through a CLAUDE_CODE_SHELL_PREFIX wrapper, the recorded parent may be that short-lived wrapper, which would look like an exit once it ends. Rare setting; to be checked in QA.
  • When the process check says Claude is running, Orca restores its idle state and can deliver queued orchestration messages without asking which program is in front. With Claude inside tmux (title passthrough on) and the user switched to a shell window, a message could be typed into that shell.
  • A process check that was already running when Claude exited can be reused for the exit title and answer "running" for that one title; the next trigger corrects it.
  • Each hook from a nested claude -p inside a running Claude triggers one owner check, so a nested run with ten tool calls costs about twenty extra ps calls on macOS.
  • Capturing the process adds roughly 8 ms to each Claude hook in an Orca pane on macOS.
  • The desktop sidebar does not yet read the store's exit: if the shell stays busy after Claude exits (for example claude; sleep 30), the host has already marked Claude ended but the sidebar row stays until the prompt returns, exactly as today. Step 2 fixes this.
  • In one live QA run on a pane that switched from the old build to this one mid-session (with a hand-patched hook endpoint), the row took 8.7 s to clear after /exit instead of about 0.3 s. It did not reproduce on fresh panes and is unexplained; treat as unconfirmed.
  • Not covered by live QA: SSH (the test rig could not start the relay without admin rights), native Windows, WSL and Linux hosts.
  • The openclaude hook command also saves its parent process id, but nothing reads it yet (harmless).

Test evidence

  • Named-file automated tests at 721de1c: 129 files, 2800 passed, 6 skipped. Node typecheck clean through the shared typecheck gate for the last two commits; node, CLI and web clean at 39b2660; CI runs all three.
  • Ablations (each turned named tests red, and passed again when restored): SessionEnd admission (4 tests), time-zone-independent start time, routing "can't tell" to the old check, the owner rules (2), the answered-read exit rule (2), a nested run inside Codex, the relay retry guards, restoring a live agent's idle state, the once-per-pane check after hook replay, and other agents' SessionEnd staying a status update (4). The last two commits: an SSH-shaped pane without a process identity whose shell check cannot answer clears its row as before (red when the renderer change is reverted), a pane the host can check keeps it, and a pane without an identified owner confirms the exit on an unanswered read as before (red when the runtime change is reverted).
  • Runtime exit producers re-run by name (orca-runtime.test.ts, orchestration-live-agent-title-restore.test.ts, orchestration-codex-completion-title.test.ts, terminal-foreground-confirmation.unit.test.ts, session-tabs-decorative-title-fanout.unit.test.ts, pty-connection-command-finished-cleanup.test.ts): 1385 passed, 1 skipped at 089e085 (one renderer test timed out at 30 s under machine load and passed on a by-name rerun, 17/17).
  • Generated hook wrappers were executed only under env -i with an isolated temporary HOME; process capture exercised on macOS with an inert parent. Linux, Windows and relay verdict cases use mocks and a loopback relay.
  • QA (live, M4Air macOS, real Claude CLI in a hidden background Orca, before = main a57863b, after = 089e085; later commits, not live-tested, only restore today's behaviour for panes without a process identity and fix non-Claude agents' SessionEnd): no regressions. /exit, Ctrl-C twice, Ctrl-D, kill -TERM, kill -9, and closing the tab clear the row at the same times as today; /exit median 381 ms after vs 330 ms before (5 runs each). /clear and /resume keep the row. A neutral terminal title while Claude is idle, and TZ=Asia/Tokyo in the agent's shell, keep the row. A nested claude -p leaves the outer Claude owning the pane and its nested SessionEnd is ignored. With no Orca window visible, /exit was recorded as ended on the host and the row was gone in 379 ms. SSH was blocked by the rig (see Known weaknesses).

Linked Issue

Related: STA-8819 (Claude Chat freezes and its tab icon becomes a terminal icon). This removes verified false-exit paths but does not claim to close that ticket; its reported incident has not been reproduced. STA-8862 (restart/tab re-binding) belongs to a separate change; STA-8902 (permission arguments) is unrelated. No automatic issue-closing keywords.

Visual Proof

After (this PR), row present before /exit:
after: row present before /exit
After, row gone after /exit:
after: row gone after /exit
After, kill -9 then prompt return, no row:
after: kill -9 and prompt return, no row
After, neutral title while Claude is idle, row kept:
after: neutral title while idle, row kept
After, TZ=Asia/Tokyo in the agent's shell, row kept through idle title changes:
after: TZ=Asia/Tokyo, row kept
After, nested claude -p, outer row kept and working afterwards:
after: nested claude -p, outer row kept
After, /exit with no Orca window visible, row gone:
after: headless /exit, row gone
Before (main), row gone after /exit:
before: row gone after /exit

Testing

  • Live-tested in the running app on a remote Mac (see QA under Test evidence)
  • Automated tests added/updated

Review

Reviewers: the owner-record rules live in src/shared/agent-hook-presence-transition.ts; the single-process check in src/shared/agent-process-presence-probe.ts; the host check in src/main/agent-hooks/server/server-agent-presence.ts and the relay's in src/relay/relay-agent-presence.ts; the runtime exit producer in src/main/runtime/orca-runtime-serialize-agent-prompt-submission.ts. Mixed versions: new fields are optional; an old relay or host sends none and gets today's behaviour, and an old desktop ignores the relay's new "exited" message.

Agent skill upstream boundary

  • Not applicable; no upstream skill source or material is copied.

Checklist

  • Step 1 scope and the whole three-step plan are explicit.
  • Explained the user-facing change, the mechanism and the alternatives.
  • Reviewed process ownership, stale results, cross-platform behaviour and SSH compatibility.
  • Live QA on macOS (SSH, Windows, WSL and Linux not live-tested); full build is CI.
  • Draft only; never merged or marked ready by an agent.

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

brennanb2025 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Review status: 5 review rounds + live QA complete (head 721de1c, still draft)

The problem. Orca sometimes decided a terminal agent had exited when it really just couldn't check (a failed or slow "which program is in front of this terminal" read, common over SSH or under load): the terminal-backed Chat view dropped to a bare terminal and the sidebar row could be deleted while Claude was still running. The opposite also happened: a real Claude /exit was ignored by the host status store, so rows lingered, especially with no Orca window open.

What changes for users (step 1 of 3). For panes whose Claude reported its own process (Claude on macOS/Linux, locally or through an updated SSH relay), an unanswered check is never treated as an exit, so Chat and the row stay while Claude runs. When Claude really exits (its own SessionEnd with an exit reason, or its process proven gone), the machine running Claude marks it ended in the status store immediately, even headless. The desktop sidebar still clears through its existing path (when the shell prompt returns, about 0.4 s after /exit, same as today); reading the store directly is step 2. Every pane without a recorded process identity (Codex, native Windows Claude, hookless agents, older relays, sessions started before the update) keeps today's exit behaviour unchanged.

How the design changed during review. The original PR decided presence per Claude session id. Review showed that model kept producing traps (a nested claude -p taking over and ending the outer pane, a lost SessionStart after /clear freezing updates, late events re-pointing the pane). It is now an owner record per pane: the first agent hook in an empty pane owns it, no other agent can take over or end it, and only the owner's own proven process (pid + start time) can end it; an owner without a process id is never ended by a hook.

Fixed in this review

  • P1: macOS start time was a local-time string, so a TZ in the agent's shell or a time-zone change turned a live Claude into a false exit (now UTC on both sides; regression test runs the real wrapper with TZ=Asia/Tokyo).
  • P1: Codex and SSH panes no longer left Chat on a real exit, and Codex's late-completion mail delivery broke ("can't tell" now falls back to the existing foreground check).
  • P1: a nested claude -p froze or ended the outer Claude's pane (owner model).
  • P1: CI typecheck and static analysis were red (shared code imported a main-process Windows module; a new file was missing from the CLI project; a relay test stub and a spool assertion were stale).
  • P1 (introduced during review, fixed): SSH Grok replies and Codex transcript polls stopped delivering (relay retries scheduled against a copied row).
  • P2: a live process check did not restore the agent's idle state, so queued orchestration mail stalled.
  • P2: agents exiting inside WSL or tmux no longer left Chat (answered non-agent reads exit again, as on main).
  • P2: Codex mid-turn at Orca restart plus a nested claude -p cleared the Codex row.
  • P2 (introduced during review, fixed): Devin, Qoder, CodeBuddy and Copilot SessionEnd hooks were dropped, leaving their rows spinning.
  • P2: hook replay after downtime forked one process check per record (now one per pane); relay title re-checks ignored spinner frames only on desktop (now both).
  • P2 (coordinator decision): panes without a process identity now keep today's exit behaviour exactly, both in the sidebar's command-finished cleanup (the renderer asks the host one yes/no question; no owner record in the renderer) and in the title-driven exit confirmation. This removes the SSH stale-row regression for Codex, hookless agents and older relays, and the Chat-waits-after-/exit case.
  • P3: Ctrl-Z suspended Claude kept Chat over the shell prompt; SessionEnd reasons are an allowlist of reasons that end the process; the hook capture no longer runs outside Orca panes.

Deferred (disclosed in the PR body)

  • Accepted for step 1: native Windows has no process-id producer yet, so Windows Claude SessionEnd is still ignored (same as main); the unused Windows process read was removed.
  • Panes without a process identity still get today's false "agent exited" on an unanswered check; only identified Claude panes are protected in step 1.
  • P3: a live check can restore idle and deliver orchestration mail without asking which program is in front (Claude inside tmux on a shell window could receive mail in the shell).
  • P3: kill -9 then an immediate relaunch hides the new idle Claude until its first prompt; claude; codex on one line with a killed Claude can briefly clear the Codex row.
  • P3: each nested claude -p hook triggers one owner check (about 20 ps calls for ten tool calls); capture adds about 8 ms per Claude hook on macOS; CLAUDE_CODE_SHELL_PREFIX wrappers could be recorded instead of Claude.
  • P2 candidates from the readiness checklist (unproven): the relay now scans every remote terminal's output for command-finished marks (cost on heavy output not measured); the fire-and-forget process-check promises have no catch (no throwing path found).

Verified

  • 129 named test files, 2800 passed; typechecks clean (node gated locally for the last commits, all three in CI); lint and changed-code gate clean; every fix has a regression test that failed without it (ablation).
  • CI green on 721de1c (17 checks passed, none failing).
  • Cross-version wire tests pass; old relay/host degrade to today's behaviour.
  • Live QA on a remote Mac with the real Claude CLI, before (main) vs after: every exit path clears the row at the same times as main (/exit median 381 ms vs 330 ms); /clear, /resume, a neutral title, TZ=Asia/Tokyo and a nested claude -p keep the row; headless /exit clears it. Screenshots are embedded in the PR body.

Not verified

  • SSH live (the rig could not start the relay without admin rights), native Windows, WSL and Linux hosts.
  • One unexplained 8.7 s row clear on a pane that switched builds mid-session (did not reproduce on fresh panes).
  • Live QA ran on 089e085; the later commits (non-Claude SessionEnd, and restoring today's behaviour for panes without a process identity) are covered by tests and CI only.

This PR is not merged and not marked ready; that is the coordinator's call.

…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.
@brennanb2025
brennanb2025 marked this pull request as ready for review September 30, 2026 03:16
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[High risk] Adds process identity tracking to agent hook infrastructure.

The PR does not appear safe to merge while live replacement agents or parked agents can lose their displayed status.

Findings

  1. P1 Replacement agent status gets deleted ▶

Summary

The PR adds host-owned process presence for identified Claude terminal panes, carries that identity through hooks and relays, and uses it to avoid treating unanswered process checks as exits. Subsequent changes also adjust parked-pane tracking, Windows hook files, and structured-agent transcript handling.

  • Identified Claude processes can now keep their host status when a check cannot answer and mark it ended on an admitted exit.
  • Parked-pane identity retirement still treats an unanswered foreground read as an exit.

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

: undefined
}
// Why: nested agents inherit ORCA_PANE_KEY; their hooks update status, never ownership.
return incoming.agentPresence === owner ? incoming : { ...incoming, agentPresence: owner }

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 Replacement agent status gets deleted

When Claude dies without a SessionEnd and another agent starts in the same pane, this transition accepts the new agent’s status but keeps the dead Claude process as owner. A later title-triggered check finds that Claude exited and deletes the new agent’s live status. This can make the replacement agent’s Chat and status disappear while it is still running, particularly for Codex, whose hooks have no process identity to trigger a check on arrival.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 7cb4f908-8873-414b-8d60-b8fe36191dad

📥 Commits

Reviewing files that changed from the base of the PR and between a1f6b47 and 5e2abfd.

📒 Files selected for processing (2)
  • src/main/claude/hook-script.ts
  • src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.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.


📝 Walkthrough

Walkthrough

The change captures Claude process identity and carries it through hook, spool, and relay events. Main and relay servers probe process presence and reconcile ended agents. Runtime exit decisions and deferred terminal status cleanup use hook-presence results alongside existing evidence.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to 5e2ab

A live Claude process can cause queued orchestration input to be submitted to a shell or tmux pane instead of Claude. This is a concrete terminal execution risk that should be guarded before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 5e2ab

A running process can now restore a terminal's ready state without confirming which program owns its input. This can initiate automated message delivery to the wrong program. Process-incarnation and stale-result controls limit exposure, but the final input safeguards and some remote trust controls remain only partially verified.

Retained concerns

  • Medium · security · inferred: Process liveness now bypasses foreground-owner validation before restoring delivery-eligible idle state. A live, non-stopped process need not own terminal input, so recovery may initiate mailbox input toward another foreground program. The base required recognized foreground-process evidence on this recovery path; final submission safeguards remain unverified.
Security review details

Security Blast Radius

  • inferred — The demonstrated recovery decision is bounded to a selected PTY incarnation and its associated leaves. If downstream submission permits wrong-program input, its effects would inherit the receiving terminal program's authority. Broader cross-user, cross-tenant or remote-host exploitation was not established.

Security Findings and Attack Paths

  • inferred — A neutral-title recovery with a live hook process can restore idle and re-offer pending mailbox delivery without the recovery path's previous foreground check. A background-running process is not excluded by incarnation matching or the stopped-process guard. This creates a potential wrong-recipient input path, but end-to-end shell execution and attacker-controlled command content were not verified.

Trust Boundaries and Controls

  • observed — Local HTTP ingestion authenticates the hook token before body processing. Presence transitions preserve an established owner despite nested senders, require matching process identity for an explicit owner exit, and reject subsequent events from the same ended incarnation. These controls do not themselves bind liveness to foreground input ownership.

Resilience and Maintainability Implications

  • observed — Server row/presence guards and runtime PTY-incarnation guards contain stale asynchronous results. Mailbox delivery also has settlement and in-flight gates. These are useful ordering controls, but they are not demonstrated substitutes for current foreground-owner authorization.

Hardening Proposals

  • proposed — Separate presence-based display preservation from input-delivery authority. Require positive current foreground-owner evidence before automated terminal submission, with revalidation across delayed submission and recovery boundaries.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 50 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 identifies the agent-status fix and the specific behavior being changed. The step indicator is relevant and does not obscure the main change.
Description check ✅ Passed The description is comprehensive and follows the template. It explains the user impact, implementation, rationale, scope, limitations, testing, visual proof, and checklist status. It lists related iss…
Linked Issues check ✅ Passed No active directly linked targets remain. Issue #23786 is closed and completed. Its coding criteria are historical context only and do not impose requirements on this pull request.
Out of Scope Changes check ✅ Passed The changes stay within step 1. They add Claude process identity capture, presence propagation, host checks, hook handling, runtime protection, relay support, and regression tests. The summary does no…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: ac138579-8f4a-4abd-a2b9-c67bfec08f25

📥 Commits

Reviewing files that changed from the base of the PR and between a57863b and 721de1c.

📒 Files selected for processing (57)
  • config/tsconfig.cli.json
  • src/main/agent-hooks/hook-post-command.ts
  • src/main/agent-hooks/hook-process-capture.test.ts
  • src/main/agent-hooks/hook-process-capture.ts
  • src/main/agent-hooks/hook-stdin-contract.ts
  • src/main/agent-hooks/runtime-home-hook-command.ts
  • src/main/agent-hooks/server-agent-presence.test.ts
  • src/main/agent-hooks/server.test-fixtures.ts
  • src/main/agent-hooks/server.ts
  • src/main/agent-hooks/server/server-agent-presence.ts
  • src/main/agent-hooks/server/server-cleanup.ts
  • src/main/agent-hooks/server/server-ingest-remote.ts
  • src/main/agent-hooks/server/server-lifecycle.ts
  • src/main/agent-hooks/server/server-persistence-validation.ts
  • src/main/agent-hooks/server/server-state.ts
  • src/main/agent-hooks/server/server-status-update.ts
  • src/main/agent-hooks/spool.test.ts
  • src/main/claude/hook-script.ts
  • src/main/ipc/agent-hooks.ts
  • src/main/orcad/orcad-entry.ts
  • src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts
  • src/main/runtime/orca-runtime-mark-pty-liveness-unverifiable.ts
  • src/main/runtime/orca-runtime-preserved-branch-cleanup.ts
  • src/main/runtime/orca-runtime-serialize-agent-prompt-submission.ts
  • src/main/runtime/orca-runtime-state-fields.ts
  • src/main/runtime/orca-runtime-test-scenario-builders.spec.ts
  • src/main/runtime/orca-runtime-tests/terminal-side-effect-facts.spec.ts
  • src/main/runtime/orchestration-codex-completion-title.test.ts
  • src/main/runtime/orchestration-live-agent-title-restore.test.ts
  • src/main/runtime/orchestration-mailbox-notification-test-harness.ts
  • src/main/startup/main-process-runtime-service.ts
  • src/preload/api/agent-status-api.ts
  • src/preload/api/agent-status-bridge.ts
  • src/relay/agent-hook-envelope-build.ts
  • src/relay/agent-hook-request.ts
  • src/relay/agent-hook-retired-pane-suppression.test.ts
  • src/relay/agent-hook-server.ts
  • src/relay/pty-handler.ts
  • src/relay/relay-agent-hook-runtime.ts
  • src/relay/relay-agent-presence.test.ts
  • src/relay/relay-agent-presence.ts
  • src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection-unanswered-command-finish.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts
  • src/renderer/src/web/preload-api/web-agent-status-api.ts
  • src/shared/agent-decorative-title-signature.ts
  • src/shared/agent-hook-listener-relay-dependency.test.ts
  • src/shared/agent-hook-listener.ts
  • src/shared/agent-hook-listener/hook-envelope.ts
  • src/shared/agent-hook-listener/listener-event.ts
  • src/shared/agent-hook-presence-transition.ts
  • src/shared/agent-hook-relay.ts
  • src/shared/agent-hook-spool.ts
  • src/shared/agent-process-presence-platform.test.ts
  • src/shared/agent-process-presence-probe.test.ts
  • src/shared/agent-process-presence-probe.ts
  • src/shared/agent-process-presence.ts

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

Comment on lines +114 to +141
private restoreDisprovedAgentExit(ptyId: string, confirmedStatus?: 'idle'): void {
const current = this.ptysById.get(ptyId)
const restoredStatus = this.ptyTitleTrackersByPtyId
.get(ptyId)
?.tracker.restoreLastAgentExit(confirmedStatus)
if (!current || restoredStatus === null || restoredStatus === undefined) {
return
}
current.lastAgentStatus = restoredStatus
if (restoredStatus === 'idle') {
this.resolvePtyTuiIdleWaiters(current, ptyId)
}
for (const leaf of this.getLeavesForPty(ptyId)) {
if (leaf.lastAgentStatus !== null) {
continue
}
// Why: the live agent disproved the neutral title's exit signal; keep runtime delivery state aligned with the restored tracker.
leaf.lastAgentStatus = restoredStatus
if (restoredStatus === 'idle') {
this.resolveTuiIdleWaiters(leaf)
// Why gated like every other delivery edge: a neutral-title restoration can
// reinstate `idle` from a name-only title, which is not evidence a turn ended.
if (this.checkDeliverySettledAndArmRecheck(leaf)) {
this.deliverPendingMessagesForLeaf(leaf)
}
}
}
}

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 | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '90,150p' src/main/runtime/orca-runtime-serialize-agent-prompt-submission.ts
cat src/main/runtime/orchestration-live-agent-title-restore.test.ts
rg -n 'deliverPendingMessages|deliverySettled|restoreDisprovedAgentExit' src/main/runtime

Repository: stablyai/orca

Length of output: 13829


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- revision availability ---'
git cat-file -t a57863ba167903309af608697daaf59607121c48 2>&1 || true
git cat-file -t 721de1cdbcc55c314725b6deee897036a56e1f13 2>&1 || true
printf '%s\n' '--- changed files base..head ---'
git diff --name-status a57863ba167903309af608697daaf59607121c48 721de1cdbcc55c314725b6deee897036a56e1f13 2>&1
printf '%s\n' '--- relevant diff ---'
git diff --unified=35 a57863ba167903309af608697daaf59607121c48 721de1cdbcc55c314725b6deee897036a56e1f13 -- \
  src/main/runtime/orca-runtime-serialize-agent-prompt-submission.ts \
  src/main/runtime/orca-runtime-deliver-pending-messages.ts \
  src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts \
  src/main/runtime/orca-runtime-apply-tracked-pty-title.ts \
  src/main/runtime/orca-runtime-title-tracker.ts \
  src/main/runtime/orchestration-live-agent-title-restore.test.ts
printf '%s\n' '--- delivery helper and callers ---'
sed -n '1,180p' src/main/runtime/orca-runtime-deliver-pending-messages.ts
sed -n '160,235p' src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts
sed -n '320,375p' src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts
printf '%s\n' '--- title/exit and tracker symbols ---'
rg -n -C 8 'confirmPtyAgentExit|restoreLastAgentExit|recordTerminalSideEffectFact|checkDeliverySettledAndArmRecheck|lastAgentStatus|agent-exited|onPtyData|applyTrackedPtyTitle' src/main/runtime -g '*.ts'
printf '%s\n' '--- leaf and foreground process definitions/usages ---'
rg -n -C 12 'getForegroundProcess|interface RuntimeLeafRecord|type RuntimeLeafRecord|RuntimeLeafRecord|ptyId|paneKey' src/main/runtime -g '*.ts' | head -n 500

Repository: stablyai/orca

Length of output: 45644


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- delivery helper remainder ---'
sed -n '70,230p' src/main/runtime/orca-runtime-deliver-pending-messages.ts
printf '%s\n' '--- delivery gate definitions and callers ---'
rg -n -C 18 'checkDeliverySettledAndArmRecheck|lastAgentStatusObservedLive|deliverPendingMessagesForLeaf' src/main/runtime -g '*.ts'
printf '%s\n' '--- title trigger and exit call sites ---'
rg -n -C 25 'confirmPtyAgentExit\(|onPtyData\(|applyTrackedPtyTitle|lastOscTitleAt|agent-exited' src/main/runtime/orca-runtime-*.ts src/main/runtime/orchestration-*.ts -g '*.ts'
printf '%s\n' '--- hook pane collection and presence contract ---'
rg -n -C 20 'collectAgentStatusPaneKeysForPty|checkHookAgentPresenceFn|checkHookAgentPresence|recheckHookAgentPresenceForPty' src/main/runtime src/main -g '*.ts'
printf '%s\n' '--- direct relevant tests ---'
rg -n -C 20 'live agent|neutral title|foreground|wrong|shell|deliver.*idle|checkDeliverySettledAndArmRecheck' src/main/runtime -g '*.test.ts' -g '*.spec.ts'

Repository: stablyai/orca

Length of output: 45652


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- gate definition locations ---'
rg -l 'checkDeliverySettledAndArmRecheck' src/main/runtime -g '*.ts'
printf '%s\n' '--- gate definition and nearby code ---'
for f in $(rg -l 'checkDeliverySettledAndArmRecheck' src/main/runtime -g '*.ts'); do
  echo "FILE:$f"
  rg -n -C 35 'checkDeliverySettledAndArmRecheck' "$f"
done
printf '%s\n' '--- presence-to-pty mapping ---'
rg -n -C 25 'collectAgentStatusPaneKeysForPty|getLeavesForPty|handleByLeafKey|leafKey|paneKey' \
  src/main/runtime/orca-runtime-serialize-agent-prompt-submission.ts \
  src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts \
  src/main/runtime/orca-runtime-state-fields.ts \
  src/main/runtime/orca-runtime-sync-window-graph.ts \
  src/main/runtime/runtime-terminal-state-records.ts
printf '%s\n' '--- title callback exact ranges ---'
rg -l 'confirmPtyAgentExit|applyTrackedPtyTitle' src/main/runtime -g '*.ts' | while read f; do
  echo "FILE:$f"
  rg -n -C 20 'confirmPtyAgentExit|applyTrackedPtyTitle' "$f"
done

Repository: stablyai/orca

Length of output: 43483


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- gate files ---'
rg -l 'checkDeliverySettledAndArmRecheck' src/main/runtime -g '*.ts'
for f in $(rg -l 'checkDeliverySettledAndArmRecheck' src/main/runtime -g '*.ts'); do
  echo "FILE:$f"
  rg -n -C 20 'checkDeliverySettledAndArmRecheck' "$f"
done
printf '%s\n' '--- presence implementation ---'
for f in src/main/agent-hooks/server/server-agent-presence.ts src/main/agent-hooks/server.ts src/shared/agent-process-presence.ts src/shared/agent-process-presence-probe.ts; do
  if test -f "$f"; then
    echo "FILE:$f"
    sed -n '1,260p' "$f"
  fi
done
printf '%s\n' '--- pane-key collection ---'
rg -n -C 20 'collectAgentStatusPaneKeysForPty|checkHookAgentPresenceFn|checkHookAgentPresence' src/main/runtime src/main/agent-hooks src/main -g '*.ts'

Repository: stablyai/orca

Length of output: 42598


Require foreground-agent verification before delivery.

The hook presence check proves that the recorded agent PID is alive. It does not prove that the agent owns the PTY foreground. A neutral title can call confirmPtyAgentExit, and a live verdict now restores idle without calling readPtyForegroundProcessFromController. The delivery helper then writes the orchestration pointer and delayed \r after only PTY and liveness checks. If the foreground is a shell or another program, the helper can inject and submit that input to the wrong process. Verify the foreground agent before the pointer write and again before the delayed carriage return.

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

Caution

The SSH relay half of this change never takes effect: the desktop mux bridge does not forward agentPresence, so the host drops the relay's ended event and treats every SSH pane as unverifiable. Details inline on server-ingest-remote.ts.

Reviewed changes

  • Owner rules: new transitionHookPresence (src/shared/agent-hook-presence-transition.ts) makes one agent's proven process own a pane, wired into both the main applyNormalizedStatus and the relay applyEvent paths.
  • Process capture + probe: the Claude managed hook script captures its parent pid and start time (hook-process-capture.ts), which travels through the hook header, spool, persisted rows and relay envelope and is validated/checked by agent-process-presence.ts and agent-process-presence-probe.ts.
  • SessionEnd admission: normalizeHookPayload marks only Claude SessionEnd reasons that end the process as agentPresence.ended, and the host reconciles an ended owner to a resume-identity remnant.
  • Runtime exit producer: confirmPtyAgentExit now consults the hook owner's process before falling back to the legacy foreground read, with rechecks on title change, command-finished and unverifiable liveness.
  • Renderer: command-finished cleanup now asks main agentStatus:hasVerifiableAgentProcess before dropping a row on an unanswered shell check.

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

})
const event: AgentHookEventPayload & { authorityRestartId?: string } = {
paneKey,
agentPresence: readAgentProcessPresence(envelope.agentPresence),

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.

envelope.agentPresence is always undefined on the SSH path, so the guard just above (160-164) rejects the relay's providerSessionOnly ended event and returns before the ended-owner reconcile runs. The relay does build the field (buildRelayHookEnvelope, src/relay/agent-hook-envelope-build.ts:19), but the sole production caller of ingestRemote rebuilds the argument field-by-field and never copies it: src/main/ssh/ssh-relay-session.ts:1622-1661.

Technical details
# SSH relay presence is dropped before it reaches the host

## Affected sites
- `src/main/ssh/ssh-relay-session.ts:1622-1661` — the mux notification handler constructs the `ingestRemote` envelope explicitly and omits `agentPresence`; this is the only production caller of `ingestRemote`, so every SSH/WSL-relayed event arrives with the field stripped. (File is unchanged by this PR.)
- `src/main/agent-hooks/server/server-ingest-remote.ts:160-164` — `!readAgentProcessPresence(undefined)?.ended` is `true` and `isValidPiProviderSessionOnly` is `false` for `agentType === 'claude'` (`server-status-identity.ts:45-50`), so the relay's ended event returns early and is never applied.
- `src/main/agent-hooks/server/server-ingest-remote.ts:262` — the reader added by this PR expects the field.
- `src/main/agent-hooks/server/server-agent-presence.ts:25-41` — with no `process`, `checkAgentPresence` returns `null` (not `unverifiable`) and `hasVerifiableAgentProcess` returns `false`, so the renderer keeps today's exit-on-silence and the runtime never takes the `keepOnSilence` branch (`orca-runtime-serialize-agent-prompt-submission.ts:102-104`).
- `src/main/ssh/ssh-relay-session-agent-hooks.integration.test.ts:616-624` — asserts the ingest shape with `expect.objectContaining`, which never covers `agentPresence`.

## Required outcome
- A remote Claude exit detected by the relay (`RelayAgentPresence` publishing `AgentProcessExit`, or an admitted `SessionEnd`) must reach the host and end/clear the host row, and `hasVerifiableAgentProcess` must return `true` for an SSH pane whose relay reported a process identity.
- A relay that sends no process field must keep today's behavior (no false keep).

## Suggested approach
- Forward `agentPresence: envelope.agentPresence` in the object passed to `ingestRemote` at `ssh-relay-session.ts:1622` (the host already re-validates it via `readAgentProcessPresence`, so no extra validation is needed here).
- Extend `ssh-relay-session-agent-hooks.integration.test.ts` to assert `agentPresence` crosses the mux, and add a case for an ended presence driving the host reconcile.

…esence-pid

# Conflicts:
#	src/relay/pty-handler.ts

@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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 2cc40a46-4a64-44bb-9fb3-e0bb64592f2a

📥 Commits

Reviewing files that changed from the base of the PR and between 721de1c and a1f6b47.

📒 Files selected for processing (2)
  • src/main/agent-hooks/spool.test.ts
  • src/relay/pty-handler.ts

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

Comment thread src/relay/pty-handler.ts
Comment on lines +1030 to +1046
const recheckAgentPresence = (): void => {
if (managed.paneKey) {
this.agentPresenceTrigger?.(managed.paneKey)
}
}
let lastTitleGateKey: string | null = null
const presenceTriggers = createTerminalTitleTracker({
onTitle: (normalizedTitle, rawTitle, meta) => {
// Why: spinner frames arrive several times a second; only a real title change re-checks.
const gateKey = getDecorativeTitleGateKey(rawTitle, normalizedTitle)
if (gateKey !== lastTitleGateKey && !meta?.staleWorkingTitleClear) {
recheckAgentPresence()
}
lastTitleGateKey = gateKey
},
onCommandFinished: recheckAgentPresence
})

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:

rg -n "recheckAgentPresence|attachIdentity|paneKey|reviv|revived" src/relay/pty-handler.ts
sed -n '980,1090p' src/relay/pty-handler.ts

Repository: stablyai/orca

Length of output: 11177


🏁 Script executed:

sed -n '1900,2120p' src/relay/pty-handler.ts
sed -n '2140,2220p' src/relay/pty-handler.ts
sed -n '2980,3220p' src/relay/pty-handler.ts

Repository: stablyai/orca

Length of output: 23374


🏁 Script executed:

sed -n '1940,2110p' src/relay/pty-handler.ts
sed -n '2160,2210p' src/relay/pty-handler.ts
sed -n '2990,3220p' src/relay/pty-handler.ts

Repository: stablyai/orca

Length of output: 20501


🏁 Script executed:

rg -n -C 5 "params\.paneKey|paneKey:.*params|pty\.spawn|spawnAfterAdmission|dispatcher\.onRequest.*pty" src/relay/pty-handler.ts src/relay

Repository: stablyai/orca

Length of output: 41616


Suggested fix
     const recheckAgentPresence = (): void => {
-      if (managed.paneKey) {
-        this.agentPresenceTrigger?.(managed.paneKey)
+      const paneKey = managed.paneKey ?? managed.attachIdentity?.paneKey
+      if (paneKey) {
+        this.agentPresenceTrigger?.(paneKey)
       }
     }
📝 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 recheckAgentPresence = (): void => {
if (managed.paneKey) {
this.agentPresenceTrigger?.(managed.paneKey)
}
}
let lastTitleGateKey: string | null = null
const presenceTriggers = createTerminalTitleTracker({
onTitle: (normalizedTitle, rawTitle, meta) => {
// Why: spinner frames arrive several times a second; only a real title change re-checks.
const gateKey = getDecorativeTitleGateKey(rawTitle, normalizedTitle)
if (gateKey !== lastTitleGateKey && !meta?.staleWorkingTitleClear) {
recheckAgentPresence()
}
lastTitleGateKey = gateKey
},
onCommandFinished: recheckAgentPresence
})
const recheckAgentPresence = (): void => {
const paneKey = managed.paneKey ?? managed.attachIdentity?.paneKey
if (paneKey) {
this.agentPresenceTrigger?.(paneKey)
}
}
let lastTitleGateKey: string | null = null
const presenceTriggers = createTerminalTitleTracker({
onTitle: (normalizedTitle, rawTitle, meta) => {
// Why: spinner frames arrive several times a second; only a real title change re-checks.
const gateKey = getDecorativeTitleGateKey(rawTitle, normalizedTitle)
if (gateKey !== lastTitleGateKey && !meta?.staleWorkingTitleClear) {
recheckAgentPresence()
}
lastTitleGateKey = gateKey
},
onCommandFinished: recheckAgentPresence
})

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Merged current main (a1f6b47): the PR merges cleanly again and CI is green (37 passed, 12 skipped, 0 failed).

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