fix(delivery): reap stale subagent freeze so busy parents keep receiving#82
fix(delivery): reap stale subagent freeze so busy parents keep receiving#82zane668 wants to merge 2 commits into
Conversation
A Task subagent that dies without a clean SubagentStop leaves the parent's running_tasks.active stuck true. While stuck, every parent PostToolUse short-circuits the subagent-context branch and delivers nothing, so hcom messages addressed to a busy parent are withheld until the user happens to type a prompt (UserPromptSubmit was the only hook running the dead-subagent cleanup). Observed in the wild as a parent whose running_tasks tracked agent_ids no longer present in the instances table. On the parent's own PostToolUse, reconcile the freeze before honoring it: reap dead subagents (reusing check_dead_subagents, which already treats a missing instances row / stale transcript / interrupt marker as death) and re-evaluate in_subagent_context. A live subagent keeps its instances row, so it is never reaped, active stays true, and parent messages remain deferred to end_task rather than being mis-injected into the subagent's own context. Scoped to PostToolUse because that is the only suppressed hook that carries a parent message delivery. Adds regression tests for both the stale-freeze-cleared and live-subagent-preserved paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Session: 1422c55e-ec89-44cf-ad66-5e5538a19fb9 Producer: ai-session Tool: cc
…th signals Review follow-up to the stale-subagent-freeze fix: reusing check_dead_subagents wholesale pulled the transcript-mtime staleness heuristic into the PostToolUse reconciliation path. A live subagent that simply hasn't written its transcript for a while (long tool call, waiting on I/O) crosses the stale threshold without being dead; reaping it would unfreeze the parent mid-Task and the next subagent tool call would inject the parent's messages into the subagent's context — a new cross-delivery vector. Introduce DeathSignals to scope which signals a sweep may act on: - All (DB row missing + stale mtime + interrupt marker) stays in effect at UserPromptSubmit, preserving existing behavior at that call site. - DefinitiveOnly (DB row missing or interrupt marker) is what reconcile_stale_subagent_freeze now uses; the mtime heuristic is excluded. Adds a regression test where a live subagent's transcript mtime is aged past the stale threshold: DeathSignals::All must flag it (fixture sanity / UserPromptSubmit behavior preserved) while reconciliation must keep the parent frozen and running_tasks intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Session: 1422c55e-ec89-44cf-ad66-5e5538a19fb9 Producer: ai-session Tool: cc
|
Heads-up for whoever picks these up 👋 The three fork PRs I opened (#82, #83, #84) all show the same red checks — 1.
2. Just flagging to save some triage time — no rush at all, and happy to rebase once |
|
thanks, did it different, should be fixed with these: 26287c2 — Routes hooks by Claude agent_id instead of running_tasks.active, so an uncleanly terminated child cannot freeze parent message delivery. |
Problem
A
Tasksubagent that dies without a cleanSubagentStopleaves the parent instance'srunning_tasks.activestuck attrue. While it is stuck, every parentPostToolUseshort-circuits down the "in subagent context" branch and delivers nothing, so hcom messages addressed to a busy parent are withheld until the user happens to type a prompt —UserPromptSubmitwas the only hook running the dead-subagent cleanup.Seen in the wild as a parent whose
running_tasksstill trackedagent_ids that were no longer present in theinstancestable: the subagent had gone, but the parent stayed frozen and silently stopped receiving.Fix
On the parent's own
PostToolUse, reconcile the freeze before honoring it: reap dead subagents (reusingcheck_dead_subagents) and re-evaluatein_subagent_context. A live subagent keeps itsinstancesrow, so it is never reaped —activestaystrueand parent messages remain deferred toend_taskrather than being mis-injected into the subagent's own context. This is scoped toPostToolUsebecause that is the only suppressed hook that carries a parent message delivery.Follow-up: only act on definitive death signals
Reusing
check_dead_subagentswholesale pulled the transcript-mtime staleness heuristic into the reconciliation path. A live subagent that simply hasn't written its transcript for a while (a long tool call, waiting on I/O) can cross the stale threshold without being dead; reaping it would unfreeze the parent mid-Task and let the next subagent tool call inject the parent's messages into the subagent's context — a new cross-delivery vector.To avoid that,
DeathSignalsscopes which signals a sweep may act on:All(DB row missing + stale mtime + interrupt marker) stays in effect atUserPromptSubmit, preserving existing behavior there.DefinitiveOnly(DB row missing or interrupt marker) is what the newPostToolUsereconciliation uses; the mtime heuristic is excluded.Tests
Regression tests cover: stale freeze cleared and delivery resumes to the parent; a live subagent kept frozen; and a live subagent whose transcript mtime is aged past the stale threshold —
DeathSignals::Allmust flag it (fixture sanity /UserPromptSubmitbehavior preserved) while reconciliation keeps the parent frozen andrunning_tasksintact.Verified locally with
cargo fmt --all -- --check,cargo clippy --all-targets --locked -- -D warnings, andcargo test --locked(all green).Only
src/hooks/claude.rsis touched.🤖 Generated with Claude Code