Skip to content

fix(backup): preserve published backups on retry and publication failure - #6295

Merged
huangruiteng merged 3 commits into
loopx-project:mainfrom
songoow:codex/backup-publication-20261012
Oct 12, 2026
Merged

huangruiteng merged 3 commits into
loopx-project:mainfrom
songoow:codex/backup-publication-20261012

Conversation

@songoow

@songoow songoow commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Full backup replaced the archive before replacing its external manifest. A failed second publication could destroy the previous usable archive while leaving its old manifest; two concurrent same-name producers could both report success. The baseline fault reproducer left a mismatched pair.

Require new output names at preflight and publication. Publish manifest then archive without replacement, using exclusive links or an exclusive private-file copy when linking is unavailable. Retain interrupted output for inspection; never delete or overwrite a path during retry. Existing archive format, payload and CLI flags remain, but reusing a backup id in the same directory now fails. CLI help and the bilingual recovery/backup docs explain inspection and fresh-id retry. Default Markdown errors now show the cause and conflicting output path instead of empty statistics.

Refs #5940. This is a bounded preservation repair for immutable recovery inputs, not an M2 capture-consistency profile, controlled reactivation, provider promotion or a new UI capability. The existing full-backup CLI changes; configuration UI and Lark entrypoints are unchanged. The staged #6279 Settings verification journey and later capture/adoption flows retain their existing owners. The tar/filesystem adapter stays Python; existing TS configuration and SQLite snapshot owners are unchanged. The adjacent refactor reuses directory durability handling and one exclusive-publication helper, without a new journal, schema or framework.

Validation at 47c3ca34305cb9240fbbdb2df87b0645cb8467fe, based on d03863fd0048f57f6dc878e7dc464b17f8d862a1:

  • Three baseline regressions failed: publication fault, native CLI same-id retry and concurrent native producers. Final backup/configuration/member/registered-source suite: 38 passed, including real WAL snapshots, source retention, two process-exit windows, exclusive-copy fallback and late collisions.
  • Review also reproduced a missing cause in the default Markdown error output; the native CLI regression now checks actionable JSON and Markdown failures.
  • Final normal wheel built with qualified frontend assets and installed separately. From a foreign directory, the installed console CLI rejects reuse without changing the old pair; a fresh id captures later work. Independent SQLite readback sees the original one-row snapshot and the later two-row snapshot.
  • After the synthetic source becomes unavailable, the installed feat(recovery): inspect full backups without activating restored state #6279 verifier reads both new-producer archives. Its audits remain held and grant no execution authority; this is candidate compatibility, not main or App reactivation.
  • State-backup smoke, Ruff, docs governance, advisory and full semantic validation passed. Final premerge: five catalog + eight risk + public-boundary checks, plus direct diff/compile checks. The registry-I/O manifest change only updates an existing source line number.

Initial validation failures are retained: the census needed its line-only regeneration, and the first wheel build correctly refused missing frontend assets; normal asset construction and the final checks succeeded without bypassing those gates. Link-less fallback was exercised by injected filesystem errors with real exclusive copies; native Windows/exFAT and power-loss qualification are not claimed. Copy fallback adds I/O and can expose incomplete output until verification; no two-file atomicity or whole-system consistency claim.

Signed-off-by: song <liusongstep@gmail.com>

@songoow songoow left a comment

Copy link
Copy Markdown
Collaborator Author

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 | OpenAI | self_reported

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

动机

通过 CLI 保存 Goal 状态的用户,需要在备份失败或重试后仍能读取此前有效的备份。故障注入证明,同名重试先替换归档、随后清单发布失败,会留下旧清单和新归档,原本有效的文件对因此不可校验;两个同名进程也会同时报告成功。现在同名输出在创建时排他拒绝,先前备份保留;用户可检查遗留文件并用新名称捕获后续进展,默认错误输出直接显示原因和冲突文件。本次只修复完整备份的发布与错误回读,不增加状态恢复授权、凭据转移、provider promotion 或新界面。跨 owner 捕获一致性、完整恢复后的身份与授权接续、平台与断电资格仍未完成。

改动思路

既有 tar/文件系统适配器保留字节发布责任,配置与 SQLite 快照继续由原 TypeScript owner 决定;排他创建足以解决覆盖问题,无需新增事务日志或控制面状态。当前 PR 只修复完整备份的创建与错误回读,捕获一致性和重新激活仍由原 owner 后续验证。预检查只用于提前报错,实际发布仍通过文件系统的排他创建防止并发覆盖。优先硬链接;遇到已处理的链接不可用错误,用 O_EXCL 创建的私有文件复制,代价是额外 I/O 和写入期间可能可见的不完整文件。先清单后归档,POSIX 刷写目录项;不宣称两个文件或跨 owner 的原子事务。

具体改动

关键代码讲解

评审完整 head 47c3ca34305cb9240fbbdb2df87b0645cb8467fe;基准规范 docs/architecture/rfcs/complete-state-recovery-v0.md,原始修订 d03863fd0048f57f6dc878e7dc464b17f8d862a1。按修改前约束判断,并明确新的同名拒绝行为:

  • §2:loopx/state_backup.py:548 的 _publish_backup_file 复用原 staging 和目录刷写,在链接与复制两条路径都不替换已存在名称。execute_state_backup_plan:570 预检查归档及清单,包括悬空链接,并在真正发布时再次由排他系统调用裁决。原来的两次 os.replace 已删除。
  • §7:归档格式、清单内容、输出命名模式和参数保持可读兼容;有意改变的是复用同名输出现在失败。_backup_name_conflict:564 统一携带冲突路径;render_state_backup_markdown:668 显示失败原因和下一步,不再以 None 统计掩盖错误。原 JSON 失败形状保留。
  • §5:字节发布不提升 capture profile。异常或退出后可能有不完整文件对,响应丢失后也可能有完整文件对;重试保留它们,验证两个文件及校验关系后再决定使用,新的捕获选择新名称。配置、SQLite、Goal、lease、effect 等 owner 均未被替代。
  • §11:本批是保留恢复输入的修复,M1 的完整资格和 M2/M3 继续 deferred。三个原有异常源测试改用新尝试名称并另存旧文件,继续真实触发损坏 SQLite、短读和不可表示配置,不能被碰撞拒绝遮蔽。I/O census 仅将已有读取点行号从 54 更新到 58;不增加分类或放宽预算。

对主干的风险

未发现剩余阻断项。固定 --backup-id 的重复创建不再覆盖,脚本需要省略该参数取得新时间戳或选择新名称;这一默认变化已在 CLI help、说明和双语 RFC 中公开。原有 v0 数据及正常成功结果保持兼容。并发证明覆盖当前 producer;旧 writer fencing、任意外部改写和断电持久性不是本次证据。失败时不删除可能已被其他进程替换的输出,遗留文件需要检查,不能凭存在就当作成功。

三个基线回归均先失败;最终源码 38 项测试通过,含真实 CLI 同名/并发、两个真实进程退出窗口、WAL 快照、源保留和排他复制/迟到冲突。评审另复现默认 Markdown 隐藏错误的失败,修复后源与最终安装包均显示原因及路径。独立安装正常 wheel 后,旧归档字节不变,新 id 捕获后续数据;SQLite 独立读回分别有原先一条和后续两条提交。移走合成源后,#6279 在 13dd7eca02c1cd240fa9a68bf51e6e917e0f6aed 的已安装 verifier 能读取两份备份,均 held、activation_eligible=false、execution_authority_granted=false。

Ruff、文档检查、state-backup smoke、advisory 和完整语义检查通过;最终预合入 5 catalog + 8 risk + public-boundary,以及 diff/compile 通过。最初 census 行号和 wheel 缺少前端资产的失败保留,分别正常更新定位与构建资产后重新通过,没有绕过检查。链接不可用以注入错误加真实复制验证,不宣称原生 Windows/exFAT、断电、用户 App 升级、线上恢复或模型资格。远端 CI 排队独立记录。

语义与 CI 对齐

没有新增共享词表、schema 或 Goal 决策 owner。errno 和 FileExistsError 是本地文件系统错误边界;显示错误不从文案推导权限。名称不可覆盖由系统调用执行,帮助文字如实描述这一约束。成功字节、恢复审计和执行授权继续区分。

