Skip to content

fix(notifications): a pane with hooks gets its finished banner from its hook turn only - #30620

Merged
brennanb2025 merged 18 commits into
mainfrom
brennanb2025/sta-10453-notification-last-message
Oct 21, 2026
Merged

brennanb2025 merged 18 commits into
mainfrom
brennanb2025/sta-10453-notification-last-message

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​635 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​634
Prod 52 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1106 $\color{#cf222e}{\Huge{\mathbf{−}}}$​137 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​969

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 /btw side 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:

  • the agent's turn-end hook (for example Claude's or Codex's Stop), which carries the last message;
  • the terminal tab title: a spinner that settles back to idle.

The title watcher announced a finished task on every spinner-to-idle change, even on panes whose agent reports turns through hooks. /btw, /compact and 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 /btw asked 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).

  • A real finished turn gives one banner, " finished", with the agent's last message.
  • Claude /btw gives no banner and no unread mark.
  • Codex /compact no longer raises the generic "Task complete" banner (see Known differences for the terminal-bell and still-working cases).
  • A real turn that ends while the pane already showed "done" now gets its banner. Example: a Codex turn woken by agent mail, which runs without a prompt hook. Today it gets none.
  • When your own blocking Stop hook sends the agent back to work, you get the banner when it first stops, and the final reply's banner is held until the existing 5-second cooldown allows it (Claude and Codex).
  • Esc or Ctrl+C mid-turn, followed by the agent's late end hook for that turn, does not add a second banner.
  • Agents without turn hooks (aider, goose, crush, ...), panes with status hooks switched off, and paired servers that have not been updated keep today's banners exactly.

Mechanism.

  • The host owns the turn-end time. The machine running the agent (Orca's main process for local panes, the remote Orca server over SSH, the WSL relay for WSL panes) stamps an optional turnEndedAt on the agent's status when it receives a real top-level turn-end hook event. One shared function, reduceHostTurnEnd in src/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.
  • Each end says how sure the host is, in an optional turnEndDisposition beside the time:
    • identified: a new turn (a prompt hook not yet used, or a new Codex turn id), or a verified "sent back to work" continuation after a blocking Stop hook. This gets a new end time.
    • proven repeat: a retry or replay of an end already sent, an idle reminder, or a Codex Stop with the same turn id. It repeats the end already shown, so no new banner.
    • ambiguous: anything the host cannot prove, such as a turn whose prompt hook never arrived or a new session. It never gets a new end time and the window keeps today's hook and title behavior.
    • A missing disposition (older hosts, agents whose turn ends are not recognised) means today's behavior exactly.
  • Both fields are saved with the status row the host already keeps (validated when read back), and carried on the existing IPC payload, the WSL relay message, and the phone and paired-server projections. Old readers ignore them.
  • One decision in the window. 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.
  • One bounded held banner after a blocking Stop hook. When an identified later end in the same "done" stretch lands inside the cooldown of the banner before it, the notifier holds only the latest one and submits it once when the cooldown allows, through today's unchanged notification rules. It is dropped if new work starts, the pane closes or hooks are switched off.
  • A cancel ends the turn. When Orca marks a turn stopped after Esc or Ctrl+C, the host completes that turn, so its late end hook is a repeat.
  • Old paired servers keep today's behavior. A server advertises the new agent-status.turn-ended-at.v1 capability; panes on a server that does not advertise it keep today's title-based behavior, including the generic /btw and /compact banners. No minimum server version.
  • Notification cooldowns, settings and the banner wording are unchanged.

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 /btw and /compact never 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:

  • Remembering in the window which hook statuses were already announced: lost on a window reload, and the title watcher still decided turn ends.
  • Treating a change in the last message as a new turn: a restated "done" after a cache reset (Grok, Qoder, Kiro and others) looked like a new turn and raised a second, empty banner. Only the host sees the actual hook events, so it decides.
  • Turning the change on only for Claude and Codex: that would make agents behave differently. Instead every agent goes through the same evidence rule, and where the evidence is missing Orca keeps today's behavior instead of guessing.

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 /btw and /compact banners, 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 /btw on 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:

    • Claude's Stop arriving more than half a second after the key is a repeat.
    • Grok's own stop_cancelled, or a plain stop arriving after Orca already showed the cancel, is a repeat.

    The next turn banners as usual. Agent by agent:

    • Codex has no inferred cancel. Orca ignores Ctrl+C for Codex, and Codex's Esc is navigation. Its own Interrupt hook ends the turn on whichever host runs Codex.
    • Claude in a WSL pane: the WSL side learns Ctrl+C from the desktop and ends its own turn. Esc in a WSL pane is never inferred.
    • Agents in a WSL pane, when the desktop shows a turn as finished without an end time of its own (a cancel Orca inferred on the desktop, a 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).
    • A new turn reaches the desktop as "working" first. That starts a new stretch, so its end banners as usual.

    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 the Interrupt'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 /compact is 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 /compact raises 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.

  • /compact and startup while Codex's status still says "working" are not guaranteed. Codex's session start and /compact can 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):

