@remnic/core 1.1.28 → 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 (122) hide show
  1. package/dist/access-cli.js +17 -17
  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/active-recall.js +1 -1
  12. package/dist/briefing.js +2 -2
  13. package/dist/causal-consolidation.js +3 -3
  14. package/dist/{chunk-25YQM6XW.js → chunk-2IWUMAES.js} +3 -3
  15. package/dist/{chunk-JUYT2J3K.js → chunk-3OWUCDKH.js} +39 -7
  16. package/dist/chunk-3OWUCDKH.js.map +1 -0
  17. package/dist/{chunk-BJ3KMYTB.js → chunk-3TNBOMQT.js} +21 -10
  18. package/dist/chunk-3TNBOMQT.js.map +1 -0
  19. package/dist/{chunk-QYHQ2JHL.js → chunk-43PJZYGL.js} +2 -2
  20. package/dist/{chunk-YITUHONZ.js → chunk-4KGVTPGD.js} +2 -2
  21. package/dist/{chunk-W7DK3CYM.js → chunk-575RMLWN.js} +2 -2
  22. package/dist/{chunk-TR4DK5OH.js → chunk-76FLAAUC.js} +2 -2
  23. package/dist/{chunk-S27EXIHY.js → chunk-77H5NU3M.js} +3 -3
  24. package/dist/{chunk-IANK6Y5W.js → chunk-A6KTB5R6.js} +2 -2
  25. package/dist/{chunk-7D6O46PF.js → chunk-BVF3AGJP.js} +2 -2
  26. package/dist/{chunk-NTUNYIF7.js → chunk-I5GLV3VE.js} +2 -2
  27. package/dist/{chunk-4H6DURG6.js → chunk-JA3AK3PT.js} +2 -2
  28. package/dist/{chunk-2DM72JF3.js → chunk-KRBK4BQH.js} +14 -14
  29. package/dist/{chunk-AMVN77EU.js → chunk-MG7NA5H3.js} +365 -90
  30. package/dist/chunk-MG7NA5H3.js.map +1 -0
  31. package/dist/{chunk-RCZRL5BE.js → chunk-MRILGULB.js} +2 -2
  32. package/dist/{chunk-2QR3XXIC.js → chunk-MZH6EHNR.js} +3 -3
  33. package/dist/{chunk-2QR3XXIC.js.map → chunk-MZH6EHNR.js.map} +1 -1
  34. package/dist/{chunk-NW7JW5GA.js → chunk-OC7KHOOX.js} +41 -6
  35. package/dist/chunk-OC7KHOOX.js.map +1 -0
  36. package/dist/{chunk-LCTP7YRU.js → chunk-QKZGQIPJ.js} +16 -7
  37. package/dist/chunk-QKZGQIPJ.js.map +1 -0
  38. package/dist/{chunk-MVAOT247.js → chunk-QLLBRHAT.js} +11 -11
  39. package/dist/{chunk-2WIPXV3Y.js → chunk-RR2PKP3I.js} +2 -2
  40. package/dist/{chunk-3F24QTRI.js → chunk-SAZS2QZB.js} +2 -2
  41. package/dist/{chunk-VYU7PXUS.js → chunk-SIC6U3GZ.js} +2 -2
  42. package/dist/{chunk-6CB4E7ZV.js → chunk-UL2NNBUL.js} +4 -4
  43. package/dist/{chunk-F33CJ5CH.js → chunk-VBJ7V5SK.js} +40 -8
  44. package/dist/chunk-VBJ7V5SK.js.map +1 -0
  45. package/dist/{chunk-TFORLO3O.js → chunk-W6AQJ2PY.js} +5 -5
  46. package/dist/{chunk-PUXCIHRL.js → chunk-XSZEP4SF.js} +2 -2
  47. package/dist/{chunk-4CRG46BG.js → chunk-ZK7I7JYV.js} +2 -2
  48. package/dist/{cli-BguVmIwO.d.ts → cli-CJKI2JIe.d.ts} +1 -1
  49. package/dist/cli.d.ts +2 -2
  50. package/dist/cli.js +22 -22
  51. package/dist/compounding/engine.js +2 -2
  52. package/dist/config.js +1 -1
  53. package/dist/connectors/codex-materialize-runner.js +2 -2
  54. package/dist/connectors/index.js +2 -2
  55. package/dist/entity-retrieval.js +2 -2
  56. package/dist/index.d.ts +4 -4
  57. package/dist/index.js +37 -27
  58. package/dist/index.js.map +1 -1
  59. package/dist/maintenance/memory-governance.js +2 -2
  60. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +2 -2
  61. package/dist/maintenance/rebuild-memory-projection.js +3 -3
  62. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  63. package/dist/namespaces/migrate.js +6 -6
  64. package/dist/namespaces/search.js +3 -3
  65. package/dist/namespaces/storage.js +2 -2
  66. package/dist/offline-sync.d.ts +49 -1
  67. package/dist/offline-sync.js +13 -1
  68. package/dist/operator-toolkit.js +9 -9
  69. package/dist/orchestrator.js +12 -12
  70. package/dist/qmd.d.ts +3 -1
  71. package/dist/qmd.js +1 -1
  72. package/dist/resume-bundles.js +2 -2
  73. package/dist/schemas.d.ts +22 -22
  74. package/dist/search/factory.js +2 -2
  75. package/dist/search/index.js +2 -2
  76. package/dist/semantic-consolidation.js +3 -3
  77. package/dist/semantic-rule-promotion.js +2 -2
  78. package/dist/semantic-rule-verifier.js +2 -2
  79. package/dist/storage.d.ts +5 -0
  80. package/dist/storage.js +1 -1
  81. package/dist/transfer/types.d.ts +12 -12
  82. package/dist/verified-recall.js +2 -2
  83. package/package.json +1 -1
  84. package/src/access-http.test.ts +184 -0
  85. package/src/access-http.ts +37 -0
  86. package/src/access-schema.ts +58 -3
  87. package/src/access-service-namespace.test.ts +56 -1
  88. package/src/access-service-offline-file-content.test.ts +17 -0
  89. package/src/access-service.ts +16 -1
  90. package/src/config.ts +2 -2
  91. package/src/index.ts +6 -0
  92. package/src/offline-sync.test.ts +1055 -1
  93. package/src/offline-sync.ts +453 -96
  94. package/src/qmd.test.ts +65 -10
  95. package/src/qmd.ts +22 -9
  96. package/src/storage.ts +36 -2
  97. package/dist/chunk-AMVN77EU.js.map +0 -1
  98. package/dist/chunk-BJ3KMYTB.js.map +0 -1
  99. package/dist/chunk-F33CJ5CH.js.map +0 -1
  100. package/dist/chunk-JUYT2J3K.js.map +0 -1
  101. package/dist/chunk-LCTP7YRU.js.map +0 -1
  102. package/dist/chunk-NW7JW5GA.js.map +0 -1
  103. /package/dist/{chunk-25YQM6XW.js.map → chunk-2IWUMAES.js.map} +0 -0
  104. /package/dist/{chunk-QYHQ2JHL.js.map → chunk-43PJZYGL.js.map} +0 -0
  105. /package/dist/{chunk-YITUHONZ.js.map → chunk-4KGVTPGD.js.map} +0 -0
  106. /package/dist/{chunk-W7DK3CYM.js.map → chunk-575RMLWN.js.map} +0 -0
  107. /package/dist/{chunk-TR4DK5OH.js.map → chunk-76FLAAUC.js.map} +0 -0
  108. /package/dist/{chunk-S27EXIHY.js.map → chunk-77H5NU3M.js.map} +0 -0
  109. /package/dist/{chunk-IANK6Y5W.js.map → chunk-A6KTB5R6.js.map} +0 -0
  110. /package/dist/{chunk-7D6O46PF.js.map → chunk-BVF3AGJP.js.map} +0 -0
  111. /package/dist/{chunk-NTUNYIF7.js.map → chunk-I5GLV3VE.js.map} +0 -0
  112. /package/dist/{chunk-4H6DURG6.js.map → chunk-JA3AK3PT.js.map} +0 -0
  113. /package/dist/{chunk-2DM72JF3.js.map → chunk-KRBK4BQH.js.map} +0 -0
  114. /package/dist/{chunk-RCZRL5BE.js.map → chunk-MRILGULB.js.map} +0 -0
  115. /package/dist/{chunk-MVAOT247.js.map → chunk-QLLBRHAT.js.map} +0 -0
  116. /package/dist/{chunk-2WIPXV3Y.js.map → chunk-RR2PKP3I.js.map} +0 -0
  117. /package/dist/{chunk-3F24QTRI.js.map → chunk-SAZS2QZB.js.map} +0 -0
  118. /package/dist/{chunk-VYU7PXUS.js.map → chunk-SIC6U3GZ.js.map} +0 -0
  119. /package/dist/{chunk-6CB4E7ZV.js.map → chunk-UL2NNBUL.js.map} +0 -0
  120. /package/dist/{chunk-TFORLO3O.js.map → chunk-W6AQJ2PY.js.map} +0 -0
  121. /package/dist/{chunk-PUXCIHRL.js.map → chunk-XSZEP4SF.js.map} +0 -0
  122. /package/dist/{chunk-4CRG46BG.js.map → chunk-ZK7I7JYV.js.map} +0 -0
@@ -12,6 +12,7 @@ import { validateRequest, type SchemaName, type SchemaTypeFor } from "./access-s
12
12
  import {
13
13
  OFFLINE_SYNC_APPLY_MAX_BODY_BYTES,
14
14
  OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
15
+ OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES,
15
16
  } from "./offline-sync.js";
16
17
  import type { RecallDisclosure, RecallPlanMode } from "./types.js";
17
18
  import { isRecallDisclosure } from "./types.js";
@@ -125,6 +126,16 @@ function parseTrustZoneFilter(raw: string | null): TrustZoneName | undefined {
125
126
  throw new HttpError(400, "zone must be one of quarantine|working|trusted", "invalid_zone_filter");
126
127
  }
127
128
 
129
+ function summarizeHttpRequest(req: IncomingMessage): string {
130
+ const method = req.method ?? "UNKNOWN";
131
+ try {
132
+ const parsed = new URL(req.url ?? "/", "http://localhost");
133
+ return `${method} ${parsed.pathname}`;
134
+ } catch {
135
+ return `${method} ${(req.url ?? "/").split("?")[0]}`;
136
+ }
137
+ }
138
+
128
139
  /**
129
140
  * Decode a `:peerId` URL path segment, converting malformed percent-encoded
130
141
  * input (e.g., `%E0%A4%A`) into a 400 client error rather than letting
@@ -217,6 +228,10 @@ export class EngramAccessHttpServer {
217
228
  res.destroy(err as Error);
218
229
  return;
219
230
  }
231
+ log.error(
232
+ `engram access HTTP internal error [${correlationId}] ${summarizeHttpRequest(req)}`,
233
+ err,
234
+ );
220
235
  this.respondJson(res, 500, { error: "internal_error", code: "internal_error" });
221
236
  });
222
237
  });
@@ -630,6 +645,27 @@ export class EngramAccessHttpServer {
630
645
  return;
631
646
  }
632
647
 
648
+ if (
649
+ req.method === "POST" &&
650
+ (pathname === "/engram/v1/offline-sync/snapshot" || pathname === "/remnic/v1/offline-sync/snapshot")
651
+ ) {
652
+ const body = await this.readValidatedBody(
653
+ req,
654
+ "offlineSyncSnapshot",
655
+ OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES,
656
+ );
657
+ const result = await this.service.offlineSyncSnapshot({
658
+ namespace: this.resolveNamespace(req, body.namespace),
659
+ principal: this.resolveRequestPrincipal(req),
660
+ includeTranscripts: body.includeTranscripts,
661
+ includeContent: body.includeContent,
662
+ baseFiles: body.baseFiles,
663
+ ...(body.baseCapturedAt ? { baseCapturedAt: new Date(body.baseCapturedAt) } : {}),
664
+ });
665
+ this.respondJson(res, 200, result);
666
+ return;
667
+ }
668
+
633
669
  if (
634
670
  req.method === "POST" &&
635
671
  (pathname === "/engram/v1/offline-sync/files" || pathname === "/remnic/v1/offline-sync/files")
@@ -717,6 +753,7 @@ export class EngramAccessHttpServer {
717
753
  namespace: this.resolveNamespace(req, body.namespace),
718
754
  principal: this.resolveRequestPrincipal(req),
719
755
  changeset: body.changeset,
756
+ returnCurrentFiles: body.returnCurrentFiles,
720
757
  });
721
758
  this.respondJson(res, 200, result);
722
759
  return;
@@ -9,8 +9,12 @@ import {
9
9
  ACTION_CONFIDENCE_RULE_KINDS,
10
10
  } from "./action-confidence.js";
11
11
  import { isValidCapsuleSince } from "./transfer/capsule-export.js";
12
+ import { validateArchiveRelativePath } from "./transfer/fs-utils.js";
12
13
  import { CAPSULE_ID_PATTERN } from "./transfer/types.js";
13
- import { OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES } from "./offline-sync.js";
14
+ import {
15
+ OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
16
+ OFFLINE_SYNC_MAX_MTIME_MS,
17
+ } from "./offline-sync.js";
14
18
 
15
19
  // ---------------------------------------------------------------------------
16
20
  // Error formatting
@@ -369,10 +373,57 @@ export const capsuleListRequestSchema = z
369
373
  // Offline sync
370
374
  // ---------------------------------------------------------------------------
371
375
 
376
+ function isValidOfflineSyncPath(value: string): boolean {
377
+ try {
378
+ validateArchiveRelativePath(value, "path");
379
+ return true;
380
+ } catch {
381
+ return false;
382
+ }
383
+ }
384
+
385
+ const offlineSyncPathSchema = z
386
+ .string()
387
+ .trim()
388
+ .min(1, "path must be non-empty")
389
+ .max(4096)
390
+ .refine(
391
+ isValidOfflineSyncPath,
392
+ "path must be a POSIX relative path without unsafe segments",
393
+ );
394
+
395
+ const offlineSyncFileStateSchema = z.object({
396
+ path: offlineSyncPathSchema,
397
+ sha256: z.string().regex(/^[a-f0-9]{64}$/i, "sha256 must be a 64-character hex digest"),
398
+ bytes: z.number().int().min(0),
399
+ mtimeMs: z.number().finite().min(0).max(OFFLINE_SYNC_MAX_MTIME_MS),
400
+ });
401
+
402
+ const offlineSyncBaseCapturedAtSchema = z
403
+ .string()
404
+ .trim()
405
+ .min(1, "baseCapturedAt must be non-empty when provided")
406
+ .max(64)
407
+ .refine((value) => Number.isFinite(Date.parse(value)), {
408
+ message: "baseCapturedAt must be a valid ISO 8601 timestamp",
409
+ });
410
+
411
+ export const offlineSyncSnapshotRequestSchema = z.object({
412
+ namespace: namespaceSchema,
413
+ includeTranscripts: z.boolean().optional(),
414
+ includeContent: z.boolean().optional(),
415
+ baseCapturedAt: offlineSyncBaseCapturedAtSchema.optional(),
416
+ baseFiles: z
417
+ .array(offlineSyncFileStateSchema)
418
+ .max(300_000, "baseFiles must contain 300000 or fewer entries")
419
+ .optional(),
420
+ });
421
+
372
422
  export const offlineSyncApplyRequestSchema = z
373
423
  .object({
374
424
  namespace: namespaceSchema,
375
425
  changeset: z.unknown(),
426
+ returnCurrentFiles: z.boolean().optional(),
376
427
  })
377
428
  .refine((value) => value.changeset !== undefined && value.changeset !== null, {
378
429
  message: "changeset is required",
@@ -383,14 +434,14 @@ export const offlineSyncFilesRequestSchema = z.object({
383
434
  namespace: namespaceSchema,
384
435
  includeTranscripts: z.boolean().optional(),
385
436
  paths: z
386
- .array(z.string().trim().min(1, "path must be non-empty").max(4096))
437
+ .array(offlineSyncPathSchema)
387
438
  .max(5000, "paths must contain 5000 or fewer entries"),
388
439
  });
389
440
 
390
441
  export const offlineSyncFileContentRequestSchema = z.object({
391
442
  namespace: namespaceSchema,
392
443
  includeTranscripts: z.boolean().optional(),
393
- path: z.string().trim().min(1, "path must be non-empty").max(4096),
444
+ path: offlineSyncPathSchema,
394
445
  offset: z.number().int().min(0).optional(),
395
446
  length: z.number().int().min(1).max(OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES).optional(),
396
447
  });
@@ -461,6 +512,7 @@ export type CapsuleExportRequest = z.infer<typeof capsuleExportRequestSchema>;
461
512
  export type CapsuleImportRequest = z.infer<typeof capsuleImportRequestSchema>;
462
513
  export type CapsuleListRequest = z.infer<typeof capsuleListRequestSchema>;
463
514
  export type OfflineSyncApplyRequest = z.infer<typeof offlineSyncApplyRequestSchema>;
515
+ export type OfflineSyncSnapshotRequest = z.infer<typeof offlineSyncSnapshotRequestSchema>;
464
516
  export type OfflineSyncFilesRequest = z.infer<typeof offlineSyncFilesRequestSchema>;
465
517
  export type OfflineSyncFileContentRequest = z.infer<typeof offlineSyncFileContentRequestSchema>;
466
518
  export type ActionConfidenceRequest = z.infer<typeof actionConfidenceRequestSchema>;
@@ -486,6 +538,7 @@ export type SchemaName =
486
538
  | "capsuleExport"
487
539
  | "capsuleImport"
488
540
  | "capsuleList"
541
+ | "offlineSyncSnapshot"
489
542
  | "offlineSyncFiles"
490
543
  | "offlineSyncFileContent"
491
544
  | "offlineSyncApply"
@@ -508,6 +561,7 @@ export type SchemaTypeFor<N extends SchemaName> =
508
561
  : N extends "capsuleExport" ? CapsuleExportRequest
509
562
  : N extends "capsuleImport" ? CapsuleImportRequest
510
563
  : N extends "capsuleList" ? CapsuleListRequest
564
+ : N extends "offlineSyncSnapshot" ? OfflineSyncSnapshotRequest
511
565
  : N extends "offlineSyncFiles" ? OfflineSyncFilesRequest
512
566
  : N extends "offlineSyncFileContent" ? OfflineSyncFileContentRequest
513
567
  : N extends "offlineSyncApply" ? OfflineSyncApplyRequest
@@ -531,6 +585,7 @@ const schemas: Record<SchemaName, z.ZodTypeAny> = {
531
585
  capsuleExport: capsuleExportRequestSchema,
532
586
  capsuleImport: capsuleImportRequestSchema,
533
587
  capsuleList: capsuleListRequestSchema,
588
+ offlineSyncSnapshot: offlineSyncSnapshotRequestSchema,
534
589
  offlineSyncFiles: offlineSyncFilesRequestSchema,
535
590
  offlineSyncFileContent: offlineSyncFileContentRequestSchema,
536
591
  offlineSyncApply: offlineSyncApplyRequestSchema,
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtemp, rm, symlink } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdir, mkdtemp, readFile, rm, symlink, utimes, writeFile } from "node:fs/promises";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
  import test from "node:test";
@@ -184,3 +185,57 @@ test("offlineSyncFiles reports symlink requested paths as input errors", async (
184
185
  await rm(root, { recursive: true, force: true });
185
186
  }
186
187
  });
188
+
189
+ test("offlineSyncSnapshot ignores client capture time for server fast-base scans", async () => {
190
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-snapshot-client-clock-"));
191
+ try {
192
+ const relPath = "facts/a.md";
193
+ const filePath = path.join(root, relPath);
194
+ const oldContent = Buffer.from("alpha");
195
+ const newContent = Buffer.from("bravo");
196
+ const mtimeMs = 1_700_000_000_000;
197
+ await mkdir(path.dirname(filePath), { recursive: true });
198
+ await writeFile(filePath, oldContent);
199
+ await utimes(filePath, mtimeMs / 1000, mtimeMs / 1000);
200
+ const baseFile = {
201
+ path: relPath,
202
+ sha256: createHash("sha256").update(oldContent).digest("hex"),
203
+ bytes: oldContent.byteLength,
204
+ mtimeMs,
205
+ };
206
+ await writeFile(filePath, newContent);
207
+ await utimes(filePath, mtimeMs / 1000, mtimeMs / 1000);
208
+
209
+ const { service } = makeService();
210
+ (service as unknown as {
211
+ orchestrator: {
212
+ config: PluginConfig;
213
+ getStorage(namespace: string): Promise<StorageManager>;
214
+ };
215
+ }).orchestrator.getStorage = async () => ({
216
+ dir: root,
217
+ async readOfflineSyncFile(targetPath: string) {
218
+ return readFile(targetPath);
219
+ },
220
+ async digestOfflineSyncFile(targetPath: string) {
221
+ const content = await readFile(targetPath);
222
+ return {
223
+ sha256: createHash("sha256").update(content).digest("hex"),
224
+ bytes: content.byteLength,
225
+ };
226
+ },
227
+ } as unknown as StorageManager);
228
+
229
+ const snapshot = await service.offlineSyncSnapshot({
230
+ namespace: "team",
231
+ principal: "reader",
232
+ includeContent: false,
233
+ baseFiles: [baseFile],
234
+ baseCapturedAt: new Date(Date.now() + 60_000),
235
+ });
236
+
237
+ assert.equal(snapshot.files[0]?.sha256, createHash("sha256").update(newContent).digest("hex"));
238
+ } finally {
239
+ await rm(root, { recursive: true, force: true });
240
+ }
241
+ });
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
4
  import { EngramAccessInputError, EngramAccessService } from "./access-service.js";
5
+ import { OFFLINE_SYNC_MAX_MTIME_MS } from "./offline-sync.js";
5
6
 
6
7
  function createOfflineService(): EngramAccessService {
7
8
  return new EngramAccessService({
@@ -34,4 +35,20 @@ test("offline apply-file-content reports invalid metadata as input errors", asyn
34
35
  error instanceof EngramAccessInputError &&
35
36
  /sha256 must be a 64-character sha256/.test(error.message),
36
37
  );
38
+
39
+ await assert.rejects(
40
+ () => service.offlineSyncApplyFileContent({
41
+ includeTranscripts: true,
42
+ sourceId: "laptop",
43
+ path: "state/lcm.sqlite",
44
+ sha256: "a".repeat(64),
45
+ bytes: 0,
46
+ mtimeMs: OFFLINE_SYNC_MAX_MTIME_MS + 1,
47
+ offset: 0,
48
+ content: Buffer.alloc(0),
49
+ }),
50
+ (error) =>
51
+ error instanceof EngramAccessInputError &&
52
+ /mtimeMs must be within JavaScript Date range/.test(error.message),
53
+ );
37
54
  });
@@ -139,11 +139,13 @@ import {
139
139
  applyOfflineSyncFileContentChunk,
140
140
  applyOfflineSyncChangeset,
141
141
  buildOfflineSyncSnapshot,
142
+ buildOfflineSyncSnapshotFromBase,
142
143
  buildOfflineSyncSnapshotForPaths,
143
144
  readOfflineSyncFileContentChunk,
144
145
  type OfflineSyncApplyFileContentChunkResult,
145
146
  type OfflineSyncApplyChangesetResult,
146
147
  type OfflineSyncFileContentChunk,
148
+ type OfflineSyncFileState,
147
149
  type OfflineSyncSnapshot,
148
150
  } from "./offline-sync.js";
149
151
  import {
@@ -619,6 +621,8 @@ export interface EngramAccessOfflineSyncSnapshotRequest {
619
621
  principal?: string;
620
622
  includeTranscripts?: boolean;
621
623
  includeContent?: boolean;
624
+ baseCapturedAt?: Date;
625
+ baseFiles?: OfflineSyncFileState[];
622
626
  }
623
627
 
624
628
  export interface EngramAccessOfflineSyncFilesRequest {
@@ -655,6 +659,7 @@ export interface EngramAccessOfflineSyncApplyRequest {
655
659
  namespace?: string;
656
660
  principal?: string;
657
661
  changeset: unknown;
662
+ returnCurrentFiles?: boolean;
658
663
  }
659
664
 
660
665
  export interface EngramAccessOfflineSyncSnapshotResponse extends OfflineSyncSnapshot {
@@ -5602,12 +5607,19 @@ export class EngramAccessService {
5602
5607
  const resolvedNamespace = this.resolveReadableNamespace(options.namespace, options.principal);
5603
5608
  const storage = await this.orchestrator.getStorage(resolvedNamespace);
5604
5609
  const storageHash = createHash("sha256").update(storage.dir).digest("hex").slice(0, 16);
5605
- const snapshot = await buildOfflineSyncSnapshot({
5610
+ const snapshotBuilder = options.includeContent === false && options.baseFiles && options.baseFiles.length > 0
5611
+ ? buildOfflineSyncSnapshotFromBase
5612
+ : buildOfflineSyncSnapshot;
5613
+ const snapshot = await snapshotBuilder({
5606
5614
  root: storage.dir,
5607
5615
  sourceId: `remnic:${resolvedNamespace}:${storageHash}`,
5616
+ ...(options.baseFiles && options.baseFiles.length > 0 ? { baseFiles: options.baseFiles } : {}),
5617
+ // Client clocks are not authoritative for server-side ctime reuse. A
5618
+ // future client timestamp can hide same-size, preserved-mtime rewrites.
5608
5619
  includeContent: options.includeContent !== false,
5609
5620
  includeTranscripts: options.includeTranscripts !== false,
5610
5621
  readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
5622
+ readFileDigest: async ({ filePath }) => storage.digestOfflineSyncFile(filePath),
5611
5623
  });
5612
5624
  return {
5613
5625
  namespace: resolvedNamespace,
@@ -5702,6 +5714,7 @@ export class EngramAccessService {
5702
5714
  content: options.content,
5703
5715
  includeTranscripts: options.includeTranscripts !== false,
5704
5716
  readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
5717
+ readFileDigest: async ({ filePath }) => storage.digestOfflineSyncFile(filePath),
5705
5718
  writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
5706
5719
  writeStagingFile: async ({ filePath, content }) => storage.writeOfflineSyncStagingFile(filePath, content),
5707
5720
  writeFileChunks: async ({ filePath, chunks }) => storage.writeOfflineSyncFileChunks(filePath, chunks),
@@ -5743,7 +5756,9 @@ export class EngramAccessService {
5743
5756
  const result = await applyOfflineSyncChangeset({
5744
5757
  root: storage.dir,
5745
5758
  changeset: options.changeset,
5759
+ returnCurrentFiles: options.returnCurrentFiles,
5746
5760
  readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
5761
+ readFileDigest: async ({ filePath }) => storage.digestOfflineSyncFile(filePath),
5747
5762
  writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
5748
5763
  deleteFile: async ({ filePath }) => storage.deleteOfflineSyncFile(filePath),
5749
5764
  });
package/src/config.ts CHANGED
@@ -96,14 +96,14 @@ function parseBoundedPositiveInteger(
96
96
  }
97
97
 
98
98
  function parseQmdSupportedVersion(value: unknown): string {
99
- if (value === undefined || value === null) return "2.5.1";
99
+ if (value === undefined || value === null) return "2.5.3";
100
100
  if (typeof value !== "string") {
101
101
  throw new Error(`qmdSupportedVersion must be a semantic version string; got ${JSON.stringify(value)}`);
102
102
  }
103
103
  const normalized = value.trim();
104
104
  if (!/^\d+\.\d+\.\d+$/.test(normalized)) {
105
105
  throw new Error(
106
- `qmdSupportedVersion must be a semantic version string like "2.5.1"; got ${JSON.stringify(value)}`,
106
+ `qmdSupportedVersion must be a semantic version string like "2.5.3"; got ${JSON.stringify(value)}`,
107
107
  );
108
108
  }
109
109
  return normalized;
package/src/index.ts CHANGED
@@ -683,13 +683,16 @@ export {
683
683
  OFFLINE_SYNC_CHANGESET_FORMAT,
684
684
  OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
685
685
  OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES,
686
+ OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES,
686
687
  OFFLINE_SYNC_SNAPSHOT_FORMAT,
687
688
  OFFLINE_SYNC_STATE_VERSION,
688
689
  applyOfflineSyncFileContentChunk,
689
690
  applyOfflineSyncChangeset,
690
691
  applyOfflineSyncSnapshot,
691
692
  buildOfflineSyncChangeset,
693
+ buildOfflineSyncChangesetFromSnapshot,
692
694
  buildOfflineSyncSnapshot,
695
+ buildOfflineSyncSnapshotFromBase,
693
696
  buildOfflineSyncSnapshotForPaths,
694
697
  defaultOfflineSyncStatePath,
695
698
  fileStatesFromSnapshot,
@@ -698,8 +701,10 @@ export {
698
701
  offlineSyncStateFromSnapshot,
699
702
  readOfflineSyncFileContentChunk,
700
703
  readOfflineSyncState,
704
+ shouldPreferIncomingOfflineRuntimeFile,
701
705
  summarizeOfflineSyncChangeset,
702
706
  summarizeOfflineSyncPendingChanges,
707
+ summarizeOfflineSyncPendingFiles,
703
708
  writeOfflineSyncState,
704
709
  type OfflineSyncApplyFileContentChunkResult,
705
710
  type OfflineSyncApplyChangesetResult,
@@ -707,6 +712,7 @@ export {
707
712
  type OfflineSyncChange,
708
713
  type OfflineSyncChangeset,
709
714
  type OfflineSyncConflict,
715
+ type OfflineSyncFileDigest,
710
716
  type OfflineSyncFileRecord,
711
717
  type OfflineSyncFileContentChunk,
712
718
  type OfflineSyncFileState,