fix(quota): name the argument-less reentry for a deferred selection - #4890
Conversation
A Turn whose explicit --todo-id selection the guard deferred commits a receipt that still carries no settlement binding, because the guard binds the autonomous replan preemption only on argument-less reentry. The refusal named a rebind with --todo-id as its repair, and that call re-enters the same preemption and defers again, so a caller following the message could not settle the Turn at all. The read model already receives the receipt details, so the refusal can tell the two unbound states apart from the selection the guard retained and name the reentry that actually binds the preemption, keeping the retained Todo in its typed details. The pre-selection repair text is unchanged. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Reviewed exact head 8d93c042d1a5666de3522f2d6d3a6d754951d51e.
动机
这个修复针对的是一个真实的恢复死循环:显式 --todo-id 在 autonomous replan 前被保留后,本轮 receipt 暂时没有 settlement identity;旧 readback 却再次要求带同一个 --todo-id 重跑 guard,于是会重新进入显式选择的 preemption 分支,无法完成绑定。这里不是缺少新的状态,而是已有 pending_action_selection_todo_id 没有被 recovery read model 正确解释。
改动思路
实现边界是对的:继续让 heartbeat receipt / live decision / quota guard 分别拥有“保留选择 / replan 优先级 / 最终绑定”,settlement_readback.ts 只读取既有 receipt marker 并选择正确的恢复命令。这样没有在诊断路径中引入第二个 binding authority,也没有改变 slot accounting、selection 或 replan 语义。
具体改动
- 当 receipt 没有 settlement identity 时,先用现有
normalizeTodoId解码pending_action_selection_todo_id。 - marker 存在时,repair 改为相同 turn、相同 agent 的 argument-less
quota should-run;marker 不存在时仍保留原来的--todo-idrepair。 - 仅在 marker 有效时附加
details.deferred_selection_todo_id,没有伪造 settlement identity,也没有写状态。 - 新 fixture 复现 retained deferred selection,并验证 repair 不含
--todo-id;原有 ordinary-unbound 行为继续受覆盖。
关键代码讲解
关键分支在 resolveIdentity:deferredSelectionTodoId === null 保留原命令,否则选择 argument-less reentry。这个条件与 producer 写入的 receipt 字段直接对齐,使用 typed field 和已有 normalization,而不是从错误文本或 Todo 名称推断状态。结果仍是原有 typed failure;真正的状态转移继续由下一次 quota guard 执行。
对主干的风险
代码层面未发现阻塞项。exact-head 和当前 origin/main 集成态的 focused suite 均为 48/48,两个环境的 TypeScript typecheck 与 diff check 通过;远端实质性 shards(含 real PostgreSQL)也已通过。
但当前远端 aggregate 仍是红色:kernel-static-checks 在 30 分钟边界被取消,继而让 checks / pytest / merge-gate 聚合失败。因此本结论是代码 review 通过,不代表现在可合并;应先 rerun 或确认该取消已被正常处置。另一个非阻塞协调点是 #4893 也修改同一 unbound 分支的 failure kind,后合入的一方需要 rebase,并同时保留 receipt_unbound vocabulary 与本 PR 的 repair 分支。
我的整体评价
这是一个范围小但闭环完整的恢复契约修复:复用了既有 marker 和 transition owner,正反分支清楚,未扩大 authority。没有发现需要作者修改的代码问题;未来向 adjacent typed failure vocabulary 演进时只需做好 rebase 协调。
English verdict: APPROVE - The exact-head code change correctly routes retained deferred selections to argument-less same-turn guard reentry without moving binding authority into readback; resolve the cancelled aggregate CI and coordinate the overlapping #4893 rebase before merge.
…selection-obligation-0922 Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> # Conflicts: # loopx/control_plane/quota/settlement_readback.ts
…selection-obligation-0922 Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Reviewed exact head be6e04c10b42f370141857f205e6a5e5acf990df (two signed merge commits: b82a2f9c6, then 912ffc3b8 which already carries #4894, both merged into codex/quota-deferred-selection-obligation-0922).
动机
这个修复针对的是一个真实的恢复死循环:显式 --todo-id 在 autonomous replan 前被保留后,本轮 receipt 暂时没有 settlement identity;旧 readback 却再次要求带同一个 --todo-id 重跑 guard,于是会重新进入显式选择的 preemption 分支,无法完成绑定。这里不是缺少新的状态,而是已有 pending_action_selection_todo_id 没有被 recovery read model 正确解释。
改动思路
实现边界是对的:继续让 heartbeat receipt / live decision / quota guard 分别拥有“保留选择 / replan 优先级 / 最终绑定”,settlement_readback.ts 只读取既有 receipt marker 并选择正确的恢复命令。这样没有在诊断路径中引入第二个 binding authority,也没有改变 slot accounting、selection 或 replan 语义。
冲突解决遵循同一判断:main 的 #4893 把该“receipt 存在但没有 settlement binding”状态的 failure kind 从 identity_mismatch 改名为 receipt_unbound,而本 PR 在这条分支上增加 deferred-selection 的 repair 选择。合并后的实现保留 main 的 typed kind(两段 unbound 状态共享 receipt_unbound,deferred 由 repair 文本与 details.deferred_selection_todo_id 区分),同时保留本 PR 的 repair 分支,因此两侧意图都成立,没有把 vocabulary 回退成 identity_mismatch。
具体改动
- 当 receipt 没有 settlement identity 时,先用现有
normalizeTodoId解码pending_action_selection_todo_id。 - marker 存在时,repair 改为相同 turn、相同 agent 的 argument-less
quota should-run;marker 不存在时仍保留原来的--todo-idrepair。 - 仅在 marker 有效时附加
details.deferred_selection_todo_id。 - failure kind 取自 main:
receipt_unbound;本 PR 新增用例的断言随之更新为receipt_unbound并保留原注释语义(两个 unbound 状态共享 kind,deferred 由 repair 与 retained selection 区分)。 - 新 fixture 复现 retained deferred selection,并验证 repair 不含
--todo-id;原有 ordinary-unbound 行为继续受覆盖。
关键代码讲解
关键分支在 resolveIdentity:deferredSelectionTodoId === null 保留原命令,否则选择 argument-less reentry。这个条件与 producer 写入的 receipt 字段直接对齐,使用 typed field 和已有 normalization,而不是从错误文本或 Todo 名称推断状态。结果仍是原有 typed failure;真正的状态转移继续由下一次 quota guard 执行。
对主干的风险
代码层面未发现阻塞项。
- 语义风险:把 deferred 与 ordinary-unbound 归到同一个
receipt_unboundkind 会让只按 kind 分支的消费者看不到差异;这是 main 已经确立的 contract,deferred 的差异通过details.deferred_selection_todo_id与 repair 文本暴露,若要再分 kind 应由 owner 单独决定。 - 冲突解决风险:第一次并入
b82a2f9c6时settlement_readback.ts是唯一冲突文件,已按上面的判断解决且无残留 marker;第二次并入912ffc3b8无冲突。git diff --check通过。 - 环境风险:本机
python3为 3.9 时调用 CLI 的 TS 用例会因dataclass(slots=True)失败,需把仓库 venv 的python3置于PATH前部;CI 使用 3.11,不受影响。
本 exact head 的本地验证:
tests/control_plane_ts/quota_settlement_readback.test.ts:48 passed;npm run test:control-plane(与 CI 同命令,Node 24 + 仓库 venv Python):2721 tests,2697 passed,24 skipped,0 failed;pytest -q tests/control_plane/test_quota_settlement.py tests/architecture/test_settlement_receipt_source_boundaries.py tests/control_plane/test_goal_handoff_mode.py:190 passed;npm run typecheck:control-plane:通过;git diff --check:通过。
远端 required checks:上一个 head(fd75e1d38)的红项是 node-minimum-compatibility(main 侧 #4887 在 Node 22.18.0 下的 authority_archive.test.ts 失败,在未修改的 origin/main 上同样复现 13 pass / 9 fail)、kernel-static-checks(在其 30 分钟上限被杀)与 node-forward-compatibility(Node 26 探针 20 分钟超时,continue-on-error),以及 test-shard (4) 中 test_todo_projection_concurrency.py::test_downlevel_runtime_cannot_acknowledge_delivery[sqlite] 的一次失败——该用例在本 head 与未修改 origin/main 上本地均 12/12 通过,未能复现。这些都与 #4890 的内容无关,但会继续让 aggregate 保持红色。
我的整体评价
这是一个范围小但闭环完整的恢复契约修复:复用了既有 marker 和 transition owner,正反分支清楚,未扩大 authority。没有发现需要作者修改的代码问题。合并时保留 receipt_unbound vocabulary 并把 deferred 差异留在 repair/details 上,是本 PR 与 #4893 重叠面最小的正确解法。
合并授权:本 PR 触及 loopx/control-plane/**,按仓库规则属于 control-plane surface,agent 不自行决定合并;本次由维护者在会话中显式指示(“自合并4890”),因此以 owner authorization 执行,并在合并说明中记录 changed surfaces、已跑检查、红色 required checks 的归因与 admin bypass 事实。红色项来自 main 侧与 CI 时限,而非本 PR 内容。
English verdict: APPROVE at exact head be6e04c. The change correctly routes a retained deferred selection to argument-less same-turn guard reentry in readback only, the merge keeps main's receipt_unbound kind while preserving this PR's repair branch, and the head's focused suite, full control-plane suite, typecheck and Python settlement tests are green locally. Remaining red required checks are inherited from main under Node 22.18.0 and from the kernel job's 30-minute ceiling.
Merge decision (owner-authorized self-merge)Authorization. This PR touches Exact head. Changed surfaces. Conflict and vocabulary resolution. The first merge conflicted in Merge readiness on this head. Validation on this exact head.
Known red required checks, all outside this PR's content. On the previous head
CI for Why the coverage is enough. The change is 79 added lines in one readback module plus its fixtures; the exact-head review is published on this head, the focused suite, full control-plane suite, typecheck, lint and the Python settlement/handoff tests are green, and the only red required checks are main-side or CI-budget conditions with reproductions outside this diff. |
Goal / gap
A Turn that selects an explicit
--todo-idand is deferred by an activeautonomous_replanpreemption commits a same-turn receipt that still carries no settlement binding: the guard binds the preemption only on argument-less reentry. When that Turn then tried to settle, the refusal named this repair:That call re-enters the same preemption and defers again, so a caller that followed the message could not settle the Turn, could not record the replan acknowledgement, and left validated work unaccounted — reproduced three times on
loopx-meta/codex-managed-steward-product(turns2026-09-21T15:11:02.332Z,2026-09-22T08:29:15.375Z,2026-09-22T08:40:14.930Z). Thebind_autonomous_replanreentry that makes the Turn settleable does exist; it just was not the repair the refusal named.Change
loopx/control_plane/quota/settlement_readback.tsreads the receipt details it already receives and distinguishes the two unbound states by the selection the guard retained:rebind it through the guard's same-turn reconciliation, then settle: … --todo-id …);pending_action_selection_todo_id→ the refusal now names the argument-less reentry that binds the preemption the guard is holding, and keeps the retained Todo in its typeddetails(deferred_selection_todo_id).No authority, binding, or gate changes: this is the repair text plus one typed detail on the failure, and it still fails closed with
identity_mismatch.Validation
node --no-warnings --experimental-sqlite --experimental-strip-types --test tests/control_plane_ts/quota_settlement_readback.test.ts→ 48/48 pass, including the newnames the argument-less guard reentry for a deferred explicit selectionand the unchanged pre-selection repair assertion.tsc --project tsconfig.control-plane.json --noEmit→ clean.npm run test:control-plane: 2367 pass / 29 fail, all 29 inlocal_authority_shadow_outbox/succession_read_conformancefrompython3 -cimport failures in a worktree without a Python environment; unrelated to this surface and reproduced before the change.Live confirmation of the repaired path
On the same lane, the newly named reentry worked: re-running the guard for the same Turn without
--todo-idreturneddecision=autonomous_replan_required,retained_action_selection.disposition=bind_autonomous_replan, andheartbeat_receipt.status=upgradedbound toreplan-8a3295b92cd2553c, with asettlement_planfor that identity.Control-plane change: proposed for review, not self-merged.