Agent (hook source) Identified when Otherwise
claude a UserPromptSubmit prompt hook was seen (SessionStart is a session boundary and never starts a turn), or a flagged re-entered Stop (held final banner) Ambiguous: a lost or already-used prompt hook, an unflagged repeat, or the late Stop after an inferred cancel. Today exactly: an end held open by background subagents or tasks (Stop arrives as "working", the all-clear arrives on a child event); Claude's existing background completion stamp still announces it.
codex a new turn id (with or without a prompt hook), or a flagged Stop (held final banner); the same turn id unflagged is a proven duplicate (silent), including after Codex's own Interrupt hook Today exactly: an end held open by a live subagent. A Stop without a turn id is ambiguous.
qoder, qoder-cn, qwen-code, dsh, zcode a UserPromptSubmit prompt hook was seen (SessionStart is a boundary for these) ambiguous
codebuddy, kimi, muse, droid, devin a UserPromptSubmit prompt hook was seen ambiguous (continuation flag not read)
gemini (BeforeAgent), antigravity (PreInvocation), amp (agent.start), jcode (turn_start) their turn-start hook was seen ambiguous. For amp, a second thread's end may be attributed to the turn (open concern).
hermes pre_llm_call seen (on_session_start is a boundary) ambiguous
cursor (beforeSubmitPrompt / sessionStart), kiro (userPromptSubmit / agentSpawn), copilot (UserPromptSubmit / SessionStart) their turn-start hook was seen; for these the session events also count as a turn start, so the first Stop after one is identified (existing classification, open concern) ambiguous
grok a user_prompt_submit prompt hook was seen ambiguous. A blocking-Stop continuation is never "done" for Grok, so it behaves as today. An end held open by a background task is today exactly.
command-code a new transcript position same position or none: ambiguous
opencode, opencode2, mimo-code, pi, omp, prime-agent never (their turn ends are not recognised) today exactly
agents without hooks (aider, goose, ...), paired servers not yet updated, terminals not yet bound to a host, status hooks switched off never today exactly

Linked Issue

Tracked in #28328 (Linear STA-10453). This PR also covers Linear STA-10899 (the extra "Task complete" banner after Codex /compact and 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 2399a65d4d2 ran 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 (main 7ce58ec).

Flow This PR (final head) Today's build
Claude normal turn, background tab one "Claude finished" banner with the last message rich banner plus a duplicate request dropped by the cooldown
Claude /btw 60 s after a turn no banner, no unread mark generic "Task complete" banner and unread mark
Claude /exit (idle, and right after /btw) nothing nothing
Claude blocking Stop (DONE-1, then DONE-2) DONE-1 banner, then the DONE-2 banner 5.1 s later, after the cooldown both delivered
Codex normal turn one rich banner rich banner plus two duplicate requests dropped by the cooldown
Codex /compact nothing (no banner, no bell, no unread) over 40 s generic "Task complete" banner
Codex fresh start, no prompt nothing over 25 s generic startup banner
Codex /quit of an idle session (twice) nothing nothing
Ctrl+C mid-turn, Claude and Codex one banner at most, no second banner when the late end hook arrives (not measured)
Agent without hooks (spinner title script) the title banner, as today title banner
Status hooks switched off, Claude turn a banner, as today banner
Terminal bell in a plain tab Bell banner, as today Bell banner

Two observations, both the same on today's build:

  • In a repeat of the blocking Stop flow, DONE-2 followed DONE-1 within 1.5 s, and only the DONE-2 banner was sent. Both builds merge hook ends that arrive within the existing 1.5-second quiet window into one banner with the latest text, so the final message was kept.
  • Codex showed "Hook failed / exit code 1" lines in its own pane on that Windows machine, while Orca still received every status and showed the right banners. This PR changes no hook scripts or hook installation; the cause looks like that machine's PowerShell setup (its profile script is blocked by execution policy) and is not investigated here.

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:

Claude turn in a background tab F1 Claude reply F1 reply
/btw: no new unread mark F2 Blocking Stop: final DONE-2 F4
Codex /compact: no banner F5 Claude Ctrl+C mid-turn F6
Codex Ctrl+C mid-turn F6 Codex /btw then /exit: nothing F3

Tests

claude-side-question-completion.test.ts drives 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.ts pins the host's identified / repeat / ambiguous decisions, cancels and restarts; wsl-guest/agent-hook-server.test.ts pins the same decisions on the WSL relay. CI runs lint, typecheck and the full suite.

  • I manually tested these changes locally (real-agent QA, see QA)
  • Automated tests added/updated, or explained why not below

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Size: 59 files, +1742 / -138 (production +1107 / -137, tests +635 / -1).
  • Cross-platform: no paths or shortcuts; WSL panes decide on the WSL relay with the same function.
  • SSH: the remote Orca server stamps its own agents' turn ends; the window compares end times only for equality, never against its own clock.
  • Mobile: phone and paired-server projections carry the same two optional fields, so phone notifications follow the same decision.
  • Backwards compatibility: one optional saved and wire field pair; missing means today's behavior. Old paired servers are gated on the advertised capability, not a version.
  • Performance: one extra lookup per title idle edge and per hook status; no new timers beyond the one bounded held banner.
  • Unchanged and outside this PR: accepting Codex's "Trust and continue" folder prompt can send an empty "Codex finished" banner, on today's build and this one.

Author: Brennan Benson (X / Twitter @BrennanKB5)

Checklist

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

…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.
@brennanb2025
brennanb2025 marked this pull request as ready for review October 19, 2026 07:52
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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: a697d9d1-4821-429f-9143-3a359968c4f2
📥 Commits

Reviewing files that changed from the base of the PR and between c1fa5d3 and 10a035d.

📒 Files selected for processing: 2
  • src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts
  • src/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; 4 remain after this review.

📝 Walkthrough

The change adds host-derived turn-end timestamps to agent status and carries them through persistence, relay, IPC, runtime projections, and renderer state. Renderer completion tracking uses these timestamps to distinguish turn ends and coordinate hook-owned completion with title, BEL, and process-exit notification paths. Desktop notification cooldown handling now allows changed text through under a same-key, same-ID condition.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 10a03

A pending Stop can lose its completion banner, and a changed notification can be followed almost immediately by a duplicate bell near cooldown expiry. These bounded but user-visible regressions should be corrected before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 23.73% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 59 functions across 50 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly identifies the main change: hook-reporting panes now obtain finished banners from hook turns instead of title completion.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The pull request description is comprehensive and follows the required template. It explains the user impact, implementation, rationale, linked issues, visual proof, testing, compatibility considerati…
Full details: Description check

Explanation

The pull request description is comprehensive and follows the required template. It explains the user impact, implementation, rationale, linked issues, visual proof, testing, compatibility considerations, known differences, and checklist status. The AI Disclosure section is not completed, but the template permits internal contributors to omit it, and the remaining required information is present.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving 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.

❤️ 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: 7bbb10b9-ce9c-47e5-a622-c5f22b0c974b
📥 Commits

Reviewing files that changed from the base of the PR and between 9ea5434 and 2208035.

📒 Files selected for processing: 9
  • src/renderer/src/components/terminal-pane/agent-completion-coordinator-types.ts
  • src/renderer/src/components/terminal-pane/agent-completion-coordinator.ts
  • src/renderer/src/components/terminal-pane/agent-completion-title-observer.ts
  • src/renderer/src/components/terminal-pane/claude-side-question-completion.test.ts
  • src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.ts
  • src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts
  • src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts
  • src/renderer/src/hooks/agent-hook-completion-notifications.ts
  • src/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.

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


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2fd76c5b-0010-4532-b5da-846dd414f754
📥 Commits

Reviewing files that changed from the base of the PR and between 2208035 and 6fc371d.

