From c1dea898b62f5d90233d4b87b68ac5e3cb4b37fa Mon Sep 17 00:00:00 2001 From: Duang777 Date: Thu, 8 Oct 2026 22:41:37 +0800 Subject: [PATCH 1/2] fix(control-plane): resume unstarted cadence reservations Signed-off-by: Duang777 --- loopx/control_plane/quota/automation_cadence.ts | 6 ++---- tests/control_plane_ts/automation_cadence.test.ts | 4 ++-- tests/test_loopx_turn_executor.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) 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 From 8ad09a739d9784051c98e09f860975f0d8a0a8a1 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Sat, 10 Oct 2026 11:41:39 +0800 Subject: [PATCH 2/2] test(control-plane): close cadence recovery review gaps Filter crash-surviving lock holder sidecars from the no-journal assertion and sync the Chinese RFC with immediate same-identity recovery at the original interval anchor. Signed-off-by: Duang777 --- .../rfcs/automatic-execution-admission-v0.zh-CN.md | 6 +++--- tests/test_loopx_turn_executor.py | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/architecture/rfcs/automatic-execution-admission-v0.zh-CN.md b/docs/architecture/rfcs/automatic-execution-admission-v0.zh-CN.md index ad37240ae1..d345f4aa1c 100644 --- a/docs/architecture/rfcs/automatic-execution-admission-v0.zh-CN.md +++ b/docs/architecture/rfcs/automatic-execution-admission-v0.zh-CN.md @@ -172,9 +172,9 @@ M1 对 App 调度管理有独立价值,但不代表多宿主产品旅程完成 本地 CLI 仍以相同 OS 用户为信任边界。 managed 启动在同一 store 内分两步:准入预留间隔位,Turn executor 只在该 host 尝试 -已写入 Turn journal 之后确认该预留。两步之间进程退出时,同一 Turn 身份在满足时间 -下限后仍可恢复,因此"已预留但未启动"不会永久卡住 Turn;已确认的启动对同一身份保持 -fail-closed,显式手动理由也无法绕过。缺少阶段字段的旧记录按"已尝试启动"读取, +已写入 Turn journal 之后确认该预留。两步之间进程退出时,同一 Turn 身份可立即恢复 +该预留,且不会移动原间隔锚点,因此"已预留但未启动"不会永久卡住 Turn。已确认的启动 +对同一身份保持 fail-closed,显式手动理由也无法绕过。缺少阶段字段的旧记录按"已尝试启动"读取, 旧版或手工改写的文件因此 fail-closed,而不是被当作可恢复预留。 M3 设置页阶段成果复用 quota 权威,一次明确的保存在内部完成修订号锁定的预览、应用与读回, diff --git a/tests/test_loopx_turn_executor.py b/tests/test_loopx_turn_executor.py index b7c7951b40..0830d6a68e 100644 --- a/tests/test_loopx_turn_executor.py +++ b/tests/test_loopx_turn_executor.py @@ -1318,7 +1318,11 @@ def die_after_reservation(identity: Mapping[str, object]) -> dict[str, object]: assert [ (row["state"], row["request_id"]) for row in _cadence_starts(runtime_root) ] == [("reserved", f"{turn_key}:1")] - assert not list((runtime_root / "goals" / "fixture-goal" / "turns").glob("*.json")) + assert not [ + path + for path in (runtime_root / "goals" / "fixture-goal" / "turns").glob("*.json") + if not path.name.endswith(".lock.holder.json") + ] assert calls == {"host": 0, "writeback": 0, "spend": 0, "scheduler": 0} started_at_ms = int(_cadence_starts(runtime_root)[0]["started_at_ms"])