synabun 2026.7.30 → 2026.7.31
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 +1 -0
- package/neural-interface/lib/backup-service.js +57 -2
- package/package.json +1 -1
- package/setup.js +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,7 @@ A point release on top of 2026.07.20, focused on correct Claude Code model selec
|
|
|
10
10
|
|
|
11
11
|
## 🛠 Fixed
|
|
12
12
|
|
|
13
|
+
- **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
14
|
- **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
15
|
- **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
16
|
- **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.
|
|
@@ -44,6 +44,21 @@ 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
|
+
|
|
47
62
|
export class BackupStorageError extends Error {
|
|
48
63
|
constructor({ availableBytes, requiredBytes, folderPath }) {
|
|
49
64
|
const availableMiB = Math.floor(availableBytes / 1024 / 1024);
|
|
@@ -112,6 +127,7 @@ function shouldSkipDataPath(relativePath) {
|
|
|
112
127
|
const name = parts.at(-1);
|
|
113
128
|
if (parts[0] === 'updater') return true;
|
|
114
129
|
if (parts[0] === 'logs') return true;
|
|
130
|
+
if (parts.length > 1 && EXCLUDED_DATA_DIRS.has(parts[0])) return true;
|
|
115
131
|
if (TRANSIENT_DATA_NAMES.has(name)) return true;
|
|
116
132
|
if (name.endsWith('.tmp') || name.endsWith('.pid')) return true;
|
|
117
133
|
if (/\.db-(?:wal|shm)$/i.test(name)) return true;
|
|
@@ -205,6 +221,33 @@ export function assertBackupWorkingSpace({
|
|
|
205
221
|
};
|
|
206
222
|
}
|
|
207
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Remove archive temps, SQLite temps, and stage dirs abandoned by an earlier
|
|
226
|
+
* run. A backup interrupted with Ctrl-C never reaches its own catch/finally,
|
|
227
|
+
* so its partial archive is orphaned forever — a killed pre-update snapshot
|
|
228
|
+
* left hundreds of MB behind with nothing to reclaim it.
|
|
229
|
+
*/
|
|
230
|
+
export function cleanupStaleBackupArtifacts(folderPath, { maxAgeMs = 60 * 60 * 1000, now = Date.now } = {}) {
|
|
231
|
+
if (!existsSync(folderPath)) return [];
|
|
232
|
+
const removed = [];
|
|
233
|
+
let entries;
|
|
234
|
+
try { entries = readdirSync(folderPath); }
|
|
235
|
+
catch { return removed; }
|
|
236
|
+
for (const name of entries) {
|
|
237
|
+
const isArchiveTemp = /\.zip\.\d+\.tmp$/.test(name);
|
|
238
|
+
const isSqliteTemp = /^\.synabun-(?:db|extra-db)-.*\.tmp$/.test(name);
|
|
239
|
+
const isStageDir = /^\.synabun-files-/.test(name);
|
|
240
|
+
if (!isArchiveTemp && !isSqliteTemp && !isStageDir) continue;
|
|
241
|
+
const path = resolve(folderPath, name);
|
|
242
|
+
try {
|
|
243
|
+
if (now() - statSync(path).mtimeMs < maxAgeMs) continue;
|
|
244
|
+
rmSync(path, { recursive: true, force: true });
|
|
245
|
+
removed.push(name);
|
|
246
|
+
} catch { /* another process may be mid-cleanup; leave it */ }
|
|
247
|
+
}
|
|
248
|
+
return removed;
|
|
249
|
+
}
|
|
250
|
+
|
|
208
251
|
function snapshotMutableBackupFiles(files, folderPath) {
|
|
209
252
|
if (!files.some(isMutableBackupInput)) return { files, stageDir: null };
|
|
210
253
|
const stageDir = mkdtempSync(resolve(folderPath, `.synabun-files-${process.pid}-`));
|
|
@@ -260,7 +303,10 @@ async function writeArchive({ tempPath, files, manifest }) {
|
|
|
260
303
|
archive.on('warning', error => error.code === 'ENOENT' ? null : reject(error));
|
|
261
304
|
archive.on('error', reject);
|
|
262
305
|
archive.pipe(output);
|
|
263
|
-
for (const file of files)
|
|
306
|
+
for (const file of files) {
|
|
307
|
+
const store = INCOMPRESSIBLE_EXTENSIONS.has(extname(file.archivePath).toLowerCase());
|
|
308
|
+
archive.file(file.diskPath, { name: `${BACKUP_PREFIX}/${file.archivePath}`, store });
|
|
309
|
+
}
|
|
264
310
|
archive.append(JSON.stringify(manifest, null, 2), { name: `${BACKUP_PREFIX}/manifest.json` });
|
|
265
311
|
archive.finalize().catch(reject);
|
|
266
312
|
});
|
|
@@ -294,11 +340,16 @@ export async function createVerifiedBackup({
|
|
|
294
340
|
freeSpaceReserveBytes = BACKUP_FREE_SPACE_RESERVE_BYTES,
|
|
295
341
|
getAvailableBytes = backupAvailableBytes,
|
|
296
342
|
onBeforeArchive = null,
|
|
343
|
+
onProgress = null,
|
|
297
344
|
now = () => new Date(),
|
|
298
345
|
} = {}) {
|
|
346
|
+
// A large data home spends minutes in checksum/archive with no output. Phase
|
|
347
|
+
// reporting is what stops that from reading as a hang to the caller.
|
|
348
|
+
const report = typeof onProgress === 'function' ? onProgress : () => {};
|
|
299
349
|
if (!dataHome || !folderPath) throw new Error('dataHome and folderPath are required');
|
|
300
350
|
const createdAt = now();
|
|
301
351
|
mkdirSync(folderPath, { recursive: true });
|
|
352
|
+
cleanupStaleBackupArtifacts(folderPath);
|
|
302
353
|
const destination = resolve(folderPath, archiveNameFor(kind, createdAt));
|
|
303
354
|
const tempPath = `${destination}.${process.pid}.tmp`;
|
|
304
355
|
const sqliteTemp = resolve(folderPath, `.synabun-db-${process.pid}-${Date.now()}.tmp`);
|
|
@@ -325,7 +376,7 @@ export async function createVerifiedBackup({
|
|
|
325
376
|
file.archivePath.replace(/\\/g, '/'),
|
|
326
377
|
file,
|
|
327
378
|
])).values()];
|
|
328
|
-
assertBackupWorkingSpace({
|
|
379
|
+
const workingSpace = assertBackupWorkingSpace({
|
|
329
380
|
folderPath,
|
|
330
381
|
files: sourceFiles,
|
|
331
382
|
databasePath,
|
|
@@ -333,6 +384,7 @@ export async function createVerifiedBackup({
|
|
|
333
384
|
freeSpaceReserveBytes,
|
|
334
385
|
getAvailableBytes,
|
|
335
386
|
});
|
|
387
|
+
report({ phase: 'collect', files: sourceFiles.length, bytes: workingSpace.archiveBytes });
|
|
336
388
|
|
|
337
389
|
sqliteSnapshot = createConsistentSqliteSnapshot(databasePath, sqliteTemp);
|
|
338
390
|
files.length = 0;
|
|
@@ -357,6 +409,7 @@ export async function createVerifiedBackup({
|
|
|
357
409
|
const stableInputs = snapshotMutableBackupFiles([...unique.values()], folderPath);
|
|
358
410
|
stageDir = stableInputs.stageDir;
|
|
359
411
|
const finalFiles = stableInputs.files.sort((a, b) => a.archivePath.localeCompare(b.archivePath));
|
|
412
|
+
report({ phase: 'checksum', files: finalFiles.length });
|
|
360
413
|
const checksums = {};
|
|
361
414
|
for (const file of finalFiles) checksums[file.archivePath] = `sha256:${await hashFile(file.diskPath)}`;
|
|
362
415
|
|
|
@@ -380,7 +433,9 @@ export async function createVerifiedBackup({
|
|
|
380
433
|
verification: { algorithm: 'sha256', publication: 'verify-before-atomic-rename' },
|
|
381
434
|
};
|
|
382
435
|
if (typeof onBeforeArchive === 'function') await onBeforeArchive({ files: finalFiles, manifest });
|
|
436
|
+
report({ phase: 'archive', files: finalFiles.length, bytes: workingSpace.archiveBytes });
|
|
383
437
|
await writeArchive({ tempPath, files: finalFiles, manifest });
|
|
438
|
+
report({ phase: 'verify', files: finalFiles.length });
|
|
384
439
|
const publishedManifest = verifyBackupArchive(tempPath);
|
|
385
440
|
renameSync(tempPath, destination);
|
|
386
441
|
return {
|
package/package.json
CHANGED
package/setup.js
CHANGED
|
@@ -261,6 +261,15 @@ 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 }) => {
|
|
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...`);
|
|
271
|
+
else if (phase === 'verify') info(' Verifying archive integrity...');
|
|
272
|
+
},
|
|
264
273
|
});
|
|
265
274
|
applyBackupRetention({ folderPath });
|
|
266
275
|
snapshotPath = snapshot.path;
|