Skip to content

fix(claude): bind completed-turn spend to the selected todo and refresh state - #3342

Merged
huangruiteng merged 1 commit into
huangruiteng:mainfrom
NIU-123370:fix/claude-adapter-quota-spend-binding
Aug 21, 2026
Merged

fix(claude): bind completed-turn spend to the selected todo and refresh state#3342
huangruiteng merged 1 commit into
huangruiteng:mainfrom
NIU-123370:fix/claude-adapter-quota-spend-binding

Conversation

@NIU-123370

Copy link
Copy Markdown
Contributor

Fixes both defects from #3341 in the Claude adapter's MCP complete_task settlement path.

Defect 1: every post-completion quota spend-slot was rejected

complete_task built the spend without --todo-id, so the control plane rejected it (quota spend requires --todo-id ... so the accounted turn is bound to the selected todo) on every completion and spent_slots stayed 0 — the quota brake never engaged.

The binding is order-sensitive: passing the just-completed id is also rejected (binding mismatch: selected todo is X but --todo-id is Y), because after a completion the selection advances. The correct id is the todo a fresh quota should-run reports as selected — exactly what the interaction contract's own next_cli_actions command does (interaction_contract.py: selected todo → --todo-id arg).

complete_task now takes a post-completion should-run snapshot and binds the spend to the snapshot's selected_todo.todo_id, mirroring the contract:

snapshot spend
selected todo present spend-slot ... --source heartbeat --execute --todo-id <selected>ok:true, slots increment
no selected todo (terminal) contract fallback: spend without --todo-id, still appended honestly
unparseable snapshot same fallback; nothing is silently dropped

Defect 2: refresh-state never ran on this path

It was only ever recommended via next_cli_actions, so the active-state narrative went stale. complete_task now runs refresh-state --goal-id <goal> --agent-id <agent> after the spend.

Out of scope

The reporter's third rough edge (unregistered agent_id from /loopx arming, hardcoded waiting_on: codex) is explicitly left for its own issue per the report.

Verification

  • tests/test_kunluncode_goal_mode.py: 34 passed (31 pre-existing + 3 new regressions; 1 updated for the new command order)
  • the 4 behavior assertions fail on the pre-fix head (verified via stash), green after the fix
  • tests/canary: 20 passed, no baseline change
  • mypy: zero new errors in goal_mode_mcp.py (one pre-existing no-untyped-def on create_fastmcp_server, unchanged)

Fixes #3341

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

详细中文评审

审查对象:1451d748c4e83337f878646acfa9d3e441b3b892

动机

这个 PR 针对 #3341 的两个真实缺陷:Claude/Kunlun 共用的 complete_task 在 Todo 完成后调用未绑定 --todo-idquota spend-slot,导致配额永远不增长;同时该路径没有执行 refresh-state,使 active-state 的下一动作长期陈旧。修复方向本身是必要的,影响的是 Claude Code/KunlunCode MCP adapter 的完成、记账和状态投影链路,而不是单纯的展示问题。

改动思路

实现把完成后的处理抽到 GoalModeMCPControlPlane._settle_completed_turn:先重新运行 quota should-run,从 selected_todo.todo_id 取出当前选择,再构造 spend-slot,最后运行 refresh-state。正向路径是“完成旧 Todo → 选择后继 Todo → 以其 id 记账 → 刷新状态”;无选择或 snapshot 无法解析时,则退化为无 --todo-id 的 spend。这个抽取让职责比原先内联命令清楚,但它没有复用控制面的 typed settlement 顺序与身份。

仓库现有机器契约不是提示性 guidance:settlement plan 明确要求 validation → durable_writeback → quota_spend,且同一个 Todo/turn identity 必须贯穿 writeback 和 spend;host integration contract 也明确写成 refresh-state 后再 quota spend-slot。当前实现却在 loopx/goal_mode_mcp.py:227-235 先 spend、后做一个不带 classification/outcome/todo/turn 的 quota-neutral refresh,因此没有满足 durable writeback obligation。

