synabun 2026.7.30 → 2026.7.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/neural-interface/lib/backup-service.js +143 -6
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/package.json +1 -1
- package/setup.js +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
# SynaBun v.2026.07.32
|
|
2
|
+
|
|
3
|
+
A follow-up to 2026.07.31 that makes updating fast again on a large data home.
|
|
4
|
+
|
|
5
|
+
## 🛠 Fixed
|
|
6
|
+
|
|
7
|
+
- **Updating took several minutes on a large data home** — Before a new version starts, SynaBun snapshots your data home so the update can be rolled back. That snapshot archived *everything*, so a multi-GB data home spent minutes compressing during `npm install` — one report was 9,470 files and 3,832 MB. An upgrade snapshot only needs to protect state: it never rewrites your generated images, audio, or video, and those survive a rollback on disk regardless. Upgrade snapshots now skip generated media, and if what remains is still over ~1 GB they fall back to irreplaceable state only — your `.env`, everything under `mcp-data/`, and any `.db`, `.sqlite`, or `.json`. Compression on the blocking path drops to the fastest level, since what is left is SQLite and JSON that still shrink well. The snapshot reports what it skipped and why, and its manifest records whether it is `state-only` or `complete` so a restore can never mistake one for the other. Scheduled and manual backups are unchanged and still capture everything.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
1
11
|
# SynaBun v.2026.07.30
|
|
2
12
|
|
|
3
13
|
A point release on top of 2026.07.20, focused on correct Claude Code model selection, a new project storage cleanup tool, and a stricter default browser policy.
|
|
@@ -10,6 +20,7 @@ A point release on top of 2026.07.20, focused on correct Claude Code model selec
|
|
|
10
20
|
|
|
11
21
|
## 🛠 Fixed
|
|
12
22
|
|
|
23
|
+
- **First launch after an update could look frozen** — The pre-update snapshot protects your data home before a new version starts, but it ran silently, so on a large data home there was nothing on screen for long enough that the upgrade looked hung and got killed — and an interrupted run left a partial archive behind that nothing reclaimed. The snapshot now reports its collect, checksum, archive, and verify phases with file counts and sizes; skips `data/youtube-downloads`, a re-downloadable cache rather than user state; stores already-compressed payloads instead of spending full CPU deflating video and images for no gain; and reclaims temporary files orphaned by an interrupted run. On a 1.8 GB data home this took the snapshot from 17.8s and 543 MB to 9.7s and 172 MB.
|
|
13
24
|
- **Sidepanel launched Opus 4.8 when Opus 5 was picked** — The model picker read its list from the standalone Claude Code CLI, while sidepanel chats ran on the Agent SDK's own bundled CLI. Both accept the same alias selectors and each resolved them against its own model table, so the picker could show one model while the session launched another. `@anthropic-ai/claude-agent-sdk` moves `0.3.174` → `0.3.220` so both sides resolve identically, and a version skew between the two is now reported instead of passing silently. Takes effect after a server restart, not just an install.
|
|
14
25
|
- **Claude model pricing** — Corrected against published list prices. Opus 5 was missing from the table entirely and fell through to a Sonnet fallback, while the Opus and Fable rows were inflated roughly 3x. Displayed cost for past Opus and Fable sessions drops accordingly and now tracks what was actually billed.
|
|
15
26
|
- **Coexistence ruleset preview included neighbouring sections** — The extraction boundary matched an LF-only marker that was absent from the template's CRLF section, so the preview could return the condensed rulesets alongside the requested coexistence rules. It now stops at a stable heading.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-memory-mcp",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.32",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-memory-mcp",
|
|
9
|
-
"version": "2026.7.
|
|
9
|
+
"version": "2026.7.32",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@huggingface/transformers": "^3.0.0",
|
package/mcp-server/package.json
CHANGED
|
@@ -44,6 +44,80 @@ const IMMUTABLE_BACKUP_EXTENSIONS = new Set([
|
|
|
44
44
|
'.pdf', '.png', '.svg', '.webm', '.webp', '.woff', '.woff2', '.zip',
|
|
45
45
|
]);
|
|
46
46
|
|
|
47
|
+
// Re-downloadable bulk media under data/. These are pipeline caches, not user
|
|
48
|
+
// state: including them made a pre-update snapshot spend minutes deflating
|
|
49
|
+
// hundreds of MB of video with no progress output, which reads as a hang and
|
|
50
|
+
// gets killed. A restore re-fetches them.
|
|
51
|
+
const EXCLUDED_DATA_DIRS = new Set([
|
|
52
|
+
'youtube-downloads',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
// Payloads that are already compressed. DEFLATE spends full CPU on every byte
|
|
56
|
+
// of these for no meaningful size gain, so they are stored verbatim instead.
|
|
57
|
+
const INCOMPRESSIBLE_EXTENSIONS = new Set([
|
|
58
|
+
'.avif', '.gif', '.ico', '.jpeg', '.jpg', '.mov', '.mp3', '.mp4', '.ogg',
|
|
59
|
+
'.png', '.webm', '.webp', '.woff', '.woff2', '.zip',
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// Generated media. An upgrade snapshot exists to roll back *state* if a new
|
|
63
|
+
// version misbehaves; it never rewrites these, and they survive a rollback on
|
|
64
|
+
// disk regardless. Archiving them put multi-GB data homes through several
|
|
65
|
+
// minutes of compression on the npm update path, so upgrade snapshots skip
|
|
66
|
+
// them. Scheduled and manual backups still capture everything.
|
|
67
|
+
const BULK_MEDIA_EXTENSIONS = new Set([
|
|
68
|
+
'.avif', '.bmp', '.gif', '.heic', '.jpeg', '.jpg', '.m4a', '.m4v', '.mkv',
|
|
69
|
+
'.mov', '.mp3', '.mp4', '.ogg', '.png', '.tiff', '.wav', '.webm', '.webp',
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// Snapshot kinds taken while the user is blocked on a version change.
|
|
73
|
+
const UPGRADE_BACKUP_KINDS = new Set(['pre-update', 'pre-migration']);
|
|
74
|
+
|
|
75
|
+
// Irreplaceable state: the memory database, UI state databases, and the JSON
|
|
76
|
+
// that configures them. Everything a rollback actually needs lives here.
|
|
77
|
+
const ESSENTIAL_STATE_EXTENSIONS = new Set(['.db', '.sqlite', '.sqlite3', '.json']);
|
|
78
|
+
|
|
79
|
+
// Above this, an upgrade snapshot stops trying to be comprehensive and keeps
|
|
80
|
+
// only irreplaceable state. Dropping media is not enough on its own — a data
|
|
81
|
+
// home can reach multiple GB in ordinary files, and compressing that on the
|
|
82
|
+
// npm update path is what made upgrades look broken.
|
|
83
|
+
const UPGRADE_SNAPSHOT_SOFT_LIMIT_BYTES = 1024 * 1024 * 1024;
|
|
84
|
+
|
|
85
|
+
function fileBytes(path) {
|
|
86
|
+
try { return statSync(path).size; } catch { return 0; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pick what an upgrade snapshot archives. Generated media always goes; if the
|
|
91
|
+
* remainder is still oversized, fall back to irreplaceable state only. Returns
|
|
92
|
+
* the kept files plus what was dropped, so the caller can say so out loud.
|
|
93
|
+
*/
|
|
94
|
+
function selectUpgradeSnapshotFiles(files, { softLimitBytes = UPGRADE_SNAPSHOT_SOFT_LIMIT_BYTES } = {}) {
|
|
95
|
+
const excluded = { reason: 'generated-media', files: 0, bytes: 0 };
|
|
96
|
+
const kept = [];
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
if (BULK_MEDIA_EXTENSIONS.has(extname(file.archivePath).toLowerCase())) {
|
|
99
|
+
excluded.files += 1;
|
|
100
|
+
excluded.bytes += fileBytes(file.diskPath);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
kept.push(file);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const keptBytes = kept.reduce((total, file) => total + fileBytes(file.diskPath), 0);
|
|
107
|
+
if (keptBytes <= softLimitBytes) return { files: kept, excluded };
|
|
108
|
+
|
|
109
|
+
const essential = [];
|
|
110
|
+
for (const file of kept) {
|
|
111
|
+
const isEnv = file.archivePath === 'env.bak';
|
|
112
|
+
const isMcpState = file.archivePath.startsWith('mcp-data/');
|
|
113
|
+
const isStateFile = ESSENTIAL_STATE_EXTENSIONS.has(extname(file.archivePath).toLowerCase());
|
|
114
|
+
if (isEnv || isMcpState || isStateFile) { essential.push(file); continue; }
|
|
115
|
+
excluded.files += 1;
|
|
116
|
+
excluded.bytes += fileBytes(file.diskPath);
|
|
117
|
+
}
|
|
118
|
+
return { files: essential, excluded: { ...excluded, reason: 'oversized-data-home' } };
|
|
119
|
+
}
|
|
120
|
+
|
|
47
121
|
export class BackupStorageError extends Error {
|
|
48
122
|
constructor({ availableBytes, requiredBytes, folderPath }) {
|
|
49
123
|
const availableMiB = Math.floor(availableBytes / 1024 / 1024);
|
|
@@ -112,6 +186,7 @@ function shouldSkipDataPath(relativePath) {
|
|
|
112
186
|
const name = parts.at(-1);
|
|
113
187
|
if (parts[0] === 'updater') return true;
|
|
114
188
|
if (parts[0] === 'logs') return true;
|
|
189
|
+
if (parts.length > 1 && EXCLUDED_DATA_DIRS.has(parts[0])) return true;
|
|
115
190
|
if (TRANSIENT_DATA_NAMES.has(name)) return true;
|
|
116
191
|
if (name.endsWith('.tmp') || name.endsWith('.pid')) return true;
|
|
117
192
|
if (/\.db-(?:wal|shm)$/i.test(name)) return true;
|
|
@@ -205,6 +280,33 @@ export function assertBackupWorkingSpace({
|
|
|
205
280
|
};
|
|
206
281
|
}
|
|
207
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Remove archive temps, SQLite temps, and stage dirs abandoned by an earlier
|
|
285
|
+
* run. A backup interrupted with Ctrl-C never reaches its own catch/finally,
|
|
286
|
+
* so its partial archive is orphaned forever — a killed pre-update snapshot
|
|
287
|
+
* left hundreds of MB behind with nothing to reclaim it.
|
|
288
|
+
*/
|
|
289
|
+
export function cleanupStaleBackupArtifacts(folderPath, { maxAgeMs = 60 * 60 * 1000, now = Date.now } = {}) {
|
|
290
|
+
if (!existsSync(folderPath)) return [];
|
|
291
|
+
const removed = [];
|
|
292
|
+
let entries;
|
|
293
|
+
try { entries = readdirSync(folderPath); }
|
|
294
|
+
catch { return removed; }
|
|
295
|
+
for (const name of entries) {
|
|
296
|
+
const isArchiveTemp = /\.zip\.\d+\.tmp$/.test(name);
|
|
297
|
+
const isSqliteTemp = /^\.synabun-(?:db|extra-db)-.*\.tmp$/.test(name);
|
|
298
|
+
const isStageDir = /^\.synabun-files-/.test(name);
|
|
299
|
+
if (!isArchiveTemp && !isSqliteTemp && !isStageDir) continue;
|
|
300
|
+
const path = resolve(folderPath, name);
|
|
301
|
+
try {
|
|
302
|
+
if (now() - statSync(path).mtimeMs < maxAgeMs) continue;
|
|
303
|
+
rmSync(path, { recursive: true, force: true });
|
|
304
|
+
removed.push(name);
|
|
305
|
+
} catch { /* another process may be mid-cleanup; leave it */ }
|
|
306
|
+
}
|
|
307
|
+
return removed;
|
|
308
|
+
}
|
|
309
|
+
|
|
208
310
|
function snapshotMutableBackupFiles(files, folderPath) {
|
|
209
311
|
if (!files.some(isMutableBackupInput)) return { files, stageDir: null };
|
|
210
312
|
const stageDir = mkdtempSync(resolve(folderPath, `.synabun-files-${process.pid}-`));
|
|
@@ -250,17 +352,20 @@ function normalizeAdditionalEntries(entries = []) {
|
|
|
250
352
|
return result;
|
|
251
353
|
}
|
|
252
354
|
|
|
253
|
-
async function writeArchive({ tempPath, files, manifest }) {
|
|
355
|
+
async function writeArchive({ tempPath, files, manifest, level = 6 }) {
|
|
254
356
|
const { createWriteStream } = await import('node:fs');
|
|
255
357
|
await new Promise((resolveWrite, reject) => {
|
|
256
358
|
const output = createWriteStream(tempPath, { flags: 'wx' });
|
|
257
|
-
const archive = archiver('zip', { zlib: { level
|
|
359
|
+
const archive = archiver('zip', { zlib: { level } });
|
|
258
360
|
output.on('close', resolveWrite);
|
|
259
361
|
output.on('error', reject);
|
|
260
362
|
archive.on('warning', error => error.code === 'ENOENT' ? null : reject(error));
|
|
261
363
|
archive.on('error', reject);
|
|
262
364
|
archive.pipe(output);
|
|
263
|
-
for (const file of files)
|
|
365
|
+
for (const file of files) {
|
|
366
|
+
const store = INCOMPRESSIBLE_EXTENSIONS.has(extname(file.archivePath).toLowerCase());
|
|
367
|
+
archive.file(file.diskPath, { name: `${BACKUP_PREFIX}/${file.archivePath}`, store });
|
|
368
|
+
}
|
|
264
369
|
archive.append(JSON.stringify(manifest, null, 2), { name: `${BACKUP_PREFIX}/manifest.json` });
|
|
265
370
|
archive.finalize().catch(reject);
|
|
266
371
|
});
|
|
@@ -294,11 +399,17 @@ export async function createVerifiedBackup({
|
|
|
294
399
|
freeSpaceReserveBytes = BACKUP_FREE_SPACE_RESERVE_BYTES,
|
|
295
400
|
getAvailableBytes = backupAvailableBytes,
|
|
296
401
|
onBeforeArchive = null,
|
|
402
|
+
onProgress = null,
|
|
403
|
+
upgradeSoftLimitBytes = UPGRADE_SNAPSHOT_SOFT_LIMIT_BYTES,
|
|
297
404
|
now = () => new Date(),
|
|
298
405
|
} = {}) {
|
|
406
|
+
// A large data home spends minutes in checksum/archive with no output. Phase
|
|
407
|
+
// reporting is what stops that from reading as a hang to the caller.
|
|
408
|
+
const report = typeof onProgress === 'function' ? onProgress : () => {};
|
|
299
409
|
if (!dataHome || !folderPath) throw new Error('dataHome and folderPath are required');
|
|
300
410
|
const createdAt = now();
|
|
301
411
|
mkdirSync(folderPath, { recursive: true });
|
|
412
|
+
cleanupStaleBackupArtifacts(folderPath);
|
|
302
413
|
const destination = resolve(folderPath, archiveNameFor(kind, createdAt));
|
|
303
414
|
const tempPath = `${destination}.${process.pid}.tmp`;
|
|
304
415
|
const sqliteTemp = resolve(folderPath, `.synabun-db-${process.pid}-${Date.now()}.tmp`);
|
|
@@ -321,11 +432,20 @@ export async function createVerifiedBackup({
|
|
|
321
432
|
|
|
322
433
|
// De-duplicate before estimating work space so explicit additional entries
|
|
323
434
|
// cannot inflate the preflight or create ambiguous ZIP members.
|
|
324
|
-
const
|
|
435
|
+
const deduped = [...new Map(files.map(file => [
|
|
325
436
|
file.archivePath.replace(/\\/g, '/'),
|
|
326
437
|
file,
|
|
327
438
|
])).values()];
|
|
328
|
-
|
|
439
|
+
|
|
440
|
+
// Keep the upgrade path bounded: state is archived, generated media is not.
|
|
441
|
+
const leanSnapshot = UPGRADE_BACKUP_KINDS.has(kind);
|
|
442
|
+
const selection = leanSnapshot
|
|
443
|
+
? selectUpgradeSnapshotFiles(deduped, { softLimitBytes: upgradeSoftLimitBytes })
|
|
444
|
+
: { files: deduped, excluded: null };
|
|
445
|
+
const sourceFiles = selection.files;
|
|
446
|
+
const excluded = selection.excluded;
|
|
447
|
+
|
|
448
|
+
const workingSpace = assertBackupWorkingSpace({
|
|
329
449
|
folderPath,
|
|
330
450
|
files: sourceFiles,
|
|
331
451
|
databasePath,
|
|
@@ -333,6 +453,14 @@ export async function createVerifiedBackup({
|
|
|
333
453
|
freeSpaceReserveBytes,
|
|
334
454
|
getAvailableBytes,
|
|
335
455
|
});
|
|
456
|
+
report({
|
|
457
|
+
phase: 'collect',
|
|
458
|
+
files: sourceFiles.length,
|
|
459
|
+
bytes: workingSpace.archiveBytes,
|
|
460
|
+
skippedFiles: excluded?.files ?? 0,
|
|
461
|
+
skippedBytes: excluded?.bytes ?? 0,
|
|
462
|
+
skippedReason: excluded?.reason ?? null,
|
|
463
|
+
});
|
|
336
464
|
|
|
337
465
|
sqliteSnapshot = createConsistentSqliteSnapshot(databasePath, sqliteTemp);
|
|
338
466
|
files.length = 0;
|
|
@@ -357,6 +485,7 @@ export async function createVerifiedBackup({
|
|
|
357
485
|
const stableInputs = snapshotMutableBackupFiles([...unique.values()], folderPath);
|
|
358
486
|
stageDir = stableInputs.stageDir;
|
|
359
487
|
const finalFiles = stableInputs.files.sort((a, b) => a.archivePath.localeCompare(b.archivePath));
|
|
488
|
+
report({ phase: 'checksum', files: finalFiles.length });
|
|
360
489
|
const checksums = {};
|
|
361
490
|
for (const file of finalFiles) checksums[file.archivePath] = `sha256:${await hashFile(file.diskPath)}`;
|
|
362
491
|
|
|
@@ -376,11 +505,19 @@ export async function createVerifiedBackup({
|
|
|
376
505
|
} : null,
|
|
377
506
|
files: finalFiles.map(file => file.archivePath),
|
|
378
507
|
checksums,
|
|
508
|
+
// A lean snapshot is not a whole-data-home backup. Recording that here
|
|
509
|
+
// keeps a restore from silently presenting it as one.
|
|
510
|
+
scope: leanSnapshot ? 'state-only' : 'complete',
|
|
511
|
+
excluded,
|
|
379
512
|
verified: true,
|
|
380
513
|
verification: { algorithm: 'sha256', publication: 'verify-before-atomic-rename' },
|
|
381
514
|
};
|
|
382
515
|
if (typeof onBeforeArchive === 'function') await onBeforeArchive({ files: finalFiles, manifest });
|
|
383
|
-
|
|
516
|
+
report({ phase: 'archive', files: finalFiles.length, bytes: workingSpace.archiveBytes });
|
|
517
|
+
// Level 1 on the blocking upgrade path: the remaining payload is mostly
|
|
518
|
+
// SQLite and JSON, where fast deflate still shrinks well.
|
|
519
|
+
await writeArchive({ tempPath, files: finalFiles, manifest, level: leanSnapshot ? 1 : 6 });
|
|
520
|
+
report({ phase: 'verify', files: finalFiles.length });
|
|
384
521
|
const publishedManifest = verifyBackupArchive(tempPath);
|
|
385
522
|
renameSync(tempPath, destination);
|
|
386
523
|
return {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neural-memory-interface",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.32",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "neural-memory-interface",
|
|
9
|
-
"version": "2026.7.
|
|
9
|
+
"version": "2026.7.32",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "Apache-2.0",
|
|
12
12
|
"dependencies": {
|
package/package.json
CHANGED
package/setup.js
CHANGED
|
@@ -261,6 +261,19 @@ async function protectFirstLaunchAfterUpdate(version) {
|
|
|
261
261
|
folderPath,
|
|
262
262
|
kind: 'pre-update',
|
|
263
263
|
appVersion: manifest.lastAppVersion || null,
|
|
264
|
+
// Without this the checksum and archive phases run silently for minutes
|
|
265
|
+
// on a large data home, which users read as a hang and kill.
|
|
266
|
+
onProgress: ({ phase, files, bytes, skippedFiles, skippedBytes }) => {
|
|
267
|
+
const size = bytes ? ` (${(bytes / 1024 / 1024).toFixed(0)} MB)` : '';
|
|
268
|
+
if (phase === 'collect') {
|
|
269
|
+
info(` ${files} files to protect${size}`);
|
|
270
|
+
if (skippedFiles) {
|
|
271
|
+
info(` Skipping ${skippedFiles} generated media files (${(skippedBytes / 1024 / 1024).toFixed(0)} MB) — an upgrade never touches them`);
|
|
272
|
+
}
|
|
273
|
+
} else if (phase === 'checksum') info(` Checksumming ${files} files...`);
|
|
274
|
+
else if (phase === 'archive') info(` Compressing${size}...`);
|
|
275
|
+
else if (phase === 'verify') info(' Verifying archive integrity...');
|
|
276
|
+
},
|
|
264
277
|
});
|
|
265
278
|
applyBackupRetention({ folderPath });
|
|
266
279
|
snapshotPath = snapshot.path;
|