@remnic/core 1.1.13 → 1.1.15

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 (130) hide show
  1. package/dist/access-cli.js +34 -33
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.d.ts +2 -1
  4. package/dist/access-http.js +15 -14
  5. package/dist/access-mcp.d.ts +2 -1
  6. package/dist/access-mcp.js +14 -13
  7. package/dist/access-schema.d.ts +22 -5
  8. package/dist/access-schema.js +7 -5
  9. package/dist/{access-service-DcCDmNYC.d.ts → access-service-BCMine1s.d.ts} +21 -1
  10. package/dist/access-service.d.ts +2 -1
  11. package/dist/access-service.js +12 -11
  12. package/dist/briefing.js +4 -4
  13. package/dist/causal-consolidation.js +5 -5
  14. package/dist/{chunk-VNO6ZJ35.js → chunk-2PRLKQAH.js} +5 -5
  15. package/dist/{chunk-M23FSH32.js → chunk-5D2G67ZQ.js} +53 -6
  16. package/dist/chunk-5D2G67ZQ.js.map +1 -0
  17. package/dist/{chunk-EFJ3MQ4V.js → chunk-65HQPW6O.js} +2 -2
  18. package/dist/{chunk-GA454ALV.js → chunk-AAX3SUM3.js} +39 -39
  19. package/dist/{chunk-QQUAB63I.js → chunk-BEB4GUU5.js} +2 -2
  20. package/dist/{chunk-WZYKANL3.js → chunk-BNATB54A.js} +4 -4
  21. package/dist/{chunk-KUJVMMZQ.js → chunk-C7DGCHJE.js} +2 -2
  22. package/dist/{chunk-PR5FBTFU.js → chunk-CYFQJMUV.js} +5 -5
  23. package/dist/{chunk-KLAO5DGL.js → chunk-G7JBLD65.js} +3 -3
  24. package/dist/chunk-GSP6ZKOY.js +769 -0
  25. package/dist/chunk-GSP6ZKOY.js.map +1 -0
  26. package/dist/{chunk-CQZRLNMV.js → chunk-HJ2WMBFB.js} +42 -4
  27. package/dist/chunk-HJ2WMBFB.js.map +1 -0
  28. package/dist/{chunk-ME6ESPZU.js → chunk-IG5VGHYB.js} +2 -2
  29. package/dist/{chunk-7AAT6G4Q.js → chunk-IOAY54RF.js} +57 -5
  30. package/dist/chunk-IOAY54RF.js.map +1 -0
  31. package/dist/{chunk-XVZ7B3HG.js → chunk-JFEH2LZM.js} +2 -2
  32. package/dist/{chunk-JLFA7DQG.js → chunk-M3AA636B.js} +2 -2
  33. package/dist/{chunk-P4NEIHUT.js → chunk-MS3ULOZF.js} +2 -2
  34. package/dist/{chunk-7IASACLB.js → chunk-NOHC2L57.js} +2 -2
  35. package/dist/{chunk-6RVI47ZR.js → chunk-NTUNYIF7.js} +5 -5
  36. package/dist/{chunk-CK5NTM2S.js → chunk-OGROP7ZN.js} +2 -2
  37. package/dist/{chunk-MT25YHYH.js → chunk-OJRKZLZ4.js} +5 -5
  38. package/dist/{chunk-2F2W355T.js → chunk-QA2ZAPBU.js} +4 -4
  39. package/dist/{chunk-MC26UJIM.js → chunk-QLKBF3TI.js} +2 -2
  40. package/dist/{chunk-YNJHCGDT.js → chunk-SH5S7XYD.js} +8 -5
  41. package/dist/chunk-SH5S7XYD.js.map +1 -0
  42. package/dist/{chunk-VW676BEI.js → chunk-V7WH7DEM.js} +2 -2
  43. package/dist/{chunk-A2XUIMJ3.js → chunk-VWFIQOTJ.js} +11 -2
  44. package/dist/chunk-VWFIQOTJ.js.map +1 -0
  45. package/dist/{chunk-PU63GXWS.js → chunk-W7DK3CYM.js} +2 -2
  46. package/dist/{chunk-TFO23QT4.js → chunk-XKLD5OK4.js} +4 -4
  47. package/dist/{chunk-I5V2VDIW.js → chunk-YCVWX2NF.js} +2 -2
  48. package/dist/{chunk-UXHQAFNA.js → chunk-ZPXYWTN5.js} +4 -4
  49. package/dist/{chunk-CHEL3SKB.js → chunk-ZYRMKWVW.js} +27 -27
  50. package/dist/{chunk-GGKRUQOO.js → chunk-ZYVPLJ4T.js} +4 -4
  51. package/dist/{cli-D3VpkVwB.d.ts → cli-B71zQ6XK.d.ts} +1 -1
  52. package/dist/cli.d.ts +3 -2
  53. package/dist/cli.js +35 -34
  54. package/dist/compounding/engine.js +4 -4
  55. package/dist/connectors/codex-materialize-runner.js +4 -4
  56. package/dist/connectors/index.js +4 -4
  57. package/dist/conversation-index/backend.js +2 -2
  58. package/dist/entity-retrieval.js +4 -4
  59. package/dist/index.d.ts +4 -3
  60. package/dist/index.js +90 -58
  61. package/dist/index.js.map +1 -1
  62. package/dist/lcm/engine.js +2 -2
  63. package/dist/lcm/index.js +5 -5
  64. package/dist/maintenance/memory-governance.js +4 -4
  65. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
  66. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  67. package/dist/mcp-memory-inspector-app.d.ts +2 -1
  68. package/dist/namespaces/migrate.js +10 -10
  69. package/dist/namespaces/search.js +5 -5
  70. package/dist/namespaces/storage.js +4 -4
  71. package/dist/offline-sync.d.ts +136 -0
  72. package/dist/offline-sync.js +41 -0
  73. package/dist/offline-sync.js.map +1 -0
  74. package/dist/operator-toolkit.js +13 -13
  75. package/dist/orchestrator.js +24 -24
  76. package/dist/search/factory.js +4 -4
  77. package/dist/search/index.js +6 -6
  78. package/dist/secure-store/index.d.ts +1 -15
  79. package/dist/secure-store/index.js +2 -2
  80. package/dist/semantic-consolidation.js +5 -5
  81. package/dist/semantic-rule-promotion.js +4 -4
  82. package/dist/semantic-rule-verifier.js +4 -4
  83. package/dist/storage.d.ts +7 -0
  84. package/dist/storage.js +3 -3
  85. package/dist/transfer/backup.js +2 -2
  86. package/dist/transfer/capsule-export.js +4 -4
  87. package/dist/transfer/capsule-import.js +3 -3
  88. package/dist/transfer/import-sqlite.js +2 -2
  89. package/dist/verified-recall.js +4 -4
  90. package/package.json +1 -1
  91. package/src/access-http.test.ts +216 -0
  92. package/src/access-http.ts +54 -0
  93. package/src/access-schema.ts +18 -0
  94. package/src/access-service.ts +76 -0
  95. package/src/index.ts +33 -0
  96. package/src/offline-sync.test.ts +521 -0
  97. package/src/offline-sync.ts +998 -0
  98. package/src/qmd.test.ts +1 -0
  99. package/src/secure-store/secure-fs.ts +14 -7
  100. package/src/storage.ts +59 -0
  101. package/dist/chunk-7AAT6G4Q.js.map +0 -1
  102. package/dist/chunk-A2XUIMJ3.js.map +0 -1
  103. package/dist/chunk-CQZRLNMV.js.map +0 -1
  104. package/dist/chunk-M23FSH32.js.map +0 -1
  105. package/dist/chunk-YNJHCGDT.js.map +0 -1
  106. /package/dist/{chunk-VNO6ZJ35.js.map → chunk-2PRLKQAH.js.map} +0 -0
  107. /package/dist/{chunk-EFJ3MQ4V.js.map → chunk-65HQPW6O.js.map} +0 -0
  108. /package/dist/{chunk-GA454ALV.js.map → chunk-AAX3SUM3.js.map} +0 -0
  109. /package/dist/{chunk-QQUAB63I.js.map → chunk-BEB4GUU5.js.map} +0 -0
  110. /package/dist/{chunk-WZYKANL3.js.map → chunk-BNATB54A.js.map} +0 -0
  111. /package/dist/{chunk-KUJVMMZQ.js.map → chunk-C7DGCHJE.js.map} +0 -0
  112. /package/dist/{chunk-PR5FBTFU.js.map → chunk-CYFQJMUV.js.map} +0 -0
  113. /package/dist/{chunk-KLAO5DGL.js.map → chunk-G7JBLD65.js.map} +0 -0
  114. /package/dist/{chunk-ME6ESPZU.js.map → chunk-IG5VGHYB.js.map} +0 -0
  115. /package/dist/{chunk-XVZ7B3HG.js.map → chunk-JFEH2LZM.js.map} +0 -0
  116. /package/dist/{chunk-JLFA7DQG.js.map → chunk-M3AA636B.js.map} +0 -0
  117. /package/dist/{chunk-P4NEIHUT.js.map → chunk-MS3ULOZF.js.map} +0 -0
  118. /package/dist/{chunk-7IASACLB.js.map → chunk-NOHC2L57.js.map} +0 -0
  119. /package/dist/{chunk-6RVI47ZR.js.map → chunk-NTUNYIF7.js.map} +0 -0
  120. /package/dist/{chunk-CK5NTM2S.js.map → chunk-OGROP7ZN.js.map} +0 -0
  121. /package/dist/{chunk-MT25YHYH.js.map → chunk-OJRKZLZ4.js.map} +0 -0
  122. /package/dist/{chunk-2F2W355T.js.map → chunk-QA2ZAPBU.js.map} +0 -0
  123. /package/dist/{chunk-MC26UJIM.js.map → chunk-QLKBF3TI.js.map} +0 -0
  124. /package/dist/{chunk-VW676BEI.js.map → chunk-V7WH7DEM.js.map} +0 -0
  125. /package/dist/{chunk-PU63GXWS.js.map → chunk-W7DK3CYM.js.map} +0 -0
  126. /package/dist/{chunk-TFO23QT4.js.map → chunk-XKLD5OK4.js.map} +0 -0
  127. /package/dist/{chunk-I5V2VDIW.js.map → chunk-YCVWX2NF.js.map} +0 -0
  128. /package/dist/{chunk-UXHQAFNA.js.map → chunk-ZPXYWTN5.js.map} +0 -0
  129. /package/dist/{chunk-CHEL3SKB.js.map → chunk-ZYRMKWVW.js.map} +0 -0
  130. /package/dist/{chunk-GGKRUQOO.js.map → chunk-ZYVPLJ4T.js.map} +0 -0
@@ -13,7 +13,7 @@ import {
13
13
  runSecureStoreMigrate,
14
14
  runSecureStoreStatus,
15
15
  runSecureStoreUnlock
16
- } from "../chunk-CK5NTM2S.js";
16
+ } from "../chunk-OGROP7ZN.js";
17
17
  import {
18
18
  FILE_FORMAT_FLAGS,
19
19
  FILE_FORMAT_VERSION,
@@ -29,7 +29,7 @@ import {
29
29
  migrateMemoryDirToEncrypted,
30
30
  readMaybeEncryptedFile,
31
31
  writeMaybeEncryptedFile
32
- } from "../chunk-YNJHCGDT.js";
32
+ } from "../chunk-SH5S7XYD.js";
33
33
  import {
34
34
  HEADER_FILENAME,
35
35
  HEADER_FORMAT,
@@ -7,20 +7,20 @@ import {
7
7
  materializeAfterSemanticConsolidation,
8
8
  parseConsolidationResponse,
9
9
  parseOperatorAwareConsolidationResponse
10
- } from "./chunk-I5V2VDIW.js";
11
- import "./chunk-ME6ESPZU.js";
10
+ } from "./chunk-YCVWX2NF.js";
11
+ import "./chunk-IG5VGHYB.js";
12
12
  import "./chunk-RHY3HH7P.js";
13
13
  import {
14
14
  resolveExtensionsRoot
15
15
  } from "./chunk-EJI5XIBB.js";
16
16
  import "./chunk-3UXOZBHV.js";
17
17
  import "./chunk-U3PN77QT.js";
18
- import "./chunk-7AAT6G4Q.js";
18
+ import "./chunk-IOAY54RF.js";
19
19
  import "./chunk-5UZXUTVO.js";
20
- import "./chunk-YNJHCGDT.js";
21
- import "./chunk-P7FMDTKL.js";
20
+ import "./chunk-SH5S7XYD.js";
22
21
  import "./chunk-NN2DKE4T.js";
23
22
  import "./chunk-Q7P4WJDP.js";
23
+ import "./chunk-P7FMDTKL.js";
24
24
  import "./chunk-SCU65EZI.js";
25
25
  import "./chunk-3KW65B36.js";
26
26
  import "./chunk-3HPAPHUK.js";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  promoteSemanticRuleFromMemory
3
- } from "./chunk-XVZ7B3HG.js";
4
- import "./chunk-7AAT6G4Q.js";
3
+ } from "./chunk-JFEH2LZM.js";
4
+ import "./chunk-IOAY54RF.js";
5
5
  import "./chunk-5UZXUTVO.js";
6
- import "./chunk-YNJHCGDT.js";
7
- import "./chunk-P7FMDTKL.js";
6
+ import "./chunk-SH5S7XYD.js";
8
7
  import "./chunk-NN2DKE4T.js";
9
8
  import "./chunk-Q7P4WJDP.js";
9
+ import "./chunk-P7FMDTKL.js";
10
10
  import "./chunk-SCU65EZI.js";
11
11
  import "./chunk-3KW65B36.js";
12
12
  import "./chunk-3HPAPHUK.js";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  searchVerifiedSemanticRules
3
- } from "./chunk-MC26UJIM.js";
4
- import "./chunk-7AAT6G4Q.js";
3
+ } from "./chunk-QLKBF3TI.js";
4
+ import "./chunk-IOAY54RF.js";
5
5
  import "./chunk-5UZXUTVO.js";
6
- import "./chunk-YNJHCGDT.js";
7
- import "./chunk-P7FMDTKL.js";
6
+ import "./chunk-SH5S7XYD.js";
8
7
  import "./chunk-NN2DKE4T.js";
9
8
  import "./chunk-Q7P4WJDP.js";
9
+ import "./chunk-P7FMDTKL.js";
10
10
  import "./chunk-SCU65EZI.js";
11
11
  import "./chunk-3KW65B36.js";
12
12
  import "./chunk-3HPAPHUK.js";
package/dist/storage.d.ts CHANGED
@@ -50,6 +50,8 @@ declare class ContentHashIndex {
50
50
  /** Add content hash to the index. */
51
51
  add(content: string): void;
52
52
  get size(): number;
53
+ /** Clear all loaded hashes so the next save rewrites the index from scratch. */
54
+ clear(): void;
53
55
  /** Persist index to disk if changed. */
54
56
  save(): Promise<void>;
55
57
  /** Remove a hash from the index (used when archiving/deleting). */
@@ -230,6 +232,11 @@ declare class StorageManager {
230
232
  private get entitiesDir();
231
233
  private readStorageSecureFile;
232
234
  private writeStorageSecureFile;
235
+ private assertManagedStoragePath;
236
+ readOfflineSyncFile(filePath: string): Promise<Buffer>;
237
+ writeOfflineSyncFile(filePath: string, content: Buffer): Promise<void>;
238
+ deleteOfflineSyncFile(filePath: string): Promise<void>;
239
+ private invalidateAfterOfflineSyncMutation;
233
240
  createContentHashIndex(): ContentHashIndex;
234
241
  private appendStorageSecureFile;
235
242
  private appendStorageSecureFileUnlocked;
package/dist/storage.js CHANGED
@@ -8,12 +8,12 @@ import {
8
8
  normalizeEntityName,
9
9
  parseEntityFile,
10
10
  serializeEntityFile
11
- } from "./chunk-7AAT6G4Q.js";
11
+ } from "./chunk-IOAY54RF.js";
12
12
  import "./chunk-5UZXUTVO.js";
13
- import "./chunk-YNJHCGDT.js";
14
- import "./chunk-P7FMDTKL.js";
13
+ import "./chunk-SH5S7XYD.js";
15
14
  import "./chunk-NN2DKE4T.js";
16
15
  import "./chunk-Q7P4WJDP.js";
16
+ import "./chunk-P7FMDTKL.js";
17
17
  import "./chunk-SCU65EZI.js";
18
18
  import "./chunk-3KW65B36.js";
19
19
  import "./chunk-3HPAPHUK.js";
@@ -4,11 +4,11 @@ import {
4
4
  import "../chunk-6H2TESSP.js";
5
5
  import "../chunk-KNKUID7G.js";
6
6
  import "../chunk-J4EB7DNW.js";
7
- import "../chunk-I6K5FBRQ.js";
8
- import "../chunk-AGZQD76C.js";
9
7
  import "../chunk-BJMBJZ2Y.js";
10
8
  import "../chunk-UKJAGEXH.js";
11
9
  import "../chunk-FP2373TW.js";
10
+ import "../chunk-I6K5FBRQ.js";
11
+ import "../chunk-AGZQD76C.js";
12
12
  import "../chunk-A6XUJE5D.js";
13
13
  import "../chunk-PZ5AY32C.js";
14
14
  export {
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  exportCapsule,
3
3
  isValidCapsuleSince
4
- } from "../chunk-2F2W355T.js";
5
- import "../chunk-KNKUID7G.js";
4
+ } from "../chunk-QA2ZAPBU.js";
6
5
  import "../chunk-WEHSQBFR.js";
6
+ import "../chunk-KNKUID7G.js";
7
7
  import "../chunk-J4EB7DNW.js";
8
- import "../chunk-I6K5FBRQ.js";
9
- import "../chunk-AGZQD76C.js";
10
8
  import "../chunk-BJMBJZ2Y.js";
11
9
  import "../chunk-UKJAGEXH.js";
12
10
  import "../chunk-FP2373TW.js";
11
+ import "../chunk-I6K5FBRQ.js";
12
+ import "../chunk-AGZQD76C.js";
13
13
  import "../chunk-A6XUJE5D.js";
14
14
  import "../chunk-PZ5AY32C.js";
15
15
  export {
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  importCapsule
3
- } from "../chunk-GGKRUQOO.js";
3
+ } from "../chunk-ZYVPLJ4T.js";
4
4
  import "../chunk-FAAFWE4G.js";
5
- import "../chunk-KNKUID7G.js";
6
5
  import "../chunk-WEHSQBFR.js";
7
- import "../chunk-AGZQD76C.js";
6
+ import "../chunk-KNKUID7G.js";
8
7
  import "../chunk-BJMBJZ2Y.js";
9
8
  import "../chunk-UKJAGEXH.js";
10
9
  import "../chunk-FP2373TW.js";
10
+ import "../chunk-AGZQD76C.js";
11
11
  import "../chunk-A6XUJE5D.js";
12
12
  import "../chunk-PZ5AY32C.js";
13
13
  export {
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  importSqlite
3
- } from "../chunk-TFO23QT4.js";
4
- import "../chunk-Z734BLO3.js";
3
+ } from "../chunk-XKLD5OK4.js";
5
4
  import "../chunk-3JXBXXM2.js";
5
+ import "../chunk-Z734BLO3.js";
6
6
  import "../chunk-3HPAPHUK.js";
7
7
  import "../chunk-AGZQD76C.js";
8
8
  import "../chunk-PZ5AY32C.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  searchVerifiedEpisodes
3
- } from "./chunk-P4NEIHUT.js";
3
+ } from "./chunk-MS3ULOZF.js";
4
4
  import "./chunk-NZL6GGQE.js";
5
- import "./chunk-7AAT6G4Q.js";
5
+ import "./chunk-IOAY54RF.js";
6
6
  import "./chunk-5UZXUTVO.js";
7
- import "./chunk-YNJHCGDT.js";
8
- import "./chunk-P7FMDTKL.js";
7
+ import "./chunk-SH5S7XYD.js";
9
8
  import "./chunk-NN2DKE4T.js";
10
9
  import "./chunk-Q7P4WJDP.js";
10
+ import "./chunk-P7FMDTKL.js";
11
11
  import "./chunk-SCU65EZI.js";
12
12
  import "./chunk-3KW65B36.js";
13
13
  import "./chunk-3HPAPHUK.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -223,3 +223,219 @@ test("HTTP review show hides namespace denial as pair_not_found", async () => {
223
223
  await rm(dir, { recursive: true, force: true });
224
224
  }
225
225
  });
226
+
227
+ test("HTTP offline snapshot forwards namespace and transfer options", async () => {
228
+ const calls: Array<{
229
+ namespace: string | undefined;
230
+ principal: string | undefined;
231
+ includeTranscripts: boolean | undefined;
232
+ includeContent: boolean | undefined;
233
+ }> = [];
234
+ const service = {
235
+ offlineSyncSnapshot: async (options: {
236
+ namespace?: string;
237
+ principal?: string;
238
+ includeTranscripts?: boolean;
239
+ includeContent?: boolean;
240
+ }) => {
241
+ calls.push({
242
+ namespace: options.namespace,
243
+ principal: options.principal,
244
+ includeTranscripts: options.includeTranscripts,
245
+ includeContent: options.includeContent,
246
+ });
247
+ return {
248
+ namespace: options.namespace ?? "default",
249
+ format: "remnic.offline-sync.snapshot.v1",
250
+ schemaVersion: 1,
251
+ createdAt: new Date("2026-05-21T00:00:00Z").toISOString(),
252
+ sourceId: "remote:test",
253
+ includeTranscripts: options.includeTranscripts !== false,
254
+ files: [],
255
+ };
256
+ },
257
+ } as unknown as EngramAccessService;
258
+ const server = new EngramAccessHttpServer({
259
+ service,
260
+ port: 0,
261
+ authToken: "test-token",
262
+ principal: "reader",
263
+ adminConsoleEnabled: false,
264
+ });
265
+
266
+ const status = await server.start();
267
+ try {
268
+ const response = await fetch(
269
+ `http://127.0.0.1:${status.port}/remnic/v1/offline-sync/snapshot?namespace=team&include_transcripts=false&content=false`,
270
+ { headers: { authorization: "Bearer test-token" } },
271
+ );
272
+ const body = await response.json() as { namespace?: string; includeTranscripts?: boolean; files?: unknown[] };
273
+
274
+ assert.equal(response.status, 200);
275
+ assert.equal(body.namespace, "team");
276
+ assert.equal(body.includeTranscripts, false);
277
+ assert.deepEqual(body.files, []);
278
+ assert.deepEqual(calls, [{
279
+ namespace: "team",
280
+ principal: "reader",
281
+ includeTranscripts: false,
282
+ includeContent: false,
283
+ }]);
284
+ } finally {
285
+ await server.stop();
286
+ }
287
+ });
288
+
289
+ test("HTTP offline snapshot rejects invalid boolean query values", async () => {
290
+ let calls = 0;
291
+ const service = {
292
+ offlineSyncSnapshot: async () => {
293
+ calls += 1;
294
+ return {};
295
+ },
296
+ } as unknown as EngramAccessService;
297
+ const server = new EngramAccessHttpServer({
298
+ service,
299
+ port: 0,
300
+ authToken: "test-token",
301
+ principal: "reader",
302
+ adminConsoleEnabled: false,
303
+ });
304
+
305
+ const status = await server.start();
306
+ try {
307
+ const response = await fetch(
308
+ `http://127.0.0.1:${status.port}/engram/v1/offline-sync/snapshot?include_transcripts=maybe`,
309
+ { headers: { authorization: "Bearer test-token" } },
310
+ );
311
+ const body = await response.json() as { error?: string; code?: string };
312
+
313
+ assert.equal(response.status, 400);
314
+ assert.match(body.error ?? "", /include_transcripts must be one of: true, false/);
315
+ assert.equal(body.code, "input_error");
316
+ assert.equal(calls, 0);
317
+ } finally {
318
+ await server.stop();
319
+ }
320
+ });
321
+
322
+ test("HTTP offline apply validates and forwards changesets", async () => {
323
+ const calls: Array<{
324
+ namespace: string | undefined;
325
+ principal: string | undefined;
326
+ changeset: unknown;
327
+ }> = [];
328
+ const changeset = {
329
+ format: "remnic.offline-sync.changeset.v1",
330
+ schemaVersion: 1,
331
+ createdAt: new Date("2026-05-21T00:00:00Z").toISOString(),
332
+ sourceId: "laptop:test",
333
+ includeTranscripts: true,
334
+ changes: [],
335
+ };
336
+ const service = {
337
+ offlineSyncApply: async (options: {
338
+ namespace?: string;
339
+ principal?: string;
340
+ changeset: unknown;
341
+ }) => {
342
+ calls.push({
343
+ namespace: options.namespace,
344
+ principal: options.principal,
345
+ changeset: options.changeset,
346
+ });
347
+ return {
348
+ namespace: options.namespace ?? "default",
349
+ appliedUpserts: 0,
350
+ appliedDeletes: 0,
351
+ skipped: 0,
352
+ conflicts: [],
353
+ currentFiles: [],
354
+ };
355
+ },
356
+ } as unknown as EngramAccessService;
357
+ const server = new EngramAccessHttpServer({
358
+ service,
359
+ port: 0,
360
+ authToken: "test-token",
361
+ principal: "writer",
362
+ adminConsoleEnabled: false,
363
+ });
364
+
365
+ const status = await server.start();
366
+ try {
367
+ const response = await fetch(`http://127.0.0.1:${status.port}/remnic/v1/offline-sync/apply`, {
368
+ method: "POST",
369
+ headers: {
370
+ authorization: "Bearer test-token",
371
+ "content-type": "application/json",
372
+ },
373
+ body: JSON.stringify({ namespace: "team", changeset }),
374
+ });
375
+ const body = await response.json() as { namespace?: string; appliedUpserts?: number };
376
+
377
+ assert.equal(response.status, 200);
378
+ assert.equal(body.namespace, "team");
379
+ assert.equal(body.appliedUpserts, 0);
380
+ assert.deepEqual(calls, [{
381
+ namespace: "team",
382
+ principal: "writer",
383
+ changeset,
384
+ }]);
385
+ } finally {
386
+ await server.stop();
387
+ }
388
+ });
389
+
390
+ test("HTTP offline apply requires a changeset", async () => {
391
+ let calls = 0;
392
+ const service = {
393
+ offlineSyncApply: async () => {
394
+ calls += 1;
395
+ return {};
396
+ },
397
+ } as unknown as EngramAccessService;
398
+ const server = new EngramAccessHttpServer({
399
+ service,
400
+ port: 0,
401
+ authToken: "test-token",
402
+ principal: "writer",
403
+ adminConsoleEnabled: false,
404
+ });
405
+
406
+ const status = await server.start();
407
+ try {
408
+ const response = await fetch(`http://127.0.0.1:${status.port}/engram/v1/offline-sync/apply`, {
409
+ method: "POST",
410
+ headers: {
411
+ authorization: "Bearer test-token",
412
+ "content-type": "application/json",
413
+ },
414
+ body: JSON.stringify({ namespace: "team" }),
415
+ });
416
+ const body = await response.json() as { code?: string; details?: Array<{ field?: string; message?: string }> };
417
+
418
+ assert.equal(response.status, 400);
419
+ assert.equal(body.code, "validation_error");
420
+ assert.equal(body.details?.[0]?.field, "changeset");
421
+ assert.equal(calls, 0);
422
+
423
+ const nullResponse = await fetch(`http://127.0.0.1:${status.port}/engram/v1/offline-sync/apply`, {
424
+ method: "POST",
425
+ headers: {
426
+ authorization: "Bearer test-token",
427
+ "content-type": "application/json",
428
+ },
429
+ body: JSON.stringify({ namespace: "team", changeset: null }),
430
+ });
431
+ const nullBody = await nullResponse.json() as { code?: string; details?: Array<{ field?: string; message?: string }> };
432
+
433
+ assert.equal(nullResponse.status, 400);
434
+ assert.equal(nullBody.code, "validation_error");
435
+ assert.equal(nullBody.details?.[0]?.field, "changeset");
436
+ assert.equal(nullBody.details?.[0]?.message, "changeset is required");
437
+ assert.equal(calls, 0);
438
+ } finally {
439
+ await server.stop();
440
+ }
441
+ });
@@ -588,6 +588,60 @@ export class EngramAccessHttpServer {
588
588
  return;
589
589
  }
590
590
 
591
+ if (
592
+ req.method === "GET" &&
593
+ (pathname === "/engram/v1/offline-sync/snapshot" || pathname === "/remnic/v1/offline-sync/snapshot")
594
+ ) {
595
+ const includeTranscriptsRaw = parsed.searchParams.get("include_transcripts");
596
+ const includeContentRaw = parsed.searchParams.get("content");
597
+ if (
598
+ includeTranscriptsRaw !== null &&
599
+ includeTranscriptsRaw !== "true" &&
600
+ includeTranscriptsRaw !== "false"
601
+ ) {
602
+ throw new EngramAccessInputError(
603
+ `include_transcripts must be one of: true, false (got: ${includeTranscriptsRaw})`,
604
+ );
605
+ }
606
+ if (
607
+ includeContentRaw !== null &&
608
+ includeContentRaw !== "true" &&
609
+ includeContentRaw !== "false"
610
+ ) {
611
+ throw new EngramAccessInputError(
612
+ `content must be one of: true, false (got: ${includeContentRaw})`,
613
+ );
614
+ }
615
+ const namespaceParam = parsed.searchParams.get("namespace");
616
+ const result = await this.service.offlineSyncSnapshot({
617
+ namespace: this.resolveNamespace(
618
+ req,
619
+ namespaceParam && namespaceParam.length > 0 ? namespaceParam : undefined,
620
+ ),
621
+ principal: this.resolveRequestPrincipal(req),
622
+ includeTranscripts: includeTranscriptsRaw !== "false",
623
+ includeContent: includeContentRaw !== "false",
624
+ });
625
+ this.respondJson(res, 200, result);
626
+ return;
627
+ }
628
+
629
+ if (
630
+ req.method === "POST" &&
631
+ (pathname === "/engram/v1/offline-sync/apply" || pathname === "/remnic/v1/offline-sync/apply")
632
+ ) {
633
+ const body = await this.readValidatedBody(req, "offlineSyncApply");
634
+ this.ensureWriteRateLimitAvailable();
635
+ const result = await this.service.offlineSyncApply({
636
+ namespace: this.resolveNamespace(req, body.namespace),
637
+ principal: this.resolveRequestPrincipal(req),
638
+ changeset: body.changeset,
639
+ });
640
+ this.recordWriteRateLimitHit();
641
+ this.respondJson(res, 200, result);
642
+ return;
643
+ }
644
+
591
645
  if (req.method === "POST" && pathname === "/engram/v1/recall/explain") {
592
646
  const body = await this.readValidatedBody(req, "recallExplain");
593
647
  const response = await this.service.recallExplain({
@@ -364,6 +364,20 @@ export const capsuleListRequestSchema = z
364
364
  namespace: namespaceSchema,
365
365
  });
366
366
 
367
+ // ---------------------------------------------------------------------------
368
+ // Offline sync
369
+ // ---------------------------------------------------------------------------
370
+
371
+ export const offlineSyncApplyRequestSchema = z
372
+ .object({
373
+ namespace: namespaceSchema,
374
+ changeset: z.unknown(),
375
+ })
376
+ .refine((value) => value.changeset !== undefined && value.changeset !== null, {
377
+ message: "changeset is required",
378
+ path: ["changeset"],
379
+ });
380
+
367
381
  // ---------------------------------------------------------------------------
368
382
  // Action confidence
369
383
  // ---------------------------------------------------------------------------
@@ -429,6 +443,7 @@ export type DaySummaryRequest = z.infer<typeof daySummaryRequestSchema>;
429
443
  export type CapsuleExportRequest = z.infer<typeof capsuleExportRequestSchema>;
430
444
  export type CapsuleImportRequest = z.infer<typeof capsuleImportRequestSchema>;
431
445
  export type CapsuleListRequest = z.infer<typeof capsuleListRequestSchema>;
446
+ export type OfflineSyncApplyRequest = z.infer<typeof offlineSyncApplyRequestSchema>;
432
447
  export type ActionConfidenceRequest = z.infer<typeof actionConfidenceRequestSchema>;
433
448
 
434
449
  // ---------------------------------------------------------------------------
@@ -452,6 +467,7 @@ export type SchemaName =
452
467
  | "capsuleExport"
453
468
  | "capsuleImport"
454
469
  | "capsuleList"
470
+ | "offlineSyncApply"
455
471
  | "actionConfidence";
456
472
 
457
473
  export type SchemaTypeFor<N extends SchemaName> =
@@ -471,6 +487,7 @@ export type SchemaTypeFor<N extends SchemaName> =
471
487
  : N extends "capsuleExport" ? CapsuleExportRequest
472
488
  : N extends "capsuleImport" ? CapsuleImportRequest
473
489
  : N extends "capsuleList" ? CapsuleListRequest
490
+ : N extends "offlineSyncApply" ? OfflineSyncApplyRequest
474
491
  : N extends "actionConfidence" ? ActionConfidenceRequest
475
492
  : never;
476
493
 
@@ -491,6 +508,7 @@ const schemas: Record<SchemaName, z.ZodTypeAny> = {
491
508
  capsuleExport: capsuleExportRequestSchema,
492
509
  capsuleImport: capsuleImportRequestSchema,
493
510
  capsuleList: capsuleListRequestSchema,
511
+ offlineSyncApply: offlineSyncApplyRequestSchema,
494
512
  actionConfidence: actionConfidenceRequestSchema,
495
513
  };
496
514
 
@@ -135,6 +135,12 @@ import {
135
135
  defaultCapsulesDir,
136
136
  type CapsuleListEntry,
137
137
  } from "./capsule-cli.js";
138
+ import {
139
+ applyOfflineSyncChangeset,
140
+ buildOfflineSyncSnapshot,
141
+ type OfflineSyncApplyChangesetResult,
142
+ type OfflineSyncSnapshot,
143
+ } from "./offline-sync.js";
138
144
  import {
139
145
  evaluateActionConfidence,
140
146
  type ActionConfidenceInput,
@@ -603,6 +609,27 @@ export interface EngramAccessCapsuleListResponse {
603
609
  capsules: CapsuleListEntry[];
604
610
  }
605
611
 
612
+ export interface EngramAccessOfflineSyncSnapshotRequest {
613
+ namespace?: string;
614
+ principal?: string;
615
+ includeTranscripts?: boolean;
616
+ includeContent?: boolean;
617
+ }
618
+
619
+ export interface EngramAccessOfflineSyncApplyRequest {
620
+ namespace?: string;
621
+ principal?: string;
622
+ changeset: unknown;
623
+ }
624
+
625
+ export interface EngramAccessOfflineSyncSnapshotResponse extends OfflineSyncSnapshot {
626
+ namespace: string;
627
+ }
628
+
629
+ export interface EngramAccessOfflineSyncApplyResponse extends OfflineSyncApplyChangesetResult {
630
+ namespace: string;
631
+ }
632
+
606
633
  export type EngramAccessActionConfidenceRequest = ActionConfidenceInput;
607
634
  export type EngramAccessActionConfidenceResponse = ActionConfidenceResult;
608
635
 
@@ -5522,6 +5549,55 @@ export class EngramAccessService {
5522
5549
  return { namespace: resolvedNamespace, capsulesDir, capsules };
5523
5550
  }
5524
5551
 
5552
+ async offlineSyncSnapshot(
5553
+ options: EngramAccessOfflineSyncSnapshotRequest = {},
5554
+ ): Promise<EngramAccessOfflineSyncSnapshotResponse> {
5555
+ const resolvedNamespace = this.resolveReadableNamespace(options.namespace, options.principal);
5556
+ const storage = await this.orchestrator.getStorage(resolvedNamespace);
5557
+ const storageHash = createHash("sha256").update(storage.dir).digest("hex").slice(0, 16);
5558
+ const snapshot = await buildOfflineSyncSnapshot({
5559
+ root: storage.dir,
5560
+ sourceId: `remnic:${resolvedNamespace}:${storageHash}`,
5561
+ includeContent: options.includeContent !== false,
5562
+ includeTranscripts: options.includeTranscripts !== false,
5563
+ readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
5564
+ });
5565
+ return {
5566
+ namespace: resolvedNamespace,
5567
+ ...snapshot,
5568
+ };
5569
+ }
5570
+
5571
+ async offlineSyncApply(
5572
+ options: EngramAccessOfflineSyncApplyRequest,
5573
+ ): Promise<EngramAccessOfflineSyncApplyResponse> {
5574
+ const resolvedNamespace = this.resolveWritableNamespace(
5575
+ options.namespace,
5576
+ undefined,
5577
+ options.principal,
5578
+ );
5579
+ const storage = await this.orchestrator.getStorage(resolvedNamespace);
5580
+ try {
5581
+ const result = await applyOfflineSyncChangeset({
5582
+ root: storage.dir,
5583
+ changeset: options.changeset,
5584
+ readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
5585
+ writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
5586
+ deleteFile: async ({ filePath }) => storage.deleteOfflineSyncFile(filePath),
5587
+ });
5588
+ return {
5589
+ namespace: resolvedNamespace,
5590
+ ...result,
5591
+ };
5592
+ } catch (error) {
5593
+ const message = error instanceof Error ? error.message : String(error);
5594
+ if (message.startsWith("offline sync")) {
5595
+ throw new EngramAccessInputError(message);
5596
+ }
5597
+ throw error;
5598
+ }
5599
+ }
5600
+
5525
5601
  // ── Dreams pipeline telemetry surfaces (issue #678 PR 3+4) ──────────────
5526
5602
 
5527
5603
  /**
package/src/index.ts CHANGED
@@ -674,6 +674,39 @@ export {
674
674
  type SyncState,
675
675
  } from "./sync/index.js";
676
676
 
677
+ // ---------------------------------------------------------------------------
678
+ // Offline Sync
679
+ // ---------------------------------------------------------------------------
680
+
681
+ export {
682
+ OFFLINE_SYNC_CHANGESET_FORMAT,
683
+ OFFLINE_SYNC_SNAPSHOT_FORMAT,
684
+ OFFLINE_SYNC_STATE_VERSION,
685
+ applyOfflineSyncChangeset,
686
+ applyOfflineSyncSnapshot,
687
+ buildOfflineSyncChangeset,
688
+ buildOfflineSyncSnapshot,
689
+ defaultOfflineSyncStatePath,
690
+ fileStatesFromSnapshot,
691
+ normalizeOfflineSyncChangeset,
692
+ normalizeOfflineSyncSnapshot,
693
+ offlineSyncStateFromSnapshot,
694
+ readOfflineSyncState,
695
+ summarizeOfflineSyncChangeset,
696
+ writeOfflineSyncState,
697
+ type OfflineSyncApplyChangesetResult,
698
+ type OfflineSyncApplySnapshotResult,
699
+ type OfflineSyncChange,
700
+ type OfflineSyncChangeset,
701
+ type OfflineSyncConflict,
702
+ type OfflineSyncFileRecord,
703
+ type OfflineSyncFileState,
704
+ type OfflineSyncFileTarget,
705
+ type OfflineSyncFileWriteTarget,
706
+ type OfflineSyncSnapshot,
707
+ type OfflineSyncState,
708
+ } from "./offline-sync.js";
709
+
677
710
  // ---------------------------------------------------------------------------
678
711
  // Memory Extension Host (#382)
679
712
  // ---------------------------------------------------------------------------