Skip to content

fix(quota): give an unbound same-turn receipt its own failure kind - #4893

Merged
huangruiteng merged 1 commit into
mainfrom
codex/settlement-receipt-unbound-kind-0922
Sep 22, 2026
Merged

huangruiteng merged 1 commit into
mainfrom
codex/settlement-receipt-unbound-kind-0922

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

Goal / gap

quota settlement readback refused a Turn whose guard receipt carries no settlement binding with kind: "identity_mismatch" plus details.binding_kind: "unbound". A consumer therefore had to read inside details to tell two different states apart:

  • the receipt exists and is well-formed but declares no binding yet — produced routinely by the documented wake order (guard first, work item chosen second), and by a guard that declined an explicit choice; repair = reenter the guard so it binds;
  • two durable records disagree — repair = reconcile the identity.

identity_mismatch means the second. PR #4595 had already typed the details; the failure kind itself stayed shared, so effect_program's SettlementFailureKind could not express the state it was reporting.

Change

  • loopx/semantics/vocabulary_v0.json: receipt_unbound joins the canonical cross_runtime settlement_failure_kind vocabulary, with a value note that states the producing condition (a well-formed identity compared against a Turn receipt that declares no binding of its own) and contrasts it with identity_mismatch.
  • loopx/control_plane/effect_program.py / .ts: the enum and the literal list carry the value.
  • docs/reference/glossary.md: regenerated by uv run python scripts/generate_semantic_bindings.py.
  • loopx/control_plane/quota/settlement_readback.ts: the unbound branch returns receipt_unbound; the failure-type union and the typed details (binding_kind: "unbound", requested binding kind, turn id) are unchanged.
  • tests/control_plane_ts/quota_settlement_readback.test.ts: the unbound assertion moves to the new kind.

No new binder, no authority change, and identity_mismatch still names the two-record case. This is the (a) half of the settlement-binding signalling left by PR #4595; the guard payload already reports settlement_binding_owed (b), and the spend-path message (c) is unchanged.

Validation

  • node --experimental-strip-types --test tests/control_plane_ts/quota_settlement_readback.test.ts47 passed.
  • tsc --project tsconfig.control-plane.json --noEmit → clean.
  • uv run --extra test python -m pytest tests/architecture/test_cross_runtime_value_notes.py tests/architecture/test_semantic_vocabulary_drift.py tests/architecture/test_semantic_production.py -q193 passed (the drift test is what requires the regenerated glossary).
  • uv run --extra test python -m pytest tests/architecture/test_settlement_receipt_source_boundaries.py tests/test_loopx_turn_executor.py tests/test_loopx_turn_driver.py -q197 passed.
  • Full npm run test:control-plane: 2366 pass / 29 fail — all 29 are the pre-existing worktree-environment class (python3 resolving to macOS CommandLineTools 3.9, which rejects dataclass(slots=True) in loopx/file_lock.py), unrelated to this surface.

Interaction with #4890

#4890 (open) edits the message of this same unbound branch; this PR edits its kind. Both touch failedIdentity's call, so whichever merges second needs a small rebase — the two changes are independent and complementary (name the repair, and type the state).

Control-plane change: proposed for review, not self-merged.

The readback refused a Turn whose guard receipt carries no settlement binding with
identity_mismatch, so a consumer had to read details.binding_kind to tell "the
receipt exists but declares no binding yet" from "two durable records
disagree". Those are different states with different repairs, and the documented
wake order (guard, then select) produces the first one routinely.

Add receipt_unbound to the cross-runtime settlement_failure_kind vocabulary
(Python enum, TypeScript list, vocabulary value plus its producing-condition
note, regenerated glossary) and return it from the readback unbound branch. The
failure type union in settlement_readback.ts gains the kind, and the typed
details stay as they are; no new binder is introduced and identity_mismatch keeps
naming the two-record case.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

Exact head: ca1767724eaa97371d1cd966248604c57112a078

动机

这次修复针对的是一个真实的诊断语义错误:同一 Turn 的 guard receipt 已经存在,但既没有 todo_id,也没有 autonomous replan binding。旧实现把这种“还没有可比较身份”的状态归类为 identity_mismatch,会让调用方和运维人员误以为有两条互相冲突的身份记录,并沿错误方向排查。真正需要的恢复动作是重新完成 selection/binding,而不是处理一个不存在的 identity conflict。

