Conversation
spacexun2
force-pushed
the
fix/max-concurrent-workers
branch
from
September 10, 2026 03:14
54a5444 to
2f5573a
Compare
kickTeam dispatched every ready task to every idle member with no concurrency budget, so a 7-member team immediately ran 7 model turns. Config now accepts maxConcurrentWorkers (z.natural().default(0), 0 = unlimited, matching the previous behavior exactly). Inside the ticket transaction, after a task is selected and before beginTaskAttempt, new dispatches are blocked while the count of claimed/in_progress tasks held by current members has reached the cap: the task stays pending, the member stays idle, and nothing is written, so later idle/graph kicks re-run the decision for free. Two explicit decisions: captain takeovers do not count toward the budget (the captain is one extra lane outside the member worker pool), and cold recovery of an unobserved durable attempt bypasses the cap so a saturated budget can never wedge restart recovery (NanmiCoder#86). The lifecycle verification gains a scenario that registers a second plugin instance with maxConcurrentWorkers=2: 7 members and 7 ready tasks admit exactly 2 concurrent dispatches, completion and failure each release exactly one license to the queued tasks, and a saturated budget never blocks recovery of a disposed owner's open attempt.
spacexun2
force-pushed
the
fix/max-concurrent-workers
branch
from
September 11, 2026 02:34
2f5573a to
c2d8d80
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.