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 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.30",
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.30",
9
+ "version": "2026.7.32",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@huggingface/transformers": "^3.0.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-mcp",
3
- "version": "2026.7.30",
3
+ "version": "2026.7.32",
4
4
  "private": true,
5
5
  "description": "Private MCP server runtime bundled with SynaBun for persistent memory and agent tooling",
6
6
  "license": "Apache-2.0",
@@ -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: 6 } });
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 sourceFiles = [...new Map(files.map(file => [
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({ phase: 'collect', files: sourceFiles.length, bytes: workingSpace.archiveBytes });
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
- await writeArchive({ tempPath, files: finalFiles, manifest });
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.30",
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.30",
9
+ "version": "2026.7.32",
10
10
  "hasInstallScript": true,
11
11
  "license": "Apache-2.0",
12
12
  "dependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neural-memory-interface",
3
- "version": "2026.7.30",
3
+ "version": "2026.7.32",
4
4
  "private": true,
5
5
  "description": "Private local SynaBun interface for memory visualization and multi-agent workflows",
6
6
  "license": "Apache-2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.7.31",
3
+ "version": "2026.7.32",
4
4
  "description": "Local-first persistent vector memory, MCP tooling, and multi-agent workflows for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",
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') info(` ${files} files to protect${size}`);
269
- else if (phase === 'checksum') info(` Checksumming ${files} files...`);
270
- else if (phase === 'archive') info(` Compressing${size} — this can take several minutes...`);
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
  });