📒 Files selected for processing: 47
  • src/main/agent-hooks/server-turn-ended-at.test.ts
  • src/main/agent-hooks/server/server-ingest-remote.ts
  • src/main/agent-hooks/server/server-persistence-validation.ts
  • src/main/agent-hooks/server/server-remote-envelope-normalization.ts
  • src/main/agent-hooks/server/server-status-application.ts
  • src/main/agent-hooks/server/server-status-identity.ts
  • src/main/agent-hooks/server/server-status-retries.ts
  • src/main/notifications/notification-delivery-service.test.ts
  • src/main/notifications/notification-delivery-service.ts
  • src/main/runtime/runtime-hook-agent-row-selection.ts
  • src/main/runtime/runtime-mobile-agent-status-builder.ts
  • src/main/runtime/runtime-mobile-agent-status-projection.ts
  • src/main/runtime/runtime-terminal-contracts.ts
  • src/main/startup/main-window-agent-status.ts
  • src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-title-precedence.test.ts
  • src/renderer/src/components/terminal-pane/agent-completion-coordinator-types.ts
  • src/renderer/src/components/terminal-pane/agent-completion-coordinator.ts
  • src/renderer/src/components/terminal-pane/agent-completion-hook-observer.ts
  • src/renderer/src/components/terminal-pane/agent-completion-notification-controller.ts
  • src/renderer/src/components/terminal-pane/agent-completion-snapshot-staleness.ts
  • src/renderer/src/components/terminal-pane/claude-side-question-completion.test.ts
  • src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.ts
  • src/renderer/src/components/terminal-pane/pty-connection/agent-task-complete-notify.ts
  • src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts
  • src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts
  • src/renderer/src/hooks/agent-hook-completion-notifications.ts
  • src/renderer/src/hooks/agent-hook-turn-end-ownership.ts
  • src/renderer/src/hooks/ipc-events/agent-status-event-applicator.ts
  • src/renderer/src/hooks/ipc-events/agent-status-routing.ts
  • src/renderer/src/runtime/sync-runtime-graph/agent-status-projection.ts
  • src/renderer/src/runtime/web-session-tabs-sync/state-equality-core.ts
  • src/renderer/src/runtime/web-session-tabs-sync/store-patch.ts
  • src/renderer/src/store/slices/agent-status-contract.ts
  • src/renderer/src/store/slices/agent-status-live-entry-builder.ts
  • src/shared/agent-hook-listener.ts
  • src/shared/agent-hook-listener/listener-event.ts
  • src/shared/agent-hook-listener/provider-event-routing.ts
  • src/shared/agent-hook-relay.ts
  • src/shared/agent-hook-turn-end-timing.ts
  • src/shared/agent-status-ipc-payload.ts
  • src/shared/agent-status-types.ts
  • src/shared/protocol-version.ts
  • src/shared/runtime-capability-constants.ts
  • src/wsl-guest/agent-hook-envelope-build.ts
  • src/wsl-guest/agent-hook-event-admission.ts
  • src/wsl-guest/agent-hook-result-retry-scheduler.ts
  • src/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.

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

brennanb2025 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

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 2399a65d4d2 (full path: adversarial review rounds, a fixer per round, design rulings, real-agent QA).

  • Review: 6 rounds plus a final narrow check on the last fix. The last check found no remaining bug of an earlier round's kind (a non-turn announced as a turn end, a real turn end lost, an extra banner). The remaining differences from today's build and from the common pattern are listed and labelled intended or temporary in the description's "Known differences".
  • CI: the full PR workflow passed on this commit ("PR checks passed"), after one re-run of a job that failed while installing pnpm (network reset).
  • Real-agent QA: Windows, real Claude Code and Codex, final head. Results and screenshots are in the description's QA section: one rich banner per real turn; nothing for Claude /btw, Codex /compact, Codex startup or /quit; the final reply's banner kept after a blocking Stop hook; no second banner after Ctrl+C; agents without hooks, hooks off and the terminal bell as today.
  • Not covered by QA: a WSL pane on the real UI (covered by the WSL relay tests and by checks against the real hook server), SSH and older paired servers.
  • Follow-ups (not in this PR): send every desktop-inferred cancel to the WSL side for every agent; keep one turn state per pane on the host.

Claude turn in a background tab
Codex /compact: no banner

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