Skip to content

fix(quota): make an unreconcilable action selection self-diagnosing - #4545

Merged
huangruiteng merged 1 commit into
mainfrom
codex/quota-selection-conflict-diagnostic
Sep 16, 2026
Merged

huangruiteng merged 1 commit into
mainfrom
codex/quota-selection-conflict-diagnostic

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

Problem

When a guard is bound with --todo-id and that selection cannot be reconciled with the current projection, the preflight raised a bare RuntimeError. 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 QuotaActionSelectionConflictError instead of a bare RuntimeError, 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) and recommended_action, following the existing QuotaIdentityPreconditionError pattern. quota_error_code maps it, and quota_failure_payload publishes:

  • status: quota_action_selection_conflict (instead of quota_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-id to read the current selection, then bind that Todo, a deferred Todo, or the Todo the recovery obligation must settle");
  • a typed action_selection_conflict block (kind, requested_todo_id, selected_todo_id, qualification_state).

What Is Deliberately Unchanged

  • A Todo that the delivery frontier defers or rejects keeps its existing typed payload (quota_action_selection_deferred / quota_action_selection_rejected).
  • A qualified selection for the requested Todo still passes the preflight and never raises (this is the case that was broken two PRs ago and is fixed in 04e035864; the new test pins it).
  • No behavior change to collection, spend, receipts or defaults — only the failure report for an already-failing path.

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 returning False.
  • 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.
  • Live read on this machine: a Todo that the frontier rejects still reports quota_action_selection_rejected with 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 Exception path already exercised by the neighboring typed errors.

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 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: 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 给出真正的下一步读法,并附 typed action_selection_conflict 块(kind/requested_todo_id/selected_todo_id/qualification_state);
  • 不改动已经 typed 的相邻路径(deferred / rejected 仍是原样),也不改任何采集、spend、receipt 或默认行为。

具体改动

  • loopx/control_plane/quota/error_codes.py(+54):QuotaActionSelectionConflictKind StrEnum、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 形状与非冲突路径)。

对主干的风险

  1. 只影响失败报告:触发条件仍是"用 --todo-id 绑定且与投影无法调和",本来就必然失败;本 PR 改的是失败信息与 typed 字段,不改变成功路径。
  2. 异常基类保持一致:新异常继承 RuntimeError(与原来抛出的类型一致),调用方的宽捕获行为不变;类型判断只加码不改判。
  3. 不吞掉已有 typed 语义:deferred/rejected 分支和"qualified 且等于请求 Todo"的分支都有测试钉住(后者是上一刀修复的回归点)。
  4. live 复现受限:要现场触发新分支需要一个内部不一致的投影(qualification 指向 A、投影选择为 B)。本 PR 在单元层覆盖了抛出、映射与 payload 三跳,中间那跳是既有的 except Exception,与相邻 typed 错误共用;已在 PR body 的 residual gap 点名。
  5. 验证:pytest tests/control_plane/test_quota_action_selection_conflict.py 4 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 作为放行结论。

@huangruiteng
huangruiteng merged commit 7c65b0c into main Sep 16, 2026
14 of 19 checks passed
@huangruiteng
huangruiteng deleted the codex/quota-selection-conflict-diagnostic branch September 16, 2026 12:21
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