Repository navigation
fix(backup): preserve published backups on retry and publication failure - #6295
Conversation
Signed-off-by: song <liusongstep@gmail.com>
Signed-off-by: song <liusongstep@gmail.com>
Signed-off-by: song <liusongstep@gmail.com>
songoow
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
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 ond03863fd0048f57f6dc878e7dc464b17f8d862a1: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.