Skip to content

fix(delivery): reap stale subagent freeze so busy parents keep receiving#82

Open
zane668 wants to merge 2 commits into
aannoo:mainfrom
zane668:pr/stale-subagent-freeze
Open

fix(delivery): reap stale subagent freeze so busy parents keep receiving#82
zane668 wants to merge 2 commits into
aannoo:mainfrom
zane668:pr/stale-subagent-freeze

Conversation

@zane668

@zane668 zane668 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Problem

A Task subagent that dies without a clean SubagentStop leaves the parent instance's running_tasks.active stuck at true. While it is stuck, every parent PostToolUse short-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 — UserPromptSubmit was the only hook running the dead-subagent cleanup.

Seen in the wild as a parent whose running_tasks still tracked agent_ids that were no longer present in the instances table: 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 (reusing check_dead_subagents) 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. This is scoped to PostToolUse because that is the only suppressed hook that carries a parent message delivery.

Follow-up: only act on definitive death signals

Reusing check_dead_subagents wholesale 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, DeathSignals scopes which signals a sweep may act on:

  • All (DB row missing + stale mtime + interrupt marker) stays in effect at UserPromptSubmit, preserving existing behavior there.
  • DefinitiveOnly (DB row missing or interrupt marker) is what the new PostToolUse reconciliation 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::All must flag it (fixture sanity / UserPromptSubmit behavior preserved) while reconciliation keeps the parent frozen and running_tasks intact.

Verified locally with cargo fmt --all -- --check, cargo clippy --all-targets --locked -- -D warnings, and cargo test --locked (all green).

Only src/hooks/claude.rs is touched.

🤖 Generated with Claude Code

Codex and others added 2 commits July 14, 2026 07:55
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
@zane668

zane668 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up for whoever picks these up 👋

The three fork PRs I opened (#82, #83, #84) all show the same red checks — rust-tests, windows-build, and plan. I dug into the logs, and these look like pre-existing / toolchain-drift issues on main rather than anything the PRs introduce (each PR is a clean single-file diff that doesn't touch the files below):

1. rust-tests + windows-build — both fail at the clippy step on main's own code:

error: redundant reference in `println!` argument
  --> src/commands/list.rs:834:38
   |
834|         println!("  Time:       {}", &entry.timestamp);
   |                                      ^^^^^^^^^^^^^^^^^ help: remove the redundant `&`: `entry.timestamp`
   = note: `-D clippy::useless-borrows-in-formatting`

clippy::useless_borrows_in_formatting was tightened in Rust 1.97, and CI installs the latest stable — so the & on src/commands/list.rs:834 now trips -D warnings. Dropping the & clears it. (Local clippy on 1.96 still passes, which is why it slipped through.)
Logs: rust-tests · windows-build

2. plandist plan reports .github/workflows/release.yml has out of date contents and needs to be regenerated (the newer cargo-dist no longer emits the swatinem/rust-cache@v2 step). A dist init regen should clear it.
Log: plan

Just flagging to save some triage time — no rush at all, and happy to rebase once main is green. Thanks for the project! 🙏

@aannoo

aannoo commented Jul 24, 2026

Copy link
Copy Markdown
Owner

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.
9fbdd78 — Prevents concurrent duplicate SubagentStop hooks from racing teardown and delivery.
d6b4ac5 — Makes child teardown exactly-once and recoverable if a stop-hook process crashes.
2a841f9 — Makes failed teardown transactionally retryable instead of leaving inconsistent task tracking.
38cc5fb — Verifies the exact child actor for hcom commands, preventing parent/child/sibling misattribution.

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.

2 participants