From a45475817abd8ac804fe76a09460eca8f420d1d0 Mon Sep 17 00:00:00 2001 From: toki Date: Thu, 30 Jul 2026 12:21:11 +0900 Subject: [PATCH] =?UTF-8?q?fix(orchestrator):=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EB=B3=91=EB=A0=AC=20=EC=8B=A4=ED=96=89=20=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?3=EC=9C=BC=EB=A1=9C=20=EC=A0=9C=ED=95=9C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orchestrate-agent-task-loop/SKILL.md | 12 +++++-- .../scripts/dispatch.py | 15 ++++++--- .../tests/test_dispatch.py | 33 ++++++++----------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md b/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md index e1ba643..8c016e1 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md @@ -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/` 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 diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py index 45876b2..2f4e102 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py @@ -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 diff --git a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py index a15d1df..99fb0e4 100644 --- a/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py +++ b/agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py @@ -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"),