Skip to content

fix(quota): fail closed on unreadable ledger events - #7679

Open
gcl-coder wants to merge 5 commits into
loopx-project:mainfrom
gcl-coder:fix/issue-6869-unreadable-quota-event
Open

gcl-coder wants to merge 5 commits into
loopx-project:mainfrom
gcl-coder:fix/issue-6869-unreadable-quota-event

Conversation

@gcl-coder

Copy link
Copy Markdown
Contributor

Problem

The quota spend ledger silently dropped quota events it could not read back.

load_quota_event_from_run() returns None when an indexed
quota_slot_spent / quota_slot_voided row points at an artifact that is
missing, truncated, not UTF-8, not a JSON object, or that carries no
quota_event at all. goal_quota_with_spend_ledger() treated that None as
"this event spent nothing", so spent_slots came out lower than reality and
quota_status() could report a Goal as eligible again after it had already
spent the slots. The same read path is used by the usage summary, so the
dashboard reported the same silent loss.

The real slot count of an unreadable event is unknown, so it cannot be
invented. But "unknown" must not degrade into "zero": that is exactly the
fail-open that hands a Goal back quota it already used.

Fix

Distinguish a legitimate zero-spend event from an unreadable one, and fail
closed on the latter.

  • loopx/control_plane/quota/ledger_readback.py
    • New resolve_quota_event_from_run() and quota_slot_observation(): they
      return the event and a machine-readable reason when the event cannot be
      read back. The reasons are stable constants
      (missing_source, artifact_missing, artifact_unreadable,
      artifact_invalid_json, artifact_not_object, event_missing,
      event_invalid).
    • The artifact is now read as bytes and decoded explicitly, so a
      UnicodeDecodeError is reported instead of escaping.
    • quota_slot_contribution() now delegates to quota_slot_observation() so
      the two surfaces cannot drift; its return value is unchanged.
  • loopx/quota.py
    • goal_quota_with_spend_ledger() counts unreadable events and reports
      unreadable_event_count plus unreadable_event_reasons instead of
      inventing a slot count.
    • quota_status() maps a non-zero unreadable count to
      state=blocked_health with blocked_action_scope=automatic_agent_turns.
    • _apply_unreadable_ledger_fail_closed() keeps the plan layer from
      re-admitting a Goal through a quota projection that was computed before
      the artifact became unreadable.
  • loopx/control_plane/quota/usage_summary.py
    • Reports unreadable_event_count_24h / unreadable_event_count_7d next to
      the readable slot totals.
  • docs/status-data-contract.md documents the new fields and the fail-closed
    rule.

Behaviour

Ledger state Before After
All events readable spent_slots counted unchanged
One indexed event unreadable counted as no spend, Goal could read back as eligible counted in unreadable_event_count, Goal reads as blocked_health
Artifact restored — ledger and state return to normal on the next read

Tests

tests/control_plane/test_quota_ledger_integrity.py (new, 16 cases) covers the
missing artifact, the truncated artifact, the artifact without a quota event,
the non-object artifact, the index row with no event source, the non-UTF-8
artifact, an unreadable void, the fail-closed quota state, the stale-projection
bypass in the plan layer, the repair path, the legal void path, and the
distinction between a legal zero-spend event and an unreadable one.

The related quota suites show the same failure set before and after this
change (the pre-existing failures are the local EffectRuntimeHostPermissionError
environment failures), and ruff check is clean on the touched files.

Fixes #6869.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.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=max

动机 / Motivation

依赖滚动额度限制自动工作的用户和 Agent,通过 quota plan、should-run 和 history 读取已消费预算。 原先丢失一个已索引的消费文件,会把已用额度12/12读成11/12并重新允许执行;现在截断文件被拦住,但内容仍为 object、缺失事件类型的文件仍会放行。 可观察交付是识别部分不可读消费文件并阻止自动执行;完整的失败关闭和错误原因读回仍未成立。 范围限于现有消费账本的只读解释和额度投影;消费与撤销的 TS 事务、费用测量和宿主采用仍由原边界负责。

改动思路 / Approach

保留同一只读账本 owner,并让预算与 usage 共享 observation 是合适的边界;最小修复应在这个 owner 验证事件并完整转送原因,无需增加故障库或重写事务层。 范围限于现有消费账本的只读解释和额度投影;消费与撤销的 TS 事务、费用测量和宿主采用仍由原边界负责。真实 File/SQLite CLI 对截断、非 UTF-8、非 object 和缺失 event 的 artifact 给出 blocked_health;原件恢复后回到原预算状态。

具体改动 / Changes

精确 head 0269323f57d1d801f73d3092f8bd0d8b1fc6e5d0,完整比较基线 f2990e3de49d6aa7441e0e0b739f49bfc84aae38。验收依据 https://github.com/loopx-project/loopx/issues/6869,版本 2026-10-23T12:33:09Z;先读原合同/问题,未用本次新写的文档替代独立规范。A spend that cannot be read should not silently become no spend 在此 head 为 not_met:缺失事件类型的正槽数对象仍会放行。新增原因字段还存在下述公开投影缺口。 五个路径的全部改动及相关合同条款均已阅读;改动行数只用于定位成本,结论来自真实 caller 与反例。quota_slot_contribution 保持 compatibility export,既有导入者仍可使用原接口。

关键代码讲解

对主干的风险 / Risks

