[Hooks] interrupt_agent does not dispatch SubagentStop for interrupted child

Open 💬 1 comment Opened Aug 12, 2026 by yoavtiger

What version of Codex CLI is running?

codex-cli 0.147.0

What platform is your computer?

macOS 26.5, arm64

What issue are you seeing?

When a running subagent is interrupted through the native interrupt_agent tool, the child stops executing but Codex does not dispatch the documented SubagentStop hook for that child.

This leaves exact-ID lifecycle observers with a SubagentStart and no corresponding terminal event, so they must incorrectly retain the interrupted child as running indefinitely.

In one captured occurrence:

  • three subagents each produced a SubagentStart hook event;
  • two siblings that completed normally each produced SubagentStop;
  • interrupt_agent was called for the remaining running child and returned previous_status: running;
  • that child's rollout immediately recorded a terminal turn_aborted event with reason interrupted;
  • the child was no longer running;
  • no SubagentStop event was dispatched for that child's exact agent_id.

The hook configuration was active and functioning in the same parent turn, as shown by the two normal SubagentStop events. No transcript files or private paths are attached to this public report.

What steps can reproduce the bug?

  1. Configure trusted command hooks for both SubagentStart and SubagentStop. Have each append the event name and agent_id to an event log.
  2. Start a parent Codex session and spawn a subagent that remains running long enough to interrupt.
  3. Confirm that SubagentStart was recorded for the child.
  4. Call the native interrupt_agent tool for that running child.
  5. Confirm that the child is no longer live and that its turn ended as interrupted.
  6. Inspect the hook event log.

Actual result: there is no SubagentStop event for the interrupted child's agent_id.

Control: allow another child in the same session to complete normally; its matching SubagentStop is dispatched.

What is the expected behavior?

Every subagent identity announced by SubagentStart should receive an exact terminal lifecycle event when it completes, fails, or is interrupted. With the current documented hook surface, that should be SubagentStop carrying the same agent_id.

Interruption should therefore dispatch SubagentStop exactly once after the child becomes terminal. If interruption is intentionally distinct from stop, Codex needs another documented exact-ID terminal hook; currently there is no such hook.

Why this matters

External lifecycle consumers cannot repair this safely from the other documented hooks:

  • Stop does not identify the child agent;
  • SessionEnd does not run for subagents;
  • the observed interrupt_agent tool input/output identifies the logical target but does not return the child's exact provider agent_id;
  • parsing rollout transcripts is not a stable lifecycle interface.

Consequently, any observer that avoids timing or task-name heuristics can remain stuck in Running even though the interrupted child has stopped.

Related issues

  • #37301 reports a parent repeatedly waiting after a child was interrupted; it may share the same missing terminal-notification boundary.
  • #33700 tracks completed/interrupted descendants remaining persisted as open.
  • #33097 is related to hook dispatch but appears distinct: in this reproduction SubagentStart and normal-completion SubagentStop hooks worked, and only the interruption path omitted the terminal event.

View original on GitHub ↗

1 Comment

jdcodes1 · 9 days ago

Mechanism on main @ 1f41cc5d92: SubagentStop (like Stop) is dispatched by run_turn_stop_hooks, which is called from exactly one place — the natural-completion branch of the sampling loop, gated on !needs_follow_up (core/src/session/turn.rs#L482-L490). A turn ended by interruption takes the TurnAborted error path out of run_turn and never reaches that call — so an interrupt_agent-stopped child records turn_aborted in its rollout but no terminal hook fires. Your capture (two natural siblings emit SubagentStop, the interrupted one doesn't) matches the branch structure precisely; this is also skip-path (2) enumerated in the #38213 analysis of missing Stop dispatch.

Fix shape: move stop-hook dispatch to turn teardown so it runs for every terminal outcome, with the completion kind (completed / interrupted / error) passed in the hook payload — lifecycle observers need the terminal event most in exactly the abnormal cases that currently skip it. If blocking-continuation semantics (should_block) only make sense for natural completion, the abort-path dispatch can be fire-and-forget notification-only, which sidesteps that design question.