@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.
Files changed (78) hide show
  1. package/dist/access-cli.js +13 -13
  2. package/dist/access-http.js +8 -8
  3. package/dist/access-mcp.js +7 -7
  4. package/dist/access-schema.d.ts +4 -4
  5. package/dist/access-schema.js +2 -2
  6. package/dist/access-service.js +5 -5
  7. package/dist/briefing.js +2 -2
  8. package/dist/causal-consolidation.js +3 -3
  9. package/dist/{chunk-6F6BXB7A.js → chunk-67VP6I47.js} +2 -2
  10. package/dist/{chunk-6XSPNR6L.js → chunk-6HF5VUBQ.js} +2 -2
  11. package/dist/{chunk-V2RCP53Q.js → chunk-6QKWLP4V.js} +2 -2
  12. package/dist/{chunk-XNLXAWHX.js → chunk-7TVK7E3R.js} +2 -2
  13. package/dist/{chunk-23UORJ4S.js → chunk-BE5XAWSA.js} +2 -2
  14. package/dist/{chunk-5UHVGNZD.js → chunk-DGDQZ3JW.js} +2 -2
  15. package/dist/{chunk-2AN2L4NL.js → chunk-DKOIMCGB.js} +2 -2
  16. package/dist/{chunk-5V456VRV.js → chunk-HX5XV3GL.js} +5 -5
  17. package/dist/{chunk-UIPDNLXA.js → chunk-K4LDMTTY.js} +118 -13
  18. package/dist/chunk-K4LDMTTY.js.map +1 -0
  19. package/dist/{chunk-YDMVYYD2.js → chunk-KAUXI453.js} +8 -8
  20. package/dist/{chunk-FCOQXV3T.js → chunk-KCLX6LOV.js} +12 -12
  21. package/dist/{chunk-ZEY4KYRQ.js → chunk-LKCE3NPZ.js} +13 -2
  22. package/dist/chunk-LKCE3NPZ.js.map +1 -0
  23. package/dist/{chunk-CHBI22MI.js → chunk-O2HWL47E.js} +2 -2
  24. package/dist/{chunk-TTGZV5R3.js → chunk-OHZXPJK7.js} +4 -4
  25. package/dist/{chunk-FPGE5NVO.js → chunk-OMHRQTOD.js} +2 -2
  26. package/dist/{chunk-E62SBGQ3.js → chunk-PYGKYEDU.js} +3 -3
  27. package/dist/{chunk-IQ3OI2RR.js → chunk-QK2G4EYH.js} +2 -2
  28. package/dist/{chunk-YNXOKMJP.js → chunk-RB7LF7K7.js} +4 -4
  29. package/dist/{chunk-YHV3KRKS.js → chunk-TKUQG2PE.js} +2 -2
  30. package/dist/{chunk-HC6EKOID.js → chunk-UMZRCQFC.js} +5 -5
  31. package/dist/{chunk-LJBOVCQG.js → chunk-XI5V7WSJ.js} +2 -2
  32. package/dist/{chunk-6BR7L222.js → chunk-ZQLC75BF.js} +2 -2
  33. package/dist/cli.js +17 -17
  34. package/dist/compounding/engine.js +2 -2
  35. package/dist/connectors/codex-materialize-runner.js +2 -2
  36. package/dist/connectors/index.js +2 -2
  37. package/dist/entity-retrieval.js +2 -2
  38. package/dist/index.js +22 -22
  39. package/dist/maintenance/memory-governance.js +2 -2
  40. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +2 -2
  41. package/dist/maintenance/rebuild-memory-projection.js +3 -3
  42. package/dist/namespaces/migrate.js +3 -3
  43. package/dist/namespaces/storage.js +2 -2
  44. package/dist/offline-sync.js +1 -1
  45. package/dist/operator-toolkit.js +5 -5
  46. package/dist/orchestrator.js +9 -9
  47. package/dist/semantic-consolidation.js +3 -3
  48. package/dist/semantic-rule-promotion.js +2 -2
  49. package/dist/semantic-rule-verifier.js +2 -2
  50. package/dist/storage.d.ts +10 -0
  51. package/dist/storage.js +1 -1
  52. package/dist/verified-recall.js +2 -2
  53. package/package.json +1 -1
  54. package/src/offline-sync.test.ts +84 -0
  55. package/src/offline-sync.ts +17 -1
  56. package/src/storage.ts +156 -12
  57. package/dist/chunk-UIPDNLXA.js.map +0 -1
  58. package/dist/chunk-ZEY4KYRQ.js.map +0 -1
  59. /package/dist/{chunk-6F6BXB7A.js.map → chunk-67VP6I47.js.map} +0 -0
  60. /package/dist/{chunk-6XSPNR6L.js.map → chunk-6HF5VUBQ.js.map} +0 -0
  61. /package/dist/{chunk-V2RCP53Q.js.map → chunk-6QKWLP4V.js.map} +0 -0
  62. /package/dist/{chunk-XNLXAWHX.js.map → chunk-7TVK7E3R.js.map} +0 -0
  63. /package/dist/{chunk-23UORJ4S.js.map → chunk-BE5XAWSA.js.map} +0 -0
  64. /package/dist/{chunk-5UHVGNZD.js.map → chunk-DGDQZ3JW.js.map} +0 -0
  65. /package/dist/{chunk-2AN2L4NL.js.map → chunk-DKOIMCGB.js.map} +0 -0
  66. /package/dist/{chunk-5V456VRV.js.map → chunk-HX5XV3GL.js.map} +0 -0
  67. /package/dist/{chunk-YDMVYYD2.js.map → chunk-KAUXI453.js.map} +0 -0
  68. /package/dist/{chunk-FCOQXV3T.js.map → chunk-KCLX6LOV.js.map} +0 -0
  69. /package/dist/{chunk-CHBI22MI.js.map → chunk-O2HWL47E.js.map} +0 -0
  70. /package/dist/{chunk-TTGZV5R3.js.map → chunk-OHZXPJK7.js.map} +0 -0
  71. /package/dist/{chunk-FPGE5NVO.js.map → chunk-OMHRQTOD.js.map} +0 -0
  72. /package/dist/{chunk-E62SBGQ3.js.map → chunk-PYGKYEDU.js.map} +0 -0
  73. /package/dist/{chunk-IQ3OI2RR.js.map → chunk-QK2G4EYH.js.map} +0 -0
  74. /package/dist/{chunk-YNXOKMJP.js.map → chunk-RB7LF7K7.js.map} +0 -0
  75. /package/dist/{chunk-YHV3KRKS.js.map → chunk-TKUQG2PE.js.map} +0 -0
  76. /package/dist/{chunk-HC6EKOID.js.map → chunk-UMZRCQFC.js.map} +0 -0
  77. /package/dist/{chunk-LJBOVCQG.js.map → chunk-XI5V7WSJ.js.map} +0 -0
  78. /package/dist/{chunk-6BR7L222.js.map → chunk-ZQLC75BF.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.517",
3
+ "version": "9.3.518",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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";
@@ -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 (shouldPreferIncomingOfflineRuntimeFile(relPath) && base) {
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
- if (await this.offlineSyncFileIsEncrypted(target)) {
2520
- const content = await readMaybeEncryptedFileBuffer(target, this._secureStoreKey, this.baseDir);
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
- const hash = createHash("sha256");
2527
- let bytes = 0;
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
- sha256: hash.digest("hex"),
2535
- bytes,
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
  }