具体改动

  • complete_task:完成命令返回 typed ok/completed/done 后,改为调用 _settle_completed_turn。这个入口仍保留了“只有完成成功才尝试记账”的正确保护。
  • _settle_completed_turn:调用 should_run_args 获取 fresh snapshot;随后把 snapshot 中选中的 Todo 附加到 spend。但 fresh snapshot 发生在旧 Todo 已完成之后,所以这里绑定的是后继 Todo,不是刚完成工作的原始 settlement identity。
  • _selected_todo_id:只把 JSON dict + 非空字符串视为选中项;它把“合法终态没有 selected Todo”和“CLI 失败/输出损坏”都压成 None,随后走同一条无绑定 spend 路径。
  • tests/test_kunluncode_goal_mode.py:新增/更新四组断言,覆盖完成后调用顺序、后继 Todo 绑定、无选择 fallback 和 malformed JSON fallback。它们完整覆盖了新 helper 的内部命令拼装,但 subprocess mock 对所有非 completion 命令一律返回 {"ok": true},因此没有执行真实 refresh-state/spend-slot 的 receipt、身份和顺序校验;commands[-1] == refresh-state 还把错误顺序固定成了预期行为。

对主干的风险

[P1] 当前顺序和绑定仍会让配额结算失去因果一致性,核心缺陷没有被可靠修复。 在 typed settlement 路径中,spend 会因为缺少同 identity 的 accountable refresh receipt 而 fail closed;在 legacy inference 路径中,它可能消费最近一条旧的 unspent accountable run,随后才写入本轮 state-only refresh,造成记账滞后一轮或错绑到后继 Todo。这样 spent_slots 仍可能不增长,或者增长却代表错误的工作项,配额 brake 与审计都会失真。

最小修复应当从 mutation 前的 authoritative selection/settlement packet 取得原始 Todo identity,并让 accountable refresh-statespend-slot 使用同一个 Todo/turn;顺序必须是 writeback 后 spend,再做 Todo lifecycle closeout/必要的 quota-neutral projection refresh。优先复用现有 settlement plan/driver,不要在 adapter 中另造一套命令顺序。对应测试应至少有一条真实 CLI 或 contract-level seam,证明缺少 writeback receipt 时 spend 会失败、修复后同 identity 的 writeback/spend 成功,且后继 Todo 不会承担前一 Todo 的配额。

[P1] malformed snapshot 不能等同于合法终态并继续无绑定 spend。 这是状态分类规则,不应由 None 同时表达“terminal”与“invalid”。请使用 typed result(例如 selected / terminal-no-selection / invalid),对 invalid fail closed 并原样暴露 blocker;合法终态也应通过原始 settlement identity 完成已发生工作的记账,而不是无绑定猜测。

验证矩阵:精确 head 的 tests/test_kunluncode_goal_mode.pytests/control_plane/test_quota_settlement.pytests/test_ark_managed_agent_issue_fix_matrix.py63 passed;远端 Sign-off、dependency-review、pytest、build、windows-powershell 均成功。未独立验证真实 Claude host 端到端运行;上述 P1 来自 exact-head 命令序列与仓库 typed settlement/host contract 的直接矛盾,而不是 CI 状态推断。

我的整体评价

改动规模(2 文件,199 additions / 16 deletions)集中,抽 helper 和补回归测试都具备可维护性;问题在于测试验证了实现自身,而没有验证控制面权威语义。当前不能合并。请先把原始 Todo/turn identity、accountable writeback、quota spend 和 lifecycle closeout 收敛到同一 settlement contract,并把 malformed snapshot 改为显式失败状态;修订后我会按新的 exact head 复审。

English verdict: REQUEST_CHANGES on exact head 1451d748c4e83337f878646acfa9d3e441b3b892: the adapter spends before an accountable refresh and binds the completed turn to the successor Todo; malformed snapshots also fall through to an unbound spend. Relevant tests pass (63/63) but mock away the production settlement guards, so the PR needs same-identity writeback-before-spend coverage before merge.

@huangruiteng

Copy link
Copy Markdown
Owner

补充一个从历史演进和多 host 架构角度的判断,供这个 PR 继续探索。前一条 review 中的 same-identity / writeback-before-spend blocker 仍然成立;下面主要解释它为什么会出现,以及怎样避免只修 Claude 的一个症状。

根因不是单纯漏了 --todo-id

