Skip to content

fix(quota): make deferred selection recovery executable before settlement - #4654

Merged
huangruiteng merged 6 commits into
mainfrom
codex/heartbeat-selection-binding-4650
Sep 18, 2026
Merged

huangruiteng merged 6 commits into
mainfrom
codex/heartbeat-selection-binding-4650

Conversation

@huangruiteng

@huangruiteng huangruiteng commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #4650.

When replanning preempts a pending Todo selection, the original heartbeat receipt remains unbound. The response previously advertised settlement for an identity that had never been admitted, leaving the caller unable to finish the wake.

The rejected/deferred response now provides one same-turn guard command without a Todo/replan argument. Executing it uses the existing serialized receipt upgrade to bind the current lane or obtain a fresh portfolio. Delivery and spending remain disabled until admission; existing bound receipts cannot be retargeted.

Ownership and compatibility

  • TypeScript selection qualification returns a discriminated result: accepted selection or typed reenter_guard_without_selection recovery.
  • The existing CLI owner reconciles the decision and execution obligation. The recovery renderer reuses registry/runtime routing, scheduler arguments and effective capability projection. No new writer, durable state, permission or capability is introduced.
  • recommended_action retains the existing human-readable refusal guidance. The executable command belongs to next_cli_actions and agent_channel.primary_action.
  • A failed selection removes settlement instructions and its unadmitted replan action packet, so the compact TurnEnvelope preserves recovery instead of replacing it with stale replan work. Compact command previews retain the existing full-decision readback boundary.

Review repairs and validation

The previous exact-head review and CI exposed two regressions caused by overwriting refusal guidance with the command. Both were reproduced after main integration and repaired without weakening their assertions. Further review reproduced a second gap: full-decision recovery passed, but TurnEnvelope lost its command because the old replan packet won. Both Todo-bound and Todo-less replan paths now cover the real compact CLI output as well as full recovery, refresh, spend, duplicate-spend replay and retarget rejection.

At 9fb51446a17f33263912cbe68bd1bef79fcbcca3:

  • Complete CLI settlement/reentry files: 67 passed, no failures/skips, on qualified Node 22.22.3; includes File/SQLite hidden-lifecycle consumers.
  • Native action-portfolio and TurnEnvelope tests: 26 passed. TypeScript noEmit, Ruff and mypy (22 files) passed.
  • The same real CLI harness at immutable main bd1df2521 and this head produced identical preserved observations for direct/explicit selection, receipt identity/replay, refusal guidance and retarget rejection. The recovery assertion fails on old main and passes here; the compact stale-plan regression was separately demonstrated before repair.
  • All eight public changed paths passed the boundary scan; all commits are signed off. Exact-scope quality receipt cqr_3e0df39cb1c28b6fb8e9 is valid.
  • Final-head CI and the goal-bound premerge canary are pending. Earlier failed or interrupted attempts are not counted as passes.

The shipped entrypoints are full CLI decisions and the existing compact TurnEnvelope. No configuration fields, frontend controls or Lark inputs are added; human-readable rejection guidance remains compatible. The related replan checkpoint change #4655 is already on the integrated main. This closes the reported same-turn recovery gap without claiming broader replan or collaboration completion.

Rollback restores the prior code; there is no persisted schema migration. Self-merge is authorized for this request and remains conditional on the final exact-head review, required validation and merge-readiness gate.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
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.

Request changes conclusion (author-owned PR; GitHub blocks formal self-review)

Reviewed exact head: cc41af0f7b5cbcf701a19f601668843757373c0f (refactor(quota): keep execution obligation updates in the CLI owner).

动机

动机成立:当 agent 显式传 --todo-id 而选择在 turn 内被拒绝/推迟时,payload 里既没有可执行的回退,也没有下一步命令,agent 只能自己拼一条 guard 调用——这正是我们刚在 replan smoke 里修掉的那类「手写命令与真实投影漂移」问题。

改动思路

方向也对:在 TypeScript 侧把 qualification 做成判别联合(rejected/deferred 必须带 recovery_action: reenter_guard_without_selection,qualified 必须带 selected_todo),在 CLI 侧新增 _reconcile_requested_quota_action_selection,拒绝时用 apply_action_selection_recovery 渲染「重进 guard」命令、清掉本 turn 的 settlement 许诺,并把 must_attempt_work/delivery_allowed 置 false。新字段、新命令、新测试都是加法。

具体改动

8 个文件、+179/-13:

  • loopx/control_plane/work_items/action_portfolio.ts:ActionSelectionQualification 判别联合,rejected/deferred 补 recovery_action。
  • loopx/control_plane/work_items/action_selection_contract.py:apply_action_selection_recovery——拼 loopx --registry … quota should-run --goal-id … --agent-id … [--turn-instance-id …] [--available-capability …] + scheduler args,并把 payload["recommended_action"] 覆写成这条命令,同时 next_cli_actions=[command]、清空 settlement_plan/replan_settlement_contract/selection_command。
  • loopx/cli_commands/quota.py:preflight 与 recovery 合并进 _reconcile_requested_quota_action_selection。
  • tests/control_plane/test_selection_replan_reentry.py(+79):新场景——拒绝 → 按返回命令重进 → receipt 升级 → 结算一次(含幂等重放);tests/control_plane_ts/action_portfolio.test.ts +3。

我复核的证据(都在这个 head 与当前 main 上跑过):

  • 新场景通过:pytest -q tests/control_plane/test_selection_replan_reentry.py → 2 passed;TS 侧 node --test tests/control_plane_ts/action_portfolio.test.ts → 12 passed。
  • 但是:pytest -q tests/control_plane/test_quota_settlement_cli.py -k "selection or monitor_auxiliary" 在 head 上 2 failed / 14 passed,在 main 9118568dd 上 16 passed——即这两个失败是本 PR 引入的。
  • 完整文件在 head 上是 4 failed / 63 passed;另外两个 test_prior_host_closeout_survives_hidden_todo_lifecycle[0-sqlite] / [6-sqlite] 在 main 上以完全相同的报错失败(canonical_authority_fixture.py:39 → local_authority_provider.ts:257),属环境性、与本 PR 无关,我没有把它们算在阻塞项里。

阻塞项(P1)

两处失败的原因同一个:recommended_action 从「拒绝原因的散文」被换成了「shell 命令」。

  • test_selection_added_after_pending_guard_reports_final_boundary 断言 'rerun quota should-run' in repeated["recommended_action"];
  • test_due_monitor_auxiliary_context_has_typed_selection_rejection 断言 selected["recommended_action"].startswith("the due monitor is visible as auxiliary context")。

新渲染器把 recommended_action 直接写成 loopx --registry … quota should-run …,于是「为什么被拒」这个信息在 payload 里消失了,而这条信息不只是测试在用:任何把该字段当人读文案渲染的 host/dashboard 都会跟着变。PR 只给两处 dict 断言补了 recovery_action,没有更新这两个断言,也没有在正文里声明字段语义变化。

最小修法:把命令只放进 next_cli_actions(以及 agent_channel.primary_action),保留 recommended_action 原有的类型化原因;若确实想改语义,需要同时改这两条测试与文档并在正文里披露。改完请重跑:

pytest -q tests/control_plane/test_quota_settlement_cli.py -k "selection or monitor_auxiliary"

对主干的风险

这是控制面 payload 的语义改动,风险不在新命令(新命令本身有很好的端到端测试),而在既有字段被复用:recommended_action 在同一批拒绝路径里从「原因」变成「命令」,属于静默默认行为变化。其余部分我检查后认为是净收益:typed union 让 rejected/deferred 必须声明 recovery,apply_action_selection_recovery 在缺 marker 时直接抛错(不会渲染半条命令),settlement 被清空而不是被绕过,receipt 身份与 spend 次数仍由既有测试覆盖。回退成本一个 commit。

我的整体评价

结论 REQUEST_CHANGES(仅此一条阻塞)。恢复路径本身设计合理、测试扎实,我确认它能把同 turn 的重进与「只结算一次」跑通;但它在实现里顺手改掉了 recommended_action 的含义,导致仓库自己的两条断言在 head 上失败、在 main 上通过。按最小修法改完即可转 APPROVE。

English verdict: REQUEST_CHANGES - exact head cc41af0. The same-turn recovery is well designed and the new re-entry test passes (plus 12 TypeScript cases), but apply_action_selection_recovery overwrites recommended_action with the rendered command, so the typed refusal reason disappears from the payload. That breaks two shipped tests at this head while the identical runs pass at main 9118568: test_selection_added_after_pending_guard_reports_final_boundary (expects 'rerun quota should-run') and test_due_monitor_auxiliary_context_has_typed_selection_rejection (expects the reason prose). The other two failures in the same file (test_prior_host_closeout_survives_hidden_todo_lifecycle[0-sqlite]/[6-sqlite]) reproduce identically at main and are environmental. Minimum repair: keep recommended_action as the reason and carry the command only in next_cli_actions, then re-run "pytest -q tests/control_plane/test_quota_settlement_cli.py -k "selection or monitor_auxiliary"".

…60918

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
@huangruiteng
huangruiteng merged commit 575c0a6 into main Sep 18, 2026
22 of 23 checks passed
@huangruiteng
huangruiteng deleted the codex/heartbeat-selection-binding-4650 branch September 18, 2026 03:30

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

审阅对象:PR #4654(已合并),exact head 9fb51446a17f33263912cbe68bd1bef79fcbcca3(作者 huangruiteng),merge commit 575c0a6331de3c9dd85d82a2c929eade911926fe,2026-09-18T03:30:30Z 合并。本文是该 exact head 的 post-merge audit,重点复核此前一次 REQUEST_CHANGES 是否已在合并 head 上关闭。

动机

当 agent 显式传 --todo-id 而该选择在本回合被拒绝(例如在 advancement lane 里请求一个辅助 monitor)或被前置交付门推迟时,旧的 payload 既没有可执行的恢复动作,也会继续为这个回合许诺结算——但这个回合的 receipt 其实没有绑定。结果是 agent 只能自己手写一条 guard 命令,而手写命令很容易与真实投影漂移;这正是仓库此前在 replan smoke 里修掉的那类问题。

