Repository navigation
fix(notifications): a pane with hooks gets its finished banner from its hook turn only - #30620
Conversation
…nces, not a 10 s window A title-only working->idle edge (Claude's /btw side question fires no hook) raised a generic "Task complete" banner because the pane's hook row was older than 10 s. On a hook-reporting pane the hook row now decides: a row not yet announced supplies the banner content however old it is; an already-announced row means no turn ended, so no banner. Hookless agents keep the generic title fallback.
… its hook turn only The title lanes (mounted pane and parked watcher) announced a finished task on every working->idle title edge, even on panes whose agent reports turns through hooks. A title spin that ends no hook turn (Claude's /btw side question, Codex's /compact) raised an extra banner that could only be filled with the workspace name and tab title. On a pane the hook lane is tracking, with a settled Done row from the same agent, the title lane now defers before it claims the turn, so the hook lane's banner (with the last message) is the only one. A BEL in the same burst waits on the hook lane's pending Done. Hookless agents, restored rows, hooks switched off and the post-restart re-arm keep the title banner. This replaces the previous commit's announcement memory in the dispatcher.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Merge Risk: 🟡 Moderate · up to
|
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
7bbb10b9-ce9c-47e5-a622-c5f22b0c974b
📒 Files selected for processing: 9
src/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-title-observer.tssrc/renderer/src/components/terminal-pane/claude-side-question-completion.test.tssrc/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.tssrc/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.tssrc/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.tssrc/renderer/src/hooks/agent-hook-completion-notifications.tssrc/renderer/src/hooks/agent-hook-turn-end-ownership.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.
…announce re-entered Stops - A title idle edge that defers to the hook lane now settles the pane's turn, so quitting the agent after a /btw no longer raises a generic "Task complete" banner. - A BEL held while the hook lane's Done is pending is superseded by that Done at fire time instead of being held until the next turn start. - A Stop re-entered after a blocking Stop hook carries its new final message in its identity, so the hook lane announces it instead of deduping it against the first Stop. - Title lanes ask the hook lane whether it can announce the turn end (live, agent evidence, not re-arming) instead of copying its conditions, and fall back when the agent's own hooks are switched off. - The hook lane's non-quiet Done forwards its own snapshot, so its banner content no longer depends on the stored row's age. - The whole-flow test now drives the real pane wiring, the real hook lane and the real dispatcher.
…status can't raise a second banner The renderer told a Stop re-entered after a blocking Stop hook from a restatement by its final message. The message also changes when the listener's cache resets (a session reset, then an idle reminder), so a restated finished turn raised a second, empty "finished" banner. The execution host now classifies each provider's own turn-end event by name and stamps an optional `turnEndedAt` on the status row. Idle reminders, session events, replays, child re-emits and result retries restate it; a turn-end event on an already finished row is a new end only when the agent says it re-entered (`stop_hook_active`). Local hosts and SSH hosts (orcad) mint it in `attachStatusTiming`; the WSL relay mints it at admission and sends it on its envelope. The field is persisted, forwarded over IPC and the paired-server session projection, and the renderer's done identity is `turnEndedAt ?? stateStartedAt` (after the existing `turnCompletedAt` precedence). `stateStartedAt` and every other clock are unchanged. Paired servers that predate the stamp advertise no `agent-status.turn-ended-at.v1` capability; their panes keep the terminal-title completion fallback exactly as before.
…/compact and quitting an idle agent stay quiet Real-agent QA on the hook-only completion design found three gaps: - A Stop re-entered after a blocking Stop hook could be dropped twice over: inside 1 s of the first banner by the replay guard (a new host turn-end stamp is now a real end), and by main's 5 s per-workspace desktop cooldown. A banner with the same notification id and changed text now replaces the one already shown instead of being dropped; every other cooldown rule is unchanged. - On a mounted pane, a hook-owned idle edge (Codex /compact) no longer claimed a completion, so the burst's BEL went out as a Bell banner. The deferred idle edge now absorbs that BEL like a claimed completion did. - Quitting an idle agent after a title spin raised a generic process-exit banner. On a pane whose hook lane owns turn ends and whose last turn is done, an exit ends no turn; a crash mid-turn still gets its banner.
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
2fd76c5b-0010-4532-b5da-846dd414f754
📒 Files selected for processing: 47
src/main/agent-hooks/server-turn-ended-at.test.tssrc/main/agent-hooks/server/server-ingest-remote.tssrc/main/agent-hooks/server/server-persistence-validation.tssrc/main/agent-hooks/server/server-remote-envelope-normalization.tssrc/main/agent-hooks/server/server-status-application.tssrc/main/agent-hooks/server/server-status-identity.tssrc/main/agent-hooks/server/server-status-retries.tssrc/main/notifications/notification-delivery-service.test.tssrc/main/notifications/notification-delivery-service.tssrc/main/runtime/runtime-hook-agent-row-selection.tssrc/main/runtime/runtime-mobile-agent-status-builder.tssrc/main/runtime/runtime-mobile-agent-status-projection.tssrc/main/runtime/runtime-terminal-contracts.tssrc/main/startup/main-window-agent-status.tssrc/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-title-precedence.test.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-hook-observer.tssrc/renderer/src/components/terminal-pane/agent-completion-notification-controller.tssrc/renderer/src/components/terminal-pane/agent-completion-snapshot-staleness.tssrc/renderer/src/components/terminal-pane/claude-side-question-completion.test.tssrc/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.tssrc/renderer/src/components/terminal-pane/pty-connection/agent-task-complete-notify.tssrc/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.tssrc/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.tssrc/renderer/src/hooks/agent-hook-completion-notifications.tssrc/renderer/src/hooks/agent-hook-turn-end-ownership.tssrc/renderer/src/hooks/ipc-events/agent-status-event-applicator.tssrc/renderer/src/hooks/ipc-events/agent-status-routing.tssrc/renderer/src/runtime/sync-runtime-graph/agent-status-projection.tssrc/renderer/src/runtime/web-session-tabs-sync/state-equality-core.tssrc/renderer/src/runtime/web-session-tabs-sync/store-patch.tssrc/renderer/src/store/slices/agent-status-contract.tssrc/renderer/src/store/slices/agent-status-live-entry-builder.tssrc/shared/agent-hook-listener.tssrc/shared/agent-hook-listener/listener-event.tssrc/shared/agent-hook-listener/provider-event-routing.tssrc/shared/agent-hook-relay.tssrc/shared/agent-hook-turn-end-timing.tssrc/shared/agent-status-ipc-payload.tssrc/shared/agent-status-types.tssrc/shared/protocol-version.tssrc/shared/runtime-capability-constants.tssrc/wsl-guest/agent-hook-envelope-build.tssrc/wsl-guest/agent-hook-event-admission.tssrc/wsl-guest/agent-hook-result-retry-scheduler.tssrc/wsl-guest/agent-hook-server.test.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.
…lay retry restates the end
…blocking-Stop banners past main's cooldown - The execution host (local main and the WSL relay, one shared reducer) now tracks which turn an agent session is in: Codex by its own turn id, Command Code by its transcript prompt position, every other agent by its prompt-opening hook. A turn end mints a new end when its turn differs from the last ended one, so a Codex turn woken by agent mail and a text-only Command Code turn get their banner; duplicates, replays, tool reopens and result retries restate it. The record is saved with the status row (validated on restore) and never published. - Main's desktop and mobile notification cooldowns are back to main's code exactly. - A later end in the same done segment (a Stop re-entered after a blocking Stop hook) that lands inside main's cooldown is held, latest only, and sent once after that cooldown through main's unchanged gates; the renderer learns main's answer to anchor it. - One pane check gates every changed pane behavior: the pane's bound PTY runs on a host that stamps turn ends and its agent's continuation signal is verified (Claude, Codex). Every other pane, and an unbound PTY, keeps main's behavior. - A hook-owned idle edge absorbs its burst's BEL for main's full 1.5 s; exits and replacements ask the owner check with the exiting agent; a title spin on a hook-owned pane opens no turn.
… desktop cooldown, not main's service module
…d each turn end The execution host now labels every classified turn end with one optional disposition beside its end stamp, by the same rule for every agent: identified (an unused prompt opening, a new native turn id, or a verified re-entered Stop) mints a new end; a same-turn repeat is a proven duplicate only when the agent supplies both a turn id and a verified continuation flag; anything else is ambiguous and never mints. Replays and retries restate the decided answer, and local and relay hosts carry an end only within a done segment. The renderer replaces the Claude/Codex pane gate with one evidence decision: identified ends are hook-owned, ambiguous ends keep the title lane's role, and rows without a disposition (older hosts, unclassified agents) keep the previous behavior exactly. A title working edge on a hook-owned pane keeps its turn bookkeeping and only the hook receipt; an owned idle edge settles after the existing max wait, or announces through the title path if the end turned out ambiguous. Idle and working title edges resolve the same owner (the title's agent, else the foreground one). The held later banner for identified continuations is unchanged.
… late Stop folds as on main A cancel Orca inferred from Esc or Ctrl+C writes a done row with no turn-end event. The host reducer now completes the current turn key on that settled cancellation, so the same turn's late Stop is a repeat (ambiguous, no new stamp) instead of a second identified end that requested a second banner.
…y lane, so a relay's late cancel end restates none The desktop restates a WSL relay's end as decided. The relay never learns a cancel the desktop inferred for a non-Claude agent, so Grok's own late stop_cancelled reached the desktop as an identified end and gave a second banner. One predicate now decides both lanes: after a settled cancellation that carried no end, a restated end keeps the shown one, and a child event carrying the cancellation also ends the open turn. The relay's own cancel row is the main agent's, never a child's.
…keeps showing none The restatement fold tested the previous row's cancel verdict, so the row it left behind (a late plain stop) no longer matched and the relay's next restatement brought its end back. Fold on the row's turn-end fact instead, which the folded row satisfies again.
…3-notification-last-message
|
Main merged on 96ac131: main moved runtime capabilities into per-area modules (#30641), so the turn-end capability now lives in the terminal capability module. No behavior change; the PR's tests and the capability parity test pass. Review loop finished on
|
…terminal capability module


ELI5
When an agent in an Orca terminal finishes a task, Orca shows an OS banner like "my-project - Claude finished" with the agent's last message. Sometimes it showed a generic banner instead: "Task complete in my-project", with the tab title as the body. That happened when the agent's tab title spun and settled for something that was not a task at all, like Claude's
/btwside question, Codex's/compact, or Codex starting up. Orca treated the title settling as "the task finished".Now the machine that runs the agent records when a turn really ended, from the agent's own turn-end hook. A pane whose agent reports its turns that way gets one banner per real turn, with the last message, and title spins that are not turns raise nothing. When Orca cannot tell for sure, it does exactly what it does today.
What Changed
Before. Two things in the window could each decide that an agent's task had finished:
Stop), which carries the last message;The title watcher announced a finished task on every spinner-to-idle change, even on panes whose agent reports turns through hooks.
/btw,/compactand Codex startup spin the title without a turn end and fire no turn-end hook. Each raised an extra banner, and because there was no fresh hook status to read (the banner borrows the last hook status only if it is under 10 seconds old), it could only say "Task complete in " with the tab title. A/btwasked 45 seconds after a turn always got the generic text. Normal turns also sent duplicate requests that the 5-second notification cooldown happened to drop.After (what you see).
/btwgives no banner and no unread mark./compactno longer raises the generic "Task complete" banner (see Known differences for the terminal-bell and still-working cases).Mechanism.
turnEndedAton the agent's status when it receives a real top-level turn-end hook event. One shared function,reduceHostTurnEndinsrc/shared/agent-hook-turn-end-timing.ts, makes that decision on every host. A small host-only record per agent session tracks the current turn: Codex's own turn id, Command Code's transcript position, or otherwise a prompt hook the host actually saw. Session starts, idle reminders and tool activity never start a turn.turnEndDispositionbeside the time:hookTurnEndEvidence(hooks/agent-hook-turn-end-capability.ts) returns identified, ambiguous or today's behavior for a pane, the same way for every agent. When the end is identified, the hook notifier raises the one banner, and the title watcher, the process-exit and process-replacement banners, and the terminal-bell hold all defer to it. The title watcher keeps tracking the title; on an idle title it waits up to the existing 1.5-second maximum and, if the end turned out ambiguous, shows today's title banner.agent-status.turn-ended-at.v1capability; panes on a server that does not advertise it keep today's title-based behavior, including the generic/btwand/compactbanners. No minimum server version.Why
This follows the common pattern for agent desktop apps: when an agent reports its turns through hooks, completion comes from its turn-end hook, and title or screen detection is the fallback for agents without hooks. Under that pattern
/btwand/compactnever notify, because they fire no turn-end hook.It also follows the repo's rule that the execution host owns agent status. Alternatives tried on this branch, and why they were replaced:
Known differences
Each difference from today's build, or from the common pattern, is labelled intended (safe to keep, with why) or temporary (with the follow-up that removes it).
Paired servers that have not been updated behave as today, including their generic
/btwand/compactbanners, until the server is updated. Intended: improvements go only to servers that advertise the new capability, with no minimum version.Ends Orca cannot identify keep today's behavior, including today's generic banner for a later
/btwon that pane. Intended: guessing would either drop real banners or add extra ones.The final reply after a blocking Stop hook gets its own banner. The common pattern folds a re-entered Stop into the turn's one banner; Orca keeps today's later final-message banner instead. Intended: folding would replace the final reply's banner with the draft one, which is worse than today.
A Stop hook that blocks N times gives N+1 banners, each subject to the existing cooldown, and the early ones can carry draft text. Intended: this is the cost of keeping the final reply's banner.
A later final-message banner is kept only when the continuation writes text without running a tool. When a blocking Stop hook makes the agent continue and the continuation only writes text (it stays in the same "done" stretch), the final message's banner is held and shown once the 5-second notification cooldown ends. If the continuation runs a tool, the agent goes back to working, so its final end starts a new done stretch and behaves exactly as today. Its banner shows if it ends after the cooldown, and is dropped by the cooldown if it ends inside it, in which case the user sees only the first (draft) banner, as today. Intended: the hold covers only the same done stretch; the tool case equals today's build.
Agents other than Claude and Codex do not get the held final banner. Their "sent back to work" signal is not verified, so their continuations are ambiguous and behave as today. Intended: no guessing; never worse than today.
When a turn's start hook was lost and the agent shows idle well before its end hook arrives, that turn can get no banner. This needs all three: (1) the prompt hook for a turn never reached Orca (the hook POST failed or timed out), (2) the turn wrote text only (no tool use, so Orca never saw it start), and (3) the agent's title went idle more than 1.5 seconds before its Stop hook was received. Today's build shows a generic "Task complete" banner (or the previous answer's text) from the title change. This PR shows nothing, because when the title goes idle the pane still shows the previous turn's confirmed end, and the late Stop can't be told from a repeat. Claude waits for its Stop hooks to finish before it repaints the idle title (captured: idle about 90 ms after the hook ended), so its Stop arrives first and this case is practically unreachable for Claude. Codex Stops carry a turn id, so they are never ambiguous. For other agents with hooks, whether the idle title can lead the Stop by more than 1.5 s has not been verified. If the Stop arrives within 1.5 s of the idle title, the banner still shows, about 1.25 s later than today. Intended: bounded by the existing 1.5-second maximum wait; fixing it needs a new pending-title mechanism, ruled out for this design.
A Codex Stop with the same turn id and no "sent back to work" flag is silent, even after tool work, where today's build bannered it. Intended: Codex's hook contract makes that a duplicate of the turn's end.
A cancel ends the turn, so that turn's late end hook never adds a second banner. When you press Esc or Ctrl+C mid-turn and Orca marks the turn stopped, a late end hook for the same turn is treated as a repeat. This holds in local, SSH and WSL panes:
stop_cancelled, or a plainstoparriving after Orca already showed the cancel, is a repeat.The next turn banners as usual. Agent by agent:
Interrupthook ends the turn on whichever host runs Codex.Interrupt, or any other end the WSL side reports without a time): every later end inside that same finished stretch behaves exactly as in today's build. Orca keeps showing no end time, so those ends never add a banner. That includes the WSL side re-sending the end (its reply-text retry, its idle reminder).One small difference from a local pane remains, and it matches today's build: on WSL, a Codex turn that agent mail wakes, that writes text only and that comes right after a Codex
Interrupt, gets no banner. Its working state never reaches the desktop, so the desktop sees it inside theInterrupt's finished stretch. Intended: inside such a stretch it equals today's build and is never worse; the follow-up below removes the cancel part of it.Follow-up: send every cancel Orca infers on the desktop to the WSL side, for every agent, so the WSL side ends its own turn. Today the desktop forwards only Claude's Ctrl+C, and the WSL side accepts only Claude. For other agents, the desktop instead keeps the cancel's end for anything the WSL side restates afterwards. Extending the existing forward to every agent would let the WSL side record the cancel itself, so the desktop would never have to override the WSL side's decision, and the WSL-only Codex corner above would go away. It reuses the same operation, so older WSL sides refuse non-Claude requests and keep today's behavior. About 40-80 production lines. Temporary, removed by that follow-up.
Codex
/compactis not guaranteed silent. In a background tab, if Codex's terminal bell arrives more than 250 ms after the idle title, it can still raise a terminal-bell banner (today the same/compactraises a generic "Task complete" banner). A bell that arrives after the hook banner was already sent can also send an extra bell notification to the phone. Temporary: the follow-up that keeps one turn state per pane on the host lets the bell rule see the host's decision./compactand startup while Codex's status still says "working" are not guaranteed. Codex's session start and/compactcan leave the pane's status at "working", where this change's "done" rules do not apply, and before Codex reports anything the pane has no hook status and keeps today's title behavior. Real-agent QA on the final head saw no banner for either. Temporary: anything left stays tracked in STA-10899, which this PR does not close.A pane stays hook-owned after the hook agent exits. If you then run a program in the same pane whose title names no agent, its title spins raise no banner. Temporary: follow-up to keep one turn state per pane on the host, which also replaces the several separate banner producers each pane has today.
Per agent ("identified" = the hook notifier announces it and title banners defer; "ambiguous" = today's hook and title behavior; "today exactly" = no change):
UserPromptSubmitprompt hook was seen (SessionStartis a session boundary and never starts a turn), or a flagged re-entered Stop (held final banner)InterrupthookUserPromptSubmitprompt hook was seen (SessionStartis a boundary for these)UserPromptSubmitprompt hook was seenBeforeAgent), antigravity (PreInvocation), amp (agent.start), jcode (turn_start)pre_llm_callseen (on_session_startis a boundary)beforeSubmitPrompt/sessionStart), kiro (userPromptSubmit/agentSpawn), copilot (UserPromptSubmit/SessionStart)user_prompt_submitprompt hook was seenLinked Issue
Tracked in #28328 (Linear STA-10453). This PR also covers Linear STA-10899 (the extra "Task complete" banner after Codex
/compactand at Codex startup) but does not close it; see Known differences.Visual Proof
Real-agent QA screenshots of this PR's final head are in the QA section below, compared with today's build from the earlier QA.
QA
Real-agent QA on the final head
2399a65d4d2ran on Windows (the machine the QA queue admitted), with real Claude Code 2.1.303 and Codex 0.159.2 in a hidden test copy of Orca under throwaway homes. A recorder wrapped the main process's notification handler, so every banner request and the cooldown's decision was logged. The baseline for today's build is the earlier real-agent QA on macOS (main7ce58ec)./btw60 s after a turn/exit(idle, and right after/btw)/compact/quitof an idle session (twice)Two observations, both the same on today's build:
Not covered by this QA: a WSL pane on the real UI (its decisions are covered by the WSL relay tests and by checks against the real hook server), SSH and older paired servers, and the native toast pixels.
Screenshots:
/btw: no new unread mark/compact: no banner/btwthen/exit: nothingTests
claude-side-question-completion.test.tsdrives a whole pane (both real notifiers, the real host function and the real dispatcher) through a captured Claude/btw, a blocking Stop hook, lost prompt hooks and an old paired server;server-turn-ended-at.test.tspins the host's identified / repeat / ambiguous decisions, cancels and restarts;wsl-guest/agent-hook-server.test.tspins the same decisions on the WSL relay. CI runs lint, typecheck and the full suite.Review
Agent skill upstream boundary
Notes
Author: Brennan Benson (X / Twitter @BrennanKB5)
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)