fix(orchestrator): 기본 병렬 실행 수를 3으로 제한한다

This commit is contained in:
toki 2026-07-30 12:21:11 +09:00
parent 4e23ded3f4
commit 9e1a463a52
3 changed files with 33 additions and 27 deletions

View file

@ -53,7 +53,7 @@ Treat Korean text inside code spans or fenced examples as exact runtime or file-
- `workspace`: Trusted repository root containing `agent-task/` (optional; defaults to the current directory).
- `task_group`: Name of a specific `agent-task/<task_group>` to run (optional).
- `dry_run`: Inspect state, routes, and dependencies without starting a CLI (optional).
- `max_parallel`: Non-negative integer cap on unique active task-stage attempts across the physical workspace; `0` is unlimited (default). State that `--task-group` does not narrow occupancy, adopted external attempts count, internal helper coroutines do not count separately, and the flag must be supplied again on restart.
- `max_parallel`: Non-negative integer cap on unique active task-stage attempts across the physical workspace. Omission defaults to `3`; explicit `0` is unlimited. `--task-group` does not narrow occupancy, adopted external attempts count, internal helper coroutines do not count separately, and an override must be supplied again after restart.
- `retry_blocked`: Explicitly retry the same PLAN blocked by a previous dispatcher run in non-dry-run mode (optional). With `task_group`, reset only that group's blockers and 10-attempt counters while preserving other group state.
## Preconditions
@ -80,7 +80,7 @@ Treat Korean text inside code spans or fenced examples as exact runtime or file-
Concurrency limits:
- Global physical-workspace limit: `max_parallel=0` is unlimited; a positive value caps unique active task-stage attempts and is not narrowed by `task_group`. The cap applies across worker, self-check, review, and verified external-active attempts in the same physical workspace.
- Global physical-workspace limit: omitting `max_parallel` caps execution at `3`; explicit `max_parallel=0` is unlimited. A positive value caps unique active task-stage attempts and is not narrowed by `task_group`. The cap applies across worker, self-check, review, and verified external-active attempts in the same physical workspace.
- Pi `ornith:35b`: 3.
- agy: 1.
- Official Codex review: no separate review-only limit; subject to the global
@ -183,7 +183,7 @@ When recovering a KST-night `local-G07``local-G08` Laguna locator or a termin
- Never infer an implicit dependency from numeric order alone.
2. **Run the dispatcher.**
- Run all active tasks:
- Run all active tasks with the default physical-workspace cap of `3`:
```bash
python3 agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py
@ -201,6 +201,12 @@ When recovering a KST-night `local-G07``local-G08` Laguna locator or a termin
python3 agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py --max-parallel 2
```
- Explicitly disable the cap:
```bash
python3 agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py --max-parallel 0
```
- Preview classification without launching CLIs under the same cap:
```bash

View file

@ -98,6 +98,7 @@ WORK_LOG_NAME = "WORK_LOG.md"
WORK_LOG_ARCHIVE_RE = re.compile(r"^work_log_(\d+)\.log$")
AGENT_PROCESS_MARKER_ENV = "IOP_AGENT_TASK_EXECUTION_ID"
KST = timezone(timedelta(hours=9), name="KST")
DEFAULT_MAX_PARALLEL = 3
def validated_max_parallel(value: int) -> int:
@ -116,6 +117,8 @@ def validated_max_parallel(value: int) -> int:
f"--max-parallel must be >= 0, got {value}"
)
return value
STREAM_HEARTBEAT_SECONDS = 30
PI_MODEL_RESPONSE_STALL_SECONDS = 3 * 60
PI_SESSION_SCHEMA_VERSION = 3
@ -5900,7 +5903,9 @@ async def dispatch_with_store(
legacy_recoveries: dict[str, LegacyPromotionRecovery] = {}
live_external_processes: dict[str, str] = {}
capacity_waiting: set[str] = set()
max_parallel = validated_max_parallel(getattr(args, "max_parallel", 0))
max_parallel = validated_max_parallel(
getattr(args, "max_parallel", DEFAULT_MAX_PARALLEL)
)
while True:
if task_cache is None:
@ -6731,11 +6736,11 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"--max-parallel",
type=int,
default=0,
default=DEFAULT_MAX_PARALLEL,
metavar="MAX_PARALLEL",
help=(
"physical-workspace global cap on unique active task-stage "
"attempts; 0 is unlimited (default)"
f"attempts; default is {DEFAULT_MAX_PARALLEL}; 0 is unlimited"
),
)
parser.add_argument(
@ -6749,7 +6754,9 @@ def parse_args() -> argparse.Namespace:
def main() -> int:
args = parse_args()
try:
validated_max_parallel(getattr(args, "max_parallel", 0))
validated_max_parallel(
getattr(args, "max_parallel", DEFAULT_MAX_PARALLEL)
)
except ValueError as exc:
print(f"dispatcher error: {exc}", file=sys.stderr)
return 2

View file

@ -9614,7 +9614,12 @@ class ThroughputQuotaBatchTest(unittest.TestCase):
mock.patch.object(dispatch, "implementation_review_errors", return_value=[]),
mock.patch.object(selector, "probe_candidate_quota", side_effect=mock_probe),
):
args = SimpleNamespace(task_group="route", retry_blocked=False, dry_run=False)
args = SimpleNamespace(
task_group="route",
retry_blocked=False,
dry_run=False,
max_parallel=0,
)
exit_code = await asyncio.wait_for(
dispatch.dispatch_with_store(args, workspace, store),
timeout=5.0,
@ -11446,27 +11451,15 @@ class ParallelLimitSchedulingTest(unittest.IsolatedAsyncioTestCase):
tasks.append(task)
return workspace, tasks
def test_unlimited_default_selects_all_disjoint_ready(self):
"""Default max_parallel=0 admits every disjoint ready task."""
workspace, tasks = self._make_workspace()
ready = [
(tasks[0], "review"),
(tasks[1], "review"),
(tasks[2], "worker"),
(tasks[3], "selfcheck"),
]
store = dispatch.StateStore(workspace)
try:
selected, deferred, _ = dispatch.select_dispatch_candidates(
store, ready, persist=False, available_slots=None,
)
finally:
store.close()
self.assertEqual(selected, ready)
self.assertEqual(deferred, [])
def test_omitted_cli_value_defaults_to_three(self):
"""Omitting --max-parallel applies the workspace-global default of three."""
with mock.patch("sys.argv", ["dispatch.py"]):
args = dispatch.parse_args()
self.assertEqual(dispatch.DEFAULT_MAX_PARALLEL, 3)
self.assertEqual(args.max_parallel, dispatch.DEFAULT_MAX_PARALLEL)
def test_explicit_zero_selects_all_disjoint_ready(self):
"""Explicit max_parallel=0 admits every disjoint ready task."""
"""Explicit max_parallel=0 preserves the unlimited override."""
workspace, tasks = self._make_workspace()
ready = [
(tasks[0], "review"),