Skip to content

fix(desktop): make Profile migration and plugin recovery fail closed - #251

Merged
yaojin3616 merged 4 commits into
mainfrom
agent/debugger/01a051f5-524f-7d26-8b0c-6c7657fd2c15
Sep 1, 2026
Merged

fix(desktop): make Profile migration and plugin recovery fail closed#251
yaojin3616 merged 4 commits into
mainfrom
agent/debugger/01a051f5-524f-7d26-8b0c-6c7657fd2c15

Conversation

@yaojin3616

@yaojin3616 yaojin3616 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Completes the fail-closed recovery work for #250. A failed migration, rollback, registry read, or plugin restore can no longer fall through into startup maintenance that rewrites the active Profile, and recovery material is never deleted by launch count.

Root causes fixed

  1. Migration failure was indistinguishable from no-op. migrateProfileToGenerations() now returns migrated | no-op | deferred-failure. A single startup-maintenance owner stops immediately on deferred-failure; it does not project, prune, or run package repair on the legacy Profile.
  2. Rollback errors were swallowed. The migration snapshot now journals each Profile path (node_modules, package.json, lockfile, and install marker), persists restore intent before rename, verifies every quarantine/restore/cleanup step, and keeps the journal plus snapshot whenever Windows rename/rm or the journal commit fails.
  3. Removal backups were deleted automatically. Clean launches only record bootVerifiedAt. Backups remain until the user explicitly chooses cleanup in Safe Mode; cleanup itself is a resumable ledger -> trash -> delete -> final-ledger transaction.

Recovery hardening

  • A 60-second healthy renderer heartbeat is required before migration snapshots or removal state can be boot-confirmed.
  • Generation registry, desired pointer, generation metadata, package roots, Profile manifest, and dependency/peer closure all fail closed on missing, corrupt, unreadable, or unsafe state. Projection validates all authoritative inputs before touching links or the Profile manifest.
  • Plugin removal uses unique removal IDs, checksummed full backups, exact restore journals, conflict quarantine, and per-removal recovery locks. Repeated removal of the same plugin cannot restore or delete a sibling transaction.
  • Plugin-owned LaunchAgents are backed up and restored as part of the same transaction. Protocol-1 legacy component backups are adopted only with a unique owner, exact transaction time window, source digest, and path proof.
  • Recovery, backup, LaunchAgent, detached-Profile, and conflict paths reject symlink/junction traversal before copy, rename, restore, or delete.
  • Safe Mode can inspect, open, retry, restore, and explicitly delete one exact backup; incomplete recovery permits only its recorded removal ID.

Regression coverage

Includes end-to-end failure-path coverage for:

  • staging 404 and deferred retry without Profile repair
  • root and transitive dependency/peer validation
  • corrupt/unreadable desired pointer, registry metadata, generation package, and Profile manifest
  • Windows rename/rm/journal failures across snapshot and rollback
  • restore and cleanup crashes at ledger, copy, integrity, trash, and final-commit boundaries
  • repeated same-plugin removals, backup retention across launches, and explicit cleanup
  • protocol-1 legacy recovery, ownership ambiguity, stale component material, and junction/path-escape attempts

Verification

  • npx patch-package --error-on-fail
  • npm test -- --run - 76 files, 621 tests passed
  • npm run typecheck
  • npm run build
  • node --check for all three generation modules
  • git diff --check

Fixes #250

Debug Agent and others added 2 commits August 30, 2026 02:24
…, gate removal-backup deletion

Windows 0.7.1 (issue #250) chained three defects that turned one bad
pnpm fetch into a corrupted profile with missing plugins:

1. migrateProfileToGenerations collapsed staging/peer-validation
   failures into a boolean that launchHarness treated like "nothing to
   migrate" — so the shared-tree repair ran pnpm on the broken state
   and clobbered the recovery surface for the next launch.
2. rollBackMigration swallowed every rename/rm error via
   `.catch(() => undefined)` and still reported success — a single
   in-use file or virus scanner on Windows wiped the snapshot while
   logging "rolled back", and the next launch had nothing left.
3. confirmPluginRemovalsBooted deleted each plugin's recovery backup
   on the second clean launch with no user interaction, so a single
   bad launch could permanently destroy the only copy of a paid plugin.

Fix:
- migration returns a tri-state `MigrationOutcome` so launchHarness
  only runs repairProfilePackages on `no-op`. `deferred-failure` logs
  the reason and skips the repair, so a broken tree is not clobbered.
- rollBackMigration verifies each rename (snapshot path gone, live
  path present) and returns false on any failure, preserving the
  snapshot and refreshing the deferred marker so the next launch can
  show the user what went wrong.
- plugin-removal no longer auto-deletes verified backups. The launcher
  records `bootVerifiedAt` and only `cleanupVerifiedRemovalBackup`
  deletes on explicit user action; the recovery UI now has a real
  surface for the user to inspect before deletion.

Regression coverage: 5 new tests in migration-failure-paths.test.ts
cover staging 404, peer-validation failure, two flavours of rollback
rename failure, and the confirm-migration cleanup; plugin-removal
tests now assert that no backup is auto-deleted across repeated
clean launches and that cleanup is per-plugin.

Fixes #250
@yaojin3616 yaojin3616 changed the title fix(desktop): surface migration failures, preserve rollback snapshots, gate removal-backup deletion fix(desktop): make Profile migration and plugin recovery fail closed Aug 30, 2026
@yaojin3616
yaojin3616 merged commit f310431 into main Sep 1, 2026
2 of 3 checks passed
@yaojin3616
yaojin3616 deleted the agent/debugger/01a051f5-524f-7d26-8b0c-6c7657fd2c15 branch September 1, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant