Conversation
spacexun2
force-pushed
the
fix/max-concurrent-workers
branch
2 times, most recently
from
September 11, 2026 02:34
2f5573a to
c2d8d80
Compare
spacexun2
force-pushed
the
fix/max-concurrent-workers
branch
from
September 22, 2026 03:31
c2d8d80 to
40bf8a3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background: #97 — when members are all ready the whole team starts working in parallel, hits 429/quota, and then stalls until a human intervenes. In the #97 design comment the maintainer drew the boundary as three things (concurrency permits, queue fairness, failure release) and separated retryable 429s from insufficient_quota that needs a route change (the latter belongs to host fallback;
FALLBACK_FAILURE_CODESin members.ts already covers it). This PR implements the first slice: an opt-in per-team dispatch concurrency limit (one global config value). 429 backoff is deliberately left out of the plugin to avoid overlapping host llm-retry.Changes:
Config/ToolsConfiggainsmaxConcurrentWorkers(z.natural().default(0), 0 = unlimited). At the default the behavior is path-for-path identical to today (the whole guard is gated on cap > 0).beginTaskAttempt, count claimed/in_progress tasks held by non-removed members; at the cap, a new dispatch returns undefined — the task stays pending, the member stays idle, nothing is written. Dispatch is already event-driven, so graph changes and member idle edges re-kick; queueing and backfill need no new polling.recoverOwnedrecovery path is exempt from the cap so the parked recovery from fix: recover parked tasks after member loss #86 does not get blocked.Two semantic boundaries, stated explicitly:
recoverOwnedrecovery ignores the cap, and a recovered task counts toward the in-flight number like any member task — a long-parked task therefore keeps holding a permit slot. That is the current, deliberate semantics; happy to adjust in discussion if it does not match expectations.Verification:
Known scope note: queue fairness (ordering when several tasks contend for one permit) is not changed by this PR — it keeps nextReadyTask's assigned-first ordering. If #97 wants explicit fairness semantics later, that deserves its own discussion.
Rebase note (round 4, onto post-v0.1.20 main @ 87c95c9)
Mechanical adaptation: the scheduler install call now merges the concurrency value with upstream's injected
dispatchhook and the mailbox/depth guards (installTeamScheduler(ctx, { stateDir, executionPrompt, maxConcurrentWorkers, dispatch })).One semantic adaptation had to be made to the cap scene in
lifecycle-verify.mjs, because 0.1.18 changed when members actually spawn:add_membernow only writes the roster row and the first dispatch spawns the continuable child. The old scene assumed seven live sessions right afteradd_member, which no longer holds — with the cap active the non-dispatched workers legitimately never spawn. The scene now:agent_teams_status), since unspawned workers have no session to publish an idle edge from;members[].id→ live registry) instead of a captured live-agent list, with a bounded wait for the spawn a dispatch causes;children.length) instead of post-spawn deliveries, since the prompt now rides on the spawn itself;Behavior under test is unchanged: cap=2 → exactly two dispatched, five pending; completion frees exactly one license; a member failure releases its license; cold recovery of an unobserved attempt bypasses the cap. Re-run on the new base: full build,
lifecycle-verify— the four cap pins and all upstream scenes pass (the two long-standing flaky checks — captain takeover return, activity residency — fail intermittently on a clean checkout too). Red-test re-run: deleting only themaxConcurrentWorkerspass-through makes exactly the four cap pins fail (plus the spawned-holders pin), restoring the line turns them green.