Files
tai/research/background-work-limits/conclusions/supervisor-parallel-cap.md
T
retoor af080fe68e Strict goal verification, approval hardening, retrieval eval, work limits, sandbox caps (1.38.0)
- Approval: re-prompt unknown answers (max 3), shell-only affirmative
  guidance recovery; destructive actions stay strict
- GoalVerifier subagent with strict JSON verdicts, bounded follow-up
  rounds, nonzero exit plus open turn on unverified one-shots
- Retrieval eval (Hit@K/MRR/nDCG/Context Precision), FTS cascade with
  title weighting, query/read event logging, --eval-retrieval CLI
- Supervisor caps (8 parallel, 64 records) and scheduler hardening
  (4 fires/tick, per-schedule lease, stated misfire policy)
- Sandbox box resource trio: 2g/2.0/256 at create, best-effort
  update for existing boxes, rm -f recovery pointer
- Research archive: 12 subjects, 667 sources, 27 conclusions;
  suites 979 + 16 green
2026-10-18 12:13:24 +02:00

3.5 KiB

Conclusion: Cap live subagents with reject-loud semantics

Metadata

Field Value
Institute Molodetz Institute — Research & Retrieval Division
Subject background-work-limits
Document type Conclusion
Tier N/A
Date decided 2026-10-18
Decided by retoor, advised by tai (agent, Muse Spark)
Status Active
Confidence High

Question / problem statement

spawn_agent had no breadth bound: depth was capped at 2 but a turn could fork any number of workers, each holding a thread and an LLM conversation. The base rule requires a hard cap on everything unbounded. The question was where to set it and what a full pool should do: block (backpressure) or refuse (reject-loud).

Conclusion

Cap live subagents at AGENTS_MAX_PARALLEL = 8 with semaphore-TryAcquire semantics: spawn_agent raises past the cap instead of queueing, and each caller translates the refusal into its own loud message (fork tool tells the agent to poll existing workers first, the scheduler leaves the row for the next tick with a printed count, verification reports a failed check). The number is arithmetic, not magic: a scheduler burst of 4 plus interactive forks plus one verifier, single machine, free-tier friendly. Past AGENTS_MAX_RECORDS = 64 total records, spawning purges oldest collected finished records first, so delivered results stay inspectable but never accumulate without bound.

Supporting sources

  • go-semaphore: worker-pool-via-semaphore plus the explicit block-versus-TryAcquire fork; tai takes the refuse side.
  • erlang-otp-supervisor: bounding must be structural (a counter with a tripwire), which the live-count check is.
  • python-concurrent-futures-deadlock (Tier 2): blocking waits between workers are a documented deadlock shape, which rules out queueing the spawn.
  • vorpus-structured-concurrency (Tier 2): unstructured background work drops errors silently, which is why every refusal and drop is loud by construction.

Alternatives considered and rejected

  • Blocking acquire (wait for a slot): rejected, a stalled fork burns the spawning turn's step budget and risks the documented wait deadlock; polling plus retry is the honest loop.
  • Higher cap (16+) or per-profile caps: rejected for now, 8 already exceeds any legitimate burst observed (scheduler 4 plus a few forks); raise on measured need, not speculation.
  • Purging uncollected finished records: rejected, that would lose undelivered results; only collected finished records are purged, which loses nothing.
  • Auto-retry of failed workers (Temporal-style): rejected, failures surface as loud results instead; retry belongs to the caller that understands the task.

Risks / open questions

  • A pathological turn could still fork-poll-fork in a loop within the cap; step budgets, not the pool cap, bound that, and they already exist.
  • Uncollected finished records still accumulate in very long sessions; watch record counts if sessions run for days.
  • Revisit if: legitimate bursts hit the cap (raise with evidence), or free-tier rate limits argue for lowering it.

Action taken

Implemented the same day: AgentsFull exception, cap check in spawn_agent, loud handling in the fork tool, scheduler pre-check, purge of collected finished records, and unit tests pinning refusal plus recovery (collect, then spawn succeeds).

Changelog

Date Change
2026-10-18 Initial conclusion recorded.