Repository navigation
Claude subagents: count and open only this chat's spawns - #1041
Conversation
54fac82 to
d925135
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d925135 to
b7fc552
Compare
There was a problem hiding this comment.
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.
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
Skillcall's id, the agents a subagent starts run under their ownAgentids, and a/skilltyped as the prompt runs underforked-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 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
drive_runopens a subagent sink only for an id thatis_chat_spawnaccepts: 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.Skillcall is set aside (crates/harness/src/claude/normalize.rs:761). A forked skill runs as a subagent under that call's id, and itstask_startedcomes before any of its traffic. That start now re-decodes the call as a spawn namedAgent: /<skill>(normalize.rs:575). The skill then gets what anAgentcall gets: a chip that opens its tab, a row in the card, a unit on the pill, and itstask_notificationsettles it. A skill whose result arrives with no task start ran inline: it's dropped (normalize.rs:902) and stays a tool.has_subagent_spawnreuses the spawn checkupdate_subagent_chipalready made (crates/doc/src/schema.rs:888). TheAgentEvent::Subagentdocs now say the spawnToolCallmust come first (crates/proto/src/agent.rs:840).Testing
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 onmain. 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),e2e29 (2 ignored),codex_subagents2,subagent_idle_reap2,restart_resume11 (1 ignored),side_chats6.running_subagents_count_only_this_chats_spawn_chips(engine) scripts a forked skill that starts two agents, plus aforked-command-*stream, and checks that the session row counts the one chip the card lists. Without the gate it fails withleft: 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 theAgent: /fanoutcall. 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_correctlyreplays 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 itstask_notificationsettles 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).SkillandAgentcalls with tagged traffic get theirtask_startedbefore it. Only a/skillprompt'sforked-command-*traffic has none, and it has no chip either.mainchanged onlycrates/harness/src/codexandcrates/uiamong the crates. The pill and the card's rows were sampled every 5 s, until 45 s after the session went idle:Run the fanout skill.(2 foreground agents)/fanout. Pill 1 over an empty card in 1 of 33 samples, taken the second the sink opened; the next one lists the rowRun the fanout-bg skill.(2 background agents)/fanout-bg, pill = running rows in all 17 samples/fanoutforked-command-fanoutdoc left streamingAgentcall (control)c04e3749) after these runs, with no conflict. Merged there with the other five PRs of this series (Claude subagents: forward a foreground subagent's text and thinking #1042, Claude subagents: give a typed /skill's fork a chip and a tab #1043, Claude subagents: fill a typed fork's tab while the CLI holds it #1044, Fork: settle the running subagent chips a fork copies #1045, Providers: a switch to turn native subagents off #1046), it passes every suite above again, plus the rest of CI's selection, sync'smock-servertests,zeron-mcpand the UI'ssettings::tests, each executable checked to be built from that merge. The one failure, preview'sbackgrounded_scrubbed_terminal_and_out_of_tree_servers_stay_attributed, fails the same way on v0.2.124 alone, on this Mac.Run the fanout-bg skill.kept the pill equal to the card's running rows in all 20 samples, and the store held one subagent doc, the one the card lists.Known limits
Skillcalls are set aside./skilltyped as the prompt has no chip, so its run gets no tab, and its reply isn't shown either ([Bug]: A forked/skilltyped as the prompt shows nothing while it runs, and no reply after #1040). Claude subagents: give a typed /skill's fork a chip and a tab #1043 gives it a chip and a tab, and Claude: show replies to local slash commands #787 shows the reply.normalize.rs: both change thetask_startedbranch. Claude: show Workflow runs as subagents with live progress #784 addsnormalize.rs(the up-fronttool_useloop) towire.rsandscripts/ci/test-core.sh, where it already conflicts withmain. Claude: open each Workflow agent's live transcript #785 addssessions.rs(the gate) andnormalize.rsto its conflicts withmain.ToolCallahead of its opening-prompt seed. Behind it, the seed would reach the gate while the chip is stillSkilland stay in the journal (CLIs before 2.1.296, which don't say whether a task is backgrounded). Merging Claude: show Workflow runs as subagents with live progress #784: keep the"Skill"arm next to itsis_spawn_toolcheck in the up-front loop.Screenshots
Offscreen renders of the production
Shell(Window::render_to_image), attached to each engine;docs/media/subagent-spawn-gate/README.mdsays how they were made. The media are in their own commit,87e0b0fd, if you'd rather drop them before merging.Full windows
Need help on this PR? Tag
@codesmith-botwith what you need.