fix(quota): name the admission refusal when the projection already selects the requested Todo - #4985
Conversation
…lects the requested Todo
A same-turn `--todo-id` whose Todo is already the projection's current
selection was reported as a selection conflict, so the message named the same id
on both sides ("requested Todo X is neither the projection's current selection
(X) nor deferred or rejected by it") and left the caller with no next read.
The refusal that actually happened was not-admitted-to-settle, and when the
payload already carries `unsettled_host_turn_recovery` the real blocker is the
prior Turn that still owes its closeout - the case the lane hit on
2026-09-23T07:44Z, where the identical bind succeeded after that prior Turn's
refresh and spend were recorded.
The preflight now raises a distinct `not_admitted` kind when the requested Todo
equals the projection's current selection, names the prior unsettled Turn and
its typed repair when the payload carries them, and publishes both in the
failure payload. Every eligibility predicate is unchanged: only the refusal
naming changes.
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
中文评审(精确 head 012b280)
结论
建议合并(APPROVE),无阻断项。这是一次纯措辞修复:把一个自相矛盾的拒绝改成能指向下一步的拒绝,判定条件与准入谓词完全不变。作者自有 PR,无法用 GitHub 的形式化 approve,因此以本评论作为该精确 head 的评审记录。
里程碑 / 基线
- 基线:origin/main
7938834b4,分支codex/unsettled-turn-refusal-naming-0924基于它 rebase,PR base = main。 - 改动面:
loopx/control_plane/quota/error_codes.py、loopx/cli_commands/quota_action_selection.py、loopx/cli_commands/quota_failure_report.py,加一个聚焦测试文件,共 4 文件 / +140 −11。 - 行为边界:控制面(
loopx/**),仅提案不自动合并。
依据
- 复现路径来自实测:当轮请求的
--todo-id恰好是投影当前选中的 Todo、质询态为qualified、但该轮未被准入结算时,旧实现抛conflict,理由文本把同一个 id 放在句子两边("neither the projection's current selection (X) nor deferred or rejected by it"),调用方无法据以动作。 - 新实现把该情形判为独立的
not_admitted类别:conflict只描述"请求 id 与投影 id 真的不一致";not_admitted描述"投影选的就是你请求的,是这一轮没被准入结算"。 - 当同一 payload 已带
unsettled_host_turn_recovery时,理由中会点名此前未结清的 Turn 及其 typed repair,失败载荷同时发布unsettled_prior_turn_instance_id/unsettled_repair。这正是"重录上一轮 refresh + spend 后同一次绑定就成功"的那类真实成因,而不是选择冲突。 - 验证:新测试文件 6 passed;
test_unadmitted_selection_construction.py+test_quota_settlement_cli.py+test_quota_authority_settlement_journey.py合跑 106 passed;改动文件ruff check干净;mypy对改动文件无新增报错(该环境下无关模块基线本身噪声很大)。既有conflict的失败载荷字段契约由既有测试原样锁定,未被改动。
缺口
- 该拒绝路径还有第二类真实成因:准入事实(
agent_channel.must_attempt/delivery_allowed)才是原因、且没有可点名的未结清前轮。本次只把这种情形改成语义诚实的 "not admitted to settle",尚未把这两项事实作为 typed 字段一并发布到失败载荷。建议作为后继小切片,不并入本 PR。 test_selection_added_after_pending_guard_reports_final_boundary在一次三文件合跑中曾失败一次,随后在同一合跑与单文件运行中均通过(含两份重复运行),本次未能稳定复现,判定为环境/时序抖动而非本改动引入;如实记录,未将其作为绿色结论。- 未运行
loopx canary premerge --from-git-diff(本 PR 为控制面提案,交由 maintainer 与 CI 覆盖),如需我再补跑请指出。
下一步
- maintainer 决定合并;如需拆分第二类成因的 typed 准入事实字段,我按实际需要另开一个聚焦 PR。
- 合并后建议用同一条拒绝路径回归一次:同轮同 id 重复绑定,确认返回
not_admitted且在有未结清前轮时点名该 Turn。
English verdict
APPROVE for exact head 012b28071893dca3c7f58a1c8609af1de229d3bc. The change only renames an unreconcilable explicit --todo-id refusal: when the requested Todo equals the projection's current selection the guard now reports not_admitted (naming the prior unsettled Turn and its typed repair when the payload carries unsettled_host_turn_recovery) instead of a self-contradictory conflict sentence. No admission predicate, no Todo reachability and no settlement behaviour changes; the pre-existing conflict payload contract is pinned by the existing tests and stays intact. Focused validation: 6 new/updated tests plus 106 tests across the settlement CLI, unadmitted-selection and settlement-journey suites, ruff clean. Author-owned, so this comment is the exact-head review record; not self-merged.
Telling a caller "the Turn was not admitted to settle that Todo" is only actionable if it can see which side of the delivery boundary refused it. The refusal now carries `agent_channel.must_attempt` and `delivery_allowed` as typed facts: they are printed in the reason and published as `action_selection_conflict.admission`, so a caller can branch on the reason instead of re-deriving it from prose. The two facts are read once and reused for the admission predicate, so the predicate itself is unchanged (delivery_refused is still "delivery_allowed is explicitly False"). Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
中文评审(精确 head 1f9b5be)
结论
建议合并(APPROVE),无阻断项。本 head 在原改动之上补了第二个提交:把决定准入的两项事实 typed 化并发布,从而把"这一轮没被准入结算"从一句说明变成可分支的数据。准入谓词与可达性完全不变。作者自有 PR,无法用 GitHub 的形式化 approve,因此以本评论作为该精确 head 的评审记录。
里程碑 / 基线
- 基线:origin/main
7938834b4(分支已基于它 rebase),PR base = main。 - 两个提交:
012b28071(拒绝命名:not_admitted取代自相矛盾的conflict)与1f9b5be62(发布 typed 准入事实)。 - 改动面:
loopx/control_plane/quota/error_codes.py、loopx/cli_commands/quota_action_selection.py、loopx/cli_commands/quota_failure_report.py加一个聚焦测试文件;控制面(loopx/**),仅提案不自动合并。
依据
- 复现路径来自实测:请求的
--todo-id恰是投影当前选中项、质询态qualified、但该轮未被准入结算时,旧实现把同一个 id 放在理由句两边,调用方无从动作。 conflict现在只表示"请求 id 与投影 id 真的不一致";not_admitted表示"投影选的就是你请求的,是这一轮没被准入"。- 准入事实 typed 化:
agent_channel.must_attempt与delivery_allowed读一次、既喂给准入谓词也在理由中印出,并以action_selection_conflict.admission发布,调用方可以按原因分支而不必从散文本里再猜。谓词等价性由实现保证:delivery_refused仍是"delivery_allowed显式为 False"。 - 若同一 payload 带
unsettled_host_turn_recovery,理由点名此前未结清的 Turn 及其 typed repair,失败载荷同时发布unsettled_prior_turn_instance_id/unsettled_repair。 - 验证:测试文件 7 passed(含请求==选中不再是 conflict、点名未结清前轮、投递边界拒绝时 typed 事实三例);
test_unadmitted_selection_construction.py+test_quota_settlement_cli.py+test_quota_authority_settlement_journey.py合跑 106 passed;改动文件 ruff 干净、mypy 无新增报错;既有conflict载荷契约由既有测试原样锁定。
缺口
- 未覆盖:拒绝路径的第三层成因(例如自治重规划义务与显式选择的交叉)仍走既有
conflict/retained_selection分支,本次没有改动其语义,也没有为它增加 typed 事实。若需要,另开聚焦切片。 windows-powershell检查在本 head 失败;同一检查在 PR base 所指向的mainhead7938834b4上同样失败,因此不是本 PR 引入;但本 PR 未对其做任何修复。- 未运行
loopx canary premerge --from-git-diff,交给 CI 与 maintainer 覆盖。
下一步
- maintainer 决定合并。合并后建议用同一条拒绝路径回归一次:同轮同 id 重复绑定,确认返回
not_admitted、准入事实为 typed 字段、且在有未结清前轮时点名该 Turn。
English verdict
APPROVE for exact head 1f9b5be6215ebbe0a2370f0dedc6257c4a108f73. Two commits: the first replaces the self-contradictory conflict refusal (same id on both sides) with a typed not_admitted that names an unsettled prior Turn and its typed repair when the payload carries unsettled_host_turn_recovery; the second publishes agent_channel.must_attempt / delivery_allowed as typed facts on that refusal and in the failure payload so a caller can branch on the reason. No admission predicate, Todo reachability or settlement behaviour changes, and the pre-existing conflict payload contract stays pinned by its own test. Validation: 7 focused tests plus 106 tests across the settlement CLI, unadmitted-selection and settlement-journey suites, ruff clean. The windows-powershell failure also reproduces on this PR's base main head and is not introduced here. Author-owned, so this comment is the exact-head review record; not self-merged.
Summary
A same-turn
--todo-idwhose Todo is already the projection's current selection was reported as a selection conflict, so the guard named the same id on both sides of its own sentence:That message cannot be acted on: there is no conflict to reconcile, and it hides the refusal that actually happened. When the receipt write is what failed because an earlier must-attempt Turn still owes its closeout receipts, the caller needs that prior Turn named. And when the delivery boundary itself refused the work, the caller needs those facts, not prose.
What changed
QuotaActionSelectionConflictErrorgains a distinctnot_admittedkind.conflictnow only describes a genuine disagreement between the requested and projected ids;not_admitteddescribes "the projection selects exactly what you asked for, and this Turn was not admitted to settle it".not_admittedwhenrequested_todo_id == selected_todo_idand the qualification state is neitherdeferrednorrejected.agent_channel.must_attemptanddelivery_allowedare printed in the reason and published asaction_selection_conflict.admission.unsettled_host_turn_recovery, the reason names the prior Turn and its typed repair, and the failure payload publishesunsettled_prior_turn_instance_id/unsettled_repair.delivery_refusedis still "delivery_allowedis explicitly False". This is refusal naming, not admission: no Todo becomes selectable, and nothing is settled or spent differently.Validation
tests/control_plane/test_quota_action_selection_conflict.py7 passed: the requested-equals-selected case is typednot_admittedand no longer contains the contradictory sentence; the unsettled-prior-Turn case names the prior turn id and repair; the delivery-refused case publishes the typed admission facts. The pre-existingconflictpayload contract is unchanged and still pinned by its own test.tests/control_plane/test_unadmitted_selection_construction.py,tests/control_plane/test_quota_settlement_cli.py,tests/control_plane/test_quota_authority_settlement_journey.py106 passed.ruff checkclean on the changed files;mypyreports no error in the changed files (the project-wide baseline in this environment is noisy for unrelated modules).windows-powershellcheck fails on this head, and it also fails on the currentmainhead7938834b4for this PR's base commit, so it is not introduced here.Boundary
Control-plane change (
loopx/**): proposed for review and left for the maintainer to merge, not self-merged.