@remnic/core 9.3.517 → 9.3.518
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/dist/access-cli.js +13 -13
- package/dist/access-http.js +8 -8
- package/dist/access-mcp.js +7 -7
- package/dist/access-schema.d.ts +4 -4
- package/dist/access-schema.js +2 -2
- package/dist/access-service.js +5 -5
- package/dist/briefing.js +2 -2
- package/dist/causal-consolidation.js +3 -3
- package/dist/{chunk-6F6BXB7A.js → chunk-67VP6I47.js} +2 -2
- package/dist/{chunk-6XSPNR6L.js → chunk-6HF5VUBQ.js} +2 -2
- package/dist/{chunk-V2RCP53Q.js → chunk-6QKWLP4V.js} +2 -2
- package/dist/{chunk-XNLXAWHX.js → chunk-7TVK7E3R.js} +2 -2
- package/dist/{chunk-23UORJ4S.js → chunk-BE5XAWSA.js} +2 -2
- package/dist/{chunk-5UHVGNZD.js → chunk-DGDQZ3JW.js} +2 -2
- package/dist/{chunk-2AN2L4NL.js → chunk-DKOIMCGB.js} +2 -2
- package/dist/{chunk-5V456VRV.js → chunk-HX5XV3GL.js} +5 -5
- package/dist/{chunk-UIPDNLXA.js → chunk-K4LDMTTY.js} +118 -13
- package/dist/chunk-K4LDMTTY.js.map +1 -0
- package/dist/{chunk-YDMVYYD2.js → chunk-KAUXI453.js} +8 -8
- package/dist/{chunk-FCOQXV3T.js → chunk-KCLX6LOV.js} +12 -12
- package/dist/{chunk-ZEY4KYRQ.js → chunk-LKCE3NPZ.js} +13 -2
- package/dist/chunk-LKCE3NPZ.js.map +1 -0
- package/dist/{chunk-CHBI22MI.js → chunk-O2HWL47E.js} +2 -2
- package/dist/{chunk-TTGZV5R3.js → chunk-OHZXPJK7.js} +4 -4
- package/dist/{chunk-FPGE5NVO.js → chunk-OMHRQTOD.js} +2 -2
- package/dist/{chunk-E62SBGQ3.js → chunk-PYGKYEDU.js} +3 -3
- package/dist/{chunk-IQ3OI2RR.js → chunk-QK2G4EYH.js} +2 -2
- package/dist/{chunk-YNXOKMJP.js → chunk-RB7LF7K7.js} +4 -4
- package/dist/{chunk-YHV3KRKS.js → chunk-TKUQG2PE.js} +2 -2
- package/dist/{chunk-HC6EKOID.js → chunk-UMZRCQFC.js} +5 -5
- package/dist/{chunk-LJBOVCQG.js → chunk-XI5V7WSJ.js} +2 -2
- package/dist/{chunk-6BR7L222.js → chunk-ZQLC75BF.js} +2 -2
- package/dist/cli.js +17 -17
- package/dist/compounding/engine.js +2 -2
- package/dist/connectors/codex-materialize-runner.js +2 -2
- package/dist/connectors/index.js +2 -2
- package/dist/entity-retrieval.js +2 -2
- package/dist/index.js +22 -22
- package/dist/maintenance/memory-governance.js +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +3 -3
- package/dist/namespaces/migrate.js +3 -3
- package/dist/namespaces/storage.js +2 -2
- package/dist/offline-sync.js +1 -1
- package/dist/operator-toolkit.js +5 -5
- package/dist/orchestrator.js +9 -9
- package/dist/semantic-consolidation.js +3 -3
- package/dist/semantic-rule-promotion.js +2 -2
- package/dist/semantic-rule-verifier.js +2 -2
- package/dist/storage.d.ts +10 -0
- package/dist/storage.js +1 -1
- package/dist/verified-recall.js +2 -2
- package/package.json +1 -1
- package/src/offline-sync.test.ts +84 -0
- package/src/offline-sync.ts +17 -1
- package/src/storage.ts +156 -12
- package/dist/chunk-UIPDNLXA.js.map +0 -1
- package/dist/chunk-ZEY4KYRQ.js.map +0 -1
- /package/dist/{chunk-6F6BXB7A.js.map → chunk-67VP6I47.js.map} +0 -0
- /package/dist/{chunk-6XSPNR6L.js.map → chunk-6HF5VUBQ.js.map} +0 -0
- /package/dist/{chunk-V2RCP53Q.js.map → chunk-6QKWLP4V.js.map} +0 -0
- /package/dist/{chunk-XNLXAWHX.js.map → chunk-7TVK7E3R.js.map} +0 -0
- /package/dist/{chunk-23UORJ4S.js.map → chunk-BE5XAWSA.js.map} +0 -0
- /package/dist/{chunk-5UHVGNZD.js.map → chunk-DGDQZ3JW.js.map} +0 -0
- /package/dist/{chunk-2AN2L4NL.js.map → chunk-DKOIMCGB.js.map} +0 -0
- /package/dist/{chunk-5V456VRV.js.map → chunk-HX5XV3GL.js.map} +0 -0
- /package/dist/{chunk-YDMVYYD2.js.map → chunk-KAUXI453.js.map} +0 -0
- /package/dist/{chunk-FCOQXV3T.js.map → chunk-KCLX6LOV.js.map} +0 -0
- /package/dist/{chunk-CHBI22MI.js.map → chunk-O2HWL47E.js.map} +0 -0
- /package/dist/{chunk-TTGZV5R3.js.map → chunk-OHZXPJK7.js.map} +0 -0
- /package/dist/{chunk-FPGE5NVO.js.map → chunk-OMHRQTOD.js.map} +0 -0
- /package/dist/{chunk-E62SBGQ3.js.map → chunk-PYGKYEDU.js.map} +0 -0
- /package/dist/{chunk-IQ3OI2RR.js.map → chunk-QK2G4EYH.js.map} +0 -0
- /package/dist/{chunk-YNXOKMJP.js.map → chunk-RB7LF7K7.js.map} +0 -0
- /package/dist/{chunk-YHV3KRKS.js.map → chunk-TKUQG2PE.js.map} +0 -0
- /package/dist/{chunk-HC6EKOID.js.map → chunk-UMZRCQFC.js.map} +0 -0
- /package/dist/{chunk-LJBOVCQG.js.map → chunk-XI5V7WSJ.js.map} +0 -0
- /package/dist/{chunk-6BR7L222.js.map → chunk-ZQLC75BF.js.map} +0 -0
package/package.json
CHANGED
package/src/offline-sync.test.ts
CHANGED
|
@@ -211,6 +211,8 @@ test("offline sync includes live LCM sqlite artifacts for full-fidelity offline
|
|
|
211
211
|
"state/lcm.sqlite-shm",
|
|
212
212
|
"state/lcm.sqlite-wal",
|
|
213
213
|
]);
|
|
214
|
+
assert.equal(shouldPreferIncomingOfflineRuntimeFile("state/lcm.sqlite-shm"), true);
|
|
215
|
+
assert.equal(shouldPreferIncomingOfflineRuntimeFile("state/lcm.sqlite-wal"), true);
|
|
214
216
|
const focused = await buildOfflineSyncSnapshotForPaths({
|
|
215
217
|
root,
|
|
216
218
|
sourceId: "remote",
|
|
@@ -233,6 +235,14 @@ test("offline sync includes live LCM sqlite artifacts for full-fidelity offline
|
|
|
233
235
|
|
|
234
236
|
assert.equal(pull.skipped, 4);
|
|
235
237
|
assert.equal(await readUtf8(root, "state/lcm.sqlite"), "live db");
|
|
238
|
+
|
|
239
|
+
await write(root, "state/lcm.sqlite-wal", "local wal churn");
|
|
240
|
+
const changeset = await buildOfflineSyncChangeset({
|
|
241
|
+
root,
|
|
242
|
+
sourceId: "laptop",
|
|
243
|
+
baseFiles: snapshot.files,
|
|
244
|
+
});
|
|
245
|
+
assert.deepEqual(changeset.changes.map((change) => change.path), []);
|
|
236
246
|
} finally {
|
|
237
247
|
await rm(root, { recursive: true, force: true });
|
|
238
248
|
}
|
|
@@ -1269,6 +1279,43 @@ test("offline snapshot apply preserves new local runtime files without a base",
|
|
|
1269
1279
|
}
|
|
1270
1280
|
});
|
|
1271
1281
|
|
|
1282
|
+
test("offline snapshot apply removes current-only sqlite sidecars when absent remotely", async () => {
|
|
1283
|
+
const root = await tempDir("remnic-offline-runtime-sidecar-local-create");
|
|
1284
|
+
try {
|
|
1285
|
+
await write(root, "state/lcm.sqlite-wal", "stale local wal");
|
|
1286
|
+
await write(root, "state/lcm.sqlite-shm", "stale local shm");
|
|
1287
|
+
|
|
1288
|
+
const pull = await applyOfflineSyncSnapshot({
|
|
1289
|
+
root,
|
|
1290
|
+
snapshot: {
|
|
1291
|
+
format: "remnic.offline-sync.snapshot.v1",
|
|
1292
|
+
schemaVersion: 1,
|
|
1293
|
+
createdAt: new Date().toISOString(),
|
|
1294
|
+
sourceId: "remote",
|
|
1295
|
+
includeTranscripts: true,
|
|
1296
|
+
files: [],
|
|
1297
|
+
},
|
|
1298
|
+
baseFiles: [],
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
assert.equal(pull.deleted, 2);
|
|
1302
|
+
assert.equal(pull.pendingLocal, 0);
|
|
1303
|
+
assert.equal(pull.skipped, 0);
|
|
1304
|
+
assert.equal(pull.conflicts.length, 0);
|
|
1305
|
+
assert.deepEqual(pull.nextBaseFiles, []);
|
|
1306
|
+
await assert.rejects(
|
|
1307
|
+
() => readFile(path.join(root, "state/lcm.sqlite-wal")),
|
|
1308
|
+
/ENOENT/,
|
|
1309
|
+
);
|
|
1310
|
+
await assert.rejects(
|
|
1311
|
+
() => readFile(path.join(root, "state/lcm.sqlite-shm")),
|
|
1312
|
+
/ENOENT/,
|
|
1313
|
+
);
|
|
1314
|
+
} finally {
|
|
1315
|
+
await rm(root, { recursive: true, force: true });
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1272
1319
|
test("offline snapshot apply restores locally deleted remote-authoritative runtime files", async () => {
|
|
1273
1320
|
const root = await tempDir("remnic-offline-runtime-local-delete-restore");
|
|
1274
1321
|
try {
|
|
@@ -2222,6 +2269,43 @@ test("offline sync applies and snapshots through secure storage hooks", async ()
|
|
|
2222
2269
|
}
|
|
2223
2270
|
});
|
|
2224
2271
|
|
|
2272
|
+
test("offline storage digest cache avoids repeat reads and invalidates on ctime changes", async () => {
|
|
2273
|
+
const root = await tempDir("remnic-offline-digest-cache");
|
|
2274
|
+
const relPath = "facts/cache.md";
|
|
2275
|
+
const filePath = path.join(root, relPath);
|
|
2276
|
+
try {
|
|
2277
|
+
await write(root, relPath, "alpha");
|
|
2278
|
+
const storage = new StorageManager(root);
|
|
2279
|
+
const first = await storage.digestOfflineSyncFile(filePath);
|
|
2280
|
+
|
|
2281
|
+
let headerProbes = 0;
|
|
2282
|
+
(storage as unknown as { offlineSyncFileIsEncrypted: () => Promise<boolean> }).offlineSyncFileIsEncrypted =
|
|
2283
|
+
async () => {
|
|
2284
|
+
headerProbes += 1;
|
|
2285
|
+
throw new Error("cache miss");
|
|
2286
|
+
};
|
|
2287
|
+
assert.deepEqual(await storage.digestOfflineSyncFile(filePath), first);
|
|
2288
|
+
assert.equal(headerProbes, 0);
|
|
2289
|
+
|
|
2290
|
+
const before = await stat(filePath);
|
|
2291
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
2292
|
+
await writeFile(filePath, "bravo");
|
|
2293
|
+
await utimes(filePath, before.atime, before.mtime);
|
|
2294
|
+
|
|
2295
|
+
(storage as unknown as { offlineSyncFileIsEncrypted: () => Promise<boolean> }).offlineSyncFileIsEncrypted =
|
|
2296
|
+
async () => {
|
|
2297
|
+
headerProbes += 1;
|
|
2298
|
+
return false;
|
|
2299
|
+
};
|
|
2300
|
+
const second = await storage.digestOfflineSyncFile(filePath);
|
|
2301
|
+
assert.equal(headerProbes, 1);
|
|
2302
|
+
assert.notEqual(second.sha256, first.sha256);
|
|
2303
|
+
assert.equal(second.bytes, first.bytes);
|
|
2304
|
+
} finally {
|
|
2305
|
+
await rm(root, { recursive: true, force: true });
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2225
2309
|
test("offline snapshot fast-base does not probe file headers for trusted metadata", async () => {
|
|
2226
2310
|
const root = await tempDir("remnic-offline-fast-base-no-header-probe");
|
|
2227
2311
|
const relPath = "facts/same-size.md";
|
package/src/offline-sync.ts
CHANGED
|
@@ -489,16 +489,29 @@ const REMOTE_AUTHORITATIVE_RUNTIME_STATE_FILES = new Set([
|
|
|
489
489
|
"index_time.json",
|
|
490
490
|
"last_intent.json",
|
|
491
491
|
"last_recall.json",
|
|
492
|
+
"lcm.sqlite-shm",
|
|
493
|
+
"lcm.sqlite-wal",
|
|
492
494
|
"memory-lifecycle-ledger.jsonl",
|
|
493
495
|
"recall_impressions.jsonl",
|
|
494
496
|
]);
|
|
495
497
|
|
|
498
|
+
const ABSENT_INCOMING_RUNTIME_DELETE_FILES = new Set([
|
|
499
|
+
"lcm.sqlite-shm",
|
|
500
|
+
"lcm.sqlite-wal",
|
|
501
|
+
]);
|
|
502
|
+
|
|
496
503
|
export function shouldPreferIncomingOfflineRuntimeFile(relPosix: string): boolean {
|
|
497
504
|
const parts = relPosix.split("/");
|
|
498
505
|
const basename = parts[parts.length - 1] ?? "";
|
|
499
506
|
return isCanonicalRuntimeStatePath(parts) && REMOTE_AUTHORITATIVE_RUNTIME_STATE_FILES.has(basename);
|
|
500
507
|
}
|
|
501
508
|
|
|
509
|
+
function shouldDeleteAbsentIncomingOfflineRuntimeFile(relPosix: string): boolean {
|
|
510
|
+
const parts = relPosix.split("/");
|
|
511
|
+
const basename = parts[parts.length - 1] ?? "";
|
|
512
|
+
return isCanonicalRuntimeStatePath(parts) && ABSENT_INCOMING_RUNTIME_DELETE_FILES.has(basename);
|
|
513
|
+
}
|
|
514
|
+
|
|
502
515
|
function filterBaseFilesForMode(
|
|
503
516
|
files: readonly OfflineSyncFileState[],
|
|
504
517
|
includeTranscripts: boolean,
|
|
@@ -1213,7 +1226,10 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1213
1226
|
skipped += 1;
|
|
1214
1227
|
continue;
|
|
1215
1228
|
}
|
|
1216
|
-
if (
|
|
1229
|
+
if (
|
|
1230
|
+
shouldPreferIncomingOfflineRuntimeFile(relPath) &&
|
|
1231
|
+
(base || shouldDeleteAbsentIncomingOfflineRuntimeFile(relPath))
|
|
1232
|
+
) {
|
|
1217
1233
|
await deleteSafeFile(root, relPath, options.deleteFile);
|
|
1218
1234
|
nextBase.delete(relPath);
|
|
1219
1235
|
deleted += 1;
|
package/src/storage.ts
CHANGED
|
@@ -135,6 +135,15 @@ const ARTIFACT_SEARCH_STOPWORDS = new Set([
|
|
|
135
135
|
"with",
|
|
136
136
|
]);
|
|
137
137
|
|
|
138
|
+
type OfflineSyncDigestCacheEntry = {
|
|
139
|
+
statBytes: number;
|
|
140
|
+
mtimeMs: number;
|
|
141
|
+
ctimeMs: number;
|
|
142
|
+
encrypted: boolean;
|
|
143
|
+
sha256: string;
|
|
144
|
+
bytes: number;
|
|
145
|
+
};
|
|
146
|
+
|
|
138
147
|
export interface ReextractJobRequest {
|
|
139
148
|
memoryId: string;
|
|
140
149
|
model: string;
|
|
@@ -2236,6 +2245,10 @@ export class StorageManager {
|
|
|
2236
2245
|
private factHashIndexAuthoritative: boolean | null = null;
|
|
2237
2246
|
private factHashIndexAuthoritativePromise: Promise<void> | null = null;
|
|
2238
2247
|
private readonly secureAppendChains = new Map<string, Promise<void>>();
|
|
2248
|
+
private offlineSyncDigestCache: Map<string, OfflineSyncDigestCacheEntry> | null = null;
|
|
2249
|
+
private offlineSyncDigestCacheLoadPromise: Promise<Map<string, OfflineSyncDigestCacheEntry>> | null = null;
|
|
2250
|
+
private offlineSyncDigestCacheWriteChain: Promise<void> = Promise.resolve();
|
|
2251
|
+
private offlineSyncDigestCacheWriteTimer: ReturnType<typeof setTimeout> | null = null;
|
|
2239
2252
|
/** Optional: set by the orchestrator after construction to enable template-aware citation stripping during legacy hash rebuild. */
|
|
2240
2253
|
citationTemplate: string = DEFAULT_CITATION_FORMAT;
|
|
2241
2254
|
|
|
@@ -2516,24 +2529,152 @@ export class StorageManager {
|
|
|
2516
2529
|
|
|
2517
2530
|
async digestOfflineSyncFile(filePath: string): Promise<{ sha256: string; bytes: number }> {
|
|
2518
2531
|
const target = this.assertManagedStoragePath(filePath, "storage.digestOfflineSyncFile");
|
|
2519
|
-
|
|
2520
|
-
|
|
2532
|
+
const st = await stat(target);
|
|
2533
|
+
const relPath = path.relative(this.baseDir, target).split(path.sep).join("/");
|
|
2534
|
+
const cache = await this.loadOfflineSyncDigestCache();
|
|
2535
|
+
const cached = cache.get(relPath);
|
|
2536
|
+
if (
|
|
2537
|
+
cached &&
|
|
2538
|
+
cached.statBytes === st.size &&
|
|
2539
|
+
cached.mtimeMs === st.mtimeMs &&
|
|
2540
|
+
cached.ctimeMs === st.ctimeMs &&
|
|
2541
|
+
!cached.encrypted
|
|
2542
|
+
) {
|
|
2521
2543
|
return {
|
|
2544
|
+
sha256: cached.sha256,
|
|
2545
|
+
bytes: cached.bytes,
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
const encrypted = await this.offlineSyncFileIsEncrypted(target);
|
|
2550
|
+
let digest: { sha256: string; bytes: number };
|
|
2551
|
+
if (encrypted) {
|
|
2552
|
+
const content = await readMaybeEncryptedFileBuffer(target, this._secureStoreKey, this.baseDir);
|
|
2553
|
+
digest = {
|
|
2522
2554
|
sha256: createHash("sha256").update(content).digest("hex"),
|
|
2523
2555
|
bytes: content.byteLength,
|
|
2524
2556
|
};
|
|
2557
|
+
} else {
|
|
2558
|
+
const hash = createHash("sha256");
|
|
2559
|
+
let bytes = 0;
|
|
2560
|
+
for await (const rawChunk of createReadStream(target)) {
|
|
2561
|
+
const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
|
|
2562
|
+
hash.update(chunk);
|
|
2563
|
+
bytes += chunk.length;
|
|
2564
|
+
}
|
|
2565
|
+
digest = {
|
|
2566
|
+
sha256: hash.digest("hex"),
|
|
2567
|
+
bytes,
|
|
2568
|
+
};
|
|
2525
2569
|
}
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
for await (const rawChunk of createReadStream(target)) {
|
|
2529
|
-
const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
|
|
2530
|
-
hash.update(chunk);
|
|
2531
|
-
bytes += chunk.length;
|
|
2570
|
+
if (!encrypted) {
|
|
2571
|
+
this.rememberOfflineSyncDigest(relPath, st, digest);
|
|
2532
2572
|
}
|
|
2533
|
-
return
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2573
|
+
return digest;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
private async loadOfflineSyncDigestCache(): Promise<Map<string, OfflineSyncDigestCacheEntry>> {
|
|
2577
|
+
if (this.offlineSyncDigestCache) return this.offlineSyncDigestCache;
|
|
2578
|
+
if (!this.offlineSyncDigestCacheLoadPromise) {
|
|
2579
|
+
this.offlineSyncDigestCacheLoadPromise = this.readOfflineSyncDigestCache();
|
|
2580
|
+
}
|
|
2581
|
+
this.offlineSyncDigestCache = await this.offlineSyncDigestCacheLoadPromise;
|
|
2582
|
+
return this.offlineSyncDigestCache;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
private async readOfflineSyncDigestCache(): Promise<Map<string, OfflineSyncDigestCacheEntry>> {
|
|
2586
|
+
const cache = new Map<string, OfflineSyncDigestCacheEntry>();
|
|
2587
|
+
try {
|
|
2588
|
+
const raw = await readFile(this.offlineSyncDigestCachePath, "utf-8");
|
|
2589
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
2590
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return cache;
|
|
2591
|
+
const entries = (parsed as { entries?: unknown }).entries;
|
|
2592
|
+
if (!Array.isArray(entries)) return cache;
|
|
2593
|
+
for (const entry of entries) {
|
|
2594
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
2595
|
+
const record = entry as Record<string, unknown>;
|
|
2596
|
+
const cachePath = typeof record.path === "string" ? record.path : "";
|
|
2597
|
+
const statBytes = typeof record.statBytes === "number" ? record.statBytes : NaN;
|
|
2598
|
+
const mtimeMs = typeof record.mtimeMs === "number" ? record.mtimeMs : NaN;
|
|
2599
|
+
const ctimeMs = typeof record.ctimeMs === "number" ? record.ctimeMs : NaN;
|
|
2600
|
+
const bytes = typeof record.bytes === "number" ? record.bytes : NaN;
|
|
2601
|
+
const sha256 = typeof record.sha256 === "string" ? record.sha256 : "";
|
|
2602
|
+
const encrypted = record.encrypted === true;
|
|
2603
|
+
if (
|
|
2604
|
+
cachePath.length === 0 ||
|
|
2605
|
+
cachePath === ".." ||
|
|
2606
|
+
cachePath.startsWith("../") ||
|
|
2607
|
+
path.isAbsolute(cachePath) ||
|
|
2608
|
+
!Number.isFinite(statBytes) ||
|
|
2609
|
+
!Number.isFinite(mtimeMs) ||
|
|
2610
|
+
!Number.isFinite(ctimeMs) ||
|
|
2611
|
+
!Number.isFinite(bytes) ||
|
|
2612
|
+
!/^[a-f0-9]{64}$/i.test(sha256)
|
|
2613
|
+
) {
|
|
2614
|
+
continue;
|
|
2615
|
+
}
|
|
2616
|
+
cache.set(cachePath, { statBytes, mtimeMs, ctimeMs, encrypted, sha256, bytes });
|
|
2617
|
+
}
|
|
2618
|
+
} catch (err) {
|
|
2619
|
+
if (!isErrnoCode(err, "ENOENT")) {
|
|
2620
|
+
log.warn(
|
|
2621
|
+
`storage.offlineSyncDigestCache: ignoring unreadable cache: ${err instanceof Error ? err.message : String(err)}`,
|
|
2622
|
+
);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
return cache;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
private rememberOfflineSyncDigest(
|
|
2629
|
+
relPath: string,
|
|
2630
|
+
st: { size: number; mtimeMs: number; ctimeMs: number },
|
|
2631
|
+
digest: { sha256: string; bytes: number },
|
|
2632
|
+
): void {
|
|
2633
|
+
const cache = this.offlineSyncDigestCache;
|
|
2634
|
+
if (!cache) return;
|
|
2635
|
+
cache.set(relPath, {
|
|
2636
|
+
statBytes: st.size,
|
|
2637
|
+
mtimeMs: st.mtimeMs,
|
|
2638
|
+
ctimeMs: st.ctimeMs,
|
|
2639
|
+
encrypted: false,
|
|
2640
|
+
sha256: digest.sha256,
|
|
2641
|
+
bytes: digest.bytes,
|
|
2642
|
+
});
|
|
2643
|
+
this.scheduleOfflineSyncDigestCacheWrite();
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
private scheduleOfflineSyncDigestCacheWrite(): void {
|
|
2647
|
+
if (this.offlineSyncDigestCacheWriteTimer) {
|
|
2648
|
+
clearTimeout(this.offlineSyncDigestCacheWriteTimer);
|
|
2649
|
+
}
|
|
2650
|
+
this.offlineSyncDigestCacheWriteTimer = setTimeout(() => {
|
|
2651
|
+
this.offlineSyncDigestCacheWriteTimer = null;
|
|
2652
|
+
this.queueOfflineSyncDigestCacheWrite();
|
|
2653
|
+
}, 1_000);
|
|
2654
|
+
this.offlineSyncDigestCacheWriteTimer.unref?.();
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
private queueOfflineSyncDigestCacheWrite(): void {
|
|
2658
|
+
this.offlineSyncDigestCacheWriteChain = this.offlineSyncDigestCacheWriteChain
|
|
2659
|
+
.catch(() => undefined)
|
|
2660
|
+
.then(async () => {
|
|
2661
|
+
const cache = this.offlineSyncDigestCache;
|
|
2662
|
+
if (!cache) return;
|
|
2663
|
+
const entries = [...cache.entries()]
|
|
2664
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
2665
|
+
.map(([entryPath, entry]) => ({ path: entryPath, ...entry }));
|
|
2666
|
+
await mkdir(path.dirname(this.offlineSyncDigestCachePath), { recursive: true });
|
|
2667
|
+
await writeFile(
|
|
2668
|
+
this.offlineSyncDigestCachePath,
|
|
2669
|
+
`${JSON.stringify({ version: 1, entries })}\n`,
|
|
2670
|
+
"utf-8",
|
|
2671
|
+
);
|
|
2672
|
+
})
|
|
2673
|
+
.catch((err) => {
|
|
2674
|
+
log.warn(
|
|
2675
|
+
`storage.offlineSyncDigestCache: failed to write cache: ${err instanceof Error ? err.message : String(err)}`,
|
|
2676
|
+
);
|
|
2677
|
+
});
|
|
2537
2678
|
}
|
|
2538
2679
|
|
|
2539
2680
|
private async offlineSyncFileIsEncrypted(filePath: string): Promise<boolean> {
|
|
@@ -2644,6 +2785,9 @@ export class StorageManager {
|
|
|
2644
2785
|
private get stateDir(): string {
|
|
2645
2786
|
return path.join(this.baseDir, "state");
|
|
2646
2787
|
}
|
|
2788
|
+
private get offlineSyncDigestCachePath(): string {
|
|
2789
|
+
return path.join(this.baseDir, ".offline-sync", "digest-cache.v1.json");
|
|
2790
|
+
}
|
|
2647
2791
|
private get entitySynthesisQueuePath(): string {
|
|
2648
2792
|
return path.join(this.stateDir, "entity-synthesis-queue.json");
|
|
2649
2793
|
}
|