#3916·herdr

Pi: /new (session start rebind) fires a false 'finished' toast + Done sound via Unknown→Idle completion transition

Author: sbscanCreated Sep 10, 2026Updated Sep 17, 2026
Labelsbugmaintainer-neededp2macosagent-detectionintegrations

Environment

  • Herdr 0.9.0 (Homebrew, macOS 26.6.2), server + client both 0.9.0
  • Pi integration: current, HERDR_INTEGRATION_VERSION=8
  • [ui.toast] delivery = "system", [ui.sound] at defaults (enabled = true)

What happens

Starting a new Pi session — typing /new in an existing pi pane — immediately produces a false pi finished toast and the Done sound, even though no work finished. The same happens on other session-start paths that rebind the session (/resume, /fork). The user-visible effect: "every time I start a new session I instantly get a notification", on top of the legitimate per-turn finished notifications.

The toast is subject to the normal active-tab suppression, but the transition still fires whenever the pane counts as background (background tab, another client's active view, or the Herdr window unfocused — per the unfocused-window allowance), so in practice the false notification shows up most of the time.

Reproduce

  1. Run pi in a Herdr pane (integration v8). Confirm the pane reports idle.
  2. Move focus away from that tab (or unfocus the Herdr window).
  3. Type /new in the pi pane.
  4. Within ~delay_seconds, a pi finished toast + Done sound fires, without any turn having run in the new session.

Analysis (master, herdrdev/herdr)

  1. The pi extension's session_start handler first reports the new session, then force-publishes the current state:

    • src/integration/assets/pi/herdr-agent-state.ts around lines 229–241: await reportSession(event?.reason) (which reaches the server as pane.report_agent_session with session_start_source: "new"), immediately followed by publishState(true) — a forced state report, idle at a fresh prompt.
  2. The server accepts the rebind for pi: session_report_allows_session_replacement matches ("herdr:pi", "pi", Some("new" | "resume" | "fork")) (src/terminal/state.rs ~1315–1333). In the replaced_hook_session branch it clears the pane's hook authority (self.hook_authority = None;, src/terminal/state.rs ~1586–1593), so recompute_effective_state falls back to fallback_state — i.e. the effective state becomes Unknown (src/terminal/state.rs ~2149–2189).

  3. The extension's forced idle report from step 1 then re-establishes hook authority at Idle, producing an effective Unknown → Idle change. is_completion_transition_parts deliberately treats that as a completion when the agent label is unchanged:

    • src/app/actions.rs ~44–55: previous_state == Unknown && state == Idle && previous_agent_label.is_some() && previous_agent_label == agent_label which then yields the Finished toast / Done sound via notification_toast_for_state_change_with_agent_labels (src/app/actions.rs ~132–152).

So the "session just (re)started, agent sitting at the prompt" state is indistinguishable from "a turn just completed" once the rebind has reset the hook state to Unknown.

Expected

A session start (/new, /resume, /fork) should not be a completion transition. Only Working → Idle (or Blocked → Idle) should notify as finished.

Possible directions

  • Server-side: after a same-owner session replacement, suppress the completion notification for the first state report that re-binds authority (or mark the rebind so the following idle is not a completion). The rebind already knows session_start_source; the forced state report that follows it is the one to exempt.
  • Extension-side: publishState(true) right after reportSession could be delayed/omitted, but a server-side guard seems more robust since the same Unknown→Idle path can be hit by other rebind flows.

Related: #1231 and #1377 cover the rebind handling on the stuck-status side (fixed via #1189); #49 is the other false-completion family (subagents). This report is specifically about the rebind-driven Unknown→Idle completion on session starts.

Happy to test a preview build or add traces if useful.