diff --git a/scripts/idle-draft b/scripts/idle-draft index 3928531..7ef37b4 100755 --- a/scripts/idle-draft +++ b/scripts/idle-draft @@ -1369,23 +1369,26 @@ def run_dispatch(args, config: dict, repo: Path, state_path: Path) -> int: def try_submit(): nonlocal dispatched_count - if args.once and dispatched_count >= 1: - return nonlocal queue - queue = build_ready_queue(repo, config, state, in_flight) - for cand in queue: - provider, _ = select_provider(cand["work_type"], config, gates, credential_ok) - if provider is None: - continue - cand = dict(cand) - cand["provider"] = provider - cand["before_pct"] = gates.get(provider, {}).get("seven_day_pct") - in_flight.add(cand["item_key"]) - dispatched_count += 1 - log_event(repo, f"dispatch: {cand['item_key']} work_type={cand['work_type']} provider={provider}") - fut = executor.submit(run_task, repo, config, cand, defaults["max_turns"], defaults["task_timeout"]) - futures[fut] = cand - return + while len(futures) < max(1, parallel): + if args.once and dispatched_count >= 1: + return + queue = build_ready_queue(repo, config, state, in_flight) + for cand in queue: + provider, _ = select_provider(cand["work_type"], config, gates, credential_ok) + if provider is None: + continue + cand = dict(cand) + cand["provider"] = provider + cand["before_pct"] = gates.get(provider, {}).get("seven_day_pct") + in_flight.add(cand["item_key"]) + dispatched_count += 1 + log_event(repo, f"dispatch: {cand['item_key']} work_type={cand['work_type']} provider={provider}") + fut = executor.submit(run_task, repo, config, cand, defaults["max_turns"], defaults["task_timeout"]) + futures[fut] = cand + break + else: + return try_submit() diff --git a/specs/idle-draft.spec.md b/specs/idle-draft.spec.md index b212803..ca7e562 100644 --- a/specs/idle-draft.spec.md +++ b/specs/idle-draft.spec.md @@ -218,8 +218,10 @@ next cycle); the walk continues to the next candidate. Up to `parallel` tasks run concurrently (`concurrent.futures.ThreadPoolExecutor`). Each completion is handled **serially** in the main thread (state updates and git commits never race). On every completion — and before the very first dispatch — the -gates (§6) are recomputed from a fresh probe (§5); a task is submitted to fill a free -worker slot only while an eligible `(item, work_type, provider)` triple remains. The +gates (§6) are recomputed from a fresh probe (§5); tasks are then submitted until +**every** free worker slot is filled or no eligible `(item, work_type, provider)` +triple remains — one completion may therefore trigger multiple submissions, and the +initial call before the first dispatch fills the whole pool, not one slot. The loop exits (dispatch mode, non-dryrun) when no eligible candidate remains. `--once` dispatches at most one task total, then exits without waiting for further slots.