fix(quota): make an unreconcilable action selection self-diagnosing - #4545
Merged
Merged
Conversation
A guard bound with `--todo-id` whose selection cannot be reconciled with the current projection used to raise a bare RuntimeError. That surfaced as `quota_unexpected_collection_error` with the reason "quota collection failed" and a recommended action pointing at heartbeat receipt writeback, so the real cause was invisible to the Agent that hit it. The preflight now raises a typed `QuotaActionSelectionConflictError` that names the requested Todo, the projection's current selection, and the qualification state, and that carries its own `error_code` and `recommended_action`: - `kind=unqualified`: the projection carries no typed qualification at all; - `kind=conflict`: the requested Todo is neither the current selection nor deferred/rejected by it. `quota_error_code` returns `quota_action_selection_conflict`, and `quota_failure_payload` reports the conflict as its own `status` with a typed `action_selection_conflict` block (`kind`, `requested_todo_id`, `selected_todo_id`, `qualification_state`) instead of the generic collection failure. The recommended action is a real next read: rerun without `--todo-id` to see the current selection, then bind that Todo, a deferred Todo, or the Todo the recovery obligation must settle. The neighboring typed paths are unchanged: a Todo that is deferred or rejected by the delivery frontier keeps its existing typed payload, and a qualified selection for the requested Todo still passes the preflight. Covered by `tests/control_plane/test_quota_action_selection_conflict.py`: both raise kinds, the code mapping, the failure payload shape, and the non-conflict case that must keep returning False. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
commented
Sep 16, 2026
huangruiteng
left a comment
Collaborator
Author
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Reviewed exact head: f74a67b50b1d32620abdf57cd4082d289cf267e4
English verdict: APPROVE
动机
守卫用 --todo-id 绑定的选择如果和当前投影无法调和,前置校验会抛裸 RuntimeError,CLI 再把它包成:
error_code = quota_unexpected_collection_error
reason = "...quota collection failed"
recommended_action = "retry ... after repairing heartbeat receipt writeback"
两个都错:原因不是采集失败,建议也指向 receipt writeback。今天本机 lane 结算时就撞上过这条路径,真实信息(requested action selection qualification conflicts with its projection)只有靠进程内插桩才看得到——这正是用户反复强调的"失败要能自己说清原因"的反例。
改动思路
沿用仓库既有的 typed 失败模式(QuotaIdentityPreconditionError 的做法):
- 新增
QuotaActionSelectionConflictError(kind=unqualified|conflict),自带error_code与recommended_action,并在消息里点名 requested Todo、投影当前选择与 qualification 状态; quota_error_code映射到quota_action_selection_conflict;quota_failure_payload为该异常单独分支:status变成quota_action_selection_conflict、reason用真实解释、recommended_action给出真正的下一步读法,并附 typedaction_selection_conflict块(kind/requested_todo_id/selected_todo_id/qualification_state);- 不改动已经 typed 的相邻路径(deferred / rejected 仍是原样),也不改任何采集、spend、receipt 或默认行为。
具体改动
loopx/control_plane/quota/error_codes.py(+54):QuotaActionSelectionConflictKindStrEnum、QuotaActionSelectionConflictError(含recommended_action与分 kind 的 reason)、quota_error_code新增映射。loopx/cli_commands/quota.py(+15/-3):两处裸RuntimeError改为 typed 异常,并带上 requested/selected/qualification_state 事实。loopx/cli_commands/quota_failure_report.py(+18):failure payload 的新分支。tests/control_plane/test_quota_action_selection_conflict.py(+117):4 个测试(两种 kind、错误码映射、payload 形状与非冲突路径)。
对主干的风险
- 只影响失败报告:触发条件仍是"用
--todo-id绑定且与投影无法调和",本来就必然失败;本 PR 改的是失败信息与 typed 字段,不改变成功路径。 - 异常基类保持一致:新异常继承
RuntimeError(与原来抛出的类型一致),调用方的宽捕获行为不变;类型判断只加码不改判。 - 不吞掉已有 typed 语义:deferred/rejected 分支和"qualified 且等于请求 Todo"的分支都有测试钉住(后者是上一刀修复的回归点)。
- live 复现受限:要现场触发新分支需要一个内部不一致的投影(qualification 指向 A、投影选择为 B)。本 PR 在单元层覆盖了抛出、映射与 payload 三跳,中间那跳是既有的
except Exception,与相邻 typed 错误共用;已在 PR body 的 residual gap 点名。 - 验证:
pytest tests/control_plane/test_quota_action_selection_conflict.py4 passed;test_quota_settlement_cli.py -k "recovery_guard_accepts or selection"17 passed;本机实测被拒 Todo 仍走原 typed 路径;canary premerge通过(唯一 advisory 为已知基线)。
我的整体评价
正向且 proportional:约 200 行的单一目的切片,把一个"指错方向的失败"变成"能自己说清原因的 typed 失败",符合仓库对 typed 失败边界与可操作性建议的要求,且没有顺手扩大范围。
残余缺口已如实记录:没有 live 端到端样本复现这条新分支所需的不一致投影状态。
作为作者自有 PR,GitHub 不允许正式 self-approve,故以本 COMMENTED review 作为放行结论。
This was referenced Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a guard is bound with
--todo-idand that selection cannot be reconciled with the current projection, the preflight raised a bareRuntimeError. The CLI turned it into:{"error_code": "quota_unexpected_collection_error", "reason": "heartbeat receipt was not committed because quota or stall writeback did not complete: quota collection failed", "recommended_action": "retry quota should-run with the same --turn-instance-id after repairing heartbeat receipt writeback"}That is wrong twice over: the cause is not a collection failure, and the recommended action points at receipt writeback. I hit exactly this earlier today while closing out an unsettled turn — the real message (
requested action selection qualification conflicts with its projection) was only visible by instrumenting the CLI in-process.Fix
The preflight now raises a typed
QuotaActionSelectionConflictErrorinstead of a bareRuntimeError, with two kinds:unqualified— the projection carries no typed action-selection qualification at all;conflict— the requested Todo is neither the projection's current selection nor deferred/rejected by it.The error carries its own
error_code(quota_action_selection_conflict) andrecommended_action, following the existingQuotaIdentityPreconditionErrorpattern.quota_error_codemaps it, andquota_failure_payloadpublishes:status: quota_action_selection_conflict(instead ofquota_collection_failed);reason= the real message, naming the requested Todo, the projection's current selection and the qualification state;recommended_action= a real next read ("rerun without--todo-idto read the current selection, then bind that Todo, a deferred Todo, or the Todo the recovery obligation must settle");action_selection_conflictblock (kind,requested_todo_id,selected_todo_id,qualification_state).What Is Deliberately Unchanged
quota_action_selection_deferred/quota_action_selection_rejected).04e035864; the new test pins it).Validation
pytest tests/control_plane/test_quota_action_selection_conflict.py— 4 passed: both raise kinds with their named ids, the error-code mapping, the failure-payload shape (including asserting the reason is not "quota collection failed" and the recommendation does not mention receipt writeback), and the non-conflict case that must keep returningFalse.pytest tests/control_plane/test_quota_settlement_cli.py -k "recovery_guard_accepts or selection"— 17 passed, so the neighboring typed selection paths are unaffected.quota_action_selection_rejectedwith its own recommendation, i.e. the new branch does not capture the already-typed paths.loopx canary premerge --from-git-diff— passed (diff hygiene, compile checks, catalog canaries including the quota and hot-path smokes, 8 risk-profile smokes, public/private boundary scan). One advisory, unchanged: the known baseline maintainability ratchet.Residual Gap
The new branch is covered at unit level for the raise, the code mapping and the payload. Reproducing the live inconsistent state (a qualification that is qualified for one Todo while the projection's selection is another) needs an internally inconsistent projection, so there is no live end-to-end sample in this PR; the wiring between the raise and the payload is the unchanged
except Exceptionpath already exercised by the neighboring typed errors.