synabun 2026.7.31 → 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 +10 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/neural-interface/lib/backup-service.js +87 -5
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/package.json +1 -1
- package/setup.js +8 -4
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.
|
|
@@ -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
|
@@ -59,6 +59,65 @@ const INCOMPRESSIBLE_EXTENSIONS = new Set([
|
|
|
59
59
|
'.png', '.webm', '.webp', '.woff', '.woff2', '.zip',
|
|
60
60
|
]);
|
|
61
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
|
+
|
|
62
121
|
export class BackupStorageError extends Error {
|
|
63
122
|
constructor({ availableBytes, requiredBytes, folderPath }) {
|
|
64
123
|
const availableMiB = Math.floor(availableBytes / 1024 / 1024);
|
|
@@ -293,11 +352,11 @@ function normalizeAdditionalEntries(entries = []) {
|
|
|
293
352
|
return result;
|
|
294
353
|
}
|
|
295
354
|
|
|
296
|
-
async function writeArchive({ tempPath, files, manifest }) {
|
|
355
|
+
async function writeArchive({ tempPath, files, manifest, level = 6 }) {
|
|
297
356
|
const { createWriteStream } = await import('node:fs');
|
|
298
357
|
await new Promise((resolveWrite, reject) => {
|
|
299
358
|
const output = createWriteStream(tempPath, { flags: 'wx' });
|
|
300
|
-
const archive = archiver('zip', { zlib: { level
|
|
359
|
+
const archive = archiver('zip', { zlib: { level } });
|
|
301
360
|
output.on('close', resolveWrite);
|
|
302
361
|
output.on('error', reject);
|
|
303
362
|
archive.on('warning', error => error.code === 'ENOENT' ? null : reject(error));
|
|
@@ -341,6 +400,7 @@ export async function createVerifiedBackup({
|
|
|
341
400
|
getAvailableBytes = backupAvailableBytes,
|
|
342
401
|
onBeforeArchive = null,
|
|
343
402
|
onProgress = null,
|
|
403
|
+
upgradeSoftLimitBytes = UPGRADE_SNAPSHOT_SOFT_LIMIT_BYTES,
|
|
344
404
|
now = () => new Date(),
|
|
345
405
|
} = {}) {
|
|
346
406
|
// A large data home spends minutes in checksum/archive with no output. Phase
|
|
@@ -372,10 +432,19 @@ export async function createVerifiedBackup({
|
|
|
372
432
|
|
|
373
433
|
// De-duplicate before estimating work space so explicit additional entries
|
|
374
434
|
// cannot inflate the preflight or create ambiguous ZIP members.
|
|
375
|
-
const
|
|
435
|
+
const deduped = [...new Map(files.map(file => [
|
|
376
436
|
file.archivePath.replace(/\\/g, '/'),
|
|
377
437
|
file,
|
|
378
438
|
])).values()];
|
|
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
|
+
|
|
379
448
|
const workingSpace = assertBackupWorkingSpace({
|
|
380
449
|
folderPath,
|
|
381
450
|
files: sourceFiles,
|
|
@@ -384,7 +453,14 @@ export async function createVerifiedBackup({
|
|
|
384
453
|
freeSpaceReserveBytes,
|
|
385
454
|
getAvailableBytes,
|
|
386
455
|
});
|
|
387
|
-
report({
|
|
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
|
+
});
|
|
388
464
|
|
|
389
465
|
sqliteSnapshot = createConsistentSqliteSnapshot(databasePath, sqliteTemp);
|
|
390
466
|
files.length = 0;
|
|
@@ -429,12 +505,18 @@ export async function createVerifiedBackup({
|
|
|
429
505
|
} : null,
|
|
430
506
|
files: finalFiles.map(file => file.archivePath),
|
|
431
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,
|
|
432
512
|
verified: true,
|
|
433
513
|
verification: { algorithm: 'sha256', publication: 'verify-before-atomic-rename' },
|
|
434
514
|
};
|
|
435
515
|
if (typeof onBeforeArchive === 'function') await onBeforeArchive({ files: finalFiles, manifest });
|
|
436
516
|
report({ phase: 'archive', files: finalFiles.length, bytes: workingSpace.archiveBytes });
|
|
437
|
-
|
|
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 });
|
|
438
520
|
report({ phase: 'verify', files: finalFiles.length });
|
|
439
521
|
const publishedManifest = verifyBackupArchive(tempPath);
|
|
440
522
|
renameSync(tempPath, destination);
|
|
@@ -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
|
@@ -263,11 +263,15 @@ async function protectFirstLaunchAfterUpdate(version) {
|
|
|
263
263
|
appVersion: manifest.lastAppVersion || null,
|
|
264
264
|
// Without this the checksum and archive phases run silently for minutes
|
|
265
265
|
// on a large data home, which users read as a hang and kill.
|
|
266
|
-
onProgress: ({ phase, files, bytes }) => {
|
|
266
|
+
onProgress: ({ phase, files, bytes, skippedFiles, skippedBytes }) => {
|
|
267
267
|
const size = bytes ? ` (${(bytes / 1024 / 1024).toFixed(0)} MB)` : '';
|
|
268
|
-
if (phase === 'collect')
|
|
269
|
-
|
|
270
|
-
|
|
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}...`);
|
|
271
275
|
else if (phase === 'verify') info(' Verifying archive integrity...');
|
|
272
276
|
},
|
|
273
277
|
});
|