Repository navigation
fix(post-writeback): preserve retries after torn journal tails - #5705
Conversation
Signed-off-by: Duang777 <[email protected]>
loopx-agent
left a comment
There was a problem hiding this comment.
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.
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
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=truewith 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 ...andpython -m ruff format --check ...loopx checkon 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