Skip to content

CL-6942: split spawn_agent / wait_agents out of task() - #600

Merged
TheGreatAxios merged 4 commits into
mainfrom
cl-6942-split-spawn-from-wait-non-blocking-spawn_agent
Aug 24, 2026
Merged

CL-6942: split spawn_agent / wait_agents out of task()#600
TheGreatAxios merged 4 commits into
mainfrom
cl-6942-split-spawn-from-wait-non-blocking-spawn_agent

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds spawn_agent (starts a worker, returns immediately with { agent_id, status: "running" }, never awaits completion) and wait_agents (blocks until any given/live target reaches a terminal state, or a clamped timeout elapses without touching the workers) as two new fleet verbs.
  • wait_agents blocking is driven by the existing SubAgentSessionStore mailbox (subscribe) raced against a real timer — no polling.
  • task() is unchanged; it remains the fused spawn+block primitive for the single-worker case.
  • Registered spawn_agent/wait_agents in FLEET_VERBS (already present from CL-6941) and gated at the same tier-mount point in run.ts as task/search_agents, additively.

Report retention fix: the session store's finished-session cap (maxCompleted, default 20) is a TUI display cap that evicts the oldest finished session — report included — once more than that many have finished. task() never hit this because it awaits its own result before returning. spawn_agent/wait_agents can fan out well past that cap in one turn, so a report could be silently lost before wait_agents ever collected it. Fixed with fleetRecords, a small never-capped map (agent id -> terminal status/report/error) written before the session store's complete()/fail() (so the store's synchronous subscribe notification always sees the up-to-date record); an entry is removed only once wait_agents actually delivers it to a caller.

Worktree isolation: spawn_agent's whole point is running several workers at once, so two implement-intent workers sharing one cwd with no isolation could corrupt each other's edits. I chose the "refuse" option over wiring real worktree isolation into spawn_agent: duplicating task()'s worktree machinery (isolation, cleanup, stash baseline, preserve-on-dirty) inside spawn_agent's independent dispatch path was a larger, riskier surface than this PR's scope justified. spawn_agent now refuses a second concurrent implement-intent (director build) spawn against the same cwd with an actionable error naming the conflicting agent id, and releases the lane once that worker finishes. Explore/plan/review-intent spawns don't write and are unaffected — they may still run fully concurrently. A caller that wants concurrent implementation work today should use task(useWorktree: true), which already has real isolation.

Documented follow-ups (not in scope here): spawn_agent only supports closed-director dispatch (agent= a director id, or intent=) — no custom AgentProfile lookup, no worktree isolation, no nested orchestration, no re-dispatch ledger. These remain task()-only for now.

  • CHANGELOG entry under ## [Unreleased].

Test plan

  • bun run check green (lint, typecheck, build, full test suite: 5417 pass)
  • Tests in src/subagent/agent-fleet.test.ts:
    • spawn_agent returns immediately with a running agent_id without waiting for the worker
    • wait_agents on one target returns once it completes while sibling workers stay running
    • wait_agents with a short timeout on a still-running agent returns timed_out: true without cancelling it, and a second wait_agents call still works
    • wait_agents with no targets waits on all currently running spawned agents
    • reports survive well past the session store's 20-entry display cap until wait_agents collects them
    • refuses a second concurrent implement-intent spawn against the same cwd
    • does not refuse a second concurrent explore-intent spawn against the same cwd
    • releases the write lane once the implement worker finishes, allowing another
  • Existing task() / subagent tests unaffected (bun test src/subagent/: pass)
  • Rebased cleanly onto latest origin/main (no conflicts in run.ts)

@linear-code

linear-code Bot commented Aug 24, 2026

Copy link
Copy Markdown

CL-6942

@TheGreatAxios
TheGreatAxios force-pushed the cl-6942-split-spawn-from-wait-non-blocking-spawn_agent branch from 74104e4 to 9aef384 Compare August 24, 2026 05:36
@TheGreatAxios
TheGreatAxios enabled auto-merge (squash) August 24, 2026 05:38
@TheGreatAxios
TheGreatAxios force-pushed the cl-6942-split-spawn-from-wait-non-blocking-spawn_agent branch from 9aef384 to 5701827 Compare August 24, 2026 05:48
spawn_agent starts a worker and returns immediately with {agent_id,
status}; wait_agents blocks on any of a target set (default: all live
agents) reaching a terminal state, or a clamped timeout, without
touching the workers on timeout. task() is unchanged.
fleetRecords is a never-capped map of terminal spawn_agent results,
written before the session store's complete()/fail() so wait_agents
never loses a report to the store's TUI-sized finished-session cap.

spawn_agent also now refuses a second concurrent implement-intent
spawn against the same cwd (no worktree isolation yet), releasing the
lane once the running one finishes; explore/plan/review-intent spawns
are unaffected and may still run concurrently.
read_agent_trace's descendant-scoping check resolves a worker's parent
chain from its trace directory name, so spawn_agent must pass the
session-store id the same way task-tool.ts does.
@TheGreatAxios
TheGreatAxios force-pushed the cl-6942-split-spawn-from-wait-non-blocking-spawn_agent branch from 5701827 to 7b229eb Compare August 24, 2026 05:51
@TheGreatAxios
TheGreatAxios merged commit 08a32d1 into main Aug 24, 2026
5 checks passed
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.

1 participant