Skip to content

fix(desktop): atomically swap rollback bundles - #5651

Merged
huangruiteng merged 1 commit into
loopx-project:mainfrom
Duang777:codex/fix-desktop-bundle-swap-recovery-20261005
Oct 5, 2026
Merged

huangruiteng merged 1 commit into
loopx-project:mainfrom
Duang777:codex/fix-desktop-bundle-swap-recovery-20261005

Conversation

@Duang777

@Duang777 Duang777 commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Goal And Delivered Outcome

  • Outcome basis / optional anchor: Reproduced process-death window in the macOS rollback bundle replacement. Related to the adjacent backup-rotation repair in fix(desktop): recover interrupted backup rotations #5647, but independently reviewable.

  • Goal/source and gap: replace_bundle() renamed the installed App away before moving the verified rollback candidate into its path. The error branch restored the original, but process termination between the two calls bypassed that compensation and left the install path absent.

  • Observable before → after, with the validation row that proves it: before the fix, a child-process kill after the first filesystem operation left installed.app missing; after the fix, the same kill point leaves the verified replacement at the install path and the displaced App in preserved staging.

  • Issue/task and intended base: Self-contained reproduced defect; base main.

Author Declaration

  • Written by: model_agent (OpenAI Codex)

Implemented against

  • Specification and revision: No written specification; the request in this PR is the basis.
  • Criteria:
Criterion (spec clause) Disposition Symbol / path Test or command
A process kill must not leave the installed App path empty implemented replace_bundle bundle_swap_keeps_install_path_after_process_kill
An exchange failure must leave the installed target untouched implemented replace_bundle restore_verified_backup_keeps_target_when_atomic_exchange_fails
A completed rollback must install the candidate and retain the displaced App implemented replace_bundle successful_replacement_preserves_the_original
  • Self-check before submission: Read the rollback caller, installation-failure classification, and every rename in the desktop Rust tree. Verified RENAME_SWAP on macOS through rustix; unsupported exchange fails before mutating either path. The existing signature verification still runs before the swap.

Scope And Continuation

  • Completed scope and remaining work: Complete within this scope. The rollback replacement now has one atomic commit operation on macOS.
  • Slice boundary / successor: fix(desktop): recover interrupted backup rotations #5647 handles the separate backup-rotation interruption window. A two-file deep-research archive move is a different transaction and remains outside this desktop repair.

Validation

  • Tested revision: 122682d560dd980721b45d951d79af0b70b46a19
  • Run state: finished
  • Input classes: synthetic
Check kind Result Public-safe evidence / limitation
regression_parity passed The process-kill regression failed before the fix with the install-path replacement missing, then passed with the atomic exchange.
integration passed A real child test process pauses after the filesystem commit, is force-killed by its parent, and leaves the replacement at the install path plus the original in preserved staging.
unit passed cargo test --locked: 82 passed, 2 ignored; signed-archive integration target: 1 ignored.
unit passed python3 -m unittest discover -s tests/desktop -p 'test_*.py': 12 passed.
static passed cargo clippy --all-targets --locked -- -D warnings.
static passed Changed-file rustfmt, committed diff check, dependency lock readback, and sensitive-path scan passed.
static passed loopx canary premerge --from-git-diff --git-diff-base upstream/main: 4/4 selected canaries passed for three changed files.
static failed Repository-wide cargo fmt --all -- --check reports existing formatting drift in unchanged maintenance.rs; the changed Rust file passes its direct rustfmt check.
integration not_run Real backup-copy and signed-archive cases require signed macOS release artifacts; existing tests remain ignored without those inputs.
  • Coverage and gaps: The macOS test invokes the real atomic exchange syscall and force-kills a separate process at the old crash boundary. Existing restore tests cover damaged targets, missing candidates, signature checks, and preservation after normal completion. Power-loss durability is not claimed; this change addresses process termination.

See validation disclosure guidance.

Frontend / Visual Evidence

  • UI impact: none
  • Before: N/A
  • After: N/A
  • States and viewports shown: N/A
  • Source data: none
  • Attention review: N/A; no visual surface changed.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring (no functional changes)
  • Documentation update
  • Test update

LoopX Area

  • Control plane (goals, todos, quota, scheduler, registry, runtime)
  • Benchmark boundary (adapters, runners, verifiers, scoring, evidence)
  • Capability or extension (providers, adapters, skills)
  • Public docs or presentation surface (README, protocols, dashboard)
  • Build, packaging, installer, or CI
  • Host or runtime integration

