From 56049f27c72d647583aa838e216c0944a0395fef Mon Sep 17 00:00:00 2001 From: lll-hhh <169072815+lll-hhh@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:23:12 +0800 Subject: [PATCH 1/2] feat(runtime): decide scheduling write-scope overlap beside its canonical owner Quota scheduling compared child-lane write scopes with its own matcher. Add an entry point next to the canonical lease owner that composes the canonical folded glob decision with the subtree containment scheduling keeps, so the two layers share one set of primitives. Signed-off-by: lll-hhh <169072815+lll-hhh@users.noreply.github.com> --- .../control_plane/effect_runtime_handlers.ts | 1 + .../work_items/task_lease_acquire_decision.ts | 36 +++++++++++++++++++ .../effect_runtime_handlers.test.ts | 35 ++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index dff62be072..236066a588 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -465,6 +465,7 @@ export function createEffectRuntimeHandlers( lazyHandler(() => import("./coordination/legacy_writer_fence.ts"), ({checkLegacyCoordinationWriteAllowed}) => checkLegacyCoordinationWriteAllowed), ], ["task_lease.write_scopes.overlap", lazyHandler(() => import("./work_items/task_lease_acquire_decision.ts"), ({evaluateTaskLeaseWriteScopesOverlap}) => evaluateTaskLeaseWriteScopesOverlap)], + ["quota.task_orchestration.write_scope_overlap", lazyHandler(() => import("./work_items/task_lease_acquire_decision.ts"), ({evaluateQuotaSchedulingWriteScopesOverlap}) => evaluateQuotaSchedulingWriteScopesOverlap)], ["quota.monitor_poll.commit", lazyHandler(() => import("./quota/monitor_poll_commit.ts"), ({evaluateQuotaMonitorPollCommit}) => evaluateQuotaMonitorPollCommit)], ["presentation.decision_notice.project", lazyHandler(() => import("./presentation/decision_notice.ts"), ({projectDecisionNotice}) => projectDecisionNotice)], ["presentation.decision_notice.validate_references", lazyHandler(() => import("./presentation/decision_notice.ts"), ({validateDecisionNoticeReferences}) => validateDecisionNoticeReferences)], diff --git a/loopx/control_plane/work_items/task_lease_acquire_decision.ts b/loopx/control_plane/work_items/task_lease_acquire_decision.ts index 3b3df2ad7f..cf7ca25609 100644 --- a/loopx/control_plane/work_items/task_lease_acquire_decision.ts +++ b/loopx/control_plane/work_items/task_lease_acquire_decision.ts @@ -451,6 +451,42 @@ export function evaluateTaskLeaseWriteScopesOverlap(value: unknown): JsonObject }; } +/** Literal segments before the first wildcard, folded for comparison. */ +function schedulingScopeRoot(scope: string): string { + const parts: string[] = []; + for (const part of scope.replace(/^\/+|\/+$/gu, "").split("/")) { + if (part.includes("*") || part.includes("?") || part.includes("[")) break; + parts.push(part); + } + return caseFold(parts.join("/")); +} + +function schedulingScopePairOverlaps(left: string, right: string): boolean { + if (scopePairOverlaps(left, right)) return true; + const leftRoot = schedulingScopeRoot(left); + const rightRoot = schedulingScopeRoot(right); + return Boolean(leftRoot && rightRoot && ( + leftRoot === rightRoot || + leftRoot.startsWith(`${rightRoot}/`) || + rightRoot.startsWith(`${leftRoot}/`) + )); +} + +/** + * Quota scheduling keeps its own subtree rule: a literal scope owns the paths + * below it even without a trailing slash. That rule is wider than the canonical + * lease rule, so this entry point composes the two instead of restating the + * case fold or the glob matcher. + */ +export function evaluateQuotaSchedulingWriteScopesOverlap(value: unknown): JsonObject { + const input = requireJsonObject(value, "quota scheduling write-scope overlap"); + const left = comparisonWriteScopes(decisionStringArray(input.left, "left")); + const right = comparisonWriteScopes(decisionStringArray(input.right, "right")); + const overlap = left.length > 0 && right.length > 0 && + left.some((first) => right.some((second) => schedulingScopePairOverlaps(first, second))); + return {overlap}; +} + function decisionBoolean(value: unknown, label: string): boolean { if (typeof value !== "boolean") { throw new EffectRuntimeRequestError(`${label} must be a boolean`); diff --git a/tests/control_plane_ts/effect_runtime_handlers.test.ts b/tests/control_plane_ts/effect_runtime_handlers.test.ts index 3c14d35d68..1de7f3e0cf 100644 --- a/tests/control_plane_ts/effect_runtime_handlers.test.ts +++ b/tests/control_plane_ts/effect_runtime_handlers.test.ts @@ -78,6 +78,41 @@ test("runtime exposes the canonical task-lease write-scope rule", async () => { assert.equal(result.overlap, true); }); +test("scheduling write-scope overlap folds case and keeps subtree containment", async () => { + const rows: [string, string, boolean][] = [ + ["src/Main.ts", "src/main.ts", true], + ["src/MAIN.ts", "src/main.ts", true], + ["src/\u03a3.ts", "src/\u03c2.ts", true], + ["src/\u00df.ts", "src/SS.ts", true], + ["src/", "SRC/", true], + ["src/", "src/a.ts", true], + ["src/*.ts", "src/*.TS", true], + ["src/*.ts", "src/a.ts", true], + ["src/a.ts", "src/b.ts", false], + ["src/main.ts", "src/other.ts", false], + ["src/a.ts", "src/a.txt", false], + ["src", "src/a.ts", true], + ["src/main", "src/main/x", true], + ["src/a", "src/a/b", true], + ["src/a.ts", "src/a.ts/b", true], + ]; + for (const [left, right, overlap] of rows) { + const scheduling = await dispatchEffectRuntimeMethod( + handlers, + "quota.task_orchestration.write_scope_overlap", + { left: [left], right: [right] }, + ) as Record; + assert.equal(scheduling.overlap, overlap, `scheduling ${left} vs ${right}`); + } + // The scheduling entry point stays wider than the canonical lease rule. + const canonical = await dispatchEffectRuntimeMethod( + handlers, + "task_lease.write_scopes.overlap", + { left: ["src"], right: ["src/a.ts"] }, + ) as Record; + assert.equal(canonical.overlap, false); +}); + test("runtime exposes the source-session lifetime decisions", async () => { const goalRef = { goal_id: "release", From a9361897d5f437b393da8c3b14f19aae0dea791e Mon Sep 17 00:00:00 2001 From: lll-hhh <169072815+lll-hhh@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:23:12 +0800 Subject: [PATCH 2/2] fix(quota): consume the canonical overlap decision when admitting child lanes The scheduling matcher compared scopes case-sensitively, so on a case-insensitive host it admitted two lanes naming the same physical file while the canonical lease owner rejected the same pair. Consume the new scheduling entry point and keep the subtree rule the scheduler already had. Signed-off-by: lll-hhh <169072815+lll-hhh@users.noreply.github.com> --- .../quota/task_orchestration_admission.py | 42 +++------ .../test_task_orchestration_admission.py | 92 +++++++++++++++++++ 2 files changed, 104 insertions(+), 30 deletions(-) diff --git a/loopx/control_plane/quota/task_orchestration_admission.py b/loopx/control_plane/quota/task_orchestration_admission.py index dd2c40cb26..2d1e0e7eeb 100644 --- a/loopx/control_plane/quota/task_orchestration_admission.py +++ b/loopx/control_plane/quota/task_orchestration_admission.py @@ -1,11 +1,11 @@ from __future__ import annotations from dataclasses import dataclass -from fnmatch import fnmatchcase from typing import Any from ..agents.capability_gate import missing_required_capabilities from ..agents.runtime_model import peer_work_key, select_peer_for_work +from ..effect_runtime import effect_runtime_result from ..todos.contract import ( normalize_required_capabilities, normalize_required_write_scopes, @@ -472,37 +472,19 @@ def _write_scopes_conflict( left: _NormalizedAdmissionLane, right: _NormalizedAdmissionLane, ) -> bool: + """Consume the scheduling overlap decision owned by the canonical lease module.""" + if not left.required_write_scopes or not right.required_write_scopes: return False if left.effective_repository != right.effective_repository: return False - return any( - _scope_patterns_overlap(left_scope, right_scope) - for left_scope in left.required_write_scopes - for right_scope in right.required_write_scopes - ) - - -def _scope_patterns_overlap(left: str, right: str) -> bool: - if left == right or fnmatchcase(left, right) or fnmatchcase(right, left): - return True - left_prefix = _scope_static_prefix(left) - right_prefix = _scope_static_prefix(right) - return bool( - left_prefix - and right_prefix - and ( - left_prefix == right_prefix - or left_prefix.startswith(f"{right_prefix}/") - or right_prefix.startswith(f"{left_prefix}/") - ) + decision = effect_runtime_result( + "quota.task_orchestration.write_scope_overlap", + { + "left": list(left.required_write_scopes), + "right": list(right.required_write_scopes), + }, ) - - -def _scope_static_prefix(scope: str) -> str: - parts: list[str] = [] - for part in str(scope or "").strip("/").split("/"): - if any(marker in part for marker in ("*", "?", "[")): - break - parts.append(part) - return "/".join(parts) + if not isinstance(decision, dict) or not isinstance(decision.get("overlap"), bool): + raise RuntimeError("quota scheduling write-scope decision shape mismatch") + return bool(decision["overlap"]) diff --git a/tests/control_plane/test_task_orchestration_admission.py b/tests/control_plane/test_task_orchestration_admission.py index 4ed92c4b44..46609dc8da 100644 --- a/tests/control_plane/test_task_orchestration_admission.py +++ b/tests/control_plane/test_task_orchestration_admission.py @@ -264,6 +264,98 @@ def test_admission_serializes_overlapping_write_scopes() -> None: ] +def _scope_contract( + items: list[dict[str, Any]], + *, + goal_write_scope: list[str], +) -> dict[str, Any] | None: + summary = {"items": items} + contract, _work_lane = apply_task_orchestration_contract( + fallback_work_lane_contract={"lane": "advancement_task"}, + goal_boundary={ + "write_scope": goal_write_scope, + "orchestration": { + "mode": "multi_subagent", + "spawn_allowed": True, + "max_children": 2, + }, + }, + agent_identity={ + "agent_id": AGENT_ID, + "registered_agents": [AGENT_ID], + }, + agent_todo_summary=summary, + raw_agent_todo_summary=summary, + raw_user_todo_summary={"items": []}, + available_capabilities=["subagent_spawn"], + ) + return contract + + +def test_admission_serializes_case_alias_write_scopes() -> None: + contract = _scope_contract( + [ + _todo("todo_primary", action_kind="implement", required_write_scopes=["src/Main.ts"]), + _todo("todo_case_alias", action_kind="implement", required_write_scopes=["src/main.ts"]), + _todo("todo_independent", action_kind="implement", required_write_scopes=["src/other.ts"]), + ], + goal_write_scope=["src/**"], + ) + + assert contract is not None + assert [lane["todo_id"] for lane in contract["eligible_child_lanes"]] == [ + "todo_independent" + ] + assert contract["blocked_lanes"] == [ + { + "todo_id": "todo_case_alias", + "task_domain": "code", + "reason_codes": ["write_scope_conflict"], + "conflicts_with_todo_id": "todo_primary", + } + ] + + +def test_admission_keeps_scheduling_subtree_containment_without_a_trailing_slash() -> None: + contract = _scope_contract( + [ + _todo("todo_primary", action_kind="implement", required_write_scopes=["src"]), + _todo("todo_below", action_kind="implement", required_write_scopes=["src/a.ts"]), + _todo("todo_independent", action_kind="implement", required_write_scopes=["tests/a.py"]), + ], + goal_write_scope=["src", "src/**", "tests/**"], + ) + + assert contract is not None + assert [lane["todo_id"] for lane in contract["eligible_child_lanes"]] == [ + "todo_independent" + ] + assert contract["blocked_lanes"] == [ + { + "todo_id": "todo_below", + "task_domain": "code", + "reason_codes": ["write_scope_conflict"], + "conflicts_with_todo_id": "todo_primary", + } + ] + + +def test_admission_leaves_sibling_files_independent() -> None: + contract = _scope_contract( + [ + _todo("todo_primary", action_kind="implement", required_write_scopes=["src/a.ts"]), + _todo("todo_sibling", action_kind="implement", required_write_scopes=["src/b.ts"]), + ], + goal_write_scope=["src/**"], + ) + + assert contract is not None + assert [lane["todo_id"] for lane in contract["eligible_child_lanes"]] == [ + "todo_sibling" + ] + assert contract["blocked_lanes"] == [] + + def test_admission_serializes_implicit_and_explicit_goal_repository_scopes() -> None: summary = { "items": [