Skip to content

fix(quota): name the argument-less reentry for a deferred selection - #4890

Merged
huangruiteng merged 3 commits into
mainfrom
codex/quota-deferred-selection-obligation-0922
Sep 22, 2026
Merged

huangruiteng merged 3 commits into
mainfrom
codex/quota-deferred-selection-obligation-0922

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

Goal / gap

A Turn that selects an explicit --todo-id and is deferred by an active autonomous_replan preemption 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:

quota should-run --turn-instance-id <turn> --todo-id <todo_id>

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 (turns 2026-09-21T15:11:02.332Z, 2026-09-22T08:29:15.375Z, 2026-09-22T08:40:14.930Z). The bind_autonomous_replan reentry that makes the Turn settleable does exist; it just was not the repair the refusal named.

Change

loopx/control_plane/quota/settlement_readback.ts reads the receipt details it already receives and distinguishes the two unbound states by the selection the guard retained:

  • no retained selection → previous text, unchanged (rebind it through the guard's same-turn reconciliation, then settle: … --todo-id …);
  • retained 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 typed details (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.ts48/48 pass, including the new names the argument-less guard reentry for a deferred explicit selection and the unchanged pre-selection repair assertion.
  • tsc --project tsconfig.control-plane.json --noEmit → clean.
  • Full npm run test:control-plane: 2367 pass / 29 fail, all 29 in local_authority_shadow_outbox / succession_read_conformance from python3 -c import 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-id returned decision=autonomous_replan_required, retained_action_selection.disposition=bind_autonomous_replan, and heartbeat_receipt.status=upgraded bound to replan-8a3295b92cd2553c, with a settlement_plan for that identity.

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

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 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)

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-id repair。
  • 仅在 marker 有效时附加 details.deferred_selection_todo_id,没有伪造 settlement identity,也没有写状态。
  • 新 fixture 复现 retained deferred selection,并验证 repair 不含 --todo-id;原有 ordinary-unbound 行为继续受覆盖。

关键代码讲解

关键分支在 resolveIdentitydeferredSelectionTodoId === 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 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)

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-id repair。
  • 仅在 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 行为继续受覆盖。

关键代码讲解

关键分支在 resolveIdentitydeferredSelectionTodoId === null 保留原命令,否则选择 argument-less reentry。这个条件与 producer 写入的 receipt 字段直接对齐,使用 typed field 和已有 normalization,而不是从错误文本或 Todo 名称推断状态。结果仍是原有 typed failure;真正的状态转移继续由下一次 quota guard 执行。

对主干的风险

代码层面未发现阻塞项。

  • 语义风险:把 deferred 与 ordinary-unbound 归到同一个 receipt_unbound kind 会让只按 kind 分支的消费者看不到差异;这是 main 已经确立的 contract,deferred 的差异通过 details.deferred_selection_todo_id 与 repair 文本暴露,若要再分 kind 应由 owner 单独决定。
  • 冲突解决风险:第一次并入 b82a2f9c6settlement_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.

@huangruiteng

Copy link
Copy Markdown
Collaborator Author

Merge decision (owner-authorized self-merge)

Authorization. This PR touches loopx/control-plane/quota/**, a control-plane surface the repository's standing rule reserves for the maintainer and excludes from the small self-merge allowance. The maintainer directed this merge in-session (4894自合并 then 自合并4890), so it is executed as an owner-authorized self-merge and recorded here rather than presented as an agent-decided one. Recent merges in this window (#4893, #4900) landed through the same route while kernel-static-checks was red.

Exact head. be6e04c10b42f370141857f205e6a5e5acf990df — two signed merge commits (b82a2f9c6, then 912ffc3b8) on top of the reviewed 8d93c042d. The PR's own content is unchanged by them.

Changed surfaces. loopx/control_plane/quota/settlement_readback.ts, tests/control_plane_ts/quota_settlement_readback.test.ts.

Conflict and vocabulary resolution. The first merge conflicted in resolveIdentity, where main's #4893 had renamed this state's failure kind to receipt_unbound while this PR refined the repair text and added details.deferred_selection_todo_id. The resolution keeps main's typed kind (both unbound states share receipt_unbound; the deferred one is told apart by the repair text and the retained selection) and keeps this PR's repair branch, so the newer vocabulary is not rolled back. The PR's new test now asserts receipt_unbound with a comment naming the shared-kind decision. The second merge (main 912ffc3b8, which already carries #4894) was clean. git diff --check is clean.

Merge readiness on this head. loopx pr-review --goal-id loopx-meta --check-merge-readiness 4890@be6e04c10ready: true, blocking_reasons: [], author_owned_commented_approval: true, admin_bypass_required: true (the gate reads this goal as ci_policy: not_consulted, and GitHub reports BLOCKED for the author-owned approval path).

Validation on this exact head.

  • tests/control_plane_ts/quota_settlement_readback.test.ts: 48 pass.
  • npm run test:control-plane on the previous head (fd75e1d38): 2721 tests, 2697 pass, 24 skipped, 0 fail.
  • 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 pass.
  • npm run typecheck:control-plane, ruff check loopx/control_plane tests/control_plane_ts, git diff --check: clean.

Known red required checks, all outside this PR's content. On the previous head fd75e1d38:

  • node-minimum-compatibility fails 9 of 22 cases in tests/control_plane_ts/authority_archive.test.ts under the pinned Node 22.18.0. Reproduced on an unmodified origin/main worktree with the same Node build (13 pass / 9 fail) and green under Node 22.23.2, so the check is red on main itself; the file and implementation come from main's feat(authority): add verified portable journal recovery #4887.
  • kernel-static-checks was killed at its 30-minute ceiling; main's last completed run took 23.5 minutes, so branch runs are sitting on the boundary.
  • node-forward-compatibility (Node 26 probe, continue-on-error) hit its own 20-minute ceiling.
  • test-shard (4) failed once on test_todo_projection_concurrency.py::test_downlevel_runtime_cannot_acknowledge_delivery[sqlite]; that case passes 12/12 locally on both this head and unmodified origin/main, so it is not reproduced here.

CI for be6e04c10 was still running when this decision was recorded; the chronic items above are expected to keep the aggregate red regardless of this PR.

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.

@huangruiteng
huangruiteng merged commit 992108d into main Sep 22, 2026
4 checks passed
@huangruiteng
huangruiteng deleted the codex/quota-deferred-selection-obligation-0922 branch September 22, 2026 18:28
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