From 1c01d3f06f259a0bdcea5b39381dede79c58644b Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Thu, 29 Oct 2026 05:53:30 +0530 Subject: [PATCH] Backups: never prune a file's original backup before anything else Pruning (oldest first above 200 entries) kept only recent protected entries and the newest entry per source/path. A burst of automatic writes (cursor shaders: 16 in 75 s on a real session) could push out the first backup of the terminal's main config, the only copy of what it was before NMSh. The oldest entry of every file path is now pruned only as a last resort, after every unprotected entry. --- CHANGELOG.md | 1 + src/backups/journal.ts | 10 ++++++++-- tests/backupsJournal.test.ts | 12 ++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 305c933d..e942d46d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- Backups keep the oldest copy of every file NMSh changed (what it was before NMSh first touched it). Frequent automatic writes, such as cursor shaders regenerating, no longer push it out of `/changes`. - On a slow terminal connection or a busy machine, the terminal's reply to NMSh's startup image check no longer appears in the composer as `_Gi=31;OK\` and no longer turns images into character art. NMSh's startup check now ends as soon as the terminal has answered (instead of always waiting 80 ms), waits up to 0.75 s for a slow one, and still uses an answer that comes later. - A Kitty or Ghostty image drawn before NMSh had taken the screen (a welcome image that loaded during startup) is sent again instead of leaving blank space where the picture belongs. - A native welcome image (Kitty, Ghostty) stays a native image after the first key press, scroll or resize instead of turning into character art. A logo animation still stops at once and its frames are deleted; only the still remains. diff --git a/src/backups/journal.ts b/src/backups/journal.ts index c615a132..041cd864 100644 --- a/src/backups/journal.ts +++ b/src/backups/journal.ts @@ -198,11 +198,17 @@ export class Journal { const key=`${item.source}:${file.path}`; if(!anchored.has(key)){keep.add(item.id);anchored.add(key);} } + // The oldest backup of each file holds what it was before NMSh first changed it: a burst of automatic writes to other + // files (cursor shaders regenerate often) must not push that original out. It goes only when nothing else can. + const originals = new Set(), originalPaths = new Set(); + for (const item of [...existing].reverse()) for (const file of item.files) { + if (!originalPaths.has(file.path)) { originals.add(item.id); originalPaths.add(file.path); } + } const markerText = () => JSON.stringify({keys:[...pruned],count:previousCount+victims.length}); const fits = () => used + size + (victims.length ? Buffer.byteLength(markerText()) - oldMarker : 0) <= maxBytes && count + 1 <= maxEntries; - for (const old of [...existing].reverse()) { + for (const lastResort of [false, true]) for (const old of [...existing].reverse()) { if (fits()) break; - if (keep.has(old.id)) continue; + if (keep.has(old.id) || victims.includes(old) || (!lastResort && originals.has(old.id))) continue; used -= cost(old); count--; victims.push(old); for(const file of old.files)pruned.add(digest(`${old.source}:${file.path}`)); } diff --git a/tests/backupsJournal.test.ts b/tests/backupsJournal.test.ts index 218baedf..6141674c 100644 --- a/tests/backupsJournal.test.ts +++ b/tests/backupsJournal.test.ts @@ -141,3 +141,15 @@ test('the newest source/path anchor is retained even if its write was interrupte assert.ok(journal.list().some(entry=>entry.id===newest.id));assert.throws(()=>journal.read(old.id)); assert.equal(journal.provenanceComplete('cursor',f.path),false); }); +test('a burst of automatic writes to another file never prunes the original backup of a config file', t => { + const f=fixture(t);const journal=new Journal({home:f.home,directory:f.directory,maxEntries:10}); + const shader=join(f.home,'cursor.glsl');writeFileSync(shader,'rev start'); + const write=(path:string,after:string,source:string,label:string)=>{const entry=journal.record([{before:snapshot(path,f.home),after}],source,label);writeFileSync(path,after);journal.complete(entry);return entry;}; + const original=write(f.path,'appearance 1','appearance','First appearance edit'); + write(f.path,'appearance 2','appearance','Second appearance edit'); + for(let n=0;n<40;n++)write(shader,`rev ${n}`,'cursor','Cursor shader'); + const kept=journal.list(); + assert.ok(kept.length<=10); + assert.ok(kept.some(entry=>entry.id===original.id),'the first backup of the config file (its pre-NMSh content) is kept'); + assert.equal(journal.content(journal.read(original.id),0),'before\r\n'); +});