我对照了 base c4314d9b06a52073e12ec64fb76ff42e486bdc7e 与本 head,并检索了 SettlementFailureKindidentity_mismatch、序列化/反序列化和 Turn executor/driver 消费路径。这里不应只改报错文案:既然上层会按 failure.kind 做机器处理,最小完整方案就是在既有跨运行时 vocabulary 中增加一个精确的 typed kind。

改动思路

设计边界是正确的:不新增 receipt 状态、不改变 selection/quota/settlement authority,只从 canonical guard receipt 派生更精确的诊断结果。

  • receipt_unbound 表示单条 receipt 没有任何 work binding;
  • identity_mismatch 继续表示 receipt 和请求两边都有具体身份、但彼此不一致;
  • Python/TypeScript enum、语义 vocabulary 和 glossary 同步扩展,避免跨运行时漂移;
  • 修复分支仍然是只读 readback,没有写 Todo、花 quota 或改 writer fence 的副作用。

最强反对意见是:为了一个分支增加新 enum 会不会让消费者碎片化。这里答案是否定的,因为新值进入的是既有 SettlementFailureKind owner,并且所有 contract mirror 同步更新;只改 reason 文本反而会留下机器语义不准确的问题。

具体改动

文件职责和关键路径如下:

  • loopx/control_plane/quota/settlement_readback.ts:在 unbound guard 分支返回 receipt_unbound;相邻的 concrete-binding mismatch 分支保持 identity_mismatch,没有扩大新分类的覆盖面。
  • loopx/control_plane/effect_program.ts / .py:给共享 SettlementFailureKind 增加同名值;既有 decoder 仍会拒绝未知/畸形值。
  • loopx/semantics/vocabulary_v0.jsondocs/reference/glossary.md:明确“无 binding”与“两条 binding 冲突”的语义差异及恢复动作。
  • tests/control_plane_ts/quota_settlement_readback.test.ts:正向覆盖 unbound receipt,负向保留 another-Todo mismatch,并继续覆盖 malformed/missing/settled 路径。

正向 walkthrough:同一 Turn receipt 可归因但无 Todo/replan binding → readback 命中 unbound predicate → 返回 failure.kind=receipt_unbound 与重新 selection/binding 的 remediation → 不产生任何状态写入。

负向 walkthrough:receipt 已绑定到另一个 Todo → 两侧 concrete identity 被比较 → 仍返回 identity_mismatch;畸形 receipt 则在 schema/readback 边界先 fail closed,不会被当作“未绑定”。

对主干的风险

未发现阻塞实现问题。风险主要有两类:

  1. additive enum 的消费者兼容性:已通过 Python/TypeScript vocabulary、production/architecture 与 Turn caller 测试覆盖;没有 persisted-state migration。
  2. 合并态风险:当前分支 BEHIND main;exact-head 的 kernel-static-checks 在 30 分钟被取消,导致 checks / pytest / merge-gate 聚合项红。实际执行的 4 个 test shard、两组 e2e、mutants、installed、Windows、Node forward/minimum、dashboard、PostgreSQL 等均为绿,但这仍是 merge hold,不能拿局部绿灯替代一次完整 required-check rerun。

我在独立 detached worktree 上验证:

  • quota_settlement_readback.test.ts:47 passed;
  • 选定的 cross-runtime vocabulary / semantic production / receipt boundary / Turn executor-driver Python suites:390 passed;
  • npx tsc --project tsconfig.control-plane.json --noEmit:通过;
  • git diff --check:通过。

我的整体评价

APPROVE(实现结论),但暂不具备合并条件。这个 PR 用现有 typed owner 修正了一个会误导恢复路径的真实语义缺口,范围小、无额外 authority、正负路径清楚,也没有把诊断变成第二份持久状态。合并前必须先更新到最新 main,并让 required checks 在新 exact head 上完整跑完;取消的 kernel-static/红色聚合项不能被 review 结论豁免。

未来导向检查:本次已经完成最有价值且相关的边界收敛——复用共享 failure vocabulary,没有必要再引入新抽象。

English verdict: APPROVE - Exact-head implementation is sound and focused; merge remains on hold until the branch is updated and all required checks complete successfully on the new head.

@huangruiteng
huangruiteng merged commit b82a2f9 into main Sep 22, 2026
24 of 28 checks passed
@huangruiteng
huangruiteng deleted the codex/settlement-receipt-unbound-kind-0922 branch September 22, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant