@remnic/core 1.1.29 → 1.1.30

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 (98) hide show
  1. package/dist/access-cli.js +13 -13
  2. package/dist/access-http.d.ts +1 -1
  3. package/dist/access-http.js +8 -8
  4. package/dist/access-mcp.d.ts +1 -1
  5. package/dist/access-mcp.js +7 -7
  6. package/dist/access-schema.d.ts +55 -5
  7. package/dist/access-schema.js +4 -2
  8. package/dist/{access-service-CEyV8XJ5.d.ts → access-service-B5hgZPCN.d.ts} +4 -1
  9. package/dist/access-service.d.ts +1 -1
  10. package/dist/access-service.js +5 -5
  11. package/dist/briefing.js +2 -2
  12. package/dist/causal-consolidation.js +3 -3
  13. package/dist/{chunk-25YQM6XW.js → chunk-2IWUMAES.js} +3 -3
  14. package/dist/{chunk-JUYT2J3K.js → chunk-3OWUCDKH.js} +39 -7
  15. package/dist/chunk-3OWUCDKH.js.map +1 -0
  16. package/dist/{chunk-QYHQ2JHL.js → chunk-43PJZYGL.js} +2 -2
  17. package/dist/{chunk-YITUHONZ.js → chunk-4KGVTPGD.js} +2 -2
  18. package/dist/{chunk-TR4DK5OH.js → chunk-76FLAAUC.js} +2 -2
  19. package/dist/{chunk-6BFAEWQS.js → chunk-77H5NU3M.js} +2 -2
  20. package/dist/{chunk-IANK6Y5W.js → chunk-A6KTB5R6.js} +2 -2
  21. package/dist/{chunk-7D6O46PF.js → chunk-BVF3AGJP.js} +2 -2
  22. package/dist/{chunk-4H6DURG6.js → chunk-JA3AK3PT.js} +2 -2
  23. package/dist/{chunk-WDSIV3AK.js → chunk-KRBK4BQH.js} +12 -12
  24. package/dist/{chunk-AMVN77EU.js → chunk-MG7NA5H3.js} +365 -90
  25. package/dist/chunk-MG7NA5H3.js.map +1 -0
  26. package/dist/{chunk-RCZRL5BE.js → chunk-MRILGULB.js} +2 -2
  27. package/dist/{chunk-NW7JW5GA.js → chunk-OC7KHOOX.js} +41 -6
  28. package/dist/chunk-OC7KHOOX.js.map +1 -0
  29. package/dist/{chunk-LCTP7YRU.js → chunk-QKZGQIPJ.js} +16 -7
  30. package/dist/chunk-QKZGQIPJ.js.map +1 -0
  31. package/dist/{chunk-CWWDIQZB.js → chunk-QLLBRHAT.js} +8 -8
  32. package/dist/{chunk-2WIPXV3Y.js → chunk-RR2PKP3I.js} +2 -2
  33. package/dist/{chunk-3F24QTRI.js → chunk-SAZS2QZB.js} +2 -2
  34. package/dist/{chunk-VYU7PXUS.js → chunk-SIC6U3GZ.js} +2 -2
  35. package/dist/{chunk-6CB4E7ZV.js → chunk-UL2NNBUL.js} +4 -4
  36. package/dist/{chunk-F33CJ5CH.js → chunk-VBJ7V5SK.js} +40 -8
  37. package/dist/chunk-VBJ7V5SK.js.map +1 -0
  38. package/dist/{chunk-6WV2HYTZ.js → chunk-W6AQJ2PY.js} +4 -4
  39. package/dist/{chunk-PUXCIHRL.js → chunk-XSZEP4SF.js} +2 -2
  40. package/dist/{cli-BguVmIwO.d.ts → cli-CJKI2JIe.d.ts} +1 -1
  41. package/dist/cli.d.ts +2 -2
  42. package/dist/cli.js +17 -17
  43. package/dist/compounding/engine.js +2 -2
  44. package/dist/connectors/codex-materialize-runner.js +2 -2
  45. package/dist/connectors/index.js +2 -2
  46. package/dist/entity-retrieval.js +2 -2
  47. package/dist/index.d.ts +4 -4
  48. package/dist/index.js +32 -22
  49. package/dist/index.js.map +1 -1
  50. package/dist/maintenance/memory-governance.js +2 -2
  51. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +2 -2
  52. package/dist/maintenance/rebuild-memory-projection.js +3 -3
  53. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  54. package/dist/namespaces/migrate.js +3 -3
  55. package/dist/namespaces/storage.js +2 -2
  56. package/dist/offline-sync.d.ts +49 -1
  57. package/dist/offline-sync.js +13 -1
  58. package/dist/operator-toolkit.js +5 -5
  59. package/dist/orchestrator.js +9 -9
  60. package/dist/semantic-consolidation.js +3 -3
  61. package/dist/semantic-rule-promotion.js +2 -2
  62. package/dist/semantic-rule-verifier.js +2 -2
  63. package/dist/storage.d.ts +5 -0
  64. package/dist/storage.js +1 -1
  65. package/dist/verified-recall.js +2 -2
  66. package/package.json +1 -1
  67. package/src/access-http.test.ts +184 -0
  68. package/src/access-http.ts +37 -0
  69. package/src/access-schema.ts +58 -3
  70. package/src/access-service-namespace.test.ts +56 -1
  71. package/src/access-service-offline-file-content.test.ts +17 -0
  72. package/src/access-service.ts +16 -1
  73. package/src/index.ts +6 -0
  74. package/src/offline-sync.test.ts +1055 -1
  75. package/src/offline-sync.ts +453 -96
  76. package/src/storage.ts +36 -2
  77. package/dist/chunk-AMVN77EU.js.map +0 -1
  78. package/dist/chunk-F33CJ5CH.js.map +0 -1
  79. package/dist/chunk-JUYT2J3K.js.map +0 -1
  80. package/dist/chunk-LCTP7YRU.js.map +0 -1
  81. package/dist/chunk-NW7JW5GA.js.map +0 -1
  82. /package/dist/{chunk-25YQM6XW.js.map → chunk-2IWUMAES.js.map} +0 -0
  83. /package/dist/{chunk-QYHQ2JHL.js.map → chunk-43PJZYGL.js.map} +0 -0
  84. /package/dist/{chunk-YITUHONZ.js.map → chunk-4KGVTPGD.js.map} +0 -0
  85. /package/dist/{chunk-TR4DK5OH.js.map → chunk-76FLAAUC.js.map} +0 -0
  86. /package/dist/{chunk-6BFAEWQS.js.map → chunk-77H5NU3M.js.map} +0 -0
  87. /package/dist/{chunk-IANK6Y5W.js.map → chunk-A6KTB5R6.js.map} +0 -0
  88. /package/dist/{chunk-7D6O46PF.js.map → chunk-BVF3AGJP.js.map} +0 -0
  89. /package/dist/{chunk-4H6DURG6.js.map → chunk-JA3AK3PT.js.map} +0 -0
  90. /package/dist/{chunk-WDSIV3AK.js.map → chunk-KRBK4BQH.js.map} +0 -0
  91. /package/dist/{chunk-RCZRL5BE.js.map → chunk-MRILGULB.js.map} +0 -0
  92. /package/dist/{chunk-CWWDIQZB.js.map → chunk-QLLBRHAT.js.map} +0 -0
  93. /package/dist/{chunk-2WIPXV3Y.js.map → chunk-RR2PKP3I.js.map} +0 -0
  94. /package/dist/{chunk-3F24QTRI.js.map → chunk-SAZS2QZB.js.map} +0 -0
  95. /package/dist/{chunk-VYU7PXUS.js.map → chunk-SIC6U3GZ.js.map} +0 -0
  96. /package/dist/{chunk-6CB4E7ZV.js.map → chunk-UL2NNBUL.js.map} +0 -0
  97. /package/dist/{chunk-6WV2HYTZ.js.map → chunk-W6AQJ2PY.js.map} +0 -0
  98. /package/dist/{chunk-PUXCIHRL.js.map → chunk-XSZEP4SF.js.map} +0 -0
@@ -17,6 +17,7 @@ import {
17
17
 
18
18
  // src/offline-sync.ts
19
19
  import { createHash, randomUUID } from "crypto";
20
+ import { createReadStream } from "fs";
20
21
  import {
21
22
  lstat,
22
23
  mkdir,
@@ -27,6 +28,7 @@ import {
27
28
  rm,
28
29
  stat,
29
30
  unlink,
31
+ utimes,
30
32
  writeFile
31
33
  } from "fs/promises";
32
34
  import path from "path";
@@ -36,8 +38,11 @@ var OFFLINE_SYNC_STATE_VERSION = 1;
36
38
  var OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES = 64 * 1024 * 1024;
37
39
  var OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES = 8 * 1024 * 1024;
38
40
  var OFFLINE_SYNC_APPLY_MAX_BODY_BYTES = 16 * 1024 * 1024;
41
+ var OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES = 64 * 1024 * 1024;
42
+ var OFFLINE_SYNC_MAX_MTIME_MS = 864e13;
39
43
  var SYNC_INTERNAL_DIR = ".offline-sync";
40
44
  var OFFLINE_SYNC_UPLOAD_STAGING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
45
+ var OFFLINE_SYNC_FAST_BASE_MTIME_TOLERANCE_MS = 1e3;
41
46
  var EXCLUDED_FILE_NAMES = /* @__PURE__ */ new Set([
42
47
  ".sync-state.json"
43
48
  ]);
@@ -72,6 +77,13 @@ function assertNonNegativeFinite(value, field) {
72
77
  }
73
78
  return value;
74
79
  }
80
+ function assertOfflineSyncMtimeMs(value, field) {
81
+ const mtimeMs = assertNonNegativeFinite(value, field);
82
+ if (mtimeMs > OFFLINE_SYNC_MAX_MTIME_MS) {
83
+ throw new Error(`${field} must be within JavaScript Date range`);
84
+ }
85
+ return mtimeMs;
86
+ }
75
87
  function assertBoolean(value, field) {
76
88
  if (typeof value !== "boolean") {
77
89
  throw new Error(`${field} must be a boolean`);
@@ -94,7 +106,7 @@ function normalizeFileState(input, fieldPrefix) {
94
106
  path: relPath,
95
107
  sha256: assertSha256(obj.sha256, `${fieldPrefix}.sha256`),
96
108
  bytes: assertNonNegativeInteger(obj.bytes, `${fieldPrefix}.bytes`),
97
- mtimeMs: assertNonNegativeFinite(obj.mtimeMs, `${fieldPrefix}.mtimeMs`)
109
+ mtimeMs: assertOfflineSyncMtimeMs(obj.mtimeMs, `${fieldPrefix}.mtimeMs`)
98
110
  };
99
111
  }
100
112
  function normalizeFileRecord(input, fieldPrefix, requireContent) {
@@ -276,20 +288,72 @@ function isCanonicalRuntimeStatePath(parts) {
276
288
  if (parts[0] === "state") return true;
277
289
  return parts[0] === "namespaces" && parts.length >= 4 && parts[2] === "state";
278
290
  }
291
+ var REMOTE_AUTHORITATIVE_RUNTIME_STATE_FILES = /* @__PURE__ */ new Set([
292
+ ".artifact-write-version.log",
293
+ ".memory-status-version.log",
294
+ "buffer.json",
295
+ "embeddings.json",
296
+ "index_time.json",
297
+ "last_recall.json",
298
+ "memory-lifecycle-ledger.jsonl",
299
+ "recall_impressions.jsonl"
300
+ ]);
301
+ function shouldPreferIncomingOfflineRuntimeFile(relPosix) {
302
+ const parts = relPosix.split("/");
303
+ const basename = parts[parts.length - 1] ?? "";
304
+ return isCanonicalRuntimeStatePath(parts) && REMOTE_AUTHORITATIVE_RUNTIME_STATE_FILES.has(basename);
305
+ }
279
306
  function filterBaseFilesForMode(files, includeTranscripts) {
280
307
  return files.filter((file) => !shouldExcludeRelPath(file.path, includeTranscripts));
281
308
  }
309
+ function canReuseFastBaseFileState(baseEntry, st, baseCapturedAtMs) {
310
+ if (baseEntry.bytes !== st.size) return false;
311
+ if (Math.abs(baseEntry.mtimeMs - st.mtimeMs) > OFFLINE_SYNC_FAST_BASE_MTIME_TOLERANCE_MS) {
312
+ return false;
313
+ }
314
+ if (baseCapturedAtMs === null) return false;
315
+ return st.ctimeMs - baseCapturedAtMs <= OFFLINE_SYNC_FAST_BASE_MTIME_TOLERANCE_MS;
316
+ }
317
+ async function canReuseFastBaseFileStateFromDisk(baseEntry, filePath, st, baseCapturedAtMs) {
318
+ if (!canReuseFastBaseFileState(baseEntry, st, baseCapturedAtMs)) return false;
319
+ return !await fileIsSecureStoreEncrypted(filePath).catch(() => true);
320
+ }
282
321
  async function readOfflineSyncFileRecord(options) {
283
322
  const relPath = validateArchiveRelativePath(options.relPath, "offlineSyncFile.path");
284
- const bytes = options.readFile ? await options.readFile({ root: options.root.abs, path: relPath, filePath: options.filePath }) : await readFile(options.filePath);
285
- const digest = sha256Buffer(bytes);
323
+ let content = null;
324
+ let digest;
325
+ if (options.includeContent) {
326
+ content = options.readFile ? await options.readFile({ root: options.root.abs, path: relPath, filePath: options.filePath }) : await readFile(options.filePath);
327
+ digest = sha256Buffer(content);
328
+ } else if (options.readFileDigest) {
329
+ digest = await options.readFileDigest({ root: options.root.abs, path: relPath, filePath: options.filePath });
330
+ } else if (options.readFile) {
331
+ content = await options.readFile({ root: options.root.abs, path: relPath, filePath: options.filePath });
332
+ digest = sha256Buffer(content);
333
+ content = null;
334
+ } else {
335
+ digest = await sha256File(options.filePath);
336
+ }
286
337
  const st = await stat(options.filePath);
287
338
  return {
288
339
  path: relPath,
289
340
  sha256: digest.sha256,
290
341
  bytes: digest.bytes,
291
342
  mtimeMs: st.mtimeMs,
292
- ...options.includeContent ? { contentBase64: bytes.toString("base64") } : {}
343
+ ...content ? { contentBase64: content.toString("base64") } : {}
344
+ };
345
+ }
346
+ async function sha256File(filePath) {
347
+ const hash = createHash("sha256");
348
+ let bytes = 0;
349
+ for await (const chunk of createReadStream(filePath)) {
350
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
351
+ hash.update(buffer);
352
+ bytes += buffer.length;
353
+ }
354
+ return {
355
+ sha256: hash.digest("hex"),
356
+ bytes
293
357
  };
294
358
  }
295
359
  async function fileIsSecureStoreEncrypted(filePath) {
@@ -337,7 +401,58 @@ async function buildOfflineSyncSnapshot(options) {
337
401
  relPath: relPosix,
338
402
  filePath: abs,
339
403
  includeContent: options.includeContent === true,
340
- readFile: options.readFile
404
+ readFile: options.readFile,
405
+ readFileDigest: options.readFileDigest
406
+ }));
407
+ }
408
+ }
409
+ await walk(root.abs);
410
+ return {
411
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
412
+ schemaVersion: 1,
413
+ createdAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
414
+ sourceId: normalizeSourceId(options.sourceId, "sourceId"),
415
+ includeTranscripts,
416
+ files: files.sort(compareByPath)
417
+ };
418
+ }
419
+ async function buildOfflineSyncSnapshotFromBase(options) {
420
+ const rootAbs = path.resolve(options.root);
421
+ const root = await prepareSafeArchiveRoot(rootAbs, "buildOfflineSyncSnapshotFromBase", "root");
422
+ const includeTranscripts = options.includeTranscripts !== false;
423
+ const base = byPath(filterBaseFilesForMode(
424
+ normalizeFileStates(options.baseFiles),
425
+ includeTranscripts
426
+ ));
427
+ const rawBaseCapturedAtMs = options.baseCapturedAt?.getTime();
428
+ const baseCapturedAtMs = rawBaseCapturedAtMs !== void 0 && Number.isFinite(rawBaseCapturedAtMs) ? rawBaseCapturedAtMs : null;
429
+ const files = [];
430
+ async function walk(dirAbs) {
431
+ let entries = await readdir(dirAbs, { withFileTypes: true });
432
+ entries = entries.sort((left, right) => left.name.localeCompare(right.name));
433
+ for (const entry of entries) {
434
+ const abs = path.join(dirAbs, entry.name);
435
+ const relPosix = path.relative(root.abs, abs).split(path.sep).join("/");
436
+ if (shouldExcludeRelPath(relPosix, includeTranscripts)) continue;
437
+ if (entry.isSymbolicLink()) continue;
438
+ if (entry.isDirectory()) {
439
+ await walk(abs);
440
+ continue;
441
+ }
442
+ if (!entry.isFile()) continue;
443
+ const st = await stat(abs);
444
+ const baseEntry = base.get(relPosix);
445
+ if (options.includeContent !== true && baseEntry && baseCapturedAtMs !== null && await canReuseFastBaseFileStateFromDisk(baseEntry, abs, st, baseCapturedAtMs)) {
446
+ files.push(baseEntry);
447
+ continue;
448
+ }
449
+ files.push(await readOfflineSyncFileRecord({
450
+ root,
451
+ relPath: relPosix,
452
+ filePath: abs,
453
+ includeContent: options.includeContent === true,
454
+ readFile: options.readFile,
455
+ readFileDigest: options.readFileDigest
341
456
  }));
342
457
  }
343
458
  }
@@ -375,7 +490,8 @@ async function buildOfflineSyncSnapshotForPaths(options) {
375
490
  relPath,
376
491
  filePath,
377
492
  includeContent: options.includeContent === true,
378
- readFile: options.readFile
493
+ readFile: options.readFile,
494
+ readFileDigest: options.readFileDigest
379
495
  }));
380
496
  }
381
497
  return {
@@ -452,25 +568,45 @@ async function readOfflineSyncFileContentChunk(options) {
452
568
  }
453
569
  async function buildOfflineSyncChangeset(options) {
454
570
  const includeTranscripts = options.includeTranscripts !== false;
455
- const excludedPaths = new Set(
456
- (options.excludePaths ?? []).map((relPath) => normalizeRelativePath(relPath, "excludePaths[]"))
457
- );
458
- const base = byPath(filterBaseFilesForMode(
459
- normalizeFileStates(options.baseFiles),
460
- includeTranscripts
461
- ));
462
- const current = await buildOfflineSyncSnapshot({
571
+ const current = await buildOfflineSyncSnapshotFromBase({
463
572
  root: options.root,
464
573
  sourceId: options.sourceId,
574
+ baseFiles: options.baseFiles,
575
+ baseCapturedAt: options.baseCapturedAt,
465
576
  includeContent: false,
466
577
  includeTranscripts,
467
578
  now: options.now,
579
+ readFile: options.readFile,
580
+ readFileDigest: options.readFileDigest
581
+ });
582
+ return buildOfflineSyncChangesetFromSnapshot({
583
+ root: options.root,
584
+ sourceId: options.sourceId,
585
+ baseFiles: options.baseFiles,
586
+ currentFiles: current.files,
587
+ excludePaths: options.excludePaths,
588
+ includeTranscripts,
589
+ now: options.now,
468
590
  readFile: options.readFile
469
591
  });
470
- const currentMap = byPath(current.files);
592
+ }
593
+ async function buildOfflineSyncChangesetFromSnapshot(options) {
594
+ const includeTranscripts = options.includeTranscripts !== false;
595
+ const excludedPaths = new Set(
596
+ (options.excludePaths ?? []).map((relPath) => normalizeRelativePath(relPath, "excludePaths[]"))
597
+ );
598
+ const base = byPath(filterBaseFilesForMode(
599
+ normalizeFileStates(options.baseFiles),
600
+ includeTranscripts
601
+ ));
602
+ const currentMap = byPath(filterBaseFilesForMode(
603
+ normalizeFileStates(options.currentFiles),
604
+ includeTranscripts
605
+ ));
471
606
  const changes = [];
472
607
  for (const relPath of unionPaths(base, currentMap)) {
473
608
  if (excludedPaths.has(relPath)) continue;
609
+ if (shouldPreferIncomingOfflineRuntimeFile(relPath)) continue;
474
610
  const baseEntry = base.get(relPath);
475
611
  const currentEntry = currentMap.get(relPath);
476
612
  if (currentEntry && currentEntry.sha256 !== baseEntry?.sha256) {
@@ -508,7 +644,7 @@ async function buildOfflineSyncChangeset(options) {
508
644
  schemaVersion: 1,
509
645
  createdAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
510
646
  sourceId: normalizeSourceId(options.sourceId, "sourceId"),
511
- includeTranscripts: current.includeTranscripts,
647
+ includeTranscripts,
512
648
  changes: changes.sort(compareByPath)
513
649
  };
514
650
  }
@@ -523,22 +659,37 @@ function summarizeOfflineSyncChangeset(changeset) {
523
659
  }
524
660
  async function summarizeOfflineSyncPendingChanges(options) {
525
661
  const includeTranscripts = options.includeTranscripts !== false;
526
- const base = byPath(filterBaseFilesForMode(
527
- normalizeFileStates(options.baseFiles),
528
- includeTranscripts
529
- ));
530
- const current = await buildOfflineSyncSnapshot({
662
+ const current = await buildOfflineSyncSnapshotFromBase({
531
663
  root: options.root,
532
664
  sourceId: options.sourceId,
665
+ baseFiles: options.baseFiles,
666
+ baseCapturedAt: options.baseCapturedAt,
533
667
  includeContent: false,
534
668
  includeTranscripts,
535
669
  now: options.now,
536
- readFile: options.readFile
670
+ readFile: options.readFile,
671
+ readFileDigest: options.readFileDigest
672
+ });
673
+ return summarizeOfflineSyncPendingFiles({
674
+ baseFiles: options.baseFiles,
675
+ currentFiles: current.files,
676
+ includeTranscripts
537
677
  });
538
- const currentMap = byPath(current.files);
678
+ }
679
+ function summarizeOfflineSyncPendingFiles(options) {
680
+ const includeTranscripts = options.includeTranscripts !== false;
681
+ const base = byPath(filterBaseFilesForMode(
682
+ normalizeFileStates(options.baseFiles),
683
+ includeTranscripts
684
+ ));
685
+ const currentMap = byPath(filterBaseFilesForMode(
686
+ normalizeFileStates(options.currentFiles),
687
+ includeTranscripts
688
+ ));
539
689
  let upserts = 0;
540
690
  let deletes = 0;
541
691
  for (const relPath of unionPaths(base, currentMap)) {
692
+ if (shouldPreferIncomingOfflineRuntimeFile(relPath)) continue;
542
693
  const baseEntry = base.get(relPath);
543
694
  const currentEntry = currentMap.get(relPath);
544
695
  if (currentEntry && currentEntry.sha256 !== baseEntry?.sha256) {
@@ -566,30 +717,61 @@ async function applyOfflineSyncSnapshot(options) {
566
717
  requireContent: false
567
718
  });
568
719
  const root = await ensureSyncRoot(options.root, "applyOfflineSyncSnapshot");
569
- const current = await buildOfflineSyncSnapshot({
720
+ const currentFiles = options.currentFiles ? filterBaseFilesForMode(normalizeFileStates(options.currentFiles), snapshot.includeTranscripts).sort(compareByPath) : (await buildOfflineSyncSnapshot({
570
721
  root: root.abs,
571
722
  sourceId: "local",
572
723
  includeContent: false,
573
724
  includeTranscripts: snapshot.includeTranscripts,
574
- readFile: options.readFile
575
- });
576
- const currentMap = byPath(current.files);
725
+ readFile: options.readFile,
726
+ readFileDigest: options.readFileDigest
727
+ })).files;
728
+ const currentMap = byPath(currentFiles);
729
+ const deferredPaths = new Set(options.deferredPaths ?? []);
577
730
  const nextBase = new Map(baseMap);
578
731
  const conflicts = [];
579
732
  let upserted = 0;
580
733
  let deleted = 0;
581
734
  let skipped = 0;
582
735
  let pendingLocal = 0;
736
+ const conflictIncomingBuffer = (relPath) => {
737
+ if (options.writeConflictCopies === false) return void 0;
738
+ const buffer = incomingBuffers.get(relPath);
739
+ if (buffer || options.allowMissingConflictContent === true) return buffer;
740
+ return requiredBuffer(incomingBuffers, relPath);
741
+ };
583
742
  for (const relPath of unionPaths(baseMap, incomingMap, currentMap)) {
584
743
  const base = baseMap.get(relPath);
585
744
  const incoming = incomingMap.get(relPath);
586
745
  const currentEntry = currentMap.get(relPath);
746
+ if (deferredPaths.has(relPath)) {
747
+ if (base) nextBase.set(relPath, base);
748
+ else nextBase.delete(relPath);
749
+ skipped += 1;
750
+ continue;
751
+ }
587
752
  if (incoming) {
588
753
  if (currentEntry?.sha256 === incoming.sha256) {
589
- nextBase.set(relPath, toFileState(incoming));
754
+ if (await setSafeFileMtime(root, relPath, incoming.mtimeMs)) {
755
+ nextBase.set(relPath, toFileState(incoming));
756
+ } else {
757
+ if (base) nextBase.set(relPath, base);
758
+ else nextBase.delete(relPath);
759
+ pendingLocal += 1;
760
+ }
590
761
  skipped += 1;
591
762
  continue;
592
763
  }
764
+ if (shouldPreferIncomingOfflineRuntimeFile(relPath) && currentEntry && base && incoming.sha256 === base.sha256) {
765
+ nextBase.set(relPath, base);
766
+ skipped += 1;
767
+ continue;
768
+ }
769
+ if (shouldPreferIncomingOfflineRuntimeFile(relPath)) {
770
+ await writeSafeFile(root, relPath, requiredBuffer(incomingBuffers, relPath), options.writeFile, incoming.mtimeMs);
771
+ nextBase.set(relPath, toFileState(incoming));
772
+ upserted += 1;
773
+ continue;
774
+ }
593
775
  if (!currentEntry && base && incoming.sha256 === base.sha256) {
594
776
  nextBase.set(relPath, base);
595
777
  pendingLocal += 1;
@@ -603,7 +785,7 @@ async function applyOfflineSyncSnapshot(options) {
603
785
  reason: "local_deleted_remote_modified",
604
786
  baseSha256: base.sha256,
605
787
  incomingSha256: incoming.sha256,
606
- incomingBuffer: options.writeConflictCopies === false ? incomingBuffers.get(relPath) : requiredBuffer(incomingBuffers, relPath),
788
+ incomingBuffer: conflictIncomingBuffer(relPath),
607
789
  writeConflictCopies: options.writeConflictCopies !== false,
608
790
  sourceId: snapshot.sourceId,
609
791
  writeFile: options.writeFile
@@ -612,13 +794,13 @@ async function applyOfflineSyncSnapshot(options) {
612
794
  continue;
613
795
  }
614
796
  if (!currentEntry && !base) {
615
- await writeSafeFile(root, relPath, requiredBuffer(incomingBuffers, relPath), options.writeFile);
797
+ await writeSafeFile(root, relPath, requiredBuffer(incomingBuffers, relPath), options.writeFile, incoming.mtimeMs);
616
798
  nextBase.set(relPath, toFileState(incoming));
617
799
  upserted += 1;
618
800
  continue;
619
801
  }
620
802
  if (base && currentEntry && currentEntry.sha256 === base.sha256) {
621
- await writeSafeFile(root, relPath, requiredBuffer(incomingBuffers, relPath), options.writeFile);
803
+ await writeSafeFile(root, relPath, requiredBuffer(incomingBuffers, relPath), options.writeFile, incoming.mtimeMs);
622
804
  nextBase.set(relPath, toFileState(incoming));
623
805
  upserted += 1;
624
806
  continue;
@@ -636,7 +818,7 @@ async function applyOfflineSyncSnapshot(options) {
636
818
  baseSha256: base?.sha256,
637
819
  localSha256: currentEntry?.sha256,
638
820
  incomingSha256: incoming.sha256,
639
- incomingBuffer: options.writeConflictCopies === false ? incomingBuffers.get(relPath) : requiredBuffer(incomingBuffers, relPath),
821
+ incomingBuffer: conflictIncomingBuffer(relPath),
640
822
  writeConflictCopies: options.writeConflictCopies !== false,
641
823
  sourceId: snapshot.sourceId,
642
824
  writeFile: options.writeFile
@@ -649,6 +831,17 @@ async function applyOfflineSyncSnapshot(options) {
649
831
  skipped += 1;
650
832
  continue;
651
833
  }
834
+ if (shouldPreferIncomingOfflineRuntimeFile(relPath) && base) {
835
+ await deleteSafeFile(root, relPath, options.deleteFile);
836
+ nextBase.delete(relPath);
837
+ deleted += 1;
838
+ continue;
839
+ }
840
+ if (shouldPreferIncomingOfflineRuntimeFile(relPath)) {
841
+ pendingLocal += 1;
842
+ skipped += 1;
843
+ continue;
844
+ }
652
845
  if (base && currentEntry.sha256 === base.sha256) {
653
846
  await deleteSafeFile(root, relPath, options.deleteFile);
654
847
  nextBase.delete(relPath);
@@ -690,14 +883,16 @@ async function applyOfflineSyncChangeset(options) {
690
883
  const root = await ensureSyncRoot(options.root, "applyOfflineSyncChangeset");
691
884
  const records = changeset.changes.filter((change) => change.type === "upsert").map((change) => change.file);
692
885
  const incomingBuffers = verifyRecordContents(records, "offline sync changeset");
693
- const current = await buildOfflineSyncSnapshot({
886
+ const currentFiles = options.currentFiles ? filterBaseFilesForMode(normalizeFileStates(options.currentFiles), changeset.includeTranscripts).sort(compareByPath) : (await buildOfflineSyncSnapshotForPaths({
694
887
  root: root.abs,
695
888
  sourceId: "local",
889
+ paths: changeset.changes.map((change) => change.path),
696
890
  includeContent: false,
697
891
  includeTranscripts: changeset.includeTranscripts,
698
- readFile: options.readFile
699
- });
700
- const currentMap = byPath(current.files);
892
+ readFile: options.readFile,
893
+ readFileDigest: options.readFileDigest
894
+ })).files;
895
+ const currentMap = byPath(currentFiles);
701
896
  const conflicts = [];
702
897
  let appliedUpserts = 0;
703
898
  let appliedDeletes = 0;
@@ -706,12 +901,18 @@ async function applyOfflineSyncChangeset(options) {
706
901
  const currentEntry = currentMap.get(change.path);
707
902
  if (change.type === "upsert") {
708
903
  if (currentEntry?.sha256 === change.file.sha256) {
709
- skipped += 1;
904
+ if (await setSafeFileMtime(root, change.path, change.file.mtimeMs)) {
905
+ skipped += 1;
906
+ } else {
907
+ await writeSafeFile(root, change.path, requiredBuffer(incomingBuffers, change.path), options.writeFile, change.file.mtimeMs);
908
+ currentMap.set(change.path, toFileState(change.file));
909
+ appliedUpserts += 1;
910
+ }
710
911
  continue;
711
912
  }
712
913
  if (!change.baseSha256) {
713
914
  if (!currentEntry) {
714
- await writeSafeFile(root, change.path, requiredBuffer(incomingBuffers, change.path), options.writeFile);
915
+ await writeSafeFile(root, change.path, requiredBuffer(incomingBuffers, change.path), options.writeFile, change.file.mtimeMs);
715
916
  currentMap.set(change.path, toFileState(change.file));
716
917
  appliedUpserts += 1;
717
918
  continue;
@@ -730,7 +931,7 @@ async function applyOfflineSyncChangeset(options) {
730
931
  continue;
731
932
  }
732
933
  if (currentEntry?.sha256 === change.baseSha256) {
733
- await writeSafeFile(root, change.path, requiredBuffer(incomingBuffers, change.path), options.writeFile);
934
+ await writeSafeFile(root, change.path, requiredBuffer(incomingBuffers, change.path), options.writeFile, change.file.mtimeMs);
734
935
  currentMap.set(change.path, toFileState(change.file));
735
936
  appliedUpserts += 1;
736
937
  continue;
@@ -771,7 +972,15 @@ async function applyOfflineSyncChangeset(options) {
771
972
  appliedDeletes,
772
973
  skipped,
773
974
  conflicts,
774
- currentFiles: [...currentMap.values()].sort(compareByPath)
975
+ currentFiles: options.returnCurrentFiles === false ? [...currentMap.values()].sort(compareByPath) : (await buildOfflineSyncSnapshot({
976
+ root: root.abs,
977
+ sourceId: "local",
978
+ includeContent: false,
979
+ includeTranscripts: changeset.includeTranscripts,
980
+ readFile: options.readFile,
981
+ readFileDigest: options.readFileDigest
982
+ })).files,
983
+ ...options.returnCurrentFiles === false ? { currentFilesComplete: false } : {}
775
984
  };
776
985
  }
777
986
  function verifyRecordContents(records, context, options = {}) {
@@ -826,10 +1035,11 @@ function toFileState(file) {
826
1035
  mtimeMs: file.mtimeMs
827
1036
  };
828
1037
  }
829
- async function writeSafeFile(root, relPath, content, writeFileHook) {
1038
+ async function writeSafeFile(root, relPath, content, writeFileHook, mtimeMs) {
830
1039
  const target = await resolveSafeArchiveTarget(root, relPath);
831
1040
  if (writeFileHook) {
832
1041
  await writeFileHook({ root: root.abs, path: relPath, filePath: target, content });
1042
+ await setSafeFileMtime(root, relPath, mtimeMs);
833
1043
  return;
834
1044
  }
835
1045
  await mkdir(path.dirname(target), { recursive: true });
@@ -847,12 +1057,28 @@ async function writeSafeFile(root, relPath, content, writeFileHook) {
847
1057
  throw new Error(`offline sync target is a symlink: ${relPath}`);
848
1058
  }
849
1059
  await rename(tmp, target);
1060
+ await setSafeFileMtime(root, relPath, mtimeMs);
850
1061
  } catch (error) {
851
1062
  await unlink(tmp).catch(() => {
852
1063
  });
853
1064
  throw error;
854
1065
  }
855
1066
  }
1067
+ async function setSafeFileMtime(root, relPath, mtimeMs) {
1068
+ if (mtimeMs === void 0) return true;
1069
+ const target = await resolveSafeArchiveTarget(root, relPath);
1070
+ const targetStat = await lstat(target).catch((error) => {
1071
+ if (error.code === "ENOENT") return null;
1072
+ throw error;
1073
+ });
1074
+ if (!targetStat) return false;
1075
+ if (targetStat.isSymbolicLink()) {
1076
+ throw new Error(`offline sync target is a symlink: ${relPath}`);
1077
+ }
1078
+ const mtime = new Date(assertOfflineSyncMtimeMs(mtimeMs, "mtimeMs"));
1079
+ await utimes(target, mtime, mtime);
1080
+ return true;
1081
+ }
856
1082
  async function applyOfflineSyncFileContentChunk(options) {
857
1083
  const root = await ensureSyncRoot(options.root, "applyOfflineSyncFileContentChunk");
858
1084
  const sourceId = normalizeSourceId(options.sourceId, "sourceId");
@@ -863,9 +1089,10 @@ async function applyOfflineSyncFileContentChunk(options) {
863
1089
  }
864
1090
  const sha256 = assertSha256(options.sha256, "sha256");
865
1091
  const bytes = assertNonNegativeInteger(options.bytes, "bytes");
866
- const mtimeMs = assertNonNegativeFinite(options.mtimeMs, "mtimeMs");
1092
+ const mtimeMs = assertOfflineSyncMtimeMs(options.mtimeMs, "mtimeMs");
867
1093
  const offset = options.offset === void 0 ? 0 : assertNonNegativeInteger(options.offset, "offset");
868
1094
  const baseSha256 = options.baseSha256 === void 0 ? void 0 : assertSha256(options.baseSha256, "baseSha256");
1095
+ const preferIncomingRuntimeFile = shouldPreferIncomingOfflineRuntimeFile(relPath);
869
1096
  if (!Buffer.isBuffer(options.content)) {
870
1097
  throw new Error("content must be a Buffer");
871
1098
  }
@@ -886,8 +1113,85 @@ async function applyOfflineSyncFileContentChunk(options) {
886
1113
  if (options.writeFile && !options.writeStagingFile) {
887
1114
  throw new Error("offline sync upload storage hooks require writeStagingFile");
888
1115
  }
1116
+ const baseResult = {
1117
+ path: relPath,
1118
+ sha256,
1119
+ bytes,
1120
+ mtimeMs,
1121
+ offset,
1122
+ chunkBytes: options.content.length,
1123
+ done: offset + options.content.length === bytes
1124
+ };
1125
+ const currentFileConflict = async (currentFile2) => {
1126
+ if (!baseSha256 && currentFile2 && !preferIncomingRuntimeFile) {
1127
+ const conflict = await recordConflict({
1128
+ root,
1129
+ relPath,
1130
+ reason: "remote_exists_for_local_create",
1131
+ localSha256: currentFile2.sha256,
1132
+ incomingSha256: sha256,
1133
+ writeConflictCopies: false,
1134
+ sourceId,
1135
+ writeFile: options.writeFile
1136
+ });
1137
+ return {
1138
+ conflict,
1139
+ currentFile: currentFile2
1140
+ };
1141
+ }
1142
+ if (baseSha256 && currentFile2?.sha256 !== baseSha256 && !preferIncomingRuntimeFile) {
1143
+ const conflict = await recordConflict({
1144
+ root,
1145
+ relPath,
1146
+ reason: currentFile2 ? "remote_changed_for_local_update" : "remote_deleted_for_local_update",
1147
+ baseSha256,
1148
+ localSha256: currentFile2?.sha256,
1149
+ incomingSha256: sha256,
1150
+ writeConflictCopies: false,
1151
+ sourceId,
1152
+ writeFile: options.writeFile
1153
+ });
1154
+ return {
1155
+ conflict,
1156
+ ...currentFile2 ? { currentFile: currentFile2 } : {}
1157
+ };
1158
+ }
1159
+ return null;
1160
+ };
889
1161
  if (offset === 0) {
890
1162
  await pruneOfflineUploadStaging(root);
1163
+ const currentSnapshot2 = await buildOfflineSyncSnapshotForPaths({
1164
+ root: root.abs,
1165
+ sourceId: "local",
1166
+ paths: [relPath],
1167
+ includeContent: false,
1168
+ includeTranscripts,
1169
+ readFile: options.readFile,
1170
+ readFileDigest: options.readFileDigest
1171
+ });
1172
+ const currentFile2 = currentSnapshot2.files[0];
1173
+ if (currentFile2?.sha256 === sha256) {
1174
+ await setSafeFileMtime(root, relPath, mtimeMs);
1175
+ return {
1176
+ ...baseResult,
1177
+ done: true,
1178
+ chunkBytes: 0,
1179
+ applied: false,
1180
+ skipped: true,
1181
+ currentFile: toFileState(currentFile2)
1182
+ };
1183
+ }
1184
+ const conflictResult = await currentFileConflict(currentFile2 ? toFileState(currentFile2) : void 0);
1185
+ if (conflictResult) {
1186
+ return {
1187
+ ...baseResult,
1188
+ done: true,
1189
+ chunkBytes: 0,
1190
+ applied: false,
1191
+ skipped: false,
1192
+ ...conflictResult
1193
+ };
1194
+ }
891
1195
  }
892
1196
  const upload = await writeOfflineUploadChunk({
893
1197
  root,
@@ -901,16 +1205,7 @@ async function applyOfflineSyncFileContentChunk(options) {
901
1205
  writeFile: options.writeFile,
902
1206
  writeStagingFile: options.writeStagingFile
903
1207
  });
904
- const done = offset + options.content.length === bytes;
905
- const baseResult = {
906
- path: relPath,
907
- sha256,
908
- bytes,
909
- mtimeMs,
910
- offset,
911
- chunkBytes: options.content.length,
912
- done
913
- };
1208
+ const done = baseResult.done;
914
1209
  if (!done) {
915
1210
  return {
916
1211
  ...baseResult,
@@ -934,7 +1229,8 @@ async function applyOfflineSyncFileContentChunk(options) {
934
1229
  paths: [relPath],
935
1230
  includeContent: false,
936
1231
  includeTranscripts,
937
- readFile: options.readFile
1232
+ readFile: options.readFile,
1233
+ readFileDigest: options.readFileDigest
938
1234
  });
939
1235
  const currentFile = currentSnapshot.files[0];
940
1236
  const uploadedState = {
@@ -945,53 +1241,24 @@ async function applyOfflineSyncFileContentChunk(options) {
945
1241
  };
946
1242
  try {
947
1243
  if (currentFile?.sha256 === sha256) {
1244
+ await setSafeFileMtime(root, relPath, mtimeMs);
948
1245
  return {
949
1246
  ...baseResult,
950
1247
  applied: false,
951
1248
  skipped: true,
952
- currentFile: toFileState(currentFile)
1249
+ currentFile: uploadedState
953
1250
  };
954
1251
  }
955
- if (!baseSha256 && currentFile) {
956
- const conflict = await recordConflict({
957
- root,
958
- relPath,
959
- reason: "remote_exists_for_local_create",
960
- localSha256: currentFile.sha256,
961
- incomingSha256: sha256,
962
- writeConflictCopies: false,
963
- sourceId,
964
- writeFile: options.writeFile
965
- });
1252
+ const conflictResult = await currentFileConflict(currentFile ? toFileState(currentFile) : void 0);
1253
+ if (conflictResult) {
966
1254
  return {
967
1255
  ...baseResult,
968
1256
  applied: false,
969
1257
  skipped: false,
970
- conflict,
971
- currentFile: toFileState(currentFile)
972
- };
973
- }
974
- if (baseSha256 && currentFile?.sha256 !== baseSha256) {
975
- const conflict = await recordConflict({
976
- root,
977
- relPath,
978
- reason: currentFile ? "remote_changed_for_local_update" : "remote_deleted_for_local_update",
979
- baseSha256,
980
- localSha256: currentFile?.sha256,
981
- incomingSha256: sha256,
982
- writeConflictCopies: false,
983
- sourceId,
984
- writeFile: options.writeFile
985
- });
986
- return {
987
- ...baseResult,
988
- applied: false,
989
- skipped: false,
990
- conflict,
991
- ...currentFile ? { currentFile: toFileState(currentFile) } : {}
1258
+ ...conflictResult
992
1259
  };
993
1260
  }
994
- await writeSafeFileFromUpload(root, relPath, upload, options.readFile, options.writeFileChunks);
1261
+ await writeSafeFileFromUpload(root, relPath, upload, options.readFile, options.writeFileChunks, mtimeMs);
995
1262
  return {
996
1263
  ...baseResult,
997
1264
  applied: true,
@@ -1137,11 +1404,12 @@ async function digestOfflineUploadStagingContent(options) {
1137
1404
  }
1138
1405
  return { sha256: hash.digest("hex"), bytes };
1139
1406
  }
1140
- async function writeSafeFileFromUpload(root, relPath, upload, readFile2, writeFileChunks) {
1407
+ async function writeSafeFileFromUpload(root, relPath, upload, readFile2, writeFileChunks, mtimeMs) {
1141
1408
  const target = await resolveSafeArchiveTarget(root, relPath);
1142
1409
  const chunks = readOfflineUploadStagingChunks({ root, upload, readFile: readFile2 });
1143
1410
  if (writeFileChunks) {
1144
1411
  await writeFileChunks({ root: root.abs, path: relPath, filePath: target, chunks });
1412
+ await setSafeFileMtime(root, relPath, mtimeMs);
1145
1413
  return;
1146
1414
  }
1147
1415
  await mkdir(path.dirname(target), { recursive: true });
@@ -1163,6 +1431,7 @@ async function writeSafeFileFromUpload(root, relPath, upload, readFile2, writeFi
1163
1431
  throw new Error(`offline sync target is a symlink: ${relPath}`);
1164
1432
  }
1165
1433
  await rename(tmp, target);
1434
+ await setSafeFileMtime(root, relPath, mtimeMs);
1166
1435
  } catch (error) {
1167
1436
  await handle.close().catch(() => {
1168
1437
  });
@@ -1301,14 +1570,20 @@ export {
1301
1570
  OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
1302
1571
  OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES,
1303
1572
  OFFLINE_SYNC_APPLY_MAX_BODY_BYTES,
1573
+ OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES,
1574
+ OFFLINE_SYNC_MAX_MTIME_MS,
1304
1575
  normalizeOfflineSyncSnapshot,
1305
1576
  normalizeOfflineSyncChangeset,
1577
+ shouldPreferIncomingOfflineRuntimeFile,
1306
1578
  buildOfflineSyncSnapshot,
1579
+ buildOfflineSyncSnapshotFromBase,
1307
1580
  buildOfflineSyncSnapshotForPaths,
1308
1581
  readOfflineSyncFileContentChunk,
1309
1582
  buildOfflineSyncChangeset,
1583
+ buildOfflineSyncChangesetFromSnapshot,
1310
1584
  summarizeOfflineSyncChangeset,
1311
1585
  summarizeOfflineSyncPendingChanges,
1586
+ summarizeOfflineSyncPendingFiles,
1312
1587
  applyOfflineSyncSnapshot,
1313
1588
  applyOfflineSyncChangeset,
1314
1589
  applyOfflineSyncFileContentChunk,
@@ -1319,4 +1594,4 @@ export {
1319
1594
  normalizeOfflineSyncState,
1320
1595
  fileStatesFromSnapshot
1321
1596
  };
1322
- //# sourceMappingURL=chunk-AMVN77EU.js.map
1597
+ //# sourceMappingURL=chunk-MG7NA5H3.js.map