Skip to content

fix(quota): name the earlier Turn that owes closeout in receipt_unbound - #6212

Merged
huangruiteng merged 2 commits into
loopx-project:mainfrom
LIHUA919:codex/settlement-unbound-prior-turn
Oct 11, 2026
Merged

huangruiteng merged 2 commits into
loopx-project:mainfrom
LIHUA919:codex/settlement-unbound-prior-turn

Conversation

@LIHUA919

Copy link
Copy Markdown
Contributor

Fixes #6110

Delivery brief

  • Goal/source: [Bug]: a missing settlement binding is reported against the current Turn even when an earlier Turn owes its closeout #6110. If an earlier Turn hasn't closed out, every later Turn is blocked, and the receipt_unbound refusal points at the wrong Turn.
  • Gap: while an earlier Turn of the same Agent owes durable_writeback_receipt / quota_spend_receipt, the guard replays that Turn's recovery for every later Turn and never binds it. refresh-state / spend-slot for 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 read unsettled_host_turn_recovery.prior_turn_instance_id from the guard's output by hand.
  • Observable result: the refusal now names the owed Turn, its binding and its missing receipts, and points to the guard's compiled interaction_contract.cli_channel.next_cli_actions. Its details gain typed owed_prior_turn (the preflight's candidate shape plus missing_receipts) and recovery_actions_ref fields.
  • Owning boundary: TypeScript quota settlement readback (loopx/control_plane/quota/settlement_readback.ts). No Python decision logic was added.
  • Decisive evidence: a real-CLI regression in tests/control_plane/test_interrupted_turn_continuation.py. Guard Turn A, then guard Turn B, which replays the recovery. refresh-state for B names A. Following A's compiled actions and then settling A lets B bind and settle. This test fails on main.

Design

  • The issue's follow-up note expected this to need an async path. It doesn't: the settlement snapshot already holds the rollout events, so the readback can find the owed Turn synchronously, on the failure path only.
  • Single authority: commit 1 (behavior-preserving) moves candidate selection and the missing-receipt rule out of unsettled_host_turn_recovery.ts into quota/prior_turn_closeout.ts. The guard's preflight and the readback refusal now share one rule. A test checks that the refusal's owed_prior_turn equals the preflight's candidate and missing_receipts for the same runtime.
  • The refusal doesn't guess a repair command. The repair lane (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.
  • The nested readback for each candidate runs under that candidate's own binding, which can never be unbound, and with the lookup turned off, so it can't recurse.
  • If no earlier Turn owes a closeout, the existing rebind and deferred-selection repairs stay byte-identical. Negative tests cover an earlier Turn that already settled and one without closeout_required.

Vocabulary

  • Reuses the existing receipt_unbound failure kind and the guard's existing receipt names (MISSING_RECEIPT_NAMES, moved, not changed).
  • owed_prior_turn reuses the preflight candidate shape. No new enum or literal set; generate_semantic_inventory.py --changed-from HEAD reports no new vocabulary carriers.

Validation

  • tsc --project tsconfig.control-plane.json --noEmit: clean.
  • Full control-plane TS suite (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).
  • Python suites that touch unsettled-Turn recovery, settlement readback or spend-slot: 1260 passed.
  • Mutation checks: with the lookup disabled, the new TS test fails; with main's settlement_readback.ts, the new CLI test fails at the refusal assertion.
  • Not run: loopx canary premerge, PostgreSQL integration (no authority-store change).

Surfaces

  • CLI: the error text and details of refresh-state / quota spend-slot when they fail with receipt_unbound. No frontend or Lark surface renders this refusal.
  • This is a control-plane change, so it's left for maintainer review and merge.

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

Lihua and others added 2 commits October 11, 2026 01:32
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 loopx-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@huangruiteng
huangruiteng merged commit dd49d91 into loopx-project:main Oct 11, 2026
6 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

Development

Successfully merging this pull request may close these issues.

[Bug]: a missing settlement binding is reported against the current Turn even when an earlier Turn owes its closeout

3 participants