Skip to content

Claude subagents: count and open only this chat's spawns - #1041

Merged
zeronsh[bot] merged 1 commit into
zeronsh:mainfrom
alexandre-vl:subagents/spawn-gate
Oct 26, 2026
Merged

zeronsh[bot] merged 1 commit into
zeronsh:mainfrom
alexandre-vl:subagents/spawn-gate

Conversation

@alexandre-vl

@alexandre-vl alexandre-vl commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Closes #999.

The ● N pill and the session card's count could read 3 while the card listed no subagent at all. Claude Code tags more traffic than a chat's own spawns: a forked skill runs under its Skill call's id, the agents a subagent starts run under their own Agent ids, and a /skill typed as the prompt runs under forked-command-<name>. The engine opened a subagent doc for every tagged id, and the pill counts open docs. Now it opens one only for a spawn chip of the chat, and a forked skill becomes such a chip.

The same fanout-bg run 15 s in. On v0.2.121 the sidebar pill and the session card's toggle read 3 and the card has no Subagents section. On this branch they read 1 and the card lists /fanout-bg.

The same prompt, Run the fanout-bg skill., sent at once to v0.2.121 and to this branch, 15 s in. From 5 to 50 s: fanout-bg-live.gif.

What changes

  • Engine. drive_run opens a subagent sink only for an id that is_chat_spawn accepts: a spawn chip of the chat, folded in this run or already in the doc (SessionDoc::has_subagent_spawn) (crates/engine/src/sessions.rs:3026, checked at :4176). Any other tagged id is remembered as foreign until a tool call with that id folds (:4483). Its traffic stays in the run journal, and nothing counts it. The gate is engine-wide: the other drivers already emit their chip before the child's traffic, and Devin's synthetic owners for a missing replayed spawn (crates/harness/src/acp/subagent_devin.rs:55, :73) now stay journal-only and uncounted.
  • Claude adapter. Each parent-feed Skill call is set aside (crates/harness/src/claude/normalize.rs:761). A forked skill runs as a subagent under that call's id, and its task_started comes before any of its traffic. That start now re-decodes the call as a spawn named Agent: /<skill> (normalize.rs:575). The skill then gets what an Agent call gets: a chip that opens its tab, a row in the card, a unit on the pill, and its task_notification settles it. A skill whose result arrives with no task start ran inline: it's dropped (normalize.rs:902) and stays a tool.
  • Doc. has_subagent_spawn reuses the spawn check update_subagent_chip already made (crates/doc/src/schema.rs:888). The AgentEvent::Subagent docs now say the spawn ToolCall must come first (crates/proto/src/agent.rs:840).

Testing

  • macOS 27.0, rustc 1.99.0, cargo test --locked:
    • -p zeron-doc --lib: 151 passed.
    • -p zeron-harness --lib --test claude --test codex: 446, 35 and 45 passed, 3 and 4 ignored as on main. Two timing tests in files this doesn't touch (acp/devin_models.rs, opencode) fail now and then under load, and pass alone.
    • -p zeron-engine --lib --test e2e --test codex_subagents --test subagent_idle_reap --test restart_resume --test side_chats: lib 631 passed (5 ignored), e2e 29 (2 ignored), codex_subagents 2, subagent_idle_reap 2, restart_resume 11 (1 ignored), side_chats 6.
  • Each regression test fails with its fix reverted:
    • running_subagents_count_only_this_chats_spawn_chips (engine) scripts a forked skill that starts two agents, plus a forked-command-* stream, and checks that the session row counts the one chip the card lists. Without the gate it fails with left: Some(4) right: Some(1). A frame tagged before the re-decode waits in the journal until the spawn folds. Without that eviction (sessions.rs:4483) the skill's doc never opens, and the test's 10 s wait fails with "the script routed".
    • a_forked_skill_becomes_a_spawn_when_its_task_starts (adapter). Without the re-decode, the task start yields [] instead of the Agent: /fanout call. Without the drop, the inline skill stays set aside ({"toolu_inline": …}), and a late task start would re-decode it.
    • captured_live_forked_skill_frames_replay_correctly replays frames captured from Claude Code 2.1.310 (live-2.1.310-forked-skill.jsonl). The skill's chip becomes a spawn before any of its traffic, its two agents stay chips of its own transcript, and its task_notification settles it.
    • empty_subagent_events_commit_once_per_window_not_per_tick (Engine: stop the run loop spinning on empty subagent events #604's test) needs a spawn chip now, or no sink opens. It plays a background subagent: the spawn's turn ends, and the subagent speaks after the parent's commit window. It fails if an empty flush leaves the sink dirty (1114368 commits in 1 s), if the gate skips the doc (0), or if a waking sink doesn't arm its own window (12 commits for 12 events).
  • Across 6 captured sessions (2.1.228 and 2.1.310), all 10 Skill and Agent calls with tagged traffic get their task_started before it. Only a /skill prompt's forked-command-* traffic has none, and it has no chip either.
  • Live, Claude Code 2.1.310 with Haiku. The same four prompts went to the installed v0.2.122 and to this branch, each a headless engine with its own data dir. Between v0.2.122 and this branch's base, main changed only crates/harness/src/codex and crates/ui among the crates. The pill and the card's rows were sampled every 5 s, until 45 s after the session went idle:
Prompt v0.2.122 This branch
Run the fanout skill. (2 foreground agents) pill 1 over an empty card in 16 of 30 samples card lists /fanout. Pill 1 over an empty card in 1 of 33 samples, taken the second the sink opened; the next one lists the row
Run the fanout-bg skill. (2 background agents) pill up to 3 over an empty card in 58 of 90 samples; 3 docs, none listed pill ≤ 1, card lists /fanout-bg, pill = running rows in all 17 samples
/fanout idle for 45 s with the pill at 1; its forked-command-fanout doc left streaming idle with the pill at 0, no doc
One background Agent call (control) pill 1, one row same

Known limits

Screenshots

Offscreen renders of the production Shell (Window::render_to_image), attached to each engine; docs/media/subagent-spawn-gate/README.md says how they were made. The media are in their own commit, 87e0b0fd, if you'd rather drop them before merging.

v0.2.121 This branch
The fanout-bg sidebar row with a 3 pill The fanout-bg sidebar row with a 1 pill
The transcript's chip: Tool Skill The transcript's chip: Agent /fanout-bg, with an open-tab arrow
Full windows
v0.2.121 This branch
v0.2.121, full window This branch, full window

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zeronsh
zeronsh Bot force-pushed the subagents/spawn-gate branch from d925135 to b7fc552 Compare October 26, 2026 21:23

@zeronsh zeronsh 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.

Security audit clean: Rust-only changes plus PNG/GIF/README demo media and a JSONL capture. No new dependencies, build scripts, workflow changes, network hosts or trailing data in the images. The spawn-gate code (engine is_chat_spawn, Claude forked-skill re-decode, SessionDoc::has_subagent_spawn) reviewed and verified locally on Linux: doc/proto/harness (lib, claude, codex) and engine (lib, e2e, codex_subagents, subagent_idle_reap, restart_resume, side_chats, session_publication) pass, apart from 4 engine lib tests that fail only because of this cloud machine's environment. Clippy is clean on touched files, and the engine regression test fails with the gate reverted. The shared commit landed on main with #1042, so after the rebase this PR carries only the demo media.

@zeronsh
zeronsh Bot merged commit 2514e3e into zeronsh:main Oct 26, 2026
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.

[Bug]: Claude harness: forked skills and nested subagents count in the ● N badge but never show in the lanes card

1 participant