diff --git a/docs/reference/protocols/goal-vision-replan-contract-v0.md b/docs/reference/protocols/goal-vision-replan-contract-v0.md index baf92ac129..bab80994bb 100644 --- a/docs/reference/protocols/goal-vision-replan-contract-v0.md +++ b/docs/reference/protocols/goal-vision-replan-contract-v0.md @@ -1086,6 +1086,12 @@ in its durable-writeback step, the replan contract uses a same-response Different schemas remain separate, and progress-only replan does not acquire a Vision requirement. TurnEnvelope preserves the ordinary plan's guidance. +`checkpoint_context_not_missing` is an inapplicable recovery, not a request to +repeat that read. Its `reread_required` is false. Preserve the committed writeback +and quota receipt; inspect later authorized work through the normal guard with +a fresh Turn identity. A stale, missing or replaced read receipt still requires +the original scoped reread. Neither rejection grants execution or completion. + Shared `todo plan` and guided-start authoring steps also provide a routed exact readback command for each authored or reused Todo id. Inventory text is a bounded excerpt. Verify `todo_detail_projection.source_complete=true`, compare `.todo.text` @@ -1103,6 +1109,11 @@ Turn 写回后的恢复。规划与接续指引提供逐条精确读回命令, 需求和当前状态,不能拿列表摘要判断正文丢失。读取不授予执行权限;准入、租约、 恢复和单次记账规则保持不变。包体增量与模型效率、效果收益须分别测量。 +`checkpoint_context_not_missing` 表示这条恢复路径不适用,`reread_required=false`, +不应重复同一读取。保留已提交写回与记账回执;后续获授权的工作通过正常 guard 和新 +Turn 身份检查。陈旧、缺失或被替换的读取回执仍须按原范围重读。拒绝不授予执行或 +完成权限。 + A satisfied checkpoint is protocol-complete, but a material closeout also has to qualify its relationship to the final outcome. A patched checkpoint must name the active `acceptance_summary`, attach public-safe diff --git a/loopx/control_plane/goals/checkpoint_read_context.ts b/loopx/control_plane/goals/checkpoint_read_context.ts index 7455b5fd87..73ff2ed50b 100644 --- a/loopx/control_plane/goals/checkpoint_read_context.ts +++ b/loopx/control_plane/goals/checkpoint_read_context.ts @@ -77,8 +77,13 @@ function snapshot(request: JsonObject): JsonObject { } const rejected = (code: string, changed: string[] = []): JsonObject => ({ - ok: false, error_code: code, reread_required: true, changed_components: changed, - error: `${code}: run checkpoint-context for the same Goal/Agent/Todo or obligation/Turn, ` + + ok: false, error_code: code, reread_required: code !== "checkpoint_context_not_missing", changed_components: changed, + error: code === "checkpoint_context_not_missing" + ? `${code}: this committed writeback has no missing checkpoint for this recovery. ` + + "Preserve the committed writeback and quota receipt; do not repeat this recovery or quota spend. " + + "For later authorized work, inspect current state through the normal guard with a fresh Turn identity. " + + "This rejection grants no work or completion authority." + : `${code}: run checkpoint-context for the same Goal/Agent/Todo or obligation/Turn, ` + "read the returned state and judge again; submit its --checkpoint-read-context with only the vision decision. " + "Do not repeat implementation, state mutations, or quota spend.", }); diff --git a/tests/control_plane/test_checkpoint_read_context.py b/tests/control_plane/test_checkpoint_read_context.py index 3e21a89eec..dd101cd4f5 100644 --- a/tests/control_plane/test_checkpoint_read_context.py +++ b/tests/control_plane/test_checkpoint_read_context.py @@ -84,6 +84,11 @@ def test_missing_replaced_stale_context_requires_reread_and_preserves_delivery(t assert rc == 0 and result["appended"] and result["vision_checkpoint"]["satisfied"], result assert result["vision_checkpoint"]["read_context"]["read_context_id"] == fresh["read_context_id"] after = index.read_bytes() + rc, inapplicable = _run_cli(registry, runtime, "checkpoint-context", *binding, cwd=project) + assert rc == 1 and inapplicable["error_code"] == "checkpoint_context_not_missing", inapplicable + assert inapplicable["checkpoint_read_context"]["reread_required"] is False + assert "normal guard" in inapplicable["error"] + assert index.read_bytes() == after, "an inapplicable recovery must not append another receipt" state.write_text(state.read_text(encoding="utf-8") + "\n## Acceptance\n\nNew acceptance after commit.\n", encoding="utf-8") rc, replay = _run_cli(registry, runtime, *supplement, cwd=project) assert rc == 0 and replay["idempotent_replay"] and not replay["appended"], replay diff --git a/tests/control_plane_ts/checkpoint_read_context.test.ts b/tests/control_plane_ts/checkpoint_read_context.test.ts index da80428fd5..0e136c0796 100644 --- a/tests/control_plane_ts/checkpoint_read_context.test.ts +++ b/tests/control_plane_ts/checkpoint_read_context.test.ts @@ -31,6 +31,19 @@ test("read captures the exact task, transitive upstream results and goal accepta assert.equal(check(result.receipt).ok, true); }); +test("an inapplicable recovery does not direct the caller to repeat the same read", () => { + const result = read({prior: {vision_checkpoint: { + decision: "unchanged_with_reason", satisfied: true, delivery_boundary: "in_flight_continuation", + }}, writeback_committed: true, quota_spend_committed: true}); + assert.equal(result.ok, false); + assert.equal(result.error_code, "checkpoint_context_not_missing"); + assert.equal(result.reread_required, false); + assert.doesNotMatch(String(result.error), /run checkpoint-context/); + assert.match(String(result.error), /normal guard/); + const missing = check(undefined); + assert.equal(missing.reread_required, true, "a genuinely missing receipt still requires reread"); +}); + test("an unqualified blocked replan needs the exact committed source and no debit before reread", () => { const bound = settlementIdentity({goal_id: "goal", agent_id: "agent", turn_instance_id: "turn", replan_obligation_id: "replan-1111111111111111"});