Skip to content

feat(subagent): replace the computed batch ceiling with a default and a setting - #350

Merged
liuedcson merged 3 commits into
mainfrom
feat/subagent-concurrency-default-config
Nov 7, 2026
Merged

liuedcson merged 3 commits into
mainfrom
feat/subagent-concurrency-default-config

Conversation

@liuedcson

@liuedcson liuedcson commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The parallel subagent batch ceiling was "computed": the factory derived it from the host CPU count (clampWorkflowConcurrency: cpus-2, capped at 16, and it clamped even an explicitly configured value down), and execution took the min with STEP_WORKFLOW_CONCURRENCY_LIMIT on top. The effective concurrency was impossible to reason about, the env var could only ever lower it, and the subagent path was coupled to the workflow provider-limit concept.

Solution

The ceiling is now a plain default of 8 plus one optional setting:

[subagent]
max_concurrency = 8   # children running at once in a parallel batch; any positive integer, may exceed the default
  • Data flow: [subagent] max_concurrency (StepCode JSON: subagent.maxConcurrency) → StepSettings.subagentMaxConcurrency → the bootstrap accessor → resolveSubagentConcurrency(configured, option) → min(result, maxParallelTasks) → the batch mapper plus the shared workflow admission window.
  • Resolution order: a valid configured value → the extension option → the default 8. No CPU-derived ceiling and no upper clamp, so a configured value may raise the limit above the default.
  • The value is read on every call, so a config reload takes effect without a restart.
  • Invalid values (non-positive integers) are reported as settings warnings and ignored on both the TOML and JSON load paths, falling back to the option and then the default; setters reject invalid values the same way.
  • Everything else is unchanged: the maxParallelTasks batch-size cap, the shared workflow admission window (including 429 backoff), and background-lane behavior. STEP_WORKFLOW_CONCURRENCY_LIMIT still caps workflow agent() fan-out; it no longer caps subagent batches.

Testing

  • New test/subagent-concurrency.test.ts: resolver defaults, precedence, and invalid fallback; with CPUs mocked to 2, the default peak is 8, a configured 12 reaches peak 12, the env var no longer caps the batch, invalid values fall back, oversized batches are rejected, and tasks beyond the ceiling queue.
  • Extended settings / stepcode-config tests: TOML and JSON read/write, invalid values ignored plus a drainErrors() warning carrying the config path.
  • Updated the obsolete test in subagent-chain-result.test.ts that asserted the env var lowered subagent concurrency; it now asserts the removal.
  • tsgo --noEmit and the root check (biome plus all contract checks) pass; the affected test suites pass (75/75).

Review

A subagent review pass was run over the diff. Its three findings were all fixed before submitting: a drainErrors() diagnostic for invalid values on the JSON load path, a load-time warning that no longer claims a fixed fallback value, and tests that actually cover the maxParallelTasks rejection and queueing paths.

… a setting

A parallel subagent batch derived its ceiling from the host CPU count
(cpu-2, capped at 16, applied twice) and an env var, so the effective
concurrency was impossible to reason about and the env var could only
ever lower it. The ceiling is now a plain default of 8 plus one optional
setting: [subagent] max_concurrency (StepCode JSON: subagent.maxConcurrency),
read on every call so a config reload takes effect without a restart. Any
positive integer is accepted, so the configured value may raise the limit
above the default; invalid values are reported as settings warnings and
ignored, falling back to the option and then the default.

The maxParallelTasks batch-size cap, the shared workflow admission window,
and background-lane behavior are unchanged. STEP_WORKFLOW_CONCURRENCY_LIMIT
still caps workflow agent() fan-out; it no longer caps subagent batches.

The old test asserting that env cap is replaced by one asserting its
removal, and new tests cover the resolver, the default, a configured value
above the default, the env var, invalid values, and the settings paths.
liujinhui and others added 2 commits November 7, 2026 17:56
…hain

The configured value is already validated at the settings boundary
(settings-manager / stepcode-config reject anything that is not a
positive integer), so the execution path no longer re-validates it. The
ceiling is now `configured ?? option ?? default`, and the extension
factory stores the host option as-is.

This drops the double resolution (creation time plus per call) and the
`resolveSubagentConcurrency` helper whose silent fallbacks made an
invalid host option look valid two layers later. Invalid values are
still rejected with a settings warning at load time.
…d window

The previous commit removed the numeric guard along with the CPU-derived
ceiling. `maxConcurrency` is an exported extension option, so a host that
passed 0, a negative number, or NaN reached `acquireWorkflowAdmission` as the
process-wide capacity: the pool admits nothing, every queued child waits with
no error and no timeout, and concurrent workflows are wedged with it. NaN was
worse still — `Array.from({ length: NaN })` starts zero workers, so the batch
reports `0/N succeeded` as a successful result.

Resolution is now `resolveSubagentConcurrency(configured, option)` again: the
settings boundary validates the configured value, and the option is validated
here so an invalid one cannot become a capacity. The factory rejects an
invalid option outright instead of silently clamping it, and
`WorkflowSemaphore.setCeiling` clamps as the last line of defense, since the
pool's capacity is the smallest ceiling among its members.

Also fixes what the change left behind:

- The StepCode JSON path reports `subagent.maxConcurrency` instead of the
  TOML key, and `subagent.backgroundTimeoutSeconds` now reports its ignored
  value the same way its neighbour does, instead of vanishing silently.
- `reload()` drops diagnostics from the previous snapshot so a warning about
  a value the user already fixed does not resurface.
- CHANGELOG and both settings documents describe the new default, the
  removed env coupling, and the shared window's minimum-of-members capacity;
  the subagent settings section is no longer titled "Background subagents"
  even though it governs every parallel batch.
@liuedcson
liuedcson merged commit 44835d9 into main Nov 7, 2026
2 checks passed
@liuedcson
liuedcson deleted the feat/subagent-concurrency-default-config branch November 7, 2026 11:45
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