diff --git a/docs/quota-allocation.md b/docs/quota-allocation.md index 89a97a8fcd..a102e3aabe 100644 --- a/docs/quota-allocation.md +++ b/docs/quota-allocation.md @@ -1571,7 +1571,20 @@ advances through `progression_minutes` only after the applied RRULE has had one real interval to run. An immediate post-ACK reconciliation therefore verifies the settled target instead of manufacturing the next backoff target in the same turn. A changed `reset_policy.reset_token` -returns to the current profile's initial interval. This gives hosts a compact +returns to the current profile's initial interval. A deadline-constrained wait +binds its effective progression buckets into that reset token. For example, +when the same monitor's remaining horizon shrinks from `[15, 30]` to `[15]`, +the generated ACK starts a new identity at index zero; it does not bypass the +native same-identity regression guard. Time passing within the same buckets +keeps the identity stable. Host readback, receipt freshness, CAS and replay +checks still apply, including when the host already uses the tighter RRULE. + +受截止时间约束的等待会将实际可用的节奏档位纳入重置令牌。同一监控的档位从 +`[15, 30]` 缩为 `[15]` 时,生成的 ACK 以新身份从索引零开始;同档位内的时间流逝 +保持身份稳定。原生同身份防倒退、宿主回读、回执时效、CAS 和重放校验仍然有效, +包括宿主已经采用较短 RRULE 的情况。 + +This gives hosts a compact post-update ack protocol instead of requiring them to own or diff the whole quota state. If `apply_needed=false` and `ack_needed=true`, the same command records an exact matching host readback without calling `automation_update`. diff --git a/loopx/control_plane/scheduler/scheduler_hint.py b/loopx/control_plane/scheduler/scheduler_hint.py index 5e77275e35..1f4a0d12c8 100644 --- a/loopx/control_plane/scheduler/scheduler_hint.py +++ b/loopx/control_plane/scheduler/scheduler_hint.py @@ -772,7 +772,14 @@ def build( "local_scheduler_unchanged_poll_limit": cli_limit, "claude_code_loop_unchanged_poll_limit": claude_limit, } - reset_profile_snapshot = reset_profile_snapshot_override or profile_snapshot + # A deadline can shrink the allowed buckets without changing the + # monitor/phase identity. Bind the effective progression so the typed + # commit owner sees an identity reset, rather than an index regression. + # Elapsed time within the same buckets must not reset backoff. + reset_profile_snapshot = { + **(reset_profile_snapshot_override or profile_snapshot), + "app_automation_progression_minutes": app_cadence_progression, + } ( reset_token, identity_signature, diff --git a/skills/loopx-self-repair/references/repair-patterns.md b/skills/loopx-self-repair/references/repair-patterns.md index 9ae06b7f2a..5257787d1f 100644 --- a/skills/loopx-self-repair/references/repair-patterns.md +++ b/skills/loopx-self-repair/references/repair-patterns.md @@ -5,6 +5,7 @@ teaches a reusable control-plane lesson. | Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair | | --- | --- | --- | --- | --- | +| `scheduler_deadline_bucket_reset_gap` | A generated tighter deadline ACK fails with `progression_regression`, even after the host applies the requested RRULE. | Saved failed ACK, originating Turn, prior/current progression buckets and reset tokens, current host readback, and exact generated argv. | The deadline changes the effective progression while an override reset profile omits those buckets, so index zero still carries the old identity. | Bind effective buckets through the existing reset-profile digest; keep time within a bucket stable. Preserve native monotonic, host-readback, receipt-freshness, CAS and replay guards. Reproduce 30-minute/index-one to 15-minute/index-zero through disposable File/SQLite CLI and separately qualify the installed artifact; never edit active scheduler state or infer runtime adoption from source tests. | | `quiet_receipt_turn_start_context_loss` | A delegated request is delivered, but a quiet heartbeat reports no required inbox read and leaves it unassessed. | Original inbox delivery and assessment readback, initial hook dispatch, the captured final guard packet, and the automatic no-spend receipt reconstruction path. | The CLI observes pending context, then rebuilds the decision after stall writeback without forwarding that same Turn's hook dispatch. | Forward the original dispatch through the existing live-decision owner during reconstruction; preserve pending, empty and failed observations without re-running hooks or granting delivery/spend. Cover first and replayed stall receipts through final capture and CLI projection, including negative authority checks. Assess and link the original request, execute its admitted work, and report the actual result through its native return route; delivery or adoption alone is not completion. | | `benchmark_planning_clock_projection_gap` | A resumed task closes work using an earlier Host deadline even though a continuation was explicitly authorized; readable history and an old ready plan are mistaken for current executable work. | Original task constraints, Host phase receipts, saved model input, current authorized continuation, actual quota/frontier readback and terminal dispositions; keep private trajectories outside public artifacts. | Historical Host timing and closure context survived recovery without the new continuation intent; ordinary planning should not inject Host countdowns. | Keep Host deadlines, remaining time and execution reserves out of ordinary model input; retain external enforcement and task-owned time constraints. At the explicitly authorized archive-recovery boundary, preserve old history and closure records, identify earlier Host clocks as historical in the new continuation request, and reuse Goal/Todo/replan with a stable operation identity. Read back actionable work or review before calling continuation ready; an old plan or exit code zero is insufficient. Do not renew budgets on retry, reopen without authorization, clear a user stop, filter task deadlines or bypass evaluation cooldowns. Cover clock-free fresh/resumed input, idempotent recovery and legitimate stopping; qualify real model adoption and score gains separately. | | `cli_json_file_argument_contract_gap` | A `--*-json` option repeatedly rejects inline JSON as an unreadable object; reshaping it does not help, or Markdown shows only an invalid-request status. | The command's current help, file-input loader, structured error, Markdown renderer, and a synthetic valid-file recovery. | The option accepts a file path but help does not say so; file-read, JSON-decode and root-shape failures are conflated, or the renderer omits the error reason. | Clarify the existing file contract without silently adding inline input. Separate read, parse and shape diagnostics without echoing supplied contents or paths; preserve the reason in each output format. Validate missing/inline input, malformed/non-object files, same-path recovery and unchanged typed admission. Repair the owning CLI adapter, not provider readiness or Goal retry authority. | diff --git a/tests/control_plane/test_scheduler_arbitration_native_cli.py b/tests/control_plane/test_scheduler_arbitration_native_cli.py index 707df34a25..9248ee043b 100644 --- a/tests/control_plane/test_scheduler_arbitration_native_cli.py +++ b/tests/control_plane/test_scheduler_arbitration_native_cli.py @@ -5,6 +5,7 @@ import os import subprocess import sys +from datetime import datetime, timedelta, timezone from pathlib import Path import pytest @@ -23,7 +24,9 @@ TURN = "turn-scheduler-arbitration" -def make_scheduler_fixture(root: Path, provider: str, scenario: str) -> dict: +def make_scheduler_fixture( + root: Path, provider: str, scenario: str, *, monitor_clock: datetime | None = None, +) -> dict: """Keep this synthetic source usable by source and installed-package replay.""" project, runtime = root / "project", root / "runtime" project.mkdir() @@ -34,13 +37,22 @@ def make_scheduler_fixture(root: Path, provider: str, scenario: str) -> dict: f" \n" ) - if scenario == "monitor": + if scenario == "monitor" or monitor_clock is not None: row = ( "- [ ] [P2-monitor] Observe the bounded source after its deadline.\n" f" \n" ) + if monitor_clock is not None: + from urllib.parse import quote + + due = quote((monitor_clock + timedelta(minutes=25)).isoformat(), safe="") + checked = quote((monitor_clock - timedelta(minutes=30)).isoformat(), safe="") + expires = quote((monitor_clock + timedelta(minutes=85)).isoformat(), safe="") + row = row.replace("2099-01-01T00%3A00%3A00Z", due).replace( + " -->", f" last_checked_at={checked} expires_at={expires} -->", + ) gate = ( "## User Todo\n\n- [ ] [P0-user] Approve the bounded delivery.\n" "