P1:非法事件仍被当零消费。 保持 index 为 quota_slot_spent,把一个原消费文件变成 {"quota_event":{"slots":1}}。两种 backend 的真实、带已认领 Todo 和明确 agent 的 should-run 都从12/12变成11/12,返回 normal_run、should_run=true。未知 event_type 的 plan 同样 eligible。这是本问题仍未覆盖的失败关闭路径;在可读 object 后继续验证事件类型和必要字段,并单独保留合法零消费。

P2:原因列表丢失。 截断和非 UTF-8 文件在 ledger 中有原因,但 goal_quota_config 只复制 count;后续已是 blocked_health 又提前返回。真实 plan 和 scoped guard 的 quota 均缺失 unreadable_event_reasons。保留经验证的原因并在恢复后清除,才能兑现本次文档中的错误读回。

161项 focused Python 和 Ruff 通过。相邻六个 recorder keyword 失败在不可变 PR base/head 均为6 failed、100 passed,完整规范化失败段相同;这是独立的现有测试问题,不要求此 PR 修复。严格 CQR 的当前目标基线归因未单独资格化,其 hold 仍保留。 native standard premerge FAIL/HOLD:六项直接检查、17项选择检查通过;cli-output-budget-regression-smoke.py 在原120秒限制内 timed_out,尚未归属。当前 CQR 因已验证缺口失败,不能把这些红项写成通过。 不查询或等待 GitHub CI;本地失败、缺少资格和 merge readiness 分开保留。

我的整体评价 / Verdict

当前任务尚未完成验收:截断文件得到拦截,但缺失事件类型的对象仍可重新放行已耗尽额度,错误原因也未到达公开读回。应在现有账本解释和投影 owner 内补齐两项修复,用同一真实 CLI 路径重验整个 PR。没有 rendered presentation 变化;安装宿主、实际模型执行、长期效果和完整成本未资格化。相邻测试的既有失败与尚未归因的本地超时分别保留,不抵消上述可复现缺陷。

English verdict: REQUEST_CHANGES — malformed indexed quota events still reopen exhausted quota, and public diagnostics lose their reasons.

return None
return None, QUOTA_EVENT_UNREADABLE_EVENT_INVALID
return ("voided", voided_run_generated_at, slots), None
return None, None

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.

[P1] 将缺失或未知事件类型作为不可读事件。保留 quota_slot_spent 索引,把一个消费 artifact 改为 {"quota_event":{"slots":1}} 后,真实 File/SQLite scoped should-run 均把 12/12 读成 11/12,返回 normal_run、should_run=true;未知 event_type 的 quota plan 也为 eligible。这里的 fallthrough 仍将畸形正槽数事件归零,重新放行已耗尽预算。请验证事件类型和必要字段并返回 event_invalid,同时区分合法零消费与撤销,并补上同一公开 CLI 路径的反例。

Comment thread loopx/quota.py
Comment on lines +375 to +377
unreadable_event_count = _int_number(raw.get("unreadable_event_count"), default=0)
if unreadable_event_count > 0:
payload["unreadable_event_count"] = unreadable_event_count

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.

[P2] 保留不可读事件的原因列表。截断或非 UTF-8 artifact 已在账本投影中产生 unreadable_event_reasons,但这里只复制 count;后续 _apply_unreadable_ledger_fail_closed 遇到已是 blocked_health 又提前返回。真实 File/SQLite plan 和 scoped should-run 因而只显示 count=1,缺少本次文档承诺的原因读回,无法据此判断修复方式。请在规范化及该 fallback 中保留经验证的原因,恢复原件后清除原因,并验证完整公开投影。

@mergify

mergify Bot commented Oct 30, 2026

Copy link
Copy Markdown

Hi @gcl-coder, the DCO Sign-off check did not pass. Please inspect
its details first: checkout, fetch, timeout or infrastructure errors
need their own recovery, not a rewrite of otherwise signed commits.

If the log confirms a missing Signed-off-by trailer, amend the
affected commit with git commit --amend -s; for multiple commits,
use an interactive rebase against the current base from the correct
base-repository remote and sign off each affected commit. Push the
rewritten PR branch with git push --force-with-lease origin HEAD.

…sons

The first fail-closed pass covered artifacts that could not be read, but an
indexed event that reads back and still names no known event type fell
through as a zero contribution, which reopened an exhausted quota. The
ledger now reports such an event as `event_invalid`, while a readable event
that names a known type and spends zero slots stays a legal zero.

The public projection also kept only the count: `goal_quota_config` dropped
the reason list, and the fail-closed fallback returned early for an already
blocked Goal, so the reason never reached the status or plan read-back. The
count now travels with the validated reasons.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.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=max

[P1] 不要把缺失或非法 slots 当作合法零消费。 loopx/control_plane/quota/ledger_readback.py:173–177 仍以 default=0 解析额度。把已索引消费文件的 slots 删除,或改为不能解析的字符串,当前真实 File/SQLite CLI 都从耗尽的12/12读成0/12;独立新 fixture 的 scoped guard 返回 normal_run、normal_delivery_allowed=true,并提交准入回执。应区分未知量与明确合法的零,在既有 owner 报告 event_invalid,保留支持的合法历史表示与显式零,并补真实入口回归。

动机 / Motivation

依赖滚动额度限制自动工作的用户和 Agent,通过 quota plan、history 和具名 Todo 的 should-run 读取预算。

原本已消费12/12额度;只删除消费文件中的 slots 后,当前版本仍把它读为0/12并重新允许工作。

截断文件、缺失或未知事件类型现在会阻止自动执行并公开原因;缺失或不可解析的 slots 仍会静默放行。

范围是现有消费账本解释与额度投影;不新增预算、修改事务 owner 或认证已安装宿主及长期收益。

丢失消费证据后继续执行,会使滚动预算失去限制作用;修复必须恢复可信额度,不能猜一个消费数。上述探针只观察准入,没有继续业务或新增扣额。

改动思路 / Approach

同一账本只读 owner 同时服务额度和 usage 是合适边界;保留现有贡献兼容别名,补齐字段校验即可,不需要新故障库、执行器或框架。

当前PR限于现有账本只读解释和公开额度投影,不改变消费/撤销事务与宿主权限。

先解析物理文件或 inline event,再由同一 observation 生成贡献及错误,额度与 usage 均消费它;原 TS 消费/撤销事务没有改写。新健康计数是每次读取派生的投影,无需人工同步另一份状态。plan 对旧 eligible 投影再检查当前账本,能够覆盖旧投影遗漏错误的情况。

输入 当前结果 独立判断
健康12槽 / 修复原件 12/12,throttled 保留原预算
截断 / 缺失或未知类型 blocked_health,原因可见 本轮修复成立
类型合法、slots缺失或不可解析 0/12,normal_run 原失败关闭任务仍有缺口
明确合法零 eligible、无损坏计数 保留合法零语义

表格来自真实命令观察,不是流程图或采用证明。

具体改动 / Changes

审查 head e40353daff1db29cf3a2f01e8a628efaeebb39f0,实际共同基线 f2990e3de49d6aa7441e0e0b739f49bfc84aae38;最新目标 main 为 2f544f969236f5dbbe858720108c41d07be60a6c。独立接受依据为 issue #6869,spec_revision 2026-10-23T12:33:09Z。先读原问题及上一轮评审,再检查差异;新写的文档没有被当作独立规范。

  • Expected — not_met: 未知消费不能静默变成零,plan/history 至少应显示不可读计数。截断及类型缺失已满足;slots缺失/非法仍未满足,最小修复见首段。
  • Trigger honesty — implemented: 合成输入明确模拟损坏或部分恢复,没有声称同版本原子写会自然撕裂。

完整5文件 +792/-27:三生产文件 +257/-27,新增测试模块516行/21例,状态合同19行。相对上次 0269323f57d1d801f73d3092f8bd0d8b1fc6e5d0,四路径 +190/-13;当前完整提案重新判断,没有继承旧结论。旧P1中的缺失/未知事件类型、旧P2中的原因列表转送已由本头实际命令验证解决;剩余finding是同一失败模型下尚未验证完整的必需数值字段。

关键代码讲解

旧 quota_slot_contribution 兼容导出仍是同一函数的别名,内部委托共同observer。已检查history、slot_accounting、status/plan及既有额度结果消费者,没有要求新增配置或编辑页面。

对主干的风险 / Risks

最强反例不是未知事件类型,而是已知消费类型缺少数值。_int_number 把读取失败改成0,后续分支便无法区分合法零和未知预算。两种实际后端、缺失及非法字符串共四个独立新guard fixture 均提交准入;不是借前一个未结轮次的恢复回执证明新执行。原完整Todo及消费index逐字保持,探针未实际消耗额度。

同一不可变基线/head各8种输入、File/SQLite共32个case,执行64次CLI;另外10个独立guard fixture验证上述四失败及健康/类型/零控制。六个健康、明确零、恢复后完整quota对象与基线逐字相同;截断和类型缺失在基线放行而当前阻断,证明探针对已修复缺陷敏感。首次顺序探针后续命令曾进入正常的原轮次恢复,已保留该事实;最终新准入判断使用独立fixture,未混用结论。

PASS:三个完整focused模块 106项通过,四个变更Python文件Ruff、配置指定17文件mypy及完整差异空白检查通过。保留一项既有Hypothesis目录收集warning。UNVERIFIED:本头完整standard premerge未在确定阻断后重跑,不把旧头120秒超时或六个recorder keyword失败换名为当前结果,也不把旧通过项继承为本头通过。没有查询、轮询或等待远端CI。当前新增提交还缺少 Signed-off-by trailer,是另一个合并前须修复的贡献规范问题。

语义与 CI 对齐

新原因词汇扩展现有只读诊断,额度状态和执行权仍复用既有owner。健康阻断是机器执行约束;不存在“仅建议不执行”的新解释。修复公开披露了默认变化,但声明“必需字段非法也报告event_invalid”尚未由实现兑现。补字段校验及两方向反例即可,不需要重写整个控制面或增加权限门。当前失败是已证明的不完整修复;其余未执行环境保持未验。

我的整体评价 / Verdict

REQUEST_CHANGES。 完整提案有明确有用增量、边界与维护成本相称,但同一个预算失败关闭请求尚未完成。long_horizon 与 user_experience 均为 not_yet_proven:读回为可用额度仍可能导致额外自动工作,不能由测试全绿或更完整类型判断代替实际反例。

同一账本只读 owner 同时服务额度和 usage 是合适边界;保留现有贡献兼容别名,补齐字段校验即可,不需要新故障库、执行器或框架。 当前PR限于现有账本只读解释和公开额度投影,不改变消费/撤销事务与宿主权限。

请在共同observer内完成slots的缺失/解析失败判断,保留明确合法零,再重验完整plan/history/scoped guard及必要标准检查。检查CLI、history、status及现有zcode quota结果消费者;没有渲染组件、首屏或布局变化,视觉评审不适用。视觉未验证范围:未执行浏览器、已安装App/Lark或原生Windows;没有改变渲染布局。真实宿主采用、PostgreSQL与持续完整成本未在本轮资格化;本评论不授予合并或安装权。

English verdict: REQUEST_CHANGES — e40353d. Known quota events with missing or unparseable slots still reopen exhausted quota; four fresh File/SQLite guards admit normal delivery. Prior type/reason findings are fixed, 106 focused tests pass, and healthy/zero/restored controls retain baseline quota. Complete amount validation and real-entry regression coverage before requalification; current full standard premerge remains unverified.

return None
return ("spent", run_key, slots)
return None, QUOTA_EVENT_UNREADABLE_EVENT_INVALID
slots = max(0, _int_number(event.get("slots"), default=0))

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.

[P1] 区分未知 slots 与明确合法零。此处 default=0 会把缺失 slots 或 slots="lost" 当作零消费;保持 index 为 quota_slot_spent,真实 File/SQLite 的12/12耗尽额度会读成0/12,独立新 scoped guard 均 normal_run 并提交准入。请在共同observer验证字段存在及可解析性,未知量报告 event_invalid,保留显式合法0和受支持的历史表示,并补 physical artifact→plan/history/scoped guard 回归。

@gcl-coder
gcl-coder force-pushed the fix/issue-6869-unreadable-quota-event branch from e40353d to bc3a65b Compare October 30, 2026 02:51
The base/head CLI output differential budgets the agent-facing status JSON.
Two always-present counters in the usage summary grew that surface past its
growth allowance on every scenario, even when nothing was unreadable.

The counters now appear only when a quota event could not be read back, so
the healthy summary keeps its original shape and the growth budget applies
only to the integrity failure it describes.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
@gcl-coder

Copy link
Copy Markdown
Contributor Author

Both review points are addressed in bc3a65b0, with one follow-up in 8f331e3c.

P1 — a malformed quota event is no longer read as a zero spend

quota_slot_observation() in loopx/control_plane/quota/ledger_readback.py now reports an indexed quota event it cannot interpret as unreadable instead of returning no contribution:

  • a missing event_type, or an event_type outside the closed set, is event_invalid
  • a spend/void event without its required key (run_generated_at / voided_run_generated_at) is event_invalid
  • a readable event whose slots is zero or less stays a legal zero contribution and is not reported as unreadable

The reason vocabulary is closed (QUOTA_EVENT_UNREADABLE_REASONS), so the public projection cannot carry free-form text.

Before, a malformed event reduced the observed spend and a Goal at 12/12 read as 11/12. Now the count is non-zero and the Goal fails closed.

P2 — the public projection keeps the unreadable reasons

goal_quota_config() now carries unreadable_event_reasons next to the count, validated against the closed vocabulary. quota_status() keeps them on a blocked quota, and _apply_unreadable_ledger_fail_closed() fills them in even when the quota was already blocked_health, so the reasons reach config, status, and plan.

The count says how many events broke; the reasons say which artifact to repair.

Output budget

8f331e3c fixes a regression the Windows lane caught: two always-present counters in the usage summary grew the agent-facing status JSON past its base/head growth allowance on every scenario, even with nothing unreadable. The counters are now reported only when a quota event actually could not be read back, so a healthy summary keeps its original shape.

Tests

Five regression tests were added to tests/control_plane/test_quota_ledger_integrity.py: a malformed event with no event_type, an unknown event_type, a legal zero-spend event, the public projection keeping the reason, and the quota plan keeping the reason. Each fails on the previous head and passes now.

tests/control_plane/test_quota_ledger_integrity.py and tests/control_plane/test_usage_summary.py pass locally, and the CLI output base/head differential smoke passes locally.

@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=max

[P1] 缺失或非法 slots 仍会重新放行已耗尽额度。 当前 head 8f331e3cd4fca24b9edc474b03b203ca385db797 的 quota_slot_observation 仍以 default=0 解析必需数值。真实 File/SQLite 的四个独立新 guard fixture 均从12/12变成0/12,返回 normal_delivery_allowed=true 并提交 admission receipt。在共同 observation owner 区分缺失或解析失败的 slots 与明确合法零,未知量返回 event_invalid,并补真实公开入口回归。

动机 / Motivation

依赖滚动额度控制自动工作的用户和 Agent,通过 quota plan 与具名 Todo 的 should-run 读取剩余预算。

原消费文件完整时额度为12/12且禁止执行;只删除 slots 或将其改成 lost 后,当前版本读成0/12并重新准入。

截断和缺失事件类型已正确阻断且公开原因;缺失或非法额度数仍被静默解释为零,完整失败关闭尚未交付。

本次范围是现有消费账本只读解释及公开额度投影,不修改消费事务、宿主权限或实际预算。

这是当前任务同一失败模型中的未完成部分。并非要求读取器猜出损坏文件原来花了多少,也不是声称同版本原子写会自然撕裂;磁盘损坏、部分恢复或其它版本写入都可能留下可解析但不完整的对象。未知数不能被当作明确的零。

改动思路 / Approach

本次范围是现有消费账本只读解释及公开额度投影,不修改消费事务、宿主权限或实际预算。

复用原账本解释、额度和 usage owner;健康计数每次从实际 index/artifact 推导,没有新增故障库或人工同步状态。

将预算与 usage 的贡献解释并入同一 observation 是合理的有界收敛。原兼容 export 仍指向共同函数,原 TypeScript spend/void 事务没有被替换。最小修复就在当前数值读取处,不需要新开关、数据库或执行器。

相对上次 e403,当前新增的 usage 健康态字段省略有明确输出成本理由;本轮仍按整个 PR 检查五个路径。旧的类型缺失与原因转送问题在实际命令中成立修复,未将该正向结果扩展为所有未知消费都安全。

同输入观察 当前结果
健康12槽、恢复完整 artifact throttled,12槽,无准入回执
明确合法0 eligible,0槽,合法准入
截断、缺少事件类型 blocked_health,不可读计数1及具体原因,无准入回执
类型正确但 slots 缺失或为 lost eligible,0槽,错误准入并提交回执

这是实际 CLI 观察的文字对照,不是模型采用或完整成本证明。

具体改动 / Changes

实际共同基线 f2990e3de49d6aa7441e0e0b739f49bfc84aae38;本轮完整差异为五路径、+792/-27:三生产文件+253/-27,测试516行,合同23行。上一 head e403 的 finding 逐项重新审视:缺失/未知类型与公开原因已修;slots 缺失/非法的 P1 仍在当前代码上复现。三个 PR commit 现在均有 Signed-off-by,旧的 DCO 缺口不再沿用为当前问题。

独立接受依据为 issue #6869,spec_revision 2026-10-23T12:33:09Z。Expected:not_met,不可读消费不能静默变成零,plan/history 应公开未知;当前数值缺口违反同一要求。Trigger honesty:implemented,本轮只在隔离合成 artifact 中模拟损坏,保留 index,不冒称生产事故或原子写撕裂。PR 自己新增的文档不是独立规范。

关键代码讲解

  • quota_slot_observation 统一贡献与错误,类型和键判断已补齐;173行仍把必需数值的缺失/解析失败折成零。此路径影响真实读取,不是仅 helper 返回格式。
  • goal_quota_with_spend_ledger 在原 Goal/窗口过滤内汇总同一 observation;goal_quota_config 和 _apply_unreadable_ledger_fail_closed 继续把实际检测出的原因传到 plan/guard。observer 未报告的未知量无法由投影层补救。
  • build_usage_summary 使用同一解释器,在实际损坏时增加窗口计数,健康状态省略新计数;它是样本汇总,不成为第二账本 authority。

对主干的风险 / Risks

最强反例是事件类型合法而数值缺失:{"quota_event":{"event_type":"quota_slot_spent","run_generated_at":"<original timestamp>"}}。保留原 index、Todo 与认领,只改变 artifact 的 slots。两种 backend 的 plan 均为 eligible/0,随后各自全新原生 guard 提交准入;不是把前一个未结 Turn 的恢复当作新执行。探针没有继续业务或新增 spend,不能据此声称已经额外收费。

同一探针在不可变 base/head 各运行两后端×七输入,合计28个匹配案例、56次实际 CLI 调用。缺失/非法 slots 在两版本均失败,说明这是本次修复仍未完成的原缺口;截断和缺失类型则由 base 的错误准入变为当前阻断,证明探针对修复敏感。健康、明确零和恢复完整文件的已观察预算/准入控制保持原行为。每例使用独立合成根;除已验证观察时间和临时位置外,没有删除身份、状态或错误字段来制造一致。完整输出均保留,未声称所有字节或所有消费者逐字段等价。

PASS:完整 test_quota_ledger_integrity.py 与 test_usage_summary.py 共60通过、0跳过;四个改动 Python 文件 Ruff、配置17文件 mypy、完整差异 whitespace 通过。diff-scoped semantic advisory 成功且未发现受支持的新 carrier;动态 frozenset 仍由本轮人工判定为现有 quota 只读原因词汇,空 advisory 不证明所有语义已分析。

UNVERIFIED:发现确定性 P1 后未重跑完整 standard premerge;旧头的超时、相邻 recorder 失败及旧通过保留原来源,不移记为本头结果。没有查询、轮询或等待 GitHub CI。Windows、PostgreSQL、已安装 App/Lark 与实际模型使用未在本轮资格化。没有渲染布局变化,视觉评审不适用;当前未验证的运行环境与真实长期效果分别保留。

语义与 CI 对齐

失败关闭是本次明确披露的默认修复,不是可选建议或安装即启用的新能力。新原因复用原 blocked_health 与账本解释 owner,不扩张 actor 或权限。当前声明“类型必需字段非法为 event_invalid”仍未覆盖 slots;在共同 observation owner 区分缺失或解析失败的 slots 与明确合法零,未知量返回 event_invalid,并补真实公开入口回归。 以同样的 File/SQLite plan/guard 回归验证,同时保留明确零、健康、修复后恢复和完整原因。

我的整体评价 / Verdict

REQUEST_CHANGES。 有用问题和维护边界成立,范围总体相称;当前 long_horizon 与 user_experience 均为 not_yet_proven,因为损坏的消费仍能恢复自动执行资格。复用原账本解释、额度和 usage owner;健康计数每次从实际 index/artifact 推导,没有新增故障库或人工同步状态。 不需要为这处修复创建平行框架或新的确认步骤。

当前未知数→零的缺口仍需修复;本轮测试通过不替代该真实反例。检查了原 CLI/plan/status/history 和 usage 消费者,没有新增配置或页面。视觉评审不适用:本五路径差异没有渲染组件;视觉未验证范围为 none。原生平台、整包标准资格、实际宿主采用与完整成本未验,本评审不授予合并或安装权。

English verdict: REQUEST_CHANGES — 8f331e3. Missing or unparseable slots still turn exhausted quota into eligible/0 and four fresh File/SQLite guards commit admission. The same base/head probe covers28cases/56 CLI calls; type/diagnostic fixes and healthy/zero/restored controls hold.60focused tests and static checks pass; full current standard premerge remains unverified.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>

@songoow songoow 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 Astra Pro — OpenAI — self_reported

动机 / Motivation

持续运行 Goal 的自动执行器和查看配额的操作者会遇到这类问题。

一次消费已用完 12 个配额单位后,账本索引仍在而消费文件损坏;旧版会读成零并重新放行,本 PR 要把未知消费阻断并说明修复来源。

实测截断 JSON 已能阻断,缺失或不可解析的消费数量仍会重新放行。

本次边界是现有本地配额账本的正确读回,不包含新调度策略、外部 API 费用去重或长期吞吐资格。

改动思路 / Approach

账本读取、未知数量约束和公开投影各留在既有 owner;不新增存储、调度器或手工状态,机制成本与预算越界风险相称。

当前 PR 应完成既有 ledger 的不可读分类和真实 guard 阻断;最小追加修复是数量解析,完整 provider 费用计量仍在范围外。

我按现有 ledger readback owner → usage summary / quota projection → 实际 quota plan 和 should-run 的链路检查了完整五文件 diff;没有把新增错误字段本身当作端到端完成。复用了真实 canonical fixture,分别执行 File 和默认 SQLite 路径,并用当前合并基线与候选的同一套输入做对照。

具体改动 / Changes

关键代码讲解

quota_slot_observation 共用读取和贡献分类;goal_quota_with_spend_ledger 保留既有 Goal/窗口过滤后汇总,再由 quota_status 拒绝未知预算;build_usage_summary 用同一来源向 history/status 报告数量与原因。三者都不修写账本。

规范基准:https://github.com/loopx-project/loopx/issues/6869,修订时间 2026-10-23T12:33:09Z。Issue 没有原条款编号,以下用原要求简短标题映射:未知消费不能静默计零 — not_met,slots 缺口见 F1;unreadable_event_count — implemented,物理错误进入真实计数/原因/拒绝;损坏来源可恢复 — implemented,固定 index 的截断和恢复在两个 provider 上重新计算。

新增的 unreadable reason 集合与 quota_slot_observation 被账本汇总和 usage summary 共用;非零 unreadable count 会进入 blocked_health,同时保留公开 projection 的原因。缺失文件、截断 JSON、非对象、未知类型、合法零值、正常 void 和恢复读回的方向都合理。兼容 facade 继续复用原 owner,没有另建预算 authority。

[P1] 缺失或不可解析的 slots 仍被当作合法零消费。
位置:loopx/control_plane/quota/ledger_readback.py:173–179(void 分支也使用相同转换)。_int_number(event.get("slots"), default=0) 在字段缺失或值为 "lost" 时返回 0,随后返回 (None, None)。这把「无法确定消费数量」与明确写出的合法 slots: 0 合并,绕过了本次新增的 unreadable gate。

我在 exact head 6b8b5cfbd0a6b1f7f1aa841e580351679ebf97da 的真实 CLI 上验证了这个路径:每个场景使用独立的 canonical Goal/guard,预算 12,完整 index 指向消费 12 的 artifact;健康输入和恢复输入均拒绝继续执行。只删除 artifact 中的 slots,或将其写为 "lost",保留 index 字节不变,File 与 SQLite 都读回 spent_slots=0、state=eligible,should_run=true、normal_delivery_allowed=true,并给出 must_attempt_work=true。截断 JSON 对照则被候选正确拒绝;显式零值继续合法。

请先验证消费数量是否可读,转换失败或缺失时返回已有的 event_invalid,再单独处理合法零值;相同规则也应覆盖 void。回归要穿过 plan 与新的 should-run guard,保留 index,只改变 artifact,并覆盖 File/SQLite、显式零值及恢复。不要把未知数量猜成一个具体消费量。

验证与既有审查 / Validation and Review Frame

  • 本轮实际执行 tests/control_plane/test_quota_ledger_integrity.py 和 tests/control_plane/test_usage_summary.py:60 passed。
  • 同一只读/隔离 CLI 探针在当前合并基线 428f71177db4c0acb87d16ecad2abe555d952e2d 和候选分别执行 12 个场景:File/SQLite × 健康、缺失 slots、无效 slots、截断 JSON、明确零值、恢复;每个场景执行 plan、history、独立 should-run,index 均保持不变。
  • 基线会错误放行截断 JSON,候选会拒绝,证明实际修复有效且探针能区分差异;缺失/无效 slots 两端仍放行,证明这是本 PR 承诺覆盖的损坏账本缺口尚未关闭,不是新引入的回归。
  • 既有 review 提到的 event_type 和公开 reason 修复在当前源码与既有回归中已覆盖;没有把这些已修项重复列为 blocker。slots 问题仍可在当前 head 独立复现。
  • 输入均为合成隔离状态,未执行业务交付、未消费真实 provider 预算;测试数与结论仅限本轮实际执行范围。未重跑整仓测试、CI 或长期 workload。

对主干的风险 / Risks

语义与 CI 对齐

当前“未知不是零”约束仍有 F1,须修复并重跑上述真实 CLI 对照。当前 dashboard-browser (2) 的 loaded-lens 1 秒预算另有一次 1.107 秒失败;本轮未完成该远程失败的因果归因,不豁免其合并检查,也不把它充作 ledger 缺陷证据。

主要风险是账本损坏后自动执行预算仍然 fail-open;它直接违背本 PR 的修复目标。现有 60 个测试覆盖了读取和类型缺陷,但没有验证「已知事件类型、消费数量不可读」这一类。当前错误 reason 复用与默认正常形状保持是合适的 bounded refactoring,不需要另建框架;数量验证与真实入口回归应在同一修复中补齐。

我的整体评价 / Verdict

长期持续执行的配额保证尚未成立,未知数量仍可让后续工作越过原 allowance。用户体验在物理错误诊断上改善,但数量损坏仍误报可用配额。本次只改既有 CLI/JSON 诊断,没有渲染页面改动。语义上扩展读取原因并复用既有 blocked_health,没有新 actor 或权限。

REQUEST_CHANGES。 支持当前 owner 和错误投影方向;请关闭上述已复现的数量读取缺口后再复审。当前证据不支持把 unreadable ledger 的 fail-closed 修复认定为完成。

English verdict: REQUEST_CHANGES — exact head 6b8b5cf still treats missing or unparseable slot counts as zero and reopens exhausted quota through real File and SQLite CLI admission paths.

An indexed spend or void event that names its type but not how many slots it
moved was read with default=0, folding an unknown quantity into an explicit
zero spend. A Goal at 12/12 then read as 0/12, stayed eligible and was
admitted again.

_slot_count() now separates the two cases on both the spend and the void
branch: a missing, boolean or unparseable count is event_invalid, an explicit
zero stays a legal zero contribution, and the supported numeric string form
keeps working. The regression drives the real index.jsonl + artifact shape and
holds the index row fixed while only the artifact changes, so a repair is the
same edit an operator would make.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
@gcl-coder

gcl-coder commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

The slot-count gap is closed in 05eaeeaca360f7eaccbbdf42a5067119d0be5a55.

[P1] an unreadable slot count is now an unknown, not a zero spend

_slot_count() in loopx/control_plane/quota/ledger_readback.py returns None when the count cannot be read, and quota_slot_observation() turns that into the existing event_invalid reason. The same rule now covers both branches, spend and void.

artifact slots before now
absent, "lost", "", null, true, ["1"], {"value": 1} read as 0, eligible event_invalid, fail closed
explicit 0 legal zero legal zero, not unreadable
3, 3.0, "3" 3 3
readable negative clamped to 0 clamped to 0, unchanged

The observation is the single place both the ledger and the usage summary read, so the fix reaches goal_quota_with_spend_ledger, goal_quota_config, quota_status and build_quota_plan together. No new store, no new switch, no change to the TypeScript spend/void transaction.

Regressions

Six tests in tests/control_plane/test_quota_ledger_integrity.py, all driving the real index.jsonl + artifact shape: the index row is written once and never touched, only the artifact on disk changes between the healthy, broken, zero and repaired cases.

  • spend artifact with slots deleted or unparseable, over six broken shapes -> event_invalid, spent_slots=0, unreadable_event_count=1
  • the same broken row through build_quota_plan -> groups.blocked_health, next_automatic_turn is None, reason on the plan item
  • void artifact with slots missing or "lost" -> event_invalid
  • explicit slots: 0 -> not unreadable, state=eligible
  • slots: "3" -> spent_slots=3
  • repair: rewrite the artifact only, index row unchanged -> counter gone, spent_slots=4, state=eligible

Four of them fail on 6b8b5cfbd with KeyError: 'unreadable_event_count' and pass now. The explicit-zero and numeric-string ones pass on both heads on purpose: they are the guard against over-correcting a legal zero into an integrity failure.

What I am and am not claiming

build_quota_should_run() resolves the Goal through _build_quota_plan_for_goal() before it decides anything, so the plan assertions above are on the guard's own path, not beside it. The later hops of that guard call the host-local TypeScript effect runtime, which the unit lane cannot start, so I am not claiming a full CLI replay from the unit lane.

The File and SQLite backends hand this reader the same index row plus json_path, so the reader is backend-agnostic by construction; your dual-backend probe is the integration evidence for that, and I did not add a Python test that pretends to cover it.

Docs

docs/status-data-contract.md now states that the counters also fire when the slot count is missing or unparseable, and that an explicit slots: 0 is a legal zero rather than an unreadable event.

Local run

tests/control_plane/test_quota_ledger_integrity.py, test_usage_summary.py and test_int_number_non_finite.py: 87 passed. test_quota_slot_accounting.py and test_cli_entrypoint.py fail in my sandbox with EffectRuntimeHostPermissionError; the same files fail with the identical count and cause on the clean immutable base 428f71177, so that is the sandbox, not the patch.

Current head

05eaeeaca holds 36 passing checks. Two python shards ran to completion; the other four were cancelled by the shared six-lane queue before they did any work:

  • test-shard (2): 1 failed, 5181 passed, 136 skipped in 3165s
  • test-shard (3): 3 failed, 5153 passed, 162 skipped in 3285s

The four failures are test_native_restart_uses_real_accepted_work_fence_before_app_quit[completing], the same test with [interrupting], test_dashboard_launcher_selects_a_compatible_nvm_node, and test_lark_private_conversations::test_app_long_return_keeps_all_parts_and_does_not_repeat_identical_parts. Three of those are in the 26f73056c1 row of the matrix below. None of the four is a quota test, and every quota, ledger and usage test in both shards passed.

Both shards finished at 52:45 and 54:45 against a 60 minute job timeout, which is why the remaining four were cancelled rather than failed.

Attribution of the red checks

test-shard is red on main itself right now, and the same suites fail on heads with no relation to this patch:

head test-shard suites matched
26f73056c1 (push, main) (1), (2), (4), (6) failed native-restart fence, dashboard launcher, heartbeat admission budget, prompt budget, skill links, size-probe snapshot, receipt-retry vision
e70a704480 (unrelated branch) (5) failed same family
this patch 05eaeeaca (1), (3) failed; (2), (4), (5), (6) cancelled same family

The base commit 428f71177 cannot serve as a baseline here: its check-runs contain only eight release jobs (build / deploy / publish), with no test-shard and no dashboard-browser run at all.

This patch's diff against that base is five files - ledger_readback.py, usage_summary.py, quota.py, the contract document and one test module. No prompt, heartbeat, presentation, dashboard or skills file is touched, so a 400 character prompt-budget drift while main shows the identical failure on the identical assertion is not something this patch can produce.

dashboard-browser (2) stays the frozen 1s lens-switch budget (807, 906, 737, 411, 1107 ms), which also fails on unrelated heads. That remains an observation, not an exemption claim.

@songoow songoow 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 Astra Pro | OpenAI | self_reported

审阅固定在 05eaeeaca360f7eaccbbdf42a5067119d0be5a55;基线 428f71177db4c0acb87d16ecad2abe555d952e2d。核对了最新讨论与当前 main 21367015cce45294dcf99f54a3211d66f5d842c5,触及的关键生产 owner 在 main 仍与基线一致。

本轮未发现新的具体阻断性代码缺陷;当前为验证证据 hold。 当前失败 test-shard (2)/(3) 的完整诊断不可取得,缺少独立归因;这不是新增代码 finding。

动机 / Motivation

受影响的是依靠自动轮次配额控制成本的 Goal 维护者,以及读取使用量摘要的调度器。 当已经计费的索引仍在、对应账本事件的数量却缺失或损坏时,旧实现可能把未知消费当作零,给同一个 Goal 再发可用额度。 本次把不可读事件明确投影为健康阻塞;恢复有效事件后,下一次读取能够重新放行,合法的零消费仍然有效。 范围是现有配额读取、摘要和准入路径,不改变预算大小、计费策略或 Todo 的完成权威。

未发现为本次具体增量单独批准的书面验收规范;按原问题、基线现有owner契约与已有讨论判断,不把本PR新增文档当作独立验收依据。

改动思路 / Approach

不可读判断放在已有账本读取 owner,再由 quota_status 和计划入口传播,避免调度器与使用量摘要各自猜测消费。 当前 PR 的独立边界是从物理账本到自动轮次准入的完整闭环,包含明确零值与损坏事件的区分。 我重新检查了上一轮 missing slots / 非数值被当作零的 P1。当前 _slot_count 返回可区分的未知值,spent 与 void 两类事件都把该情况转成 event_invalid;不是只在显示层增加计数。最小方案应继续共享这一个读取语义,不需要新的账本格式或第二份可写健康状态。

具体改动 / Changes

关键代码讲解

  • quota_slot_observation(ledger_readback.py:182)识别索引声称的配额事件,追到真实 artifact,并分别保留读不到、格式错误、事件缺失和数量无效的原因;非配额行仍不是异常。_slot_count(:64)拒绝 bool、空值、非有限值和无效字符串,同时保留明确零与已有数值字符串兼容。
  • goal_quota_with_spend_ledger(quota.py:389)累积窗口内有效消费与不可读事件;_apply_unreadable_ledger_fail_closed(:682)让已有 eligible 投影也回到 blocked_health,避免旧投影绕过新读取结果。
  • build_usage_summary(usage_summary.py:141)复用同一 observation。健康结果省略新增不可读计数,故不把所有现有消费者强制迁移到新形状。测试多数是异常工件与修复的矩阵;新增生产逻辑集中于三个现有 owner,而不是另立策略层。

验证

实际运行 test_quota_ledger_integrity.py、test_usage_summary.py 与 test_int_number_non_finite.py:87 passed。另用真正的 quota should-run --codex-app,在独立 File 和 SQLite 注册表各执行有效 2 slots → missing slots → NaN → 明确 0 → 修复 2,共 10 次:损坏时 should_run=false / blocked_health / event_invalid;零与修复后 eligible。相同输入在不可变基线 428f711 的 missing/NaN 均错误 eligible,健康与零值结果一致。这是实际准入反证,不是 helper mock。 本PR对其不可变base执行git diff --check,检查通过。

对主干的风险 / Risks

未发现新的具体阻断性代码缺陷;上一轮数量解析 P1 已修复。当前仍无法取得失败 test-shard (2)/(3) 的完整诊断并独立归因,因此不能把它们声称为既有失败。已通过的局部套件和双 provider CLI 足以证明这次账本不变量,但不能证明未知失败不涉及其他准入调用者。最小补证是保留相同失败命令与签名,确认 exact head 的相关入口通过或与不可变 base 同因失败;无需把未知红灯描述成确定的产品 bug。 当前报告只覆盖明确列出的执行与源码证据,未完成的provider、scope矩阵或视觉状态不会标成通过;review结论与合并时的分支保护、完整CI资格分开记录。

我的整体评价 / Verdict

代码修复有效且比例适当,实际恢复路线没有把损坏状态永久锁死。保留验证 hold 的唯一当前原因是尚未归因的失败验证,不重开已修复 P1,不要求本 PR 修理无关历史问题;一旦当前失败得到归因或同覆盖有效证据,就应重新给出 approval 结论。

本变更只改账本与 CLI/摘要数据,没有修改渲染页面;已检查 quota 和 usage_summary 的输出边界。 不适用;本 PR 不改变页面布局。

English verdict: REQUEST_CHANGES — exact head 05eaeea; no new concrete code defect found; current validation gaps remain explicitly unverified.

This branch has not been deployed

No deployments
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.

quota: an unreadable spend artifact silently removes the spend (fail-open)

3 participants