From 4033a75708d78e404919647ca22d7a37cd43e902 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sun, 18 Oct 2026 09:05:30 +0800 Subject: [PATCH 1/2] fix(quota): settle qualified blocked replans without debit Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/settlement_phase.ts | 33 +++++ .../quota/settlement_readback.ts | 21 +-- .../test_dated_vision_wait_cli.py | 121 ++++++++++++++++++ .../quota_settlement_readback.test.ts | 76 +++++++++++ 4 files changed, 242 insertions(+), 9 deletions(-) diff --git a/loopx/control_plane/quota/settlement_phase.ts b/loopx/control_plane/quota/settlement_phase.ts index 8f76fda23e..a3bb76c9db 100644 --- a/loopx/control_plane/quota/settlement_phase.ts +++ b/loopx/control_plane/quota/settlement_phase.ts @@ -1,6 +1,7 @@ import type { SettlementIdentity } from "../effect_program.ts"; import { jsonObject } from "../runtime_decode.ts"; import { isCausalBlockedWait, isMonitorUnavailableWait } from "./blocked_wait.ts"; +import { isTurnScopedSettlementOutcome } from "../work_items/delivery_outcome.ts"; /** Historical entry point for blocked no-spend qualification: an advancement * retry/causal wait or a canonical Monitor unavailable attempt. The latter @@ -20,6 +21,38 @@ export function isBoundedBlockedRetry(value: unknown, todoId: string | null): bo return Number.isFinite(delay) && delay >= 60 && delay <= 30 * 60; } +/** The replan owner has already qualified the blocker/path and its evidence. + * Verify that exact observation and semantic checkpoint, rather than inventing + * a Todo binding or retry clock for this administrative no-debit closeout. + * The caller must verify the guard and durable writeback receipts first. */ +export function isAcceptedBlockedReplanWriteback( + value: unknown, identity: SettlementIdentity, selectedObligation: unknown, +): boolean { + const run = jsonObject(value), ack = jsonObject(run?.autonomous_replan_ack); + const delta = jsonObject(ack?.semantic_delta), progress = jsonObject(run?.progress_observation); + const checkpoint = jsonObject(run?.vision_checkpoint); + const outcomes = delta?.outcomes, satisfying = delta?.satisfying_outcomes; + return identity.binding_kind === "autonomous_replan" && identity.replan_obligation_id !== null + && selectedObligation === identity.replan_obligation_id + && !!run && run.goal_id === identity.goal_id && run.agent_id === identity.agent_id + && run.turn_instance_id === identity.turn_instance_id && run.replan_obligation_id === identity.replan_obligation_id + && run.delivery_outcome === "outcome_gap" + && isTurnScopedSettlementOutcome(run.delivery_outcome, progress, identity.replan_obligation_id) + && ack?.schema_version === "autonomous_replan_ack_v0" && ack.recorded === true + && delta?.schema_version === "replan_semantic_delta_v0" && delta.accepted === true + && delta.obligation_id === identity.replan_obligation_id + && typeof progress?.fingerprint === "string" && progress.fingerprint.length > 0 + && delta.observation_fingerprint === progress.fingerprint + && Array.isArray(outcomes) && Array.isArray(satisfying) && satisfying.some(outcome => + outcomes.includes(outcome) && (outcome === "new_concrete_blocker" || outcome === "fresh_vision_path_outcome")) + && checkpoint?.schema_version === "vision_checkpoint_v0" && checkpoint.agent_id === identity.agent_id + && checkpoint.satisfied === true && checkpoint.delivery_boundary === "semantic_closeout" + && Array.isArray(checkpoint.triggers) && checkpoint.triggers.some(value => { + const trigger = jsonObject(value); + return trigger?.kind === "material_delivery_outcome" && trigger.delivery_outcome === "outcome_gap"; + }); +} + /** A service-qualified capability duty can retire its exact admitted Turn. * The caller first verifies the durable writeback receipt. No Task/Goal * completion, progress or debit follows from this lifecycle evidence. */ diff --git a/loopx/control_plane/quota/settlement_readback.ts b/loopx/control_plane/quota/settlement_readback.ts index ac96650c23..fb57356502 100644 --- a/loopx/control_plane/quota/settlement_readback.ts +++ b/loopx/control_plane/quota/settlement_readback.ts @@ -33,6 +33,7 @@ import { } from "./settlement_workspace_causality.ts"; import { isBoundedBlockedRetry, + isAcceptedBlockedReplanWriteback, isCommittedMonitorPollEffect, isAcceptedProgressWriteback, isAcceptedReplanWriteback, @@ -1198,21 +1199,23 @@ function readQuotaSettlementFromRequest( const writeback = writebackResult(identity, writebackRun, writebackEvent); const spend = spendResult(identity, spendRun, spendEvent); + const semanticReplanGuard = projectSemanticReplanGuard(receiptDetails); // The exact Turn-bound blocked writeback is itself a durable no-spend // closeout. It cannot certify Todo completion or become delivery progress. // A spend already committed for this identity remains an ordinary spend // settlement, so readback never erases a historical debit. const blockedNoSpend = writeback.failure === null && spendRun === null && spendEvent === null && - identity.binding_kind === "todo" && - writebackRun !== null && writebackRun.delivery_outcome === "outcome_gap" && - isBoundedBlockedRetry(writebackRun.blocked_retry, identity.todo_id) && - isTurnScopedSettlementOutcome( - writebackRun.delivery_outcome, - writebackRun.progress_observation, - identity.todo_id, - ); - const semanticReplanGuard = projectSemanticReplanGuard(receiptDetails); + ((identity.binding_kind === "todo" && + writebackRun !== null && writebackRun.delivery_outcome === "outcome_gap" && + isBoundedBlockedRetry(writebackRun.blocked_retry, identity.todo_id) && + isTurnScopedSettlementOutcome( + writebackRun.delivery_outcome, + writebackRun.progress_observation, + identity.todo_id, + )) || isAcceptedBlockedReplanWriteback( + writebackRun, identity, semanticReplanGuard.selected_obligation_id, + )); const retiredNoSpend = writeback.failure === null && spendRun === null && spendEvent === null && isCapabilityRetirementWriteback(writebackRun, identity, semanticReplanGuard.selected_capability_guard); diff --git a/tests/control_plane/test_dated_vision_wait_cli.py b/tests/control_plane/test_dated_vision_wait_cli.py index 2d54441e40..a409817c03 100644 --- a/tests/control_plane/test_dated_vision_wait_cli.py +++ b/tests/control_plane/test_dated_vision_wait_cli.py @@ -209,3 +209,124 @@ def test_bound_monitor_keeps_exact_turn_and_agent_fences(tmp_path, isolated_effe assert rc == 0, after assert after["effective_action"] != "unsettled_host_turn_recovery" assert after["interaction_contract"]["agent_channel"]["must_attempt"] is False + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_blocked_replan_closes_without_debit_and_retains_scheduled_work( + tmp_path, isolated_effect, monkeypatch, provider, +): + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + + registry, runtime, state = fixture(tmp_path) + with state.open("a", encoding="utf-8") as output: + output.write( + "\n- [ ] [P1-monitor] Observe the existing public source.\n" + f" \n" + ) + if provider == "sqlite": + monkeypatch.setenv("NODE_OPTIONS", os.environ.get("NODE_OPTIONS", "") + " --experimental-sqlite") + rc, before = cli(registry, runtime, "todo", "list", "--goal-id", GOAL) + assert rc == 0, before + initialize_canonical_authority( + runtime, GOAL, + build_todo_runtime_shadow_projection(goal_id=GOAL, handoff_mode="soft_claim", todos=before["todos"]), + state_path=state, provider=provider, + ) + turn = "blocked-source-review" + rc, guard = cli(registry, runtime, "quota", "should-run", "--codex-app", "--goal-id", GOAL, + "--agent-id", AGENT, "--turn-instance-id", turn) + assert rc == 0, guard + assert guard["effective_action"] == "autonomous_replan_required" + identity = guard["heartbeat_receipt"]["settlement_identity"] + obligation = identity["replan_obligation_id"] + assert identity["binding_kind"] == "autonomous_replan" + assert "todo_id" not in identity + vision = { + "schema_version": "goal_vision_replan_contract_v0", "state": "vision_patch_proposed", + "todo_delta": [f"retain:{TODO}"], + "vision_patch": {"acceptance_summary": "The scheduled delivery remains unqualified.", + "advancement_policy": "as_needed", + "replan_trigger_summary": "Retain the pending scheduled work and original acceptance."}, + "path_delta": {"outcome": "continue", "prior_assumption": "The source streak needs review.", + "observed_reality": "The canonical delivery still waits at its original deadline.", + "retained": ["The original unfinished delivery and source observation."], + "evidence_refs": ["evidence:canonical-scheduled-work"]}, + } + control = guard["replan_action_packet"].get("stride_control") + if control: + vision["stride_decision"] = { + "schema_version": "stride_decision_v2", "strategy_version": "evidence_conditioned_v1", + "reference_context_id": control["reference_context_id"], + "reference_obligation_id": obligation, "basis": "uncertain_evidence", "action": "verify", + "axes": {axis: {"plan": "Verify the existing delivery when its source is ready.", + "reason": "A pending dependency does not justify duplicate execution."} + for axis in ("work_slice", "route_distance", "validation_scope")}, + "next_probe": {"scope": "The original scheduled delivery.", + "prediction": "Resume only after its original condition is ready.", + "adoption_criterion": "Current source and original acceptance qualify execution.", + "cost_bound": "No source poll or repeated work before the deadline.", + "minimum_evidence": "Canonical resume readback and applicable source evidence."}, + } + packet = tmp_path / "review.json" + packet.write_text(json.dumps(vision), encoding="utf-8") + refresh_args = ( + "refresh-state", "--goal-id", GOAL, "--agent-id", AGENT, + "--replan-obligation-id", obligation, "--turn-instance-id", turn, + "--classification", "blocked_source_review", "--progress-scope", "agent_lane", + "--delivery-batch-scale", "single_surface", "--delivery-boundary", "semantic_closeout", + "--delivery-outcome", "outcome_gap", "--progress-result-class", "blocked", + "--progress-blocker-id", "source-not-ready", "--progress-evidence-id", "evidence:canonical-scheduled-work", + "--agent-vision-json", str(packet), + ) + unchanged_state = state.read_bytes() + for flag, value in (("--turn-instance-id", "unadmitted-turn"), ("--agent-id", "foreign-agent")): + wrong = list(refresh_args) + wrong[wrong.index(flag) + 1] = value + rc, refused = cli(registry, runtime, *wrong) + assert rc == 1, refused + assert refused["ok"] is False and not refused.get("appended"), refused + assert state.read_bytes() == unchanged_state + rc, refreshed = cli(registry, runtime, *refresh_args) + assert rc == 0, refreshed + assert refreshed["settlement_identity"] == identity + assert refreshed["settlement_progress"]["state"] == "settled", refreshed["settlement_progress"] + assert refreshed["settlement_progress"]["closeout_kind"] == "typed_blocked_writeback_no_spend" + rc, replay = cli(registry, runtime, *refresh_args) + assert rc == 0 and replay["appended"] is False, replay + assert replay["settlement_progress"]["state"] == "settled" + rc, spent = cli(registry, runtime, "quota", "spend-slot", "--execute", "--goal-id", GOAL, + "--agent-id", AGENT, "--replan-obligation-id", obligation, "--turn-instance-id", turn) + assert rc == 0 and spent["appended"] is False, spent + rc, settled = cli(registry, runtime, "quota", "should-run", "--codex-app", "--goal-id", GOAL, + "--agent-id", AGENT, "--turn-instance-id", turn) + assert rc == 0 and settled["effective_action"] == "heartbeat_settled_skip", settled + rc, later = cli(registry, runtime, "quota", "should-run", "--codex-app", "--goal-id", GOAL, + "--agent-id", AGENT, "--turn-instance-id", "after-blocked-review") + assert rc == 0, later + assert later["interaction_contract"]["agent_channel"]["must_attempt"] is False + rc, after = cli(registry, runtime, "todo", "list", "--goal-id", GOAL) + assert rc == 0, after + for original in before["todos"]: + current = next(row for row in after["todos"] if row["todo_id"] == original["todo_id"]) + for field in ("status", "text", "claimed_by", "resume_when", "consecutive_no_change", + "result_hash", "material_change_generation", "next_due_at"): + assert current.get(field) == original.get(field) + runs = [json.loads(line) for line in (runtime / f"goals/{GOAL}/runs/index.jsonl").read_text().splitlines()] + assert not any(run.get("quota_event") for run in runs) + # A quiet guard may record host liveness under a historical monitor-run + # classification. Only a committed poll is an observation of the source. + events = [json.loads(line) for line in (runtime / f"goals/{GOAL}/rollout-event-log.jsonl").read_text().splitlines()] + assert not any(event.get("event_kind") in ("quota_monitor_poll", "quota_spend") for event in events) + rc, added = cli(registry, runtime, "todo", "add", "--goal-id", GOAL, "--role", "agent", + "--claimed-by", AGENT, "--text", "Validate independent public evidence.", + "--task-class", "advancement_task", "--action-kind", "validate", "--execute") + assert rc == 0 and added["added"] is True, added + rc, independent = cli(registry, runtime, "quota", "should-run", "--codex-app", "--goal-id", GOAL, + "--agent-id", AGENT, "--turn-instance-id", "independent-after-review") + assert rc == 0, independent + assert independent["selected_todo"]["todo_id"] == added["todo_id"] + assert independent["interaction_contract"]["agent_channel"]["must_attempt"] is True diff --git a/tests/control_plane_ts/quota_settlement_readback.test.ts b/tests/control_plane_ts/quota_settlement_readback.test.ts index 4615ab113a..be883e1ffa 100644 --- a/tests/control_plane_ts/quota_settlement_readback.test.ts +++ b/tests/control_plane_ts/quota_settlement_readback.test.ts @@ -146,6 +146,82 @@ test("capability retirement verifies its exact receipt and never erases a commit } finally {await rm(root, {recursive: true, force: true});} }); +test("a service-qualified blocked replan closes only its exact Turn without debit or completion", async () => { + const obligation = "replan-0000000000000001"; + const bound = settlementIdentity({goal_id: goalId, agent_id: agentId, todo_id: null, + replan_obligation_id: obligation, turn_instance_id: turnId}); + const progress = {schema_version: "typed_progress_observation_v0", work_item_id: obligation, + result_class: "blocked", blocker_id: "source-not-ready", evidence_ids: ["evidence:source-wait"], + fingerprint: "blocked-source-wait"}; + const semantic = {schema_version: "replan_semantic_delta_v0", accepted: true, + obligation_id: obligation, observation_fingerprint: progress.fingerprint, + outcomes: ["new_concrete_blocker", "fresh_vision_path_outcome"], + satisfying_outcomes: ["new_concrete_blocker"]}; + const ack = {schema_version: "autonomous_replan_ack_v0", recorded: true, semantic_delta: semantic}; + const checkpoint = {schema_version: "vision_checkpoint_v0", agent_id: agentId, satisfied: true, + delivery_boundary: "semantic_closeout", decision: "patched", + triggers: [{kind: "material_delivery_outcome", delivery_outcome: "outcome_gap"}]}; + const root = await fixture({writeback: true}); + try { + const index = join(root, "goals", goalId, "runs", "index.jsonl"); + const log = join(root, "goals", goalId, "rollout-event-log.jsonl"); + const events = (await readFile(log, "utf8")).trim().split("\n").map(line => JSON.parse(line)); + for (const event of events) { + event.details = {...event.details, todo_id: "", replan_obligation_id: obligation, + settlement_effect_id: bound.effect_id}; + if (event.event_kind === "quota_should_run") event.details.semantic_replan_obligation_id = obligation; + } + await writeFile(log, events.map(event => JSON.stringify(event)).join("\n") + "\n"); + const base = {classification: "state_refreshed", goal_id: goalId, agent_id: agentId, + turn_instance_id: turnId, replan_obligation_id: obligation, settlement_identity: bound, + delivery_outcome: "outcome_gap", progress_observation: progress, + autonomous_replan_ack: ack, vision_checkpoint: checkpoint}; + const read = () => readQuotaSettlement(request(root, {todo_id: null, replan_obligation_id: obligation})); + await writeFile(index, JSON.stringify(base) + "\n"); + const accepted = await read(); + assert.equal(accepted.replay_phase, "settled"); + assert.equal((accepted.progress as any).closeout_kind, "typed_blocked_writeback_no_spend"); + assert.equal((accepted.spend as any).payload.ok, false); + assert.equal((accepted.terminal_closeout as any).payload.ok, false); + await writeFile(index, JSON.stringify({...base, autonomous_replan_ack: {...ack, + semantic_delta: {...semantic, satisfying_outcomes: ["fresh_vision_path_outcome"]}}}) + "\n"); + assert.equal((await read()).replay_phase, "settled"); + const invalid = [ + {...base, delivery_outcome: "outcome_progress"}, + {...base, agent_id: "other-agent"}, + {...base, turn_instance_id: "other-turn"}, + {...base, autonomous_replan_ack: {...ack, recorded: false}}, + ...[{accepted: false}, {obligation_id: "replan-0000000000000002"}, + {observation_fingerprint: "different-observation"}, {satisfying_outcomes: []}, + {outcomes: []}, + {satisfying_outcomes: ["new_runnable_successor"]}].map(patch => ({...base, + autonomous_replan_ack: {...ack, semantic_delta: {...semantic, ...patch}}})), + ...[{work_item_id: "todo_other"}, {result_class: "advanced"}, {evidence_ids: []}, + {blocker_id: null}].map(patch => ({...base, progress_observation: {...progress, ...patch}})), + ...[{satisfied: false}, {agent_id: "other-agent"}, {delivery_boundary: "in_flight_continuation"}, + {triggers: []}].map(patch => ({...base, vision_checkpoint: {...checkpoint, ...patch}})), + ]; + for (const changed of invalid) { + await writeFile(index, JSON.stringify(changed) + "\n"); + assert.notEqual((await read()).replay_phase, "settled"); + } + await writeFile(index, JSON.stringify(base) + "\n"); + await writeFile(log, JSON.stringify(events[0]) + "\n"); + assert.notEqual((await read()).replay_phase, "settled"); + await writeFile(log, events.map(event => JSON.stringify(event)).join("\n") + "\n"); + await appendFile(index, JSON.stringify({classification: "quota_slot_spent", goal_id: goalId, + agent_id: agentId, turn_instance_id: turnId, replan_obligation_id: obligation, + settlement_identity: bound}) + "\n"); + await appendFile(log, JSON.stringify({schema_version: "loopx_rollout_event_v0", event_id: "already-paid", + event_kind: "quota_spend", goal_id: goalId, agent_id: agentId, run_id: turnId, + details: {settlement_effect_id: bound.effect_id}}) + "\n"); + const paid = await read(); + assert.equal(paid.replay_phase, "settled"); + assert.equal((paid.progress as any).closeout_kind, undefined); + assert.equal((paid.spend as any).payload.ok, true); + } finally {await rm(root, {recursive: true, force: true});} +}); + async function fixture(options: { guard?: boolean; /** From e88f37b0cdbaafb89d206edd4a7a77e83429a46a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sun, 18 Oct 2026 09:05:44 +0800 Subject: [PATCH 2/2] docs(quota): explain blocked replan closeout and rollback Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/quota-allocation.md | 15 +++++- .../quota-blocked-causal-closeout-v0.md | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/docs/quota-allocation.md b/docs/quota-allocation.md index f6d780f734..bf87c5590c 100644 --- a/docs/quota-allocation.md +++ b/docs/quota-allocation.md @@ -212,6 +212,19 @@ instead sets `closeout_kind=typed_blocked_writeback_no_spend` and settles the Turn without a quota debit. Todo completion and Goal acceptance retain their separate checks in both cases. +An autonomous-replan binding can also close without debit after its existing +replan owner accepts a concrete blocker or evidence-linked path disposition. +Readback must match the original selected obligation, Goal/Agent/Turn, typed +blocked observation and fingerprint, accepted semantic delta, satisfied semantic +checkpoint, and durable receipt. It uses the same +`typed_blocked_writeback_no_spend` result; it does not invent a Todo binding or +retry timer. A later spend request for that exact closed Turn is a no-op. +This corrects the prior spend-required result for a qualified blocked replan. +Canonical waits, source observations, ordinary outcome/checkpoint gates and +selection of independent work remain unchanged. A previously committed debit +is retained, and neither a bare blocker declaration nor an unqualified ACK +closes the Turn. + An accepted `vision_checkpoint_v0` for `outcome_progress` also closes its exact Todo-bound Turn after the matching writeback and spend receipts commit. Both `in_flight_continuation` and `semantic_closeout` use their corresponding typed @@ -440,7 +453,7 @@ eligible for exact Todo-bound Turn settlement only when it includes a `evidence_ids`. Missing schemas, prose-only blockers, malformed evidence, and Todo identity mismatches remain fail-closed. -New Turn-bound blocked writebacks also require a bounded retry on the same +New Todo-bound blocked writebacks also require a bounded retry on the same unfinished advancement Todo. A legacy Todo must have a pending `resume_when=resume_at:` due in 1–30 minutes. With promoted File/SQLite authority, an open Todo without its own resume condition can use diff --git a/docs/reference/protocols/quota-blocked-causal-closeout-v0.md b/docs/reference/protocols/quota-blocked-causal-closeout-v0.md index 118addd1cd..5cedc8fe5f 100644 --- a/docs/reference/protocols/quota-blocked-causal-closeout-v0.md +++ b/docs/reference/protocols/quota-blocked-causal-closeout-v0.md @@ -46,6 +46,35 @@ CLI/provider acceptance checks both dependency kinds, replay, no debit, original validator preservation and independent next-Turn selection. It does not claim live research adoption or PostgreSQL qualification. +### Admitted autonomous-replan closeout + +An admitted autonomous-replan Turn has an obligation binding rather than a Todo +binding. Its existing replan owner can accept a concrete blocker or a fresh +evidence-linked path while the underlying work remains unqualified. Previously, +the verified blocked writeback still required a quota spend because the +no-spend readback recognized only Todo-bound attempts. + +The same TypeScript settlement owner now recognizes that qualified blocked +replan. The original guard's selected obligation, exact Goal/Agent/Turn, typed +blocker/evidence and observation fingerprint, accepted semantic delta and +satisfied semantic-closeout checkpoint must agree with the durable writeback. +The existing `typed_blocked_writeback_no_spend` result closes only that Turn. +No Todo, wait clock, source observation, delivery credit or completion is +created. Exact replay and a later spend request cannot add a debit; an already +committed debit remains intact. Capability retirement keeps its separate guard +and proof requirements. + +This is settlement qualification, not a new wait or acceptance policy. Current +canonical resume conditions still decide when work becomes runnable; independent +work and genuine uncovered gaps retain ordinary admission. A blocker string, +stale or rejected semantic ACK, mismatched fingerprint, missing checkpoint or +missing durable receipt cannot qualify. Existing source-qualified historical +writebacks can be read using their original receipts without rewriting history. +Older runtimes may still request a spend for this binding; reconcile it with a +compatible runtime before rollback rather than paying or rewriting the receipt. +CLI and managed heartbeat consume the same settlement result; no frontend +control, Lark authority, configuration or Python decision owner is added. + ### Registering a source-satisfied successor External-wait authoring recomputes a successor's `todo_done` or @@ -175,6 +204,26 @@ Dashboard 已消费 canonical 等待与回执,Chat/Lark 仍复用 Todo updat 验收覆盖两种依赖、重放、零扣额、原验收器保留与下一 Turn 独立选择;不据此 宣称投研真实采用或 PostgreSQL 资格已通过。 +### 已准入的自主重规划收尾 + +已准入的 autonomous-replan Turn 绑定 obligation 而非 Todo。既有 replan owner +可能在底层工作仍未满足验收时,接受具体 blocker 或带证据的新路径决策。过去虽然 +阻塞写回及收据已核验,no-spend 读回只识别 Todo 绑定,仍会要求扣额。 + +现有 TypeScript settlement owner 现在识别这种已合格的阻塞重规划:原 guard 选定的 +obligation、精确 Goal/Agent/Turn、typed blocker/evidence 及观察 fingerprint、已接受 +semantic delta、满足的 semantic-closeout checkpoint 与持久收据必须一致。沿用 +`typed_blocked_writeback_no_spend`,只关闭本次 Turn,不生成 Todo、等待时钟、来源 +观察、交付信用或完成声明。精确重放及后续 spend 请求均不追加扣额;已有真实扣额 +保留,capability retirement 仍需其独立 guard 和证明。 + +这修正结算资格,不改变等待或验收规则。canonical 恢复条件仍决定何时可执行,独立 +工作和真正未覆盖的缺口保持严格准入。单独 blocker 文本、过期或被拒绝的 semantic +ACK、错配 fingerprint、缺 checkpoint 或持久收据均不合格。已有合格历史写回可按 +原收据读回,不改历史。旧运行时可能仍要求这类绑定扣额;回滚前须用兼容运行时 +核对,不得为旧提示补扣或改写收据。CLI 与 managed heartbeat 消费同一结果,不新增前端控制、 +Lark 权限、配置或 Python 决策源。 + ### 登记源证据已满足的后继 external-wait authoring 复用既有 typed resume evaluator,从同一份锁定的 Todo