改动思路

两侧各做一件事,且方向一致:TypeScript 把 qualification 变成判别联合,让"被拒绝/被推迟"这一状态必须携带 recovery_action: reenter_guard_without_selection(qualified 则必须携带 selected_todo),从类型上消灭"没有恢复动作的拒绝";Python 新增 apply_action_selection_recovery,用与投影相同的命令构造器渲染出确切的 loopx … quota should-run … 命令,并同时撤掉本回合的结算许诺。关键是后者不是"再写一份文档建议",而是让发出的命令与拒绝它的那次投影同源。

具体改动

8 个文件、+201/-13,其中 97 行是新测试。

  • action_portfolio.ts:判别联合;rejected/deferred 分支补 recovery_action。
  • action_selection_contract.py:apply_action_selection_recovery 组装命令(含 capabilities 与 scheduler args),把 recommended_action 覆写为该命令,清空 settlement_plan、replan_settlement_contract、selection_command、selection_policy_ref,并把 must_attempt/delivery_allowed/spend_allowed_now 置 false;同时移除本回合的 replan_action_packet,避免它在紧凑 TurnEnvelope 里反过来顶掉恢复路径。若 qualification 缺少该类型化恢复动作则直接抛错。
  • quota.py:preflight 与 recovery 合并为 _reconcile_requested_quota_action_selection。

本轮复核的最重要一项,是此前评审提出的具体阻塞:tests/control_plane/test_quota_settlement_cli.py -k "selection or monitor_auxiliary" 在当时被审的 head cc41af0f7b 上 2 failed / 14 passed(另在 main 上 16 passed,说明是本 PR 引入)。我在合并 head 上重跑同一选择:16 passed / 0 failed——即该回归在合并前已关闭。另外 tests/control_plane/test_selection_replan_reentry.py 2 passed(拒绝→按返回命令重进→receipt 升级→只结算一次,含幂等重放),tests/control_plane_ts/action_portfolio.test.ts 12 passed。内容完整性:merge commit 575c0a63 的父提交为 bd1df2521(main) 与 9fb51446a(PR head),按作者提交落地;两份文档(docs/quota-allocation.md、docs/reference/protocols/turn-envelope-v0.md)在同一 diff 内描述了新边界。验证使用的是临时 fixture registry,未触碰任何真实 goal 状态。

对主干的风险

主要风险是"撤回结算许诺"被误读为放松了安全属性。实际相反:之所以撤回,正是因为该回合的 identity 没有绑定——此前会为它许诺结算,才是真正的不安全。判别联合把"拒绝却没有恢复动作"变成不可表示的状态,CLI 侧再对缺失动作抛错,属于双保险。

次要风险是恢复命令与投影漂移。这里的缓解是让渲染复用既有命令前缀与 capability 投影,并由 re-entry journey 测试真实走一遍 CLI;也就是说这次不是靠文案约定,而是靠同源构造。残留的证据边界:本回合验证在 fixture registry 上完成,未在真实 goal 上执行;覆盖不到"外部环境在渲染与重进之间变化"这类情况,但那属于 guard 自身的语义,不在本 PR 范围内。

我的整体评价

结论 APPROVE。这次审计的价值主要在"阻塞是否真的关闭"这一项:此前评审以具体测试失败为证据提出 REQUEST_CHANGES,而合并 head 上同一选择已全绿,恢复路径也有独立 journey 覆盖,因此该阻塞可以确认关闭。改动本身小而锋利——把状态做成判别联合、让恢复命令与投影同源、并撤掉不该存在的结算许诺——正好对应"不要手写与投影漂移的命令"这条既有教训。该 head 合并没有留下绑定到它的评审记录(此前那条停在更早的 head),本文补上。

English verdict: APPROVE - Audit of the merged exact head 9fb5144 of PR #4654 (merge commit 575c0a6, with the PR head as a parent, so the content landed as authored). The change makes a rejected or deferred quota action selection self-recovering: the TypeScript qualification becomes a discriminated union that makes a recovery-less rejection unrepresentable, and the CLI renders the exact re-entry guard command from the same contract helper the projection uses while withdrawing the settlement promise for that unbound turn (settlement plan, replan settlement contract, selection command and replan action packet removed; must_attempt, delivery_allowed and spend flags false). The key audit question was whether the previously published REQUEST_CHANGES blocker was closed: at the reviewed head cc41af0 the selection test_quota_settlement_cli.py -k "selection or monitor_auxiliary" failed 2 of 16 while passing on main, and at the merged head the same selection passes 16 of 16, with the re-entry journey (reject -> re-enter -> single settlement, including idempotent replay) and the TypeScript portfolio tests also green. Verification ran against a temporary fixture registry, not a live goal. Process note: this PR's existing review was bound to an earlier head, so this exact-head audit supplies the merged head's record.

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.

Bug: a same-turn selection rerun leaves the heartbeat receipt unbound, so the wake cannot settle

1 participant