Technical Direction

  • Direction / acceptance reference, when applicable: N/A

Shared-authority RFC fixture impact

N/A

Boundary Checklist

  • Neither the diff nor this PR body/comments/attachments disclose private state, credentials, raw traces or verifier output, internal links, or local machine paths (including .loopx/, .codex/goals/, and live ACTIVE_GOAL_STATE.md).
  • I did not duplicate maintainer-owned benchmark work unless a maintainer split out a public issue for it.
  • I kept the change scoped to the linked issue/task.
  • I completed the visual evidence section for UI changes, or marked UI impact none.
  • Every commit includes a DCO Signed-off-by trailer (git commit -s).

@mergify

mergify Bot commented Oct 5, 2026

Copy link
Copy Markdown

Hi @Duang777, 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.

@Duang777
Duang777 force-pushed the codex/fix-desktop-bundle-swap-recovery-20261005 branch from eaab7f2 to 974b22d Compare October 5, 2026 13:28

@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

动机

APPROVE — 未发现当前 head 的阻塞问题。 需要在 macOS 上恢复上一版本的桌面 App 用户。 以前回退要先移走当前 App,再移入备份,进程在两步之间被杀死会留下空安装路径;现在一次交换提交后,安装路径仍有可运行的备份,旧 App 保存在临时恢复目录。 独立强杀对照在固定 base 复现安装路径消失,在当前 head 验证替换 App 签名有效、旧 App 与恢复记录保留;损坏备份和恢复记录失败仍拒绝覆盖。 本次只修 macOS 回退的进程终止窗口,不覆盖断电持久性、自动更新插件自身的替换流程、备份轮换、Windows 行为或实际发布安装升级。

改动思路

head 974b22d461ce0aa5bd2c54747cb9619ec5345081,固定 merge base 053a8c246d16f0bbf88491905038295a5c0843e1。原 error compensation 只能处理第二次 rename 返回错误,进程被杀时根本没有机会恢复。更小的修复是在现有 replace_bundle owner 用 macOS 原生交换完成唯一提交;继续添加补偿或另造多阶段恢复状态,都不能比这次有界替换更直接。

生产调用仍是既有固定 rollback action → 当前执行文件确定 App → 校验备份副本 → 记录既有 pending journal → 替换 → 既有 restart/continuation。临时目录建在安装路径旁边,同卷。交换后旧 App 已安全位于 candidate,随后 candidate→failed 只是命名:即使进程此时死亡或命名失败,安装路径仍有替换 App,原始 App 没有被删掉。没有新设置、权限、CLI 参数、Goal 状态或第二控制面 owner。

具体改动

spec_ref:apps/desktop/loopx-control-plane/README.md;spec_revision:053a8c246d16f0bbf88491905038295a5c0843e1。已先读原文:Updates And Recovery 的签名备份/既有回退/Goal 数据边界、Runtime Model 的 native App owner、Validation 的原生 cargo/build 验证,在本次有界修复均 implemented。强杀连续性另外由独立复现的实际缺陷建立,未把作者声明当成已验收规范。

关键代码讲解

  1. update_backup.rs:204 restore_verified_backup 仍从可信当前执行路径定位 target;真实 ditto 复制和 codesign 校验先于 journal 与 commit。staging 在交换前保留,失败保留源与目标的责任没有转给新状态机制。
  2. :227 replace_bundle 在 macOS 用 rustix::fs::renameat_with(... EXCHANGE),失败返回既有 rollback_failed,两个路径都未先移空;不回退到有同样死亡窗口的两次 rename。非 macOS 原代码保留,rollback availability 仍只支持 macOS。
  3. :372 bundle_swap_keeps_install_path_after_process_kill 启动、等待、force-kill 并 reap 真正的子进程,再检查路径。正常保留原 App 与 candidate 消失失败场景也覆盖;旧 second-rename failure 测试重命名为 atomic exchange failure。

完整 diff 为3文件 +134/-14,大部分 Rust 增量是 cfg(test) 强杀/正常回归覆盖;Cargo 只把已有锁定的 rustix 加成 macOS direct dependency,没有版本升级或框架。OS 效果归现有 Rust App,通用 TS/Goal authority 不受改动。#5647 的备份轮换和 updater 插件自动安装是其他事务,不能把此修复泛化过去。

对主干的风险

我在独立本地 source copy 加同一组3项 oracle,只在第一次原生文件操作返回后插入 test-only pause,不改替换逻辑,编译整个现有 crate。调用实际 restore_verified_backup,使用真实 macOS ditto、ad-hoc codesign、文件交换和 SIGKILL;恢复记录 hook 写真实文件。固定 base 2通过/1失败,失败为明确的 installed-path continuity oracle:进程死亡后 target 不存在。当前 head 3通过:target 是签名有效的替换、旧 App 在 preserved staging、恢复记录和备份源仍在。初次 copy harness 缺 repository-relative include 路径,已恢复同一源码布局后才得到有效结果;未用编译失败充当回归。

其余独立路径:损坏 sealed resource 先拒绝,journal hook 失败先拒绝,之后授权恢复可继续;连续两次恢复保持已记录结果与签名;同目录 peer App 和后建 future App 保持不变。原生 full crate 还覆盖 missing candidate、damaged target、安全重启和固定 action 规则。它们没有真实替换本人安装的 App;这不证明完整 owner AppHandle journal adoption 或 packaged UI 操作已验收。

本轮 cargo test --locked 当前完整 base83/head85通过,两项需实际 installed signed App 的测试和一项 released signed archive 测试仍 ignored;这次决定性签名/复制/交换由独立真实 ad-hoc signed fixture 另外验证。head clippy、两端 cargo fmt、changed rustfmt、12项 Python Desktop 测试、DCO/diff 检查通过。完整 runtime snapshot 在两端按 exact source 准备,包含原前端 build;未声称实际安装/UI 运行。

初次 head 大套件有 84通过/1失败,是未改服务 fixture5秒内未达 Matching;保留这个观察及未证实根因。后续完整 B/H 和单独同源服务测试通过;服务源/依赖和 socket 路径未由此 PR 改动,且新增 filesystem 不调用 socket,因此已有当前证据充分覆盖本 PR 改动,未称其修好了服务间歇问题,也未只展示一次绿色结果。初次 base 缺 runtime 资源的 build 失败也保留,按原生 snapshot 恢复环境后重跑。按配置不查询/等待/轮询 CI。

语义 triage 覆盖完整 diff;这里没有新增 shared enum/state vocabulary,复用已有 native owner。advisory 首次 cwd 错误已保留,改到 root 后得到0 supported candidates;工具不支持 Rust,0不等于证明无语义。未调整任何预算、扫描根或必需断言。Future-facing pass 已应用:删掉 macOS 脆弱补偿路径,复用一个原生效果边界;没有必要再加恢复 schema 或无用抽象。

我的整体评价

这次 named defect 的有界 goal_achieved,long_horizon 和 user_experience 都 improved:它减少回退死亡后需要人工救安装路径的情况,已有操作和授权步骤保持。正向判断有同输入、固定源码、实际签名后端和强杀前后对照支持;频率、常规耗时、token、长期吞吐、断电耐久性都没有实测,不能宣称普遍提速。

兼容性保留既有 pending journal/backup 数据和非 macOS 分支;没新增 request decoder。风险主要是 scoped macOS 文件系统效果和未测的真实发布/installed owner journey,不是 quota/agent 权限。当前无阻塞发现,历史服务间歇诊断独立保留;这份 APPROVE 不授予 runtime/control-plane 自合并权限。

English verdict: APPROVE — 974b22d461ce0aa5bd2c54747cb9619ec5345081 removes the reproduced macOS rollback process-death gap in the existing owner. An independent actual signed-copy/journal/restore probe fails install continuity at immutable base and passes all3cases at head, using real ditto/codesign/filesystem and SIGKILL. Current full cargo B83/H85, head clippy, both fmt and12Desktop tests pass; initial unrelated service timeout and preparation failures are preserved. No power-loss, live installed UI/journal adoption, normal-path speedup or merge authority is claimed.

The previous two-rename replacement left the installed App path empty when the process died before the second rename, bypassing in-process compensation. Use macOS RENAME_SWAP so the install path stays occupied, and cover the original crash window with a child-process kill regression.

Signed-off-by: Duang777 <[email protected]>
@Duang777
Duang777 force-pushed the codex/fix-desktop-bundle-swap-recovery-20261005 branch from 974b22d to 90b1be0 Compare October 5, 2026 19:34
@huangruiteng
huangruiteng merged commit 279c2fc into loopx-project:main Oct 5, 2026
6 of 9 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