From d2de622744212ea0ba720b3f4212b82940ffe84b Mon Sep 17 00:00:00 2001 From: Agando Date: Tue, 27 Oct 2026 11:23:10 +0100 Subject: fix(newos4): rollback keeps scaffolding, checkpoints cannot collide, tests gate the build Review of the 08:30 implementer run (v4.6.102, checkpoint name fix). The shipped fix was correct; these are the defects it introduced or left behind, each measured first. - virtual-fs.js rollback(): the snapshot strips _snapshots/_sessions/.trash for bloat. Restoring it verbatim deleted every checkpoint, every agent session and the trash, made rollback irreversible and unrepeatable. Re-attach the live copies after the root swap. - checkpoint(): names are millisecond-resolution, so tight calls overwrote each other (measured 6 calls -> 3 snapshots). Dedupe with a -N suffix, refuse names containing "/" (written but unlistable), and prune by snapshot createdAt instead of filename sort, which let a custom name ("aaa-oldest") evict newer auto-named checkpoints. - module-predictor.js: legacy pre-v2 points have no `usable`, so a 0.079 s aborted run re-entered similarity as evidence and floored every prediction at 1 s. Backfill `usable` from the measurement on load, drop the ones it rejects, add a belt-and-braces guard in _findSimilar and count only usable points as actual executions. - newos4-maintain.py: run every node-runnable modules/test/test-*.js on each maintenance pass. The v4.6.102 regression was written but no job ever executed it. - newos4-steer.py: `done` now requires --commit that exists in the repo, and gains `reopen`. A written plan was being closed as done at 06:17 while the predictor fix shipped nowhere, so the implementer's pre-flight queue was empty. - steering.json: predictor item reopened, checkpoint item stamped with f7249ff. - maintenance board: MAINT-002/005/015 were fixed in v4.6.81-85 but still listed open/high; closed with their commit ids, and MAINT-010/016 annotated with the probes that keep them open. Tests: test-vfs-checkpoint 23/23 (RED 14/9 before), test-predictor-accuracy 38/38 (T1-T3 RED on HEAD), maintain --fix all PASS, browser round-trip "Checkpoint created: " -> "Rolled back to checkpoint: " on v4.6.103. Also carries the already-deployed 2026-10-26 syntax-gate hunk in newos4-maintain.py (syntax_failures + gated rebuild) that had never been committed. --- modules/test/test-predictor-accuracy.js | 33 ++++++++++++++++++++ modules/test/test-vfs-checkpoint.js | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) (limited to 'modules/test') diff --git a/modules/test/test-predictor-accuracy.js b/modules/test/test-predictor-accuracy.js index 3688b03..74f0fcc 100644 --- a/modules/test/test-predictor-accuracy.js +++ b/modules/test/test-predictor-accuracy.js @@ -247,5 +247,38 @@ function ok(name, cond, detail) { ok('S3 pending consumed', p._pending.length === 0); } +// T. v1 payload carrying an aborted garbage point must not train similarity (2026-10-27) +{ + const { p, term, out } = mk(); + const feats = p._extractFeatures('read the logs'); + term.fs.fsWriteJSON('/system/metrics/predictor.json', { + dataPoints: [{ features: feats, observedDuration: 0.079, observedTurns: 1 }, + { features: feats, observedDuration: 30, observedTurns: 3 }], + predictions: 2 + }); + p._loadFromVFS(); + ok('T1 legacy aborted point dropped on load', p._dataPoints.length === 1, 'n=' + p._dataPoints.length); + const similar = p._findSimilar(p._extractFeatures('read the logs')); + ok('T2 similarity ignores the aborted point', + similar.length === 1 && similar[0].observedDuration === 30, + JSON.stringify(similar.map(s => s.observedDuration))); + out.length = 0; p.showStatus(); + const line = out.find(l => l.includes('Actual executions')) || ''; + ok('T3 status counts only usable executions', /Actual executions:\s*1\s*$/.test(line.trim()), line.trim()); +} + +// U. a lone aborted point cannot floor the next prediction +{ + const { p, term } = mk(); + term.fs.fsWriteJSON('/system/metrics/predictor.json', { + dataPoints: [{ features: { hasRead: true }, observedDuration: 0.079, observedTurns: 1 }], + predictions: 1 + }); + p._loadFromVFS(); + const pred = p.notePrediction('read the logs', 'test'); + ok('U1 prediction is not floored by a lone aborted point', pred && pred.floored === false, + JSON.stringify(pred && { duration: pred.duration, floored: pred.floored })); +} + console.log(passes + '/' + (passes + fails) + ' PASS'); process.exit(fails ? 1 : 0); diff --git a/modules/test/test-vfs-checkpoint.js b/modules/test/test-vfs-checkpoint.js index 03a8c8a..65f32dc 100644 --- a/modules/test/test-vfs-checkpoint.js +++ b/modules/test/test-vfs-checkpoint.js @@ -84,6 +84,61 @@ function ok(name, cond, detail) { 'output=' + JSON.stringify(out.join('\n'))); } +// D. same-millisecond calls must not overwrite each other +{ + const fs = mkFs(); + const names = []; + // 4 calls: one more than the cap of 3, so every name is still stored when the next one is + // minted (at 6+, prune legitimately frees a name and it may be reused). + for (let i = 0; i < 4; i++) names.push(fs.checkpoint('')); + ok('D1 four unnamed checkpoints are four distinct names', new Set(names).size === 4, + 'names=' + JSON.stringify(names)); + ok('D2 prune still caps the store at three', fs.listCheckpoints().length === 3, + 'stored=' + JSON.stringify(fs.listCheckpoints())); +} + +// E. prune keeps the NEWEST three, not the alphabetically last +{ + const fs = mkFs(); + fs.checkpoint('aaa-oldest'); + fs.checkpoint(''); + fs.checkpoint(''); + fs.checkpoint(''); // 4th -> prune + const left = fs.listCheckpoints(); + ok('E1 oldest custom-named checkpoint was pruned', !left.includes('aaa-oldest'), 'left=' + JSON.stringify(left)); + ok('E2 three checkpoints remain', left.length === 3, 'left=' + JSON.stringify(left)); + ok('E3 rollback still works after pruning', fs.rollback(left[0]) === true); +} + +// F. rollback must not destroy scaffolding +{ + const fs = mkFs(); + fs.mkdirRecursive('/_sessions/agent1'); + fs.writeFile('/_sessions/agent1/log.json', '{"turns":3}'); + fs.mkdirRecursive('/.trash'); + fs.writeFile('/.trash/junk.txt', 'x'); + fs.writeFile('/work.txt', 'before'); + const name = fs.checkpoint('keep-me'); + fs.writeFile('/work.txt', 'after'); + ok('F1 rollback succeeds', fs.rollback(name) === true); + ok('F2 file restored', fs.readFile('/work.txt') === 'before', 'got ' + fs.readFile('/work.txt')); + ok('F3 checkpoint survived rollback', fs.listCheckpoints().includes('keep-me'), + 'list=' + JSON.stringify(fs.listCheckpoints())); + ok('F4 sessions survived rollback', !!fs.readDir('/_sessions')); + ok('F5 trash survived rollback', !!fs.readDir('/.trash')); + ok('F6 rollback is repeatable', fs.rollback('keep-me') === true); +} + +// G. a name with a slash is refused, not silently unlistable +{ + const fs = mkFs(); + let threw = false; + try { fs.checkpoint('a/b'); } catch (e) { threw = true; } + ok('G1 slash in name throws', threw); + ok('G2 nothing was written', fs.listCheckpoints().length === 0, + 'list=' + JSON.stringify(fs.listCheckpoints())); +} + if (fails === 0) { console.log('test-vfs-checkpoint: ' + passes + ' passed, 0 failed'); } else { -- cgit v1.2.3