fix(quota): resolve an explicit --todo-id from the Goal's own rows - #4837
Conversation
The presented planning lanes are a display budget, so an owned, open, typed advancement Todo that sat outside them was unreachable by an explicit --todo-id and the guard refused it as candidate_not_currently_eligible, even though the caller had just worked on that row and had no other legal way to bind its own settlement to it. The by-id lookup now also resolves the requested row from the Goal's own state rows. Every eligibility predicate still runs in the existing builder, so this widens reachability of the lookup, not what may be selected. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Assert that a row absent from the presented lanes is reachable through the Goal state while another agent's row and a blocked row still refuse. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
精确 head:4d537d82340c2545e314a80d5dedb957dcfed7c3(base main 09f10c4e3)。无阻断发现,建议合并;合并决定留给 maintainer。
动机
按 problem_context:quota should-run --todo-id <id> 对本泳道自己刚做完的那一行会拒绝——decision=skip state=quota_action_selection_rejected reason=candidate_not_currently_eligible。行 todo_414bf4c7a75c 里已记了三次真实复现(同一天、含 turn id),后果不是"少选一个动作",而是只能把结算挂到可选带内的另一行,账本因此长期归属偏移。
根因在 look-up 的种子:候选来自 quota_runnable_action_candidates(...) + agent_todo_planning_source_items,两者上游都是 status payload 里有界的展示 lane。于是"开放、已认领、typed advancement"的行只要落在展示预算之外就查不到,TS reducer 收到 candidate=null,只能给出 candidate_not_currently_eligible。PR #4512 已把 look-up 从展示 lane 扩到规划清单,但那份清单本身仍是有界投影,缺口仍在。
真实 Goal(loopx-meta)上的端到端对比:
base: decision=skip state=quota_action_selection_rejected reason=candidate_not_currently_eligible(16:11Z / 19:07Z / 21:16Z 三个 turn 记录在案)
head: decision=run action_selection_qualification.state=qualified selected todo_431983a1e942 回执 unbound → bound
改动思路
按 architecture_flow / repository_reuse / walkthroughs:显式 --todo-id 是调用方已经做出的选择,不是从菜单里挑,所以按 id 解析应当读 Goal 自己的状态行。新增 _authoritative_requested_agent_rows(...):用既有 loopx.materials.goal_state_path 取 state file、用既有 goal_todo_summaries(..., roles=["agent"], status="open", todo_id=<id>) 解析这一行,并入既有 builder 的候选集合。所有 eligibility 谓词仍在原 owner 执行,因此只扩大可达性,不扩大可选项。
被否掉的替代方案:放宽谓词(扩大可选项)、给 reducer 加新 reason(行依旧不可达)、改从 status 的 rollout-event index 取行(那里的行文本是 todo add recorded for ... 占位、没有 task_class,builder 无法判定)。
具体改动
按 changed_line_classification / symbol_map:生产 1 个文件(should_run_prepare.py +47/-10),测试新增 1 个文件(+118/-0)。
关键代码讲解
loopx/control_plane/quota/should_run_prepare.py:446_authoritative_requested_agent_rows—— 只为显式请求解析一行;未知 id / 缺 state file / 读失败 / 投影非法一律返回空表(不抛错、不编造),保持原有拒绝形状。- 同文件显式选择分支的候选并集:两个来源变三个(runnable candidates、planning inventory、authoritative row)。该并集只在
--todo-id存在时构建,未选择的默认路径完全不变。
对主干的风险
按 failure_analysis / walkthroughs.negative / validation_matrix / scope_fit:
- 最强回归场景:显式请求会多解析一次 Goal state(仅在带
--todo-id时),大 state 的解析成本未测量。 - 触发到观测:同一 Goal 上"此前被拒、现在 qualified";展示 lane 里没有该行时仍能被 builder 接受;别的 agent 的行、blocked 行、未知 id、缺失 state 一律照旧拒绝。
- 爆炸半径:显式选择路径;可选带、判定码、展示预算、调度与扣费路径未动。回滚即 revert 两个提交。
- 生效调用点:任何带
--todo-id的quota should-run都会走该分支(非仅测试可达)。 - 证据边界(诚实记录):TS reducer 是通过真实 CLI 走通的,没有单独重跑 TS 套件;反例来自同一天同一 Goal 的三个已记录 turn,而不是重跑一次 base;未跑全量仓库套件。
我的整体评价
按 observable_semantics / code_volume / change_proportionality / default_off_isolation / authority_semantics:
- 三条对比行分别是"真实 Goal 显式请求""展示 lane 缺失的行""他人/阻塞/未知/缺 state":第一条从 rejected 变 qualified;第二条证明可达;第三条证明可达性没有放宽资格。判定码与可选带一字未改,记为
intentional_change_validated。 - 规模与收益相称:一个只读 resolver + 一处并集成员 + 3 条断言;无新模块、依赖、配置、状态或 CLI 面。
- 隔离性:只在显式
--todo-id时激活,默认守卫路径不额外读文件、不扣费,default_off_isolation记isolated。 - authority:没有放宽权限——谓词与 reducer 仍是唯一判定者,菜单也没多出这一行。
- 类型/域中立/义务表述:按
todo_id精确匹配、由 typed 字段判定资格,无子串或自然语言启发式;新增文案只提 quota、guard 与 Todo,不含 goal/产品/benchmark 措辞。 - 结论:无阻断性 finding。残余风险三条:显式请求的额外解析成本未测量;TS reducer 未单独重跑;证据是有界集合(3 + 158 + 45 + 真实 CLI),未跑全量套件。
English verdict: APPROVE - reviewed head 4d537d8 makes the explicit by-id lookup read the Goal's own rows, keeps every eligibility predicate and the offered band unchanged, and is backed by a live command that flips from candidate_not_currently_eligible to qualified, three focused cases, a 158-case adjacent run, a clean-worktree rerun, and an explicit statement of the unmeasured parse cost and un-rerun TS suite.
动机
quota should-run --todo-id <id>对本泳道自己刚做完的那一行会拒绝:decision=skip state=quota_action_selection_rejected reason=candidate_not_currently_eligible。行内已经记了三次真实复现(同一台机器、同一天,含 turn id),后果不是"少选一个动作",而是只能把结算挂到可选带内的另一行,账本因此长期存在归属偏移。根因是 look-up 的种子:
build_explicit_advancement_next_action的候选集合来自quota_runnable_action_candidates(...)+agent_todo_planning_source_items,两者的上游都是 status payload 里有界的展示 lane(primary + 2 alternatives ≈first_executable_items/executable_backlog_items,以及items/deferred/blocker/monitor)。于是"开放、已认领、typed advancement"的行只要落在展示预算之外就查不到,TS reducer 收到candidate=null,只能给出candidate_not_currently_eligible。PR #4512 已经把 look-up 从"仅展示 lane"扩到"规划清单",但那份清单本身仍是有界投影,缺口依旧。改动思路
显式
--todo-id是调用方已经做出的选择,不是从菜单里挑;因此按 id 解析时应当读 Goal 自己的状态行。新增_authoritative_requested_agent_rows(...):用loopx.materials.goal_state_path取目标 Goal 的 state file,用goal_todo_summaries(..., roles=["agent"], status="open", todo_id=<id>)解析这一行,再并入既有 builder 的候选集合。所有 eligibility 谓词(actionable open / task_class / capability / claimed_by)仍在原 owner 里执行,所以这只扩大可达性,不扩大可选项。具体改动
loopx/control_plane/quota/should_run_prepare.py(+47/-10):新增_authoritative_requested_agent_rows;显式选择分支的候选集合追加该来源;状态文件缺失/不可读或 id 不存在时返回空表并保持原有拒绝,不改变主路径。tests/control_plane/test_explicit_todo_id_beyond_presented_lanes.py(+104/-0):3 条断言——展示 lane 之外的行可由 id 解析并被 builder 接受(同时先证明展示 lane 里确实没有它);未知 id / 缺失 state file 解析为空;别的 agent 的行与 blocked 行仍被谓词拒绝。风险边界
--todo-id时读取一次 Goal state(缺失或不可读即跳过),不改变无选择时的热路径,也不新增持久化状态。验证
loopx-meta,本次唤醒 turn id):同一条此前三次被拒的命令现在返回decision=run / qualification=qualified / selected=todo_431983a1e942,回执由 unbound 升级为绑定。quota_action_selection_rejected / candidate_not_currently_eligible(本泳道 16:11Z、19:07Z、21:16Z 三个 turn,均已记录在案)。ruff干净。