Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions loopx/cli_commands/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,13 @@ def handle_todo_command(
payload = todo_error_payload(
args, exc, registry_path=registry_path, runtime_root_arg=runtime_root_arg,
)
if (
args.todo_command == "complete"
and payload.get("ok")
and settlement_identity is None
and isinstance(payload.get("settlement_identity"), dict)
):
settlement_identity = SettlementIdentity.from_runtime_payload(payload["settlement_identity"])
append_todo_rollout_event(
payload,
args=args,
Expand All @@ -802,8 +809,8 @@ def handle_todo_command(

if (
args.todo_command == "complete"
and getattr(args, "turn_instance_id", None)
and payload.get("ok")
and settlement_identity is not None
and not payload.get("dry_run")
):
runtime_root = resolve_runtime_root(
Expand All @@ -815,7 +822,7 @@ def handle_todo_command(
goal_id=args.goal_id,
agent_id=args.agent_id,
todo_id=args.todo_id,
turn_instance_id=getattr(args, "turn_instance_id", None),
turn_instance_id=settlement_identity.turn_instance_id,
registry_path=registry_path,
goal_ref=goal_ref,
)
Expand Down Expand Up @@ -859,6 +866,7 @@ def handle_todo_command(
and not payload.get("dry_run")
and post_writeback_hooks
and settlement_identity is not None
and not getattr(args, "completion_identity_key", None)
):
identity = settlement_identity.as_dict()
committed_at = str(payload.get("updated_at") or "").strip()
Expand Down
5 changes: 5 additions & 0 deletions loopx/cli_commands/todo_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ def append_todo_rollout_event(
append_cli_rollout_event: RolloutEventAppender,
) -> None:
turn_instance_id = getattr(args, "turn_instance_id", None)
identity = payload.get("settlement_identity")
if turn_instance_id is None and isinstance(identity, Mapping):
# Lifecycle reentry carries the TS-verified historical binding; the
# opaque completion key is not a Turn id and must never be parsed.
turn_instance_id = identity.get("turn_instance_id")
terminal_closeout = bool(
turn_instance_id
and args.todo_command == "complete"
Expand Down
32 changes: 26 additions & 6 deletions loopx/control_plane/coordination/todo_terminal_lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,9 @@ function terminalRequestSha(input: CoordinationTodoTerminalLifecycleInput): stri
validation_declaration_sha256: input.user_update !== undefined ? null :
input.validation_declaration_sha256 ?? (input.validation_declaration === null
? null : canonicalAuthoritySha256(input.validation_declaration)),
dry_run: input.dry_run,
// Preview reads the same intent as a committed command. Only non-preview
// operations can persist receipts, whose historical fingerprint is false.
dry_run: false,
});
}

Expand Down Expand Up @@ -1200,7 +1202,7 @@ export async function executeCoordinationTodoTerminalLifecycle(
rawInput: CoordinationTodoTerminalLifecycleInput,
authoritySourcesCurrent: AuthoritySourceCheck = uncheckedAuthoritySource,
terminalEvidenceGuard: TodoTerminalEvidenceGuard | null = null,
verifyLifecycleSettlement?: (input: CoordinationTodoTerminalLifecycleInput) => Promise<void>,
verifyLifecycleSettlement?: (input: CoordinationTodoTerminalLifecycleInput) => Promise<JsonObject>,
): Promise<CoordinationTodoTerminalLifecycleResult> {
let normalized: CoordinationTodoTerminalLifecycleInput;
try {
Expand Down Expand Up @@ -1230,7 +1232,21 @@ export async function executeCoordinationTodoTerminalLifecycle(
// Named operation recovery reports history, not present execution authority.
const receipt = terminalReceipt(input, requestSha);
const replay = await receipt.read(store);
if (replay !== null) return replay;
if (replay !== null) {
// Older accepted lifecycle receipts lack the original quota binding.
// Add it to this response from verified history; never rewrite the receipt.
if (replay.status === "replayed" && isLifecycleCloseout(input) &&
requiresReentrySettlement(input) && replay.settlement_identity == null &&
verifyLifecycleSettlement !== undefined) {
try {
return {...replay, settlement_identity: await verifyLifecycleSettlement(input)};
} catch (error) {
return terminalFailure("invalid_todo_completion_transaction",
error instanceof Error ? error.message : "Original terminal settlement unavailable", {}, "decision_rejection");
}
}
return replay;
}
if (normalized.operation_identity.kind === "completion_turn" && normalized.requested_no_followup) {
// Older adapters used the ordinary id for an already accepted closeout.
// Recover that exact historical request before reading current authority.
Expand Down Expand Up @@ -1433,12 +1449,13 @@ export async function executeCoordinationTodoTerminalLifecycle(
}
if (isLifecycleCloseout(input)) {
let closed: JsonObject | null;
let settlement: JsonObject | null = null;
try {
closed = prepareTerminalReentry(todo, input, activeLease(projection.leases.get(input.todo_id), input.now));
if (closed !== null && requiresReentrySettlement(input)) {
if (requiresReentrySettlement(input)) {
if (verifyLifecycleSettlement === undefined) throw new AuthorityStoreProtocolError(
"Lifecycle closeout requires original quota settlement verification");
await verifyLifecycleSettlement(input);
settlement = await verifyLifecycleSettlement(input);
}
} catch (error) {
return terminalFailure("invalid_todo_completion_transaction",
Expand All @@ -1457,6 +1474,7 @@ export async function executeCoordinationTodoTerminalLifecycle(
completion_recovery: target.completion_recovery ?? null,
completion_receipt_id: target.completion_receipt_id ?? null,
completed_at: target.completed_at ?? null,
...(settlement === null ? {} : {settlement_identity: settlement}),
}, closed === null ? [] : [{kind: "todo_upsert", todo: closed}], authoritySourcesCurrent);
}
let untypedCompletionRepair: UntypedCompletionRepairPlan | null;
Expand Down Expand Up @@ -1671,7 +1689,9 @@ export async function executeCoordinationTodoTerminalLifecycle(
requested_has_successor:
input.successor_intents.length > 0 || input.linked_successor_todo_ids.length > 0,
dry_run: input.dry_run,
validation_receipt: implicitMonitorNoChange ? null : terminalUpgradeReceipt !== null
// Preview never declares a validation effect. The parent receipt above
// already proves the original check; retain it in the returned result.
validation_receipt: input.dry_run || implicitMonitorNoChange ? null : terminalUpgradeReceipt !== null
? terminalUpgradeReceipt.validation_receipt : input.validation_receipt,
// Link ids are caller intent; role/status are facts owned by this head.
// Receipt recovery above preserves the originally accepted policy.
Expand Down
6 changes: 4 additions & 2 deletions loopx/control_plane/coordination/todo_terminal_reentry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {settlementIdentity, type JsonObject} from "../effect_program.ts";
import {settlementIdentity, settlementIdentityPayload, type JsonObject} from "../effect_program.ts";
import {jsonObject} from "../runtime_decode.ts";
import {AuthorityStoreProtocolError, canonicalAuthoritySha256} from "./authority_store_codec.ts";
import type {CoordinationTodoTerminalLifecycleInput} from "./todo_terminal_lifecycle.ts";
Expand All @@ -19,7 +19,7 @@ export function requiresReentrySettlement(input: CoordinationTodoTerminalLifecyc
/** Resolve historical quota identity from its durable receipt, never by parsing
* an opaque completion key or treating missing history as unscoped work. */
export async function verifyReentrySettlement(runtimeRoot: string,
input: CoordinationTodoTerminalLifecycleInput): Promise<void> {
input: CoordinationTodoTerminalLifecycleInput): Promise<JsonObject> {
const {readQuotaSettlementSnapshot, readQuotaSettlementFromSnapshot,
QUOTA_SETTLEMENT_READBACK_REQUEST_SCHEMA} = await import("../quota/settlement_readback.ts");
const snapshot = await readQuotaSettlementSnapshot(runtimeRoot, input.goal_id);
Expand All @@ -43,6 +43,8 @@ export async function verifyReentrySettlement(runtimeRoot: string,
if (jsonObject(jsonObject(readback.settlement)?.payload)?.ok !== true) {
throw new AuthorityStoreProtocolError("Lifecycle closeout requires matching writeback and quota spend receipts");
}
return settlementIdentityPayload({goal_id: input.goal_id, agent_id: input.actor_agent_id,
todo_id: input.todo_id, turn_instance_id: turn});
}

/** A migrated completion is already authoritative. Close only its continuation;
Expand Down
21 changes: 17 additions & 4 deletions tests/control_plane/test_declared_terminal_settlement_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,17 +193,30 @@ def test_declared_leased_completion_writeback_spend_terminal_and_replay(
"todo", "complete", "--goal-id", cli.GOAL_ID, "--todo-id", cli.TODO_ID,
"--agent-id", cli.AGENT_ID, "--completion-identity-key", completed["completion_identity_key"],
"--no-follow-up")
code, before_preview = run("todo", "list", "--goal-id", cli.GOAL_ID)
assert code == 0, before_preview
code, preview = run(*terminal_args, "--dry-run")
assert code == 0, json.dumps(preview, ensure_ascii=False, indent=2)
assert preview["dry_run"] is True
code, after_preview = run("todo", "list", "--goal-id", cli.GOAL_ID)
assert code == 0, after_preview
assert after_preview["todos"] == before_preview["todos"]
assert marker.read_text() == "run\n"
assert cli._spend_run_count(runtime) == 1
code, terminal = run(*terminal_args)
assert code == 0, terminal.get("error") or terminal.get("reason") or terminal
assert terminal["completion_continuation"] == "no_followup"
assert terminal["completion_recovery"] == (
"same_turn_terminal_closeout" if closeout_source == "turn" else "lifecycle_reentry_terminal_closeout")
assert terminal["changed"] is True
assert terminal["idempotent_replay"] is False
if closeout_source == "turn":
assert [r["step_kind"] for r in terminal["settlement_result"]["receipts"]] == [
"validation", "durable_writeback", "quota_spend", "terminal_closeout",
]
assert terminal["settlement_identity"] == completed["settlement_identity"]
assert [r["step_kind"] for r in terminal["settlement_result"]["receipts"]] == [
"validation", "durable_writeback", "quota_spend", "terminal_closeout",
]
code, preview_replay = run(*terminal_args, "--dry-run")
assert code == 0, preview_replay
assert preview_replay["dry_run"] is True
for args in (complete_args, refresh_args, spend_args, terminal_args):
code, replay = run(*args)
assert code == 0, replay
Expand Down
58 changes: 57 additions & 1 deletion tests/control_plane_ts/terminal_reentry_conformance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
import type {JsonObject} from "../../loopx/control_plane/effect_program.ts";
import {settlementIdentityPayload, type JsonObject} from "../../loopx/control_plane/effect_program.ts";
import type {AuthorityStore} from "../../loopx/control_plane/coordination/authority_store.ts";
import {
canonicalAuthorityObject,
Expand All @@ -17,6 +17,62 @@ import {localTodoCompletionIdentity} from "../../loopx/control_plane/todos/compl
import type {AuthorityStoreConformanceFactory} from "./authority_store_conformance.ts";

export function registerTerminalReentryConformance(provider: string, factory: AuthorityStoreConformanceFactory): void {
test(`${provider}: historical lifecycle receipt recovers its verified quota binding without rewriting history`, async t => {
const {store} = await factory(t);
const binding = settlementIdentityPayload({goal_id: "goal-a", agent_id: "agent-a",
todo_id: "todo_historical", turn_instance_id: "original-turn"});
const todo: JsonObject = {schema_version: "todo_item_v0", todo_id: "todo_historical",
role: "agent", status: "done", done: true, archive_state: "active", source_section: "Agent Todo",
text: "Preserve an accepted outcome", claimed_by: "agent-a", last_actor_agent_id: "agent-a",
completed_at: "2026-09-01T00:00:00Z", completion_turn_key: binding.effect_id,
completion_continuation: "active_goal", evidence: "Original accepted evidence"};
await store.commitAuthority({operation_id: "import-historical", expected_provider_revision: null,
next_projection: authorityProjectionFixture("goal-a", [todo], [], "legacy", {handoff_mode: "hard_lease"}),
events: [], receipts: []});
const request: CoordinationTodoTerminalLifecycleInput = {
goal_id: "goal-a", todo_id: "todo_historical", expected_role: "agent", command: "complete",
operation_identity: {kind: "explicit", operation_id: "legacy-adapter"},
actor_agent_id: "agent-a", registered_agents: ["agent-a"], lifecycle_grants: [],
authority_reason: null, decision_outcome: null, lease_idempotency_key: null, lease_expected_version: null,
allow_user_gate_auto_acquire: false, requested_no_followup: true,
requested_completion_turn_key: String(binding.effect_id), requested_completion_identity_source: "lifecycle_reentry",
linked_successor_todo_ids: [], successor_intents: [], note: null, evidence: null, reason: null,
clear_claim: false, validation_declaration: null, validation_receipt: null, completion_policy_request: null,
dry_run: false, now: new Date("2026-09-07T07:00:00Z"),
};
// Model the old serializer's omission at its original commit, never by
// editing an already accepted provider head, receipt or history record.
const legacyWriter = new Proxy(store, {get(target, name) {
if (name === "commitAuthority") return async (...args: Parameters<AuthorityStore["commitAuthority"]>) => {
const commit = structuredClone(args[0]);
for (const receipt of commit.receipts) {
const result = receipt.result as JsonObject | undefined;
if (result) delete result.settlement_identity;
}
return target.commitAuthority(commit);
};
const member = Reflect.get(target, name);
return typeof member === "function" ? member.bind(target) : member;
}});
const verified = async (input: CoordinationTodoTerminalLifecycleInput) => {
assert.equal(input.requested_completion_turn_key, binding.effect_id);
return binding;
};
const original = await execute(legacyWriter, request, async () => true, null, verified);
assert.equal(original.status, "applied", JSON.stringify(original));
assert.equal(original.settlement_identity, undefined);
const receiptId = String((original.original_receipt as JsonObject).operation_id);
const before = {head: await store.loadAuthority(), receipt: await store.readReceipt(receiptId)};
const recovered = await execute(store, request, async () => true, null, verified);
assert.equal(recovered.status, "replayed", JSON.stringify(recovered));
assert.deepEqual(recovered.settlement_identity, binding);
assert.deepEqual({head: await store.loadAuthority(), receipt: await store.readReceipt(receiptId)}, before);
const denied = await execute(store, request, async () => true, null, async () => {
throw new Error("Original settlement is unavailable");
});
assert.equal(denied.status, "failed");
assert.deepEqual({head: await store.loadAuthority(), receipt: await store.readReceipt(receiptId)}, before);
});
for (const schema of ["legacy", "native"] as const) {
for (const policyClaim of [null, "agent-a"]) {
test(`${provider}: imported untyped completion repairs once before terminal closeout (${schema}, claim=${policyClaim})`, async t => {
Expand Down
Loading