我回溯了这条路径:

  1. 最初的 Claude adapter(Add loopx mode for Claude Code CLI #604)已经采用了 adapter 内手工编排的 todo complete -> quota spend-slot,没有 refresh-state,spend 也没有显式 Todo/turn identity。当时旧控制面仍可做部分 legacy inference,因此缺陷没有完全显性化。
  2. 后续 fix(quota): bind heartbeat spend to selected todo #2989 收紧为 heartbeat spend 必须绑定 Todo,feat(quota): add typed Codex App settlement #3016 又建立了 typed settlement,要求 validation -> durable_writeback -> quota_spend 共享同一个 settlement identity。这些核心变化是正确的,但没有触发 Claude adapter 的迁移。
  3. Integrate KunlunCode native Goal Pro with LoopX #2791 将旧 Claude MCP 逻辑抽为 Claude/Kunlun 共用的 goal_mode_mcp.py。该分支开始于核心合同变化之前、合入于变化之后,代码层面能够 rebase,语义层面却保留了旧合同。这是一次 semantic merge drift。

所以 #3341 暴露的是:LoopX 已经有多 host 的注册、runtime profile 和 read path,但关键 write/settlement path 仍允许各 adapter 自己拼 CLI,核心合同升级时没有统一的 conformance gate。

建议本 PR 探索的窄路径

本 PR 不必一次重构所有 host,但建议不要再从“完成后的 fresh should-run”猜 Todo。可以围绕以下最小闭环修改:

  • 在 Todo mutation 之前取得 authoritative settlement packet,保留原始 goal_id / agent_id / todo_id / turn_instance_id
  • 让 accountable writeback、quota spend 和 terminal closeout 消费同一 identity,并遵循控制面已有的 effect-program 顺序;
  • adapter 只负责把 MCP 输入转换成核心 settlement 调用,不再复制一套 host-specific 命令序列;
  • selectedterminal-no-selectioninvalid snapshot 表达成显式 typed result;invalid 必须 fail closed,不能退化为无绑定 spend;
  • 至少补一条 contract-level 或真实 CLI seam:证明缺少 matching writeback 时 spend 被拒,完整链路后 spent_slots 与 active-state readback 同时更新,而且 successor Todo 不会承担前一 Todo 的消费。

仓库已经有 settlement_driver、turn-driver settlement 和 effect-program fault/replay conformance,可以优先判断能否复用或提炼一个 provider-neutral settle_turn contract,而不是继续扩充 _settle_completed_turn 的 CLI orchestration。

建议拆出的结构性 follow-up

当前 typed settlement projection 仍由 _codex_app_settlement_plan() 生成,只对 Codex App heartbeat profile 生效;现有 adapter-conformance 也主要覆盖 quota、turn driver、task lease,没有把 Claude/Kunlun MCP 等 shipping host adapter 纳入同一矩阵。这部分可以另开后续 PR:

  • 将 settlement plan 从 Codex-App-specific projection 提炼为 capability/profile 驱动的 provider-neutral plan;
  • 对 Claude MCP、Kunlun MCP/native、generic CLI、Codex App 建立统一 conformance:identity、顺序、失败短路、幂等、terminal/no-follow-up、最终 quota/state readback;
  • 核心 settlement/quota/todo 合同变化时自动运行这张矩阵;
  • 每个 Tier-1 host 保留一条低成本真实 host writer-to-reader smoke,避免 subprocess mock 只能证明命令被拼出来。

这不是在否定本 PR 的动机:#3341 是真实且需要修复的用户问题,抽 helper 也改善了局部可读性。关键是不要把“旧 Todo 的完成”绑定到“新选出的 successor”,也不要让这个修复继续成为另一套与核心 effect program 平行的 settlement 实现。

建议 delivery boundary:本 PR 先完成 Claude/Kunlun 共用 MCP 的 same-identity 正确接入与可执行回归;全 host conformance 和 Codex-specific plan 泛化可以拆成明确 follow-up。当前 merge decision 仍为 hold / request changes,修订后可按新 head 复审。

NIU-123370 added a commit to NIU-123370/loopx that referenced this pull request Aug 21, 2026
GH-C85 / huangruiteng#3082 follow-up: keep the review-approved real-control-plane negative regression proving that MCP complete_task inherits the declared-command validation gate. A failing command returns the typed validation_blocked_completion receipt, leaves the Todo open, and skips post-completion settlement. The branch is now test-only on main; positive settlement coverage remains with the blocked huangruiteng#3342 dependency instead of accepting failed receipts as success.

Signed-off-by: 牛瑞博 <912906590@qq.com>
@NIU-123370
NIU-123370 force-pushed the fix/claude-adapter-quota-spend-binding branch from 1451d74 to 3749ce9 Compare August 21, 2026 11:02
Resolve and persist one turn-scoped settlement identity before the MCP completion mutates the Todo frontier. The provider-neutral control-plane path now validates lifecycle completion, records accountable refresh-state writeback, and spends quota in order against the original Todo.

Fail closed on malformed, terminal-without-selection, mismatched, and incomplete receipts. Replays recover the original binding from the heartbeat receipt so a successor Todo is never charged and quota spend remains idempotent.

Add real CLI coverage for missing-writeback rejection, successful writeback/spend, successor selection, and replay, while preserving the declared completion-validation gate.

Fixes huangruiteng#3341

Signed-off-by: 牛瑞博 <912906590@qq.com>
@NIU-123370
NIU-123370 force-pushed the fix/claude-adapter-quota-spend-binding branch from 3749ce9 to 6b35e19 Compare August 21, 2026 11:09
@NIU-123370

Copy link
Copy Markdown
Contributor Author

已按 review 和后续架构建议重做,当前新 head 为 6b35e19。请按新 head 复审。

这次不再在完成后重新猜 selected_todo,也不再 fallback 到无绑定 spend:

  • mutation 前通过 turn-scoped quota guard 固定 goal/agent/original todo/turn identity;重试时优先读取持久化 heartbeat receipt,后继 Todo 不会接管旧消费;
  • goal_mode_mcp 只把 MCP 输入翻译成 HostTodoSettlementRequest,结算顺序移到 provider-neutral control-plane 模块;
  • 普通链路为 guard → todo validation/lifecycle completion → 同 identity accountable refresh-state writeback → 同 identity quota spend;任一步 malformed、identity mismatch 或 receipt failure 都 fail closed;
  • no-follow-up 复用现有 same_turn_terminal_closeout:先做一次普通 completion 运行 validation,再 writeback/spend,最后以同 identity 升级 terminal closeout,因此不会在 validation 失败前扣额;
  • selected、terminal-no-selection、invalid snapshot 为显式 typed 状态;invalid 不再退化成 unbound spend;
  • 增加真实 CLI seam,覆盖 missing writeback 拒绝、完整链路 spent_slots=1、successor 不被扣额、幂等 replay,以及 terminal closeout 四步 receipt。

本地验证:相关 MCP/quota/refresh/validation 套件 75 passed;全量套件首次 3641 passed,52 个环境型失败在固定 Python 3.12、PATH、端口/运行目录权限后逐项全部通过;Ruff 与 LoopX contract check 通过。全 host conformance / Codex-specific plan 泛化仍按建议保留为结构性 follow-up。

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

详细中文评审

审查对象:6b35e196241e06357293468a4dcdfdf781dc5015

未发现阻塞性问题。

动机

这个 PR 修复 Claude/Kunlun 共用 MCP complete_task 的两个实际结算缺陷:旧实现完成 Todo 后用未绑定 Todo/turn identity 的 quota spend-slot,在当前 quota contract 下会被拒绝;同时没有 accountable refresh-state,所以 quota brake 与 active-state 投影都可能停留在旧状态。受影响的是所有通过 GoalModeMCPControlPlane.complete_task 完成工作的 shipping host adapter。与旧 head 相比,新实现不再在 mutation 后从 successor 猜 identity,而是在 mutation 前取得原始 Todo 的 turn-scoped guard;完成后让同一 identity 贯穿 validation、writeback、spend 和可选 terminal closeout。这个问题不能靠只补一个 --todo-id 解决,因为真正需要保持的是整条 receipt chain 的顺序与因果身份。

改动思路

实现将 host adapter 的结算编排收敛到 provider-neutral 的 host_adapter_settlement 模块,MCP 层只负责把 goal/agent/todo、runtime profile 和原始 completion 参数翻译成 HostTodoSettlementRequest。权威输入是 mutation 前的 quota should-run guard;决策 owner 是 settle_host_todo_completion,它只接受与请求 Todo 一致的 selected state,并以稳定的 SettlementIdentity 执行 todo complete → accountable refresh-state → quota spend-slotno_follow_up 先做普通 completion 以产生 validation receipt,writeback/spend 成功后才以同一 completion key 做 same_turn_terminal_closeout

正向路径是:MCP 完成请求触发 guard,原始 Todo 被验证并完成,refresh-state 写入同 identity 的 durable receipt,quota 记一槽,最后返回一个 typed settlement envelope;有 successor 时它保持 open,且 successor 不承担前一 Todo 的消费。负向路径是:malformed guard、terminal-no-selection、Todo mismatch、effect mismatch、缺失 writeback receipt、spend 拒绝或 terminal closeout receipt 不完整时都在对应 stage fail closed,并返回 settlement_blocked_completion,不会退化为 unbound spend。

具体改动

  • loopx/control_plane/host_adapter_settlement.py 新增 454 行 provider-neutral orchestration,集中保存 guard state、request schema、retry-stable turn id、identity matching、blocked payload 和完整结算顺序。
  • loopx/goal_mode_mcp.py 删除 adapter 内旧的 completion-success + unbound spend 拼接,改为调用共享结算入口;Claude 与 Kunlun 的 active call site 都走这条路径。
  • loopx/cli_commands/project_lifecycle.py 将已有 completion_todo_id / completion_turn_key 语义接到 refresh-state CLI,用当前 completion identity 识别“本次完成刚产生的 replan obligation”,避免把它误判为未完成的旧 replan。
  • tests/test_goal_mode_mcp_settlement.py 增加真实 CLI seam,覆盖缺 writeback 拒绝、同 identity 完整结算、successor 不扣额、幂等 replay,以及 terminal closeout 的四步 receipt。
  • tests/test_kunluncode_goal_mode.py 更新 MCP mock contract,并覆盖命令顺序、typed identity、terminal/invalid guard 的失败短路。

关键代码讲解

  1. HostGuardState / classify_host_guard_snapshot(19、62 行)把 selectedterminal_no_selectioninvalid 做成显式枚举;malformed payload 与合法终态不再共用 None,降低错误 fallback 的误分类风险。
  2. host_adapter_turn_instance_id(55 行)从 goal/agent/todo 生成 retry-stable、public-safe turn id。重试时同一 Todo 会恢复同一 heartbeat receipt,避免 successor 或另一轮工作接管消费。
  3. settle_host_todo_completion(185 行)是行为主入口:223 行校验 guard,254 行执行 lifecycle completion,287 行写 accountable refresh,341 行记 quota,392 行以后完成同 turn terminal closeout;每一步都核对同一个 effect id 与 typed receipt。
  4. GoalModeMCPControlPlane.complete_task(201 行)现在只是 adapter translation boundary,不再复制 quota/writeback 的 host-specific 顺序。
  5. refresh-state 的 hidden completion flags(project_lifecycle.py 301、305 行)只把既有 semantic-replan 参数接入 CLI;真正的 completion identity 仍由 current Todo row 与 effect id 共同验证,不能靠 prose 绕过 obligation。

对主干的风险

当前 strongest regression scenario 是“Todo 已 durable complete,但后续 writeback/spend 的输出丢失或被拒”。新路径通过稳定 identity 与 idempotent receipt readback 允许安全重试;失败会暴露具体 stage,blast radius 限于该 Todo 的 quota/state settlement,不会把额度记到 successor。另一个风险是 output shape 从旧的拼接文本变为 typed JSON envelope;这是 MCP model-facing contract 的可观察变化,PR 描述与更新后的测试已经明确披露。

代码量方面,生产改动约 482 additions / 29 deletions,测试约 570 additions / 13 deletions;其中 454 行集中在一个新 orchestration 模块,后续仍可把 command builder 与 receipt predicate 再压缩到共享 settlement driver,但当前模块有 Claude/Kunlun 两个真实 caller、没有 speculative provider,且把原本散落的 identity/order/failure contract 收到一个可回滚边界,本 PR 不需要为了代码量再拆散行为闭环。

验证矩阵:exact head 的 Ruff(5 个 changed paths)通过;focused settlement/Kunlun/quota suite 在本地完整依赖环境中为 58 passed,另外 2 个 real-CLI fixture case 被本机 status-health adapter gate 拦在 settlement assertions 之前;在最小 exact-source 环境中这 2 个 case 单独为 2 passed。远端 clean-checkout 的 exact-head pytestwindows-powershell、Sign-off、dependency-review、build 均成功。直接对新模块做 strict mypy 时只触发 todos/contract.py 中 6 个既有 import-closure 错误;仓库配置的远端 mypy 已随 pytest job 通过。未独立运行真实 Claude host 的交互式端到端会话,残余风险主要是 installed host 的进程级重试体验。

我的整体评价

前一轮的两个 P1(successor identity、spend-before-writeback)以及 malformed snapshot fallback 都已在这个 exact head 上关闭。实现把强制 obligation 表达为 typed guard 与 receipt checks,核心错误文案保持 goal/domain neutral,默认行为变化也在 PR body 与 regression tests 中可见。整体设计比 adapter 内继续拼 CLI 更通用,真实 CLI seam 证明了 quota 与 state readback 的结果,不只是 mock 命令顺序。结论为 APPROVE;这只是复审结论,不授予或执行 merge authority,后续合并仍应遵循仓库门禁与 maintainer policy。

English verdict: APPROVE exact head 6b35e196241e06357293468a4dcdfdf781dc5015: the revision now preserves the original Todo/turn identity across guard, validation, accountable writeback, quota spend, and terminal closeout; malformed or mismatched state fails closed, focused coverage is substantive, and all remote required checks are green. No blocking finding remains; merge stays subject to repository policy.

@huangruiteng
huangruiteng merged commit 6e7dd0a into huangruiteng:main Aug 21, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants