Skip to content

fix(post-writeback): preserve retries after torn journal tails - #5705

Merged
huangruiteng merged 1 commit into
loopx-project:mainfrom
Duang777:codex/post-writeback-jsonl-tail-recovery-20261006
Oct 6, 2026
Merged

huangruiteng merged 1 commit into
loopx-project:mainfrom
Duang777:codex/post-writeback-jsonl-tail-recovery-20261006

Conversation

@Duang777

@Duang777 Duang777 commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Goal

A post-writeback composition retry journal can end with an unterminated JSONL record after an interrupted write. The next append currently concatenates its valid receipt onto that tail, returns appended=true, and leaves the new recovery receipt invisible to status and retry readers. The same framing gap can hide a settled row and leave an already-settled receipt projected as pending.

Change

  • separate any non-empty unterminated journal tail before appending a new record while holding the existing journal lock
  • apply the same boundary to both retryable receipt writes and settled receipt writes
  • preserve the prior tail for audit/recovery instead of treating it as the new record
  • cover a torn tail, a valid final JSON object without LF, and settlement after a torn tail

No schema, status vocabulary, provider effect, or public API changes.

Reproduction and validation

Before the fix, both a truncated tail and a valid JSON tail without LF reproduced appended=true with no readable new receipt in 20/20 runs. The three new regressions failed on the previous implementation and pass after the change.

  • python -m pytest -q tests/control_plane/test_post_writeback_composition_retry.py tests/control_plane/test_post_writeback_capability_hooks.py (82 passed)
  • python -m ruff check ... and python -m ruff format --check ...
  • loopx check on the two changed paths with an explicit temporary registry/runtime (ok: true)
  • loopx canary premerge --from-git-diff --git-diff-base upstream/main (10/10 selected checks passed)
  • git diff --check upstream/main...HEAD

@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; model=gpt-6.1-sol; provider=OpenAI; reasoning_effort=xhigh; declaration_source=runtime_reported

动机

完成工作后需要保存恢复记录、并在后续查看进展或继续重试的 CLI 用户与 Agent。旧版在写入中断留下未换行的尾部后,把下一条记录拼到坏尾部,命令称已追加但状态读不到;新版先补行界,使新重试记录可发现、结算后待处理提示消失。同一输入的真实文件和状态 CLI 对照证明新版修复可解码尾部的行界:新记录可读、恢复后清除待处理、主写回不重复,其他 Agent 的待处理记录保留。范围是可解码 JSONL 尾部的行界恢复;不认证任意字节损坏修复、业务报告质量、外部发送、安装升级或持续吞吐收益。这里的恢复记录是工作已保存后,附加报告投影失败时留下的待重试凭据;丢失它会让下一轮找不到恢复入口,结算记录不可见则会让已完成的恢复继续显示待处理。这是能独立交付的实际可靠性修复,主工作和附加报告的权限没有变化。

改动思路

沿用当前 journal owner、文件锁和精确身份,不新增状态源或重试调度器。dispatch_committed_cli_post_writeback_hooks 在主写回提交后记录投影失败;正常恢复仍交给 TypeScript hook transaction 验证来源、生成无副作用意图并去重,Python 负责本地持久化与传输。两条 journal writer 在已有锁内调用同一个行界 helper,随后按原格式写入。正常有换行的文件保持原语义,完整 JSON 但缺换行的旧尾部被保留,残缺 JSON 单独成行而不会吞掉新记录。

已对 base、精确 head 与当前 main 检索 writer、读取与尾部处理。receipt_log_snapshot.ts 是另一个严格 receipt reader 的解析加速,不是这个 composition journal 的可替换 writer,也不承担相同容错语义;强行复用会扩大错误处理和跨语言边界。保留 Python 文件操作符合现有 provider/transport 归属,新的决策 owner 没有出现。

具体改动

Head 75f5b3626289115faebfcbee719bcdd8a1e8ed88,base 3b454ab3652ae6c204233535be18ef031e34c29d。完整 diff 只有 runtime +9/-1 和三个 focused regression tests +89;没有 CLI、schema、quota、调度、授权或界面变化。内部 helper 接近现有持久化职责;新测试补的是此前缺失的文件边界,不是复制整个生命周期测试。

关键代码讲解

  • post_writeback_composition_retry.py:240 的 _separate_unterminated_jsonl_tail 从头读到 EOF,仅在非空且没有换行时追加换行。它不截断旧尾部,不解析业务状态;两条 writer 都已持锁。这里检查的是文本行界,不是 arbitrary-byte 修复。
  • append_composition_retry_receipt:247 保留 schema/id 校验和 settled 终态提前返回,在真正写新行之前补边界,再执行原有 compaction。缺 LF 的合法旧 receipt 和新 receipt 都可读;超过 512 行的原有原子压缩仍按每个 receipt 的最新记录折叠。
  • settle_composition_retry_receipt:350 保留精确 goal/event/agent/todo/turn/effect/state-version/hook-set 身份。缺失或已 settled 不写;找到未完成 receipt 才补行界并写 settled。恢复后再次调用不追加,换 Agent 或 hook policy 不会清掉原任务。
  • 三个新测试分别保护残缺 JSON 尾部、合法 JSON 缺 LF、坏尾部之后的 settlement;生产消费者仍是 status 的 pending projection 与 committed CLI bridge。

先读 base 已接受的 post-writeback RFC,再看 diff。§4 ownership:implemented,TS 决策/Python 文件边界保持;§5.2/§9 retry and identity(recoverability/idempotency):在可解码的未换行尾部范围 implemented;§10 isolation(failure isolation):implemented,投影/producer 失败不回滚主写回;§12 acceptance matrix(off/malformed/replay/conflict/no-authority):既有测试、成对真实状态与负例均覆盖。Arbitrary-byte corruption/full external delivery:任意 UTF-8 字节截断恢复与完整外部业务交付为 out_of_scope,不把本 PR 说成整个 RFC 全部落地。

对主干的风险

独立验证:Python head 82 passed/base 79 passed;TS hook + transaction 43 passed;ruff check/format、语义 advisory + full smoke、公开边界扫描通过。premerge 实际执行 10/10 selected checks,直接 diff/compile/maintainability 检查也通过。按当前配置没有获取或等待 CI。早期 preview 的自动 inherited-failure 标签没有保存具体失败签名,因此不把它当根因证明;相同 immutable head 的直接 ratchet 和完整执行都通过,base 的相同 ratchet 也通过,原 preview 留存。这没有更改预算或减少验证范围。

同样的 19 条合成 Agent/Goal 关系,base 0/19 完成所有恢复断言,head 19/19;这覆盖共享 journal 隔离,领域业务与模型没有运行。ASCII receipt 的全部 710 个尾部截断位置,base 仅空文件情形 1/710 可读新记录,head 710/710 可读。两个独立 OS writer 在坏尾部后追加不同身份,base 丢一条,head 两条都可读。真实 status --limit 1 仍给出完整 matching pending count:23 个其他 Agent 条目之后,head 当前 Agent 的新记录可见、结算后仅自身清除,其他 23 条保持;base 新记录隐藏。已有测试还覆盖超过 compaction 边界、活跃 reader、原子替换失败、后续 Turn 与 Agent filter。

真实隔离 CLI 先准入并提交 refresh,再使用该原始 receipt 经过实际 bridge/TS sidecar 做投影失败、恢复和重放:新版 pending 出现后清除,producer 仅执行一次,replay invoked=0;base 同样保留主写回,却丢失失败提示。两版原主写回文件、STATE 和 run index 的 bytes 均未改变;同 Turn CLI refresh 返回 original-writeback replay。source projection/provider 是明确合成的 host,真实生产 Goal、外部消息、模型未参与。最初自建 provider 多带 error_code 被现有 exact-field decoder 拒绝;修正合成输入后重新跑成对路径,不能把那次拒绝归咎于 PR。

剩余边界明确:把合法 Unicode goal id 的 UTF-8 字符截断到一半,两版都在原有严格读取路径报 UnicodeDecodeError,主文件未被篡改。这不是本 diff 新引入的错误,当前修复不会使这种损坏自动可恢复。普通 append/settle 的 flush/fsync 策略没有扩展,断电持久性未新增保证;先前 compaction 仍可能丢弃无法解析的旧行,不把“补 LF 不删尾部”扩大成永久审计保留承诺。

语义与 CI 对齐

本改动修复既有持久化行界,复用 retryable/settled 与原 hook-set 身份,不新增 vocabulary、权限或机器义务。无 hook 的真实 bridge 成对为 registered_count=0、无 composition sidecar,既有正常 CLI 可接受输入、主写回与重放保持。通用文本没有领域术语或 denylist;replay action 是操作指导,终态不可倒退与身份约束仍由机器执行。语义 advisory 的零 candidate 有分析范围限制,full smoke 和手工 caller 检查作为补充。

我的整体评价

APPROVE。long_horizon=improved:这条失败恢复链能持续找到原始失败并在成功后退出待处理,降低沉默丢失恢复入口和永久重试提示的风险;user_experience=improved:使用原有状态与重放入口即可恢复,不要求补填参数、重新描述工作或额外确认。两项判断限于已验证的 journal 修复,未认证真实领域采用或整体长期业务效果。

代价是 helper 每次实际写入多读一遍 journal。两组相同 1000 个不同 receipt、710 KB 的 healthy append+compaction 局部测量,中位数 base/head 分别为 14.47/16.55 ms 与 20.75/19.89 ms;符号随共享负载变化,不能据此宣称吞吐变快或变慢。确定的效率收益来自避免丢失恢复入口与无效重试,额外线性读取成本保留为风险。future-facing pass:已合并两条 writer 的行界规则;仅查末字节/统一损坏尾部读取可作为该 owner 后续有证据的优化,当前没有足够瓶颈证据支持跨 reader/TS 大重构,也无需兼容 wrapper 或新 capability。现范围 +8 runtime 行换取两个真实 writer 的恢复正确性是 proportionate。批准不等于合并、完整数据损坏修复或升级验收。

English verdict: APPROVE - 75f5b3626289115faebfcbee719bcdd8a1e8ed88: restores readable retry and settled records after an unterminated decodable JSONL tail. Paired real-file/status/committed-CLI recovery, all 19 synthetic identity relations, 710 byte cuts, OS concurrency, 82 Python tests, 43 TS tests and premerge checks pass. Partial UTF-8 corruption is an unchanged residual boundary; sustained throughput and real domain adoption are unqualified.

@huangruiteng
huangruiteng merged commit a64fffb into loopx-project:main Oct 6, 2026
29 of 33 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.

3 participants