fix(quota): name the quota spend a committed writeback still owes - #4834
Conversation
A turn-scoped writeback commits the accountable run, but the Turn is only settled once the quota spend for the same effect identity exists. The refresh result reported success either way, so the debt surfaced only on the next wake as unsettled_host_turn_recovery with a missing quota_spend_receipt. The refresh finalizer now adds a typed settlement_owed block with the effect identity and the exact spend command when no spend run exists for the identity, and adds nothing once the spend is recorded. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Assert the owed block on an unspent identity, nothing once the spend run exists, and that the real refresh-state result carries the step while a dry run does not. 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:b1cfcea815f340a4f07d84a6bf22093b49e89299(base main 09f10c4e3)。无阻断发现,建议合并;合并决定留给 maintainer。
动机
按 problem_context:Turn 级 writeback 会把 accountable run 落盘,但 Turn 只有在同一 effect identity 的 quota spend 存在后才算结算。此前 refresh-state 不管有没有 spend 都报成功,于是这笔债只在下一次唤醒以 unsettled_host_turn_recovery + missing_receipts=[quota_spend_receipt] 出现(本泳道同一天记了三次,都是事后用上一个 turn id 补 spend 修回)。base/head 用同一隔离探针(真实 registry + guard receipt,非 dry-run)实测:
base (09f10c4e3): appended True ok True settlement_owed None
head: appended True ok True settlement_owed {kind quota_spend, effect_id <identity>,
command "loopx quota spend-slot --goal-id ... --slots 1 --source heartbeat --execute --todo-id <id> --turn-instance-id <turn>"}
行里给的验收是"writeback 结果(以及相应 Python spend 路径)说明这笔 Turn 仍欠 quota spend,并重复同一 effect identity 的 spend 命令",本 head 在真实 refresh 路径上做到,因此 problem_context 记为 goal_achieved。
改动思路
按 architecture_flow / repository_reuse / walkthroughs:
- refresh 的最终组装点
finish_external_delivery_refresh本来就持有QuotaSettlementReadback(identity、writeback、spend、spend_run),所以"还欠哪一步"就在同一个所有者里算,不需要把结算知识复制到 CLI。 - 判定只看真实字段:
spend_run is None表示 spend 步骤还没发生 → 报欠;一旦存在 spend run 就不再输出。命令文本复用 typed 计划所有者(build_turn_scoped_cli_settlement_plan+settlement_step_command),不手写第二份命令行。 - 正向:
refresh_state_run -> read_heartbeat_settlement -> finish_external_delivery_refresh -> turn_settlement_owed -> payload.settlement_owed;反向:dry-run 或不 ok 的 payload 不带该块,已有 spend 的身份返回 None,缺唯一 work item 时仍报欠但不编造命令。
具体改动
按 changed_line_classification / symbol_map:生产 1 个文件(refresh_external_delivery.py +71/-1),测试新增 1 个文件(+195/-0)。
关键代码讲解
loopx/control_plane/quota/refresh_external_delivery.py:19turn_settlement_owed—— 从 readback 读出 identity 与spend_run:无身份或已有 spend run 返回None;否则给出turn_settlement_owed_v0块(kind、effect id、goal/agent/todo/replan/turn、reason、recovery_does_not_spend: true)并在恰好有一个 work item 时附上由 typed 计划生成的 spend 命令。- 同文件
finish_external_delivery_refresh—— 在ok且非 dry-run 时挂上该块;既有的 external-delivery 投影与一次性 transition 追加保持不变。 - 同文件 imports —— 引入
SettlementStepKind/build_turn_scoped_cli_settlement_plan,让命令只有一个所有者。
对主干的风险
按 failure_analysis / walkthroughs.negative / validation_matrix / scope_fit:
- 最强回归场景:消费者固定 refresh payload 的键集合。缓解:字段是新增、且只在真的欠费时出现;既有键值不变。
- 触发到观测:同一 fixture 下 base 无该字段、head 有该字段;已有
quota_slot_spentrun 时 helper 返回None;dry-run 不带该字段。 - 爆炸半径:refresh 返回值与其消费者;结算/扣费账本、journal 效果与 quota 策略未动。回滚即 revert 两个提交。
- 生效调用点:
finish_external_delivery_refresh是 refresh-state 的唯一最终组装点(正常与 recovery 两条路径共用)。 - 证据边界(诚实记录):判定读的是 spend 步骤的 run 状态,而我用
read_heartbeat_settlement与真实 refresh 入口验证;没有跑活的 quota store 或真实心跳 turn,payload 的 markdown 渲染也未针对新字段复查。相邻 131 条 + 干净 worktree 70 条通过,ruff干净;未跑全量套件。
我的整体评价
按 observable_semantics / code_volume / change_proportionality / default_off_isolation / authority_semantics:
- 三条对比行分别是"未 spend""已 spend""dry-run":分别给出欠费块、
None、无块。判定码、扣费策略与 recovery 行为完全不变,记为intentional_change_validated。 - 规模与收益相称:一个 derived 字段 + 一个 helper + 3 条断言;无新模块、依赖、配置、状态或 CLI 面,命令文本复用既有 typed 计划。
- 隔离性:无 opt-in/default-off 语义,
default_off_isolation记not_applicable;没有新增或修改任何被自动加载的指令面。 - authority:没有放宽权限——该信号不授权扣费(
recovery_does_not_spend: true),扣费仍需调用方显式执行同一 identity 的quota spend-slot。 - 类型/域中立/义务表述:判定基于 readback 的类型字段而非时间或文本启发式;句子只提 writeback、Turn 与 quota spend,不含 goal/产品/benchmark 措辞。
- 结论:无阻断性 finding。残余风险三条:(1) 判定键在 spend run 状态上,若存在"有 spend run 但从未应扣"的情况会压制信号(偏保守而非错误);(2) 活体 quota store / 真实心跳与 markdown 渲染未覆盖;(3) 证据是有界集合(3 + 131 + 70),未跑全量套件。
English verdict: APPROVE - reviewed head b1cfcea names the owed quota spend on the real refresh path with the exact effect identity and command, adds only a derived response field, and is backed by 3 focused assertions, a 131-case adjacent run, a clean-worktree rerun, a reproduced base counterfactual, and an explicit statement of the unexercised live-quota and rendering dimensions.
动机
一次 Turn 级 writeback 会把 accountable run 落盘,但 Turn 只有在同一 effect identity 的 quota spend 存在后才算结算。此前 refresh-state 的返回值不管有没有 spend 都报成功,于是这笔债只在下一次唤醒以
unsettled_host_turn_recovery+missing_receipts=[quota_spend_receipt]冒出来(本泳道 2026-09-20 出现三次,只能事后补 spend 修回)。base/head 用同一探针(隔离 registry + guard receipt,非 dry-run)实测:
改动思路
refresh 的最终组装点
finish_external_delivery_refresh已经拿到QuotaSettlementReadback(identity、writeback、spend、spend_run 都在里面),因此"这笔 Turn 还欠哪一步"就在同一个所有者里算,不需要把结算知识复制到 CLI。判定只看真实字段:spend_run is None表示 spend 步骤还没发生 → 报欠;一旦有 spend run 就不再输出。命令文本复用 typed 计划所有者build_turn_scoped_cli_settlement_plan+settlement_step_command,不再手写第二份命令行。具体改动
loopx/control_plane/quota/refresh_external_delivery.py(+71/-1):新增turn_settlement_owed(readback, command_prefix=...),返回turn_settlement_owed_v0的 typed 块(kind=quota_spend、effect id、goal/agent/todo/replan/turn、reason、recovery_does_not_spend: true、以及同一 identity 的 spend 命令);finish_external_delivery_refresh在ok且非 dry-run 时挂上该块。tests/control_plane/test_turn_settlement_owed_signal.py(+191/-0):3 条断言——未 spend 的身份给出块与可执行命令;已有 spend run 时返回 None;真实refresh_state_run结果带该块而 dry-run 不带。风险边界
quota spend-slot执行,recovery 自身不 spend(recovery_does_not_spend: true明示)。spend_run;若 identity 缺少唯一 work item,仍报欠但不编造命令。验证
ruff干净。09f10c4e3的文件里settlement_owed/turn_settlement_owed出现 0 次,同一探针在该 base 上返回settlement_owed: None。