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
2 changes: 1 addition & 1 deletion docs/reference/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ Typed failure class of a settlement step.
- Tier / 层级: `cross_runtime`; status / 状态: `canonical`.
- python: [`SettlementFailureKind`](../../loopx/control_plane/effect_program.py).
- typescript: [`SETTLEMENT_FAILURE_KINDS`](../../loopx/control_plane/effect_program.ts).
- Values / 值: `invalid_identity`, `receipt_missing`, `identity_mismatch`, `writeback_missing`, `writeback_rejected`, `quota_spend_rejected`, `terminal_closeout_rejected`, `cancelled`, `permission_denied`, `budget_rejected`, `effect_outcome_unknown`.
- Values / 值: `invalid_identity`, `receipt_missing`, `receipt_unbound`, `identity_mismatch`, `writeback_missing`, `writeback_rejected`, `quota_spend_rejected`, `terminal_closeout_rejected`, `cancelled`, `permission_denied`, `budget_rejected`, `effect_outcome_unknown`.

## settlement_step_kind

Expand Down
1 change: 1 addition & 0 deletions loopx/control_plane/effect_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ class SettlementBindingKind(StrEnum):
class SettlementFailureKind(StrEnum):
INVALID_IDENTITY = "invalid_identity"
RECEIPT_MISSING = "receipt_missing"
RECEIPT_UNBOUND = "receipt_unbound"
IDENTITY_MISMATCH = "identity_mismatch"
WRITEBACK_MISSING = "writeback_missing"
WRITEBACK_REJECTED = "writeback_rejected"
Expand Down
1 change: 1 addition & 0 deletions loopx/control_plane/effect_program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export type SettlementBindingKind = (typeof SETTLEMENT_BINDING_KINDS)[number];
export const SETTLEMENT_FAILURE_KINDS = [
"invalid_identity",
"receipt_missing",
"receipt_unbound",
"identity_mismatch",
"writeback_missing",
"writeback_rejected",
Expand Down
12 changes: 9 additions & 3 deletions loopx/control_plane/quota/settlement_readback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,11 @@ function inferPersistedIdentity(

function failedIdentity(
reason: string,
kind: "invalid_identity" | "identity_mismatch" | "receipt_missing",
kind:
| "invalid_identity"
| "identity_mismatch"
| "receipt_missing"
| "receipt_unbound",
details?: JsonObject,
) {
return settlementFailed<JsonObject>({
Expand Down Expand Up @@ -615,13 +619,15 @@ function resolveIdentity(
// the guard's own same-turn reconciliation owns that, so there is one
// binder rather than two -- which means the caller has to be told the state
// and the exact repair instead of being handed a binding mismatch it cannot
// act on.
// act on. The state also gets its own failure kind, so a consumer can branch
// on the missing binding without reading details.binding_kind: the receipt
// exists and is well-formed here, which is not what identity_mismatch means.
return failedIdentity(
"the quota should-run receipt for this turn carries no settlement binding " +
`yet (turn_instance_id ${turnInstanceId}); rebind it through the guard's ` +
"same-turn reconciliation, then settle: quota should-run --turn-instance-id " +
`${turnInstanceId} --todo-id ${identity.todo_id ?? "<todo_id>"}`,
"identity_mismatch",
"receipt_unbound",
{
binding_kind: "unbound",
requested_binding_kind: identity.binding_kind,
Expand Down
2 changes: 2 additions & 0 deletions loopx/semantics/vocabulary_v0.json
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@
"values": [
"invalid_identity",
"receipt_missing",
"receipt_unbound",
"identity_mismatch",
"writeback_missing",
"writeback_rejected",
Expand All @@ -593,6 +594,7 @@
],
"value_notes": {
"invalid_identity": "A validation-step rejection of the identity's own shape, judged in isolation before any comparison with a durable record: no identity object, a blank goal, agent or turn instance id, both or neither work-item binding, a turn instance id failing the public-safe pattern, a missing effect id, or an effect id that disagrees with the one recomputed from that same identity. Self-inconsistency inside one plan, as opposed to identity_mismatch which compares two records.",
"receipt_unbound": "A settlement identity was compared with this Turn's persisted guard receipt, and that receipt declares no binding of its own: the guard committed before any work item was chosen, or it declined the explicit choice it was handed, so there is nothing to compare against yet. identity_mismatch needs two records that disagree; receipt_unbound names the single-record case where the receipt exists but carries neither a Todo nor an autonomous replan binding.",
"receipt_missing": "A receipt or phase record needed to proceed is absent or not durably committed and no more specific kind applies: completed phases are not an ordered prefix of the transaction phases, validation is required but not completed, a phase marked completed carries a payload that is not committed, or readback finds no heartbeat receipt, quota spend or terminal closeout event.",
"identity_mismatch": "A well-formed identity compared against a second durable record names a different effect or binding: the journal's committed effect id differs from the plan's, a heartbeat receipt's binding or settlement effect id differs from the request, or a prepared attempt's effect reference does not match the effect id and step kind. The contrast with invalid_identity is that this failure needs two records to detect.",
"writeback_missing": "Raised at exactly one site, in the writeback readback, when the accountable refresh-state run or its matching event is null for this identity: the durable writeback receipt was never found. It is absence, never refusal, which is what separates it from writeback_rejected.",
Expand Down
4 changes: 3 additions & 1 deletion tests/control_plane_ts/quota_settlement_readback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,9 @@ test("names the unbound same-turn receipt and the repair instead of a mismatch",
const result = await readQuotaSettlement(request(runtimeRoot));

const failure = (result.settlement as any).result.failure;
assert.equal(failure.kind, "identity_mismatch");
// The receipt exists and is well-formed, so the missing binding has its own
// kind instead of reading as a mismatch against a second record.
assert.equal(failure.kind, "receipt_unbound");
assert.match(failure.reason, /carries no settlement binding yet/);
assert.match(
failure.reason,
Expand Down
Loading