diff --git a/docs/architecture/rfcs/automatic-execution-admission-v0.md b/docs/architecture/rfcs/automatic-execution-admission-v0.md index af70b0c69c..29fbdfe2f4 100644 --- a/docs/architecture/rfcs/automatic-execution-admission-v0.md +++ b/docs/architecture/rfcs/automatic-execution-admission-v0.md @@ -233,11 +233,12 @@ the interval. The local CLI remains a same-UID trust boundary. A managed start is two-phase in the same store: admission reserves the interval slot, and the Turn executor confirms that reservation only after the host attempt is durable in its journal. A crash between the two leaves the -reservation resumable by the same Turn identity once the floor is reached, so a -reserved-but-unstarted start never strands a Turn; a confirmed start stays -fail-closed for the same identity, and an explicit manual reason cannot bypass -that. A store record written without the phase field is read as an attempted -start, so an older or hand-edited file fails closed rather than resuming. +reservation immediately resumable by the same Turn identity without moving the +original interval anchor, so a reserved-but-unstarted start never strands a +Turn. A confirmed start stays fail-closed for the same identity, and an explicit +manual reason cannot bypass that. A store record written without the phase field +is read as an attempted start, so an older or hand-edited file fails closed +rather than resuming. The M3 settings companion presents the quota-owned Goal/agent/automation policy through one explicit Save backed by revision-locked preview, apply and readback, diff --git a/loopx/control_plane/quota/automation_cadence.ts b/loopx/control_plane/quota/automation_cadence.ts index e3e1a897ad..7985817da1 100644 --- a/loopx/control_plane/quota/automation_cadence.ts +++ b/loopx/control_plane/quota/automation_cadence.ts @@ -143,10 +143,8 @@ export async function admitAutomationStart(p: JsonObject): Promise { } if (held !== undefined) { // Same identity, no durable host attempt: keep the original interval anchor. - return manual === null && current.eligible_now !== true - ? {...current, admitted: false, reserved: false, reason: "minimum_interval_wait"} - : {...current, admitted: true, reserved: true, resumed: true, - reason: "resumed_unstarted_reservation"}; + return {...current, admitted: true, reserved: true, resumed: true, + reason: "resumed_unstarted_reservation"}; } if (manual === null && current.eligible_now !== true) { return {...current, admitted: false, reserved: false, reason: "minimum_interval_wait"}; diff --git a/tests/control_plane_ts/automation_cadence.test.ts b/tests/control_plane_ts/automation_cadence.test.ts index 7d1531241d..7c78bb8650 100644 --- a/tests/control_plane_ts/automation_cadence.test.ts +++ b/tests/control_plane_ts/automation_cadence.test.ts @@ -130,8 +130,8 @@ test("an unconfirmed reservation resumes while a confirmed start fails closed", await manage({...base, operation: "configure", expected_revision: 0, min_interval_minutes: 60, owner_reference: "owner-request", execute: true}); assert.equal((await start("turn:1", 1000)).reserved, true); - assert.equal((await start("turn:1", 1000 + 3_600_000 - 1)).reason, "minimum_interval_wait"); - const resumed = await start("turn:1", 1000 + 3_600_000); + assert.equal((await start("turn:2", 1001)).reason, "minimum_interval_wait"); + const resumed = await start("turn:1", 1001, {trigger_at_ms: 1000}); assert.equal(resumed.admitted, true); assert.equal(resumed.resumed, true); assert.equal(resumed.reason, "resumed_unstarted_reservation"); diff --git a/tests/test_loopx_turn_executor.py b/tests/test_loopx_turn_executor.py index 6c804a4c39..b7c7951b40 100644 --- a/tests/test_loopx_turn_executor.py +++ b/tests/test_loopx_turn_executor.py @@ -1322,7 +1322,7 @@ def die_after_reservation(identity: Mapping[str, object]) -> dict[str, object]: assert calls == {"host": 0, "writeback": 0, "spend": 0, "scheduler": 0} started_at_ms = int(_cadence_starts(runtime_root)[0]["started_at_ms"]) - monkeypatch.setattr(turn_cadence, "time", _FrozenTurnClock(started_at_ms + 120_000)) + monkeypatch.setattr(turn_cadence, "time", _FrozenTurnClock(started_at_ms + 1)) restart = _managed_cadence(runtime_root) recovered = run_loopx_turn_once( plan, admit_start=restart.admit, confirm_start=restart.confirm, **common @@ -1368,7 +1368,7 @@ def die_before_attempt_record(path: Path, journal: dict[str, object]) -> None: assert calls == {"host": 0, "writeback": 0, "spend": 0, "scheduler": 0} started_at_ms = int(_cadence_starts(runtime_root)[0]["started_at_ms"]) - monkeypatch.setattr(turn_cadence, "time", _FrozenTurnClock(started_at_ms + 120_000)) + monkeypatch.setattr(turn_cadence, "time", _FrozenTurnClock(started_at_ms + 1)) restart = _managed_cadence(runtime_root) recovered = run_loopx_turn_once( plan, admit_start=restart.admit, confirm_start=restart.confirm, **common