Repository navigation
fix(quota): name the earlier Turn that owes closeout in receipt_unbound - #6212
huangruiteng merged 2 commits into
Conversation
Move the prior host Turn closeout candidate selection and the missing-receipt rule out of the preflight into prior_turn_closeout.ts, and expose the heartbeat receipt filter over already-read events, so a second reader of the same settlement snapshot can name the same owed Turn. Behavior-preserving: the preflight result is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lihua <t96vj6547t@privaterelay.appleid.com>
When an earlier Turn of the same Agent owes its closeout, the guard replays that Turn's recovery for every later Turn instead of binding it. The receipt_unbound refusal for the later Turn still told the caller to rebind the current Turn, which only replays, so the operator had to dig prior_turn_instance_id out of the guard payload by hand. The readback now looks for the owed prior Turn in the same snapshot, using the candidate and missing-receipt rule the guard's preflight applies. If it finds one, the refusal names that Turn, its binding and missing receipts, and points to the guard's compiled next_cli_actions. It also adds typed owed_prior_turn and recovery_actions_ref details. Without an owed Turn, the existing rebind and deferred-selection repairs are unchanged. Fixes loopx-project#6110 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lihua <t96vj6547t@privaterelay.appleid.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent | gpt-6.1-sol | OpenAI | runtime_reported | reasoning_effort=xhigh
APPROVE — 未发现阻断项。本结论绑定 090c14c1f9712b438c0761d01142cbbbb2f2794d,独立核验完整 base→head;不是继承作者测试或其它 PR 的批准。
动机
运行连续 Turn 的调用者,在旧 Turn 尚欠回写和额度结算时,需要知道应该先恢复哪一次工作。
此前 A 未结算时,B 的拒绝提示要求重绑 B;这条命令只会重复返回 A 的恢复决定。现在提示明确指出 A、原绑定和缺少的回执,调用者可以先按 guard 的恢复契约结算 A,再继续 B。
实测 refresh-state 与 spend-slot 都指出 A;按原身份恢复后,A、B 各仅扣一次额度,B 的同 Turn 重入返回已结算。
本 PR 修复拒绝提示及其共用选择规则,不自动制造进度、绑定工作、扣额度或完成 Todo;guard 首次提交无绑定回执的单独提示仍属另一边界。
改动思路
候选 Turn 和缺失回执来自现有持久化 heartbeat、run 与 settlement receipts,不增加一份需要人工同步的欠账状态。guard preflight 与结算错误现在复用 quota/prior_turn_closeout.ts;同步快照已有所需事实,无须为提示增加异步传输或第二个 Python 判断源。绑定、借用 authority、恢复 lane 和实际写入继续由原 owner 处理。
选择规则必须共享,否则下一次 closeout 策略变化可能让 guard 和错误提示指向不同 Turn;恢复命令应由已有 interaction contract 编译,因为只读快照不知道 Todo 的恢复 lane。
当前 PR 的边界是共用既有候选/缺失回执规则,并让无绑定拒绝指出真正欠账的 Turn;不改变恢复资格、写入协议或调度策略。
这同时完成了有价值且有界的后续维护整理:将规则集中,而不是新建通用恢复框架。普通已绑定读回不进入历史诊断扫描;无绑定失败才做 lookup,候选按自身绑定读回且禁用嵌套 lookup,避免递归。
具体改动
基线为 be15bf4ffb3bf354fb255853a96f14b047874bb8。6 个文件 +425/-111:4 个生产文件负责规则抽取、快照过滤与错误投影,2 个测试文件验证恢复与反例;没有生成物或公共文档改写。
独立依据为 #6110 的实际误导路径,以及 docs/quota-allocation.md,spec_revision=be15bf4ffb3bf354fb255853a96f14b047874bb8。验收映射:Original Turn identity 已实现,恢复使用 A 原绑定而不让 B 抢占;Receipt-backed closeout 已实现,缺失回执仍拒绝,真实完成后才允许 B;No automatic spending 已实现,诊断不扣额度,重复 spend 也不重复扣款。这些来自修改前的契约,不用新增测试输出反向定义规则。
关键代码讲解
selectCloseoutCandidates(prior_turn_closeout.ts:56)继续逐 Turn 验证有效 heartbeat 身份、按最后追加次序从新到旧选择,并只让已持久化closeout_required的原绑定进入候选;新模块搬运既有策略,未增加默认义务。missingCloseoutReceipts(同文件 :131)共同判断有效 settlement、writeback 与 spend bundle。合法 typed blocked 无扣款 closeout 仍可直接视为 settled;畸形 bundle 仍 fail closed。owedPriorTurnFromSnapshot(settlement_readback.ts:1047)使用原 Goal/Agent 和 accounting-owner fence 过滤完整快照,对候选各自的原身份读回,找到第一项仍欠 closeout 的 Turn;不会从展示分页或 prose 推导欠账。resolveIdentity(同文件 :807)保持receipt_missing、真实绑定 mismatch 等拒绝分支。仅无绑定分支增加owed_prior_turn与recovery_actions_ref,无旧欠账时原 same-Turn/deferred-selection 修复文本保持不变。
对主干的风险
最强反例是“较新已结算 Turn 隐藏较旧欠账”、其它 Agent/authority 的历史污染当前恢复,或旧回执被新增规则强制要求补结算。现有实际文件日志测试覆盖原身份冲突、owner fence、历史次序、无 closeout 的旧回执、monitor、blocked-no-spend 与 replan;两侧 5 个 TS suite 分别 225/225、229/229 通过。两个 Python CLI suite 两侧完整执行,基线 102、head 103 均通过。
独立 public CLI 脚本在相同合成 A/B 输入上比较完整拒绝文本和明细:基线指向 B,head 指向 A;拒绝时零扣款,随后 A 和 B 都完成原身份回写及幂等扣款。新增 real-CLI 回归在基线因“不包含 owed-original”失败,在 head 通过。文本逐条比较了主体、旧 Turn 优先、随后重入、身份来源和不得伪造进度的边界;没有把仅字段/枚举相等当作语义相等,也未声称模型已经采用提示。
语义与 CI 对齐
复用已有 receipt vocabulary 和 TypeScript owner;owed_prior_turn 是诊断投影,不是新 authority 或持久化状态。开发 advisory 未发现新受支持 vocabulary carrier;完整 semantic drift canary 通过。closeout_required 未启用的历史义务保持关闭,已结算历史保持原 rebind,默认调度、quota gate、合法 blocked/monitor 分支均未改变。原恢复义务仍由机器执行;新增文本只是告诉调用者去哪读取准确命令。
本地 tsc、改动 Python 的 Ruff、4 项 direct check 和 15 项风险选择 canary 全通过;没有失败、skip 或 manual hold。未查询、轮询或等待远端 CI。未改 authority store,未运行 PostgreSQL 集成;未涉及新的前端/Lark 渲染或可配置能力。私有 Goal、绝对本地路径、原始日志未带入此公开评审。当前 main 额外合并的 #6210 是既有 release/chat 测试对齐,未改变本 PR 的 quota 生产 owner。
我的整体评价
对持续工作与使用体验的判断均为 improved:调用者能从拒绝走回原 Turn 完成真实恢复,再继续下一 Turn,减少误导重试而不放松结算保障。完整 PR 与 #6110 的有界目标相称;保留 persisted receipt 兼容有实际读回理由,没有平行 decoder 或 Python 规则。新增诊断正常路径不执行,错误路径复用已读取快照;尚未量化超长历史错误路径延迟,也未证明模型理解率,这不改变已验证的恢复链。
无阻断 finding。剩余风险是极长历史下错误诊断成本,以及 guard 自身无绑定提示的独立改进;本 PR 已交付可用的拒绝修复,不以完整父路线图或新增仪式化任务作为条件。批准与平台 aggregate、closeout、merge 权限分开,控制面变更仍由维护者合并。
English verdict: APPROVE
The complete exact-head change fixes the misleading prior-Turn recovery diagnostic and consolidates the existing selector. Baseline/head CLI evidence confirms the intended text/details delta, recovery of A before B, and one debit per Turn under replay. Existing admission and receipt obligations remain intact. Local checks pass; remote CI was not consulted. This review grants no merge authority.
Fixes #6110
Delivery brief
receipt_unboundrefusal points at the wrong Turn.durable_writeback_receipt/quota_spend_receipt, the guard replays that Turn's recovery for every later Turn and never binds it.refresh-state/spend-slotfor the later Turn told the caller to rebind that Turn (quota should-run --turn-instance-id <current> --todo-id …), which only replays. The only way out was to readunsettled_host_turn_recovery.prior_turn_instance_idfrom the guard's output by hand.interaction_contract.cli_channel.next_cli_actions. Its details gain typedowed_prior_turn(the preflight's candidate shape plusmissing_receipts) andrecovery_actions_reffields.loopx/control_plane/quota/settlement_readback.ts). No Python decision logic was added.tests/control_plane/test_interrupted_turn_continuation.py. Guard Turn A, then guard Turn B, which replays the recovery.refresh-statefor B names A. Following A's compiled actions and then settling A lets B bind and settle. This test fails onmain.Design
unsettled_host_turn_recovery.tsintoquota/prior_turn_closeout.ts. The guard's preflight and the readback refusal now share one rule. A test checks that the refusal'sowed_prior_turnequals the preflight'scandidateandmissing_receiptsfor the same runtime.resume_prior_turn/monitor_poll/lifecycle) depends on Todo facts this read model doesn't read, so it points to the guard's compiled actions instead.closeout_required.Vocabulary
receipt_unboundfailure kind and the guard's existing receipt names (MISSING_RECEIPT_NAMES, moved, not changed).owed_prior_turnreuses the preflight candidate shape. No new enum or literal set;generate_semantic_inventory.py --changed-from HEADreports no new vocabulary carriers.Validation
tsc --project tsconfig.control-plane.json --noEmit: clean.tests/control_plane_ts/*.test.ts): 4436 tests, 4399 pass, 0 fail, 37 skipped.quota_settlement_readback.test.ts: 129/129, including 2 new tests (one with 2 subtests).spend-slot: 1260 passed.main'ssettlement_readback.ts, the new CLI test fails at the refusal assertion.loopx canary premerge, PostgreSQL integration (no authority-store change).Surfaces
refresh-state/quota spend-slotwhen they fail withreceipt_unbound. No frontend or Lark surface renders this refusal.Future-facing pass
Applied: the duplicated owed-Turn rule now lives in one module. Not done: the issue's second follow-up, a typed hint when a guard commits a receipt with no binding at all. That's a separate change to the guard's own output.
🤖 Generated with Claude Code