Skip to content

Backups: stable Theme Bridge ownership, isolated damaged targets, reconciled interrupted writes (D2–D4) - #535

Draft
raiseCatError wants to merge 4 commits into
fix/backup-retention-originals-20261029from
fix/backup-integrity-20261029
Draft

raiseCatError wants to merge 4 commits into
fix/backup-retention-originals-20261029from
fix/backup-integrity-20261029

Conversation

@raiseCatError

@raiseCatError raiseCatError commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #534. Fixes D2–D4 from #534's follow-up list. Design notes: docs/development/native-appearance-welcome/BACKUPS_AND_RESET.md → "Integrity follow-ups".

D2 — Theme Bridge ledger rewritten every launch. Cause: writeArtifact (src/themeBridge/artifacts.ts) called the unjournaled saveLedger with a fresh generatedAt even when the artifact was unchanged, so every launch replaced ledger.json and older entries read "edited since". Restoring an older entry wrote its whole ledger before-image and forgot tools managed later (e.g. Vim).

  • Unchanged artifacts keep generatedAt; commitLedger writes nothing when the record is the same and journals every real record change. saveLedger is removed.
  • Restore reconciles the record per tool: tools whose file/include this backup restores take the backup's record (that part only); every other tool keeps its current record. Record-only entries restore their record only where it still matches the same file bytes; restoring the first record removes it.
  • Real-binary repro (isolated HOME, policy Follow, 3 launches each): 1c01d3f build changes the ledger sha on every launch with no backup; this branch keeps it byte-identical after the first launch.

D3 — one damaged target blocked every write. Cause: Journal.read ran the filesystem guard on every recorded target path, and every write listed the journal strictly; a directory replaced by a file threw "A configuration parent is not a directory" for all writes.

  • Targets are checked lexically in the journal; full checks (no symlinks, real directories, owner, permissions) still apply whenever a target is read or written, and their errors now name the path and reason.
  • A damaged target blocks only its own restore (message names entry, path, reason; nothing written). /changes shows unsafe: <reason>.
  • Entries that fail verification are listed with their reason, kept, counted toward limits and never pruned; writes to user files they name fail closed (NMSh's own JSON records are always re-captured, so they are not blocked).

D4 — interrupted writes. outcome stays prepared|applied (older builds still read every manifest); a resolution field records reverted / rollback-incomplete / finalized / reviewed. Journal.transactionState classifies from on-disk hashes: not applied, partially applied, applied but unfinalized, conflicted, in progress (another live writer, < 10 min), indeterminate. /changes counts and marks them; Enter reviews a rollback (normal restore, own undo checkpoint), F finalizes only with proof, D sets aside as reviewed. In-process failures (including an edit racing the backup) settle as reverted. Unresolved entries are never pruned.

Tests (isolated HOME via scripts/test.mjs runTestFiles):

Review: independent Sonnet review, no blockers; should-fix items fixed in the second commit.

Limits: per-host appearance/shader records cover one file group and restore together, so they are not reconciled per tool. Each journaled write still lists and verifies the whole history (~0.4 s at 200 entries). Physical terminal QA NOT RUN.

Exact-head CI: run 40808808548 on b0b473b: success. Earlier heads failed only on a wrap-sensitive assertion in the new test (fixed in the last two commits) plus known live-zsh/browser timing flakes.

…errupted writes reconciled

- Theme Bridge: re-applying unchanged colors no longer rewrites ledger.json;
  every real record change is journaled; a restore reconciles the record per
  tool so an older backup never forgets files NMSh manages for other tools.
- A damaged backup target (directory replaced by a file, symlink, wrong type,
  inaccessible parent) blocks only its own restore, with the exact entry, path
  and reason. Unreadable entries are kept, counted, never pruned, and block
  writes only to the files they name.
- Unfinished entries are classified from on-disk hashes (not applied, partially
  applied, applied but unfinalized, conflicted, reverted, ...), shown in
  /changes, and can be rolled back with an undo checkpoint or finalized only
  with proof. In-process failures record reverted/rollback-incomplete; unresolved
  entries are never pruned. Older builds still read every manifest.
…rites

From independent review:
- An edit that lands while the backup is written now settles the entry as
  reverted (the recheck runs inside the transaction), so it never lingers as
  an unprunable 'prepared' entry.
- /changes: D sets an unfinished entry aside as reviewed without touching
  files; it then no longer needs review and can be pruned. Rollback-incomplete
  entries stay protected.
- A restore of an entry another live NMSh process is writing is refused; a
  'live' writer of an entry older than 10 minutes is treated as a reused PID.
- A damaged older copy of NMSh's own JSON records no longer blocks writes that
  re-capture them; user files named by a damaged entry still fail closed.
- Record-only Theme Bridge entries restore their record (only where it still
  describes the same file bytes); restoring the first record removes it.
- The unjournaled saveLedger is gone.

This branch has not been deployed

No deployments
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.

1 participant