我的整体评价

APPROVE,作为可独立回退的恢复输入保留修复。long_horizon improved:失败不再破坏已有恢复依据;user_experience improved:同名失败可理解,并有已验证的检查/新 id 重试路径。9 文件 +331/-11,生产变更围绕一个发布 owner;测试覆盖真实数据丢失、竞争和退出,不引入第二个恢复框架。相邻整理复用既有目录刷写,统一冲突错误,正常成功输出保留。完整 Goal 恢复及维护者合并授权仍未满足,不能由本评审替代。

English verdict: APPROVE - 47c3ca3. Exclusive publication preserves prior backup pairs across retries, races and process exit, with actionable default CLI errors. 38 source tests, normal-wheel CLI/readback, candidate inert verification and all selected premerge checks passed. Capture consistency, reactivation, platform/power-loss qualification and merge remain separate.

@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

动机

未发现阻塞问题。精确 head 47c3ca34305cb9240fbbdb2df87b0645cb8467fe,不可变基线 d03863fd0048f57f6dc878e7dc464b17f8d862a1。

使用完整备份命令保存工作进度的操作者。旧版本复用名称会覆盖原快照;第二次清单发布失败还能留下不匹配的文件对。当前版本拒绝占用名称,保留原快照,并让新名称保存后续工作。独立安装包中,原备份仍读回一条已提交记录,新备份读回两条;故障与并发不会覆盖原验证单元。本批修复既有备份发布,不交付恢复后执行、跨 owner 一致性或新 UI 能力。

改动思路

一个本地二进制发布 helper 即可实现不覆盖;复用目录耐久 IO,不建立第二个状态、身份或恢复决策 owner。当前 PR 交付既有 full-backup CLI 的完整保存、占用拒绝、故障后检查和新名称恢复路径;M2 捕获一致性及 M3 受控恢复仍由现有 RFC 阶段负责。

只在原来的文件系统备份 adapter 内修复:私有 staging 完整捕获、校验并刷写后,先排他发布清单,再排他发布归档,每次发布复用既有目录刷写。不支持硬链接时通过 O_EXCL 创建私有文件再复制;普通错误不回退为覆盖。预检查只能尽早报错,最终排他操作负责处理并发。仅加预检查仍有竞争;失败后删除输出可能删除另一写入者的文件。本设计保留部分输出供检查,不增加恢复 journal、协议或第二个控制面决策源。

具体改动

关键代码讲解

  • loopx/state_backup.py:570 的 execute_state_backup_plan 检查两个目标,包括 dangling symlink,再完成原有 tar、配置和 SQLite snapshot。删除两个 os.replace,发布阶段再次通过排他操作拒绝碰撞;只有两次发布完成才返回成功。
  • loopx/state_backup.py:548 的 _publish_backup_file 优先硬链接;仅对明确不支持/不允许链接的 errno 走私有排他复制,并刷写文件和目录。复制失败可留下不完整文件,这个边界已披露;没有覆盖或自动删除 fallback。
  • loopx/state_backup.py:669 的 render_state_backup_markdown 在错误时显示原始原因和保留文件的恢复建议;CLI help 说明 id 用于新备份,JSON 推荐动作同步更新。

全部九条路径均已核对:三份公开文档同步说明兼容性、并发和重试;registry-I/O manifest 只移动原调用点行号;两项旧 source-error 测试改用另一组 prior 名称,保留原断言,使占用检查不会遮蔽短读和有损配置的失败;主测试增加真实 CLI、并发、进程退出、copy fallback、晚碰撞和 dangling link 回归。生产变化 +47/-4,主体仍是最近的 binary IO owner。

规范依据:docs/architecture/rfcs/complete-state-recovery-v0.md,审阅的是 d03863fd0048f57f6dc878e7dc464b17f8d862a1 的已接受文本,而非本 PR 修改后的规范。§2.Invariant4(不可变验证单元)已由同名重试、故障和并发反例验证;§2.Invariant1(字节不授予执行权限)保持原 owner 与历史数据边界;§7.Compatibility(v0、flags 和输出路径兼容)由正常捕获与独立读回验证,覆盖行为的刻意改变明确披露。M2 一致性 profile 和 M3 受控 Goal 恢复属于原 RFC 的后续边界,不在本修复中宣称完成。仓库经验只作为可反驳的建议:本次确实验证先保留已接受工作、再捕获后来工作,没有继承历史 verdict,也未把上下文投递当作采用或长期收益。

对主干的风险

复用相同名称的旧脚本现在返回失败,需要新 id 或目录。这增加一次必要的命名输入,换来保留唯一可用 checkpoint;依据是改动前规范的不可变性,不是作者偏好。预览仍不写文件、不保留名称;普通首次捕获、配置和 WAL snapshot 保持。Settings 配置恢复、Lark、Goal/lease/quota 和 Host 准入未改变,本批没有新增 caller-facing 能力或 UI 操作。

独立正常构建的基线与当前 wheel,从源码目录外跑相同 CLI 和相同合成输入:基线同名重试 exit0 并覆盖旧快照,第二次发布注入失败造成 digest 不匹配,两种不同来源的并发双方 exit0;当前版本分别拒绝且保留原 pair、保留 checksum 关系、并发只有一方成功。原 SQLite 快照一行,新名称捕获后两行,UTF-8 内容、checksum、600 私有模式与 dry-run 均读回。额外安装态反例在 linkless copy 中写入八字节后中断:旧 pair 不变,不完整归档与清单保留,同名重试拒绝,新名称继续捕获成功。

源码两组共 43 测试、备份 smoke、Ruff、语义 advisory 后全树语义检查、正常前端资源构建和独立 base/head wheel 均通过;原生 premerge 的 5 项直接检查、5 项 catalog、8 项风险检查和 1 项九路径公开边界扫描均通过,无失败、跳过或 manual hold。初次 wheel 因缺少合格前端资源被拒绝,首次前端构建缺依赖;按正常安装/构建恢复,没有绕过门或改源代码。故障注入操作真实文件/SQLite/copy,但不是实际 power-loss、原生 Windows 或 exFAT 资格。两文件仍非原子事务;不完整输出必须先验后用,不能把文件存在当作成功。

语义与 CI 对齐

沿用 v0 与现有 owner,无新增共享 vocabulary;errno 判断是本地 IO 能力分支,未引入 prose/substring 状态分类。默认变化在三份文档、CLI help 和回归名称中披露。不覆盖是强制保证,检查/新名称是可操作的恢复指引,不伪装为可忽略建议。全树语义、原 I/O census 和九路径公开边界检查读回;按本 Goal policy 未读取、轮询或等待远端 CI。

我的整体评价

APPROVE。长期工作延续改善:后来捕获和失败重试不能销毁先前已接受结果。用户路径是有依据的有界权衡:错误能指出目标,保存并检查旧文件后换新名称可继续,而不是重复失败的盲重试。原生 premerge 已完成:5 项直接检查、14 项执行检查全部通过,没有失败或 manual hold。

一个本地二进制发布 helper 即可实现不覆盖;复用目录耐久 IO,不建立第二个状态、身份或恢复决策 owner。当前 PR 交付既有 full-backup CLI 的完整保存、占用拒绝、故障后检查和新名称恢复路径;M2 捕获一致性及 M3 受控恢复仍由现有 RFC 阶段负责。

相邻未来重构已合理完成:一个本地发布 helper、复用目录耐久 IO、删除覆盖路径;再添加通用事务框架无当前需求。未知仍包括真实 linkless 文件系统/Windows/power-loss、跨 owner 一致性、恢复后实际执行和长期采用。独立安装用于本次验收,不代表本机升级或整个恢复里程碑完成;合并还须当前精确 readiness 和维护者授权。

English verdict: APPROVE - 47c3ca3. Exclusive manifest-first publication preserves existing backup units. Matched independently built baseline/current native CLI probes exposed three baseline failures and passed at the head; 43 source tests and an installed partial-copy failure/retry/fresh-capture readback passed. Two-file atomicity, native Windows/filesystem power-loss and controlled Goal reactivation remain unqualified.

@huangruiteng
huangruiteng merged commit a663ce3 into loopx-project:main Oct 12, 2026
8 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