@remnic/core 1.1.22 → 1.1.24

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 (103) hide show
  1. package/dist/access-cli.js +15 -15
  2. package/dist/access-http.d.ts +9 -1
  3. package/dist/access-http.js +9 -9
  4. package/dist/access-mcp.d.ts +1 -1
  5. package/dist/access-mcp.js +8 -8
  6. package/dist/access-schema.js +3 -3
  7. package/dist/{access-service-DT9L2DW4.d.ts → access-service-CEyV8XJ5.d.ts} +19 -2
  8. package/dist/access-service.d.ts +1 -1
  9. package/dist/access-service.js +6 -6
  10. package/dist/briefing.js +3 -3
  11. package/dist/causal-consolidation.js +4 -4
  12. package/dist/{chunk-YO3AZEE5.js → chunk-25YQM6XW.js} +3 -3
  13. package/dist/{chunk-LDJANWTK.js → chunk-2DM72JF3.js} +12 -12
  14. package/dist/{chunk-TLM762GT.js → chunk-2WIPXV3Y.js} +2 -2
  15. package/dist/{chunk-QOHBYVZG.js → chunk-3F24QTRI.js} +2 -2
  16. package/dist/{chunk-5IQC4OG6.js → chunk-4H6DURG6.js} +2 -2
  17. package/dist/{chunk-26OQECWH.js → chunk-6CB4E7ZV.js} +4 -4
  18. package/dist/{chunk-NOQ74SJN.js → chunk-7D6O46PF.js} +2 -2
  19. package/dist/{chunk-FF46Q3SN.js → chunk-AMVN77EU.js} +360 -32
  20. package/dist/chunk-AMVN77EU.js.map +1 -0
  21. package/dist/{chunk-7Q2P774N.js → chunk-F33CJ5CH.js} +13 -3
  22. package/dist/chunk-F33CJ5CH.js.map +1 -0
  23. package/dist/{chunk-FSODDMR2.js → chunk-IANK6Y5W.js} +2 -2
  24. package/dist/{chunk-UA6OCL6S.js → chunk-JUYT2J3K.js} +106 -11
  25. package/dist/chunk-JUYT2J3K.js.map +1 -0
  26. package/dist/{chunk-NGPO6S3M.js → chunk-LCTP7YRU.js} +42 -5
  27. package/dist/chunk-LCTP7YRU.js.map +1 -0
  28. package/dist/{chunk-GGCJ253V.js → chunk-MVAOT247.js} +8 -8
  29. package/dist/{chunk-SH5S7XYD.js → chunk-MXFBBHJU.js} +72 -2
  30. package/dist/chunk-MXFBBHJU.js.map +1 -0
  31. package/dist/{chunk-VMQRBXJ5.js → chunk-NW7JW5GA.js} +2 -2
  32. package/dist/{chunk-SZKCBLS5.js → chunk-PUXCIHRL.js} +2 -2
  33. package/dist/{chunk-2IRT26RZ.js → chunk-QYHQ2JHL.js} +2 -2
  34. package/dist/{chunk-CN4P6SVA.js → chunk-RCZRL5BE.js} +2 -2
  35. package/dist/{chunk-SGIXDVSF.js → chunk-S27EXIHY.js} +2 -2
  36. package/dist/{chunk-5ML4TH3E.js → chunk-TFORLO3O.js} +4 -4
  37. package/dist/{chunk-TOFUTKQN.js → chunk-TR4DK5OH.js} +2 -2
  38. package/dist/{chunk-6ORWKANA.js → chunk-VYU7PXUS.js} +2 -2
  39. package/dist/{chunk-FFU4GMST.js → chunk-WNARATI3.js} +2 -2
  40. package/dist/{chunk-KSFBM6TV.js → chunk-YITUHONZ.js} +2 -2
  41. package/dist/{cli-BN0CkYzI.d.ts → cli-BguVmIwO.d.ts} +1 -1
  42. package/dist/cli.d.ts +2 -2
  43. package/dist/cli.js +18 -18
  44. package/dist/compounding/engine.js +3 -3
  45. package/dist/connectors/codex-materialize-runner.js +3 -3
  46. package/dist/connectors/index.js +3 -3
  47. package/dist/entity-retrieval.js +3 -3
  48. package/dist/index.d.ts +4 -4
  49. package/dist/index.js +30 -24
  50. package/dist/index.js.map +1 -1
  51. package/dist/maintenance/memory-governance.js +3 -3
  52. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  53. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  54. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  55. package/dist/namespaces/migrate.js +4 -4
  56. package/dist/namespaces/storage.js +3 -3
  57. package/dist/offline-sync.d.ts +38 -1
  58. package/dist/offline-sync.js +8 -2
  59. package/dist/operator-toolkit.js +6 -6
  60. package/dist/orchestrator.js +11 -11
  61. package/dist/schemas.d.ts +22 -22
  62. package/dist/secure-store/index.js +2 -2
  63. package/dist/semantic-consolidation.js +4 -4
  64. package/dist/semantic-rule-promotion.js +3 -3
  65. package/dist/semantic-rule-verifier.js +3 -3
  66. package/dist/storage.d.ts +2 -0
  67. package/dist/storage.js +2 -2
  68. package/dist/transfer/types.d.ts +12 -12
  69. package/dist/verified-recall.js +3 -3
  70. package/package.json +1 -1
  71. package/src/access-http.test.ts +239 -0
  72. package/src/access-http.ts +128 -7
  73. package/src/access-service-offline-file-content.test.ts +37 -0
  74. package/src/access-service.ts +70 -0
  75. package/src/index.ts +4 -0
  76. package/src/offline-sync.test.ts +395 -79
  77. package/src/offline-sync.ts +473 -32
  78. package/src/secure-store/secure-fs.ts +84 -3
  79. package/src/storage.ts +12 -0
  80. package/dist/chunk-7Q2P774N.js.map +0 -1
  81. package/dist/chunk-FF46Q3SN.js.map +0 -1
  82. package/dist/chunk-NGPO6S3M.js.map +0 -1
  83. package/dist/chunk-SH5S7XYD.js.map +0 -1
  84. package/dist/chunk-UA6OCL6S.js.map +0 -1
  85. /package/dist/{chunk-YO3AZEE5.js.map → chunk-25YQM6XW.js.map} +0 -0
  86. /package/dist/{chunk-LDJANWTK.js.map → chunk-2DM72JF3.js.map} +0 -0
  87. /package/dist/{chunk-TLM762GT.js.map → chunk-2WIPXV3Y.js.map} +0 -0
  88. /package/dist/{chunk-QOHBYVZG.js.map → chunk-3F24QTRI.js.map} +0 -0
  89. /package/dist/{chunk-5IQC4OG6.js.map → chunk-4H6DURG6.js.map} +0 -0
  90. /package/dist/{chunk-26OQECWH.js.map → chunk-6CB4E7ZV.js.map} +0 -0
  91. /package/dist/{chunk-NOQ74SJN.js.map → chunk-7D6O46PF.js.map} +0 -0
  92. /package/dist/{chunk-FSODDMR2.js.map → chunk-IANK6Y5W.js.map} +0 -0
  93. /package/dist/{chunk-GGCJ253V.js.map → chunk-MVAOT247.js.map} +0 -0
  94. /package/dist/{chunk-VMQRBXJ5.js.map → chunk-NW7JW5GA.js.map} +0 -0
  95. /package/dist/{chunk-SZKCBLS5.js.map → chunk-PUXCIHRL.js.map} +0 -0
  96. /package/dist/{chunk-2IRT26RZ.js.map → chunk-QYHQ2JHL.js.map} +0 -0
  97. /package/dist/{chunk-CN4P6SVA.js.map → chunk-RCZRL5BE.js.map} +0 -0
  98. /package/dist/{chunk-SGIXDVSF.js.map → chunk-S27EXIHY.js.map} +0 -0
  99. /package/dist/{chunk-5ML4TH3E.js.map → chunk-TFORLO3O.js.map} +0 -0
  100. /package/dist/{chunk-TOFUTKQN.js.map → chunk-TR4DK5OH.js.map} +0 -0
  101. /package/dist/{chunk-6ORWKANA.js.map → chunk-VYU7PXUS.js.map} +0 -0
  102. /package/dist/{chunk-FFU4GMST.js.map → chunk-WNARATI3.js.map} +0 -0
  103. /package/dist/{chunk-KSFBM6TV.js.map → chunk-YITUHONZ.js.map} +0 -0
@@ -1,11 +1,12 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { createHash } from "node:crypto";
3
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { mkdir, mkdtemp, readdir, readFile, rm, utimes, writeFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import test from "node:test";
7
7
 
8
8
  import {
9
+ applyOfflineSyncFileContentChunk,
9
10
  applyOfflineSyncChangeset,
10
11
  applyOfflineSyncSnapshot,
11
12
  buildOfflineSyncChangeset,
@@ -58,7 +59,21 @@ test("offline snapshot captures source-of-truth files and excludes private/inter
58
59
 
59
60
  assert.deepEqual(
60
61
  snapshot.files.map((file) => file.path),
61
- ["assets/blob.bin", "facts/a.md", "facts/fact-hashes.txt", "transcripts/session.jsonl"],
62
+ [
63
+ "assets/blob.bin",
64
+ "facts/a.md",
65
+ "facts/fact-hashes.txt",
66
+ "state/fact-hashes.ready",
67
+ "state/fact-hashes.txt",
68
+ "state/last_graph_recall.json",
69
+ "state/last_intent.json",
70
+ "state/last_qmd_recall.json",
71
+ "state/last_recall.json",
72
+ "state/lcm.sqlite",
73
+ "state/lcm.sqlite-shm",
74
+ "state/lcm.sqlite-wal",
75
+ "transcripts/session.jsonl",
76
+ ],
62
77
  );
63
78
  const binary = snapshot.files.find((file) => file.path === "assets/blob.bin");
64
79
  assert.equal(Buffer.from(binary?.contentBase64 ?? "", "base64")[3], 255);
@@ -71,14 +86,27 @@ test("offline snapshot captures source-of-truth files and excludes private/inter
71
86
  });
72
87
  assert.deepEqual(
73
88
  withoutTranscripts.files.map((file) => file.path),
74
- ["assets/blob.bin", "facts/a.md", "facts/fact-hashes.txt"],
89
+ [
90
+ "assets/blob.bin",
91
+ "facts/a.md",
92
+ "facts/fact-hashes.txt",
93
+ "state/fact-hashes.ready",
94
+ "state/fact-hashes.txt",
95
+ "state/last_graph_recall.json",
96
+ "state/last_intent.json",
97
+ "state/last_qmd_recall.json",
98
+ "state/last_recall.json",
99
+ "state/lcm.sqlite",
100
+ "state/lcm.sqlite-shm",
101
+ "state/lcm.sqlite-wal",
102
+ ],
75
103
  );
76
104
  } finally {
77
105
  await rm(root, { recursive: true, force: true });
78
106
  }
79
107
  });
80
108
 
81
- test("offline sync excludes volatile retrieval debug snapshots without deleting existing local copies", async () => {
109
+ test("offline sync includes retrieval debug snapshots for full-fidelity offline recall", async () => {
82
110
  const root = await tempDir("remnic-offline-debug-snapshots");
83
111
  try {
84
112
  await write(root, "facts/a.md", "alpha");
@@ -93,25 +121,25 @@ test("offline sync excludes volatile retrieval debug snapshots without deleting
93
121
  includeContent: true,
94
122
  });
95
123
 
96
- assert.deepEqual(snapshot.files.map((file) => file.path), ["facts/a.md"]);
97
- await assert.rejects(
98
- () =>
99
- buildOfflineSyncSnapshotForPaths({
100
- root,
101
- sourceId: "remote",
102
- paths: ["state/last_graph_recall.json"],
103
- includeContent: true,
104
- }),
105
- /offline sync snapshot path is excluded: state\/last_graph_recall\.json/,
106
- );
107
- await assert.rejects(
108
- () =>
109
- readOfflineSyncFileContentChunk({
110
- root,
111
- path: "state/last_graph_recall.json",
112
- }),
113
- /offline sync file content path is excluded: state\/last_graph_recall\.json/,
114
- );
124
+ assert.deepEqual(snapshot.files.map((file) => file.path), [
125
+ "facts/a.md",
126
+ "state/last_graph_recall.json",
127
+ "state/last_intent.json",
128
+ "state/last_qmd_recall.json",
129
+ "state/last_recall.json",
130
+ ]);
131
+ const focused = await buildOfflineSyncSnapshotForPaths({
132
+ root,
133
+ sourceId: "remote",
134
+ paths: ["state/last_graph_recall.json"],
135
+ includeContent: true,
136
+ });
137
+ assert.deepEqual(focused.files.map((file) => file.path), ["state/last_graph_recall.json"]);
138
+ const chunk = await readOfflineSyncFileContentChunk({
139
+ root,
140
+ path: "state/last_graph_recall.json",
141
+ });
142
+ assert.equal(chunk.content.toString("utf-8"), "graph");
115
143
 
116
144
  const oldGraph = Buffer.from("old graph");
117
145
  const pull = await applyOfflineSyncSnapshot({
@@ -125,14 +153,14 @@ test("offline sync excludes volatile retrieval debug snapshots without deleting
125
153
  }],
126
154
  });
127
155
 
128
- assert.equal(pull.deleted, 0);
156
+ assert.equal(pull.skipped, 5);
129
157
  assert.equal(await readUtf8(root, "state/last_graph_recall.json"), "graph");
130
158
  } finally {
131
159
  await rm(root, { recursive: true, force: true });
132
160
  }
133
161
  });
134
162
 
135
- test("offline sync excludes live LCM sqlite artifacts without deleting existing local copies", async () => {
163
+ test("offline sync includes live LCM sqlite artifacts for full-fidelity offline mode", async () => {
136
164
  const root = await tempDir("remnic-offline-lcm-sqlite");
137
165
  try {
138
166
  await write(root, "facts/a.md", "alpha");
@@ -146,17 +174,19 @@ test("offline sync excludes live LCM sqlite artifacts without deleting existing
146
174
  includeContent: true,
147
175
  });
148
176
 
149
- assert.deepEqual(snapshot.files.map((file) => file.path), ["facts/a.md"]);
150
- await assert.rejects(
151
- () =>
152
- buildOfflineSyncSnapshotForPaths({
153
- root,
154
- sourceId: "remote",
155
- paths: ["state/lcm.sqlite"],
156
- includeContent: true,
157
- }),
158
- /offline sync snapshot path is excluded: state\/lcm\.sqlite/,
159
- );
177
+ assert.deepEqual(snapshot.files.map((file) => file.path), [
178
+ "facts/a.md",
179
+ "state/lcm.sqlite",
180
+ "state/lcm.sqlite-shm",
181
+ "state/lcm.sqlite-wal",
182
+ ]);
183
+ const focused = await buildOfflineSyncSnapshotForPaths({
184
+ root,
185
+ sourceId: "remote",
186
+ paths: ["state/lcm.sqlite"],
187
+ includeContent: true,
188
+ });
189
+ assert.deepEqual(focused.files.map((file) => file.path), ["state/lcm.sqlite"]);
160
190
 
161
191
  const oldDb = Buffer.from("old live db");
162
192
  const pull = await applyOfflineSyncSnapshot({
@@ -170,14 +200,14 @@ test("offline sync excludes live LCM sqlite artifacts without deleting existing
170
200
  }],
171
201
  });
172
202
 
173
- assert.equal(pull.deleted, 0);
203
+ assert.equal(pull.skipped, 4);
174
204
  assert.equal(await readUtf8(root, "state/lcm.sqlite"), "live db");
175
205
  } finally {
176
206
  await rm(root, { recursive: true, force: true });
177
207
  }
178
208
  });
179
209
 
180
- test("offline sync excludes runtime-derived state without deleting existing local copies", async () => {
210
+ test("offline sync includes durable runtime state and excludes only transient sync temp files", async () => {
181
211
  const root = await tempDir("remnic-offline-runtime-state");
182
212
  try {
183
213
  await write(root, "facts/a.md", "alpha");
@@ -209,17 +239,30 @@ test("offline sync excludes runtime-derived state without deleting existing loca
209
239
  assert.deepEqual(snapshot.files.map((file) => file.path), [
210
240
  "assets/state/fact-hashes.txt",
211
241
  "facts/a.md",
242
+ "namespaces/generalist-project-origin-6ebeaa54/state/.memory-status-version.log",
243
+ "namespaces/generalist-project-origin-6ebeaa54/state/entity-mention-index.json",
244
+ "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json",
245
+ "state/.artifact-write-version.log",
246
+ "state/.memory-status-version.log",
247
+ "state/buffer-surprise-ledger.jsonl",
248
+ "state/buffer.json",
249
+ "state/embeddings.json",
250
+ "state/entity-mention-index.json",
251
+ "state/index_tags.json",
252
+ "state/index_time.json",
253
+ "state/memory-lifecycle-ledger.jsonl",
254
+ "state/memory-projection.sqlite",
255
+ "state/memory-projection.sqlite-shm",
256
+ "state/memory-projection.sqlite-wal",
257
+ "state/recall_impressions.jsonl",
212
258
  ]);
213
- await assert.rejects(
214
- () =>
215
- buildOfflineSyncSnapshotForPaths({
216
- root,
217
- sourceId: "remote",
218
- paths: ["state/memory-lifecycle-ledger.jsonl"],
219
- includeContent: true,
220
- }),
221
- /offline sync snapshot path is excluded: state\/memory-lifecycle-ledger\.jsonl/,
222
- );
259
+ const focused = await buildOfflineSyncSnapshotForPaths({
260
+ root,
261
+ sourceId: "remote",
262
+ paths: ["state/memory-lifecycle-ledger.jsonl"],
263
+ includeContent: true,
264
+ });
265
+ assert.deepEqual(focused.files.map((file) => file.path), ["state/memory-lifecycle-ledger.jsonl"]);
223
266
  await assert.rejects(
224
267
  () =>
225
268
  readOfflineSyncFileContentChunk({
@@ -228,16 +271,15 @@ test("offline sync excludes runtime-derived state without deleting existing loca
228
271
  }),
229
272
  /offline sync file content path is excluded: state\/buffer\.json\.tmp-123-456/,
230
273
  );
231
- await assert.rejects(
232
- () =>
233
- buildOfflineSyncSnapshotForPaths({
234
- root,
235
- sourceId: "remote",
236
- paths: ["namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json"],
237
- includeContent: true,
238
- }),
239
- /offline sync snapshot path is excluded: namespaces\/generalist-project-origin-6ebeaa54\/state\/last_intent\.json/,
240
- );
274
+ const namespaced = await buildOfflineSyncSnapshotForPaths({
275
+ root,
276
+ sourceId: "remote",
277
+ paths: ["namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json"],
278
+ includeContent: true,
279
+ });
280
+ assert.deepEqual(namespaced.files.map((file) => file.path), [
281
+ "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json",
282
+ ]);
241
283
 
242
284
  const oldLedger = Buffer.from("old ledger");
243
285
  const pull = await applyOfflineSyncSnapshot({
@@ -251,14 +293,14 @@ test("offline sync excludes runtime-derived state without deleting existing loca
251
293
  }],
252
294
  });
253
295
 
254
- assert.equal(pull.deleted, 0);
296
+ assert.equal(pull.skipped, 18);
255
297
  assert.equal(await readUtf8(root, "state/memory-lifecycle-ledger.jsonl"), "ledger");
256
298
  } finally {
257
299
  await rm(root, { recursive: true, force: true });
258
300
  }
259
301
  });
260
302
 
261
- test("offline sync ignores runtime-derived records from older peers", async () => {
303
+ test("offline sync accepts durable runtime records from older peers", async () => {
262
304
  const root = await tempDir("remnic-offline-legacy-runtime-state");
263
305
  try {
264
306
  const fact = Buffer.from("alpha");
@@ -284,6 +326,13 @@ test("offline sync ignores runtime-derived records from older peers", async () =
284
326
  mtimeMs: 0,
285
327
  contentBase64: runtime.toString("base64"),
286
328
  },
329
+ {
330
+ path: "state/buffer.json.tmp-123-456",
331
+ sha256: runtimeSha,
332
+ bytes: runtime.byteLength,
333
+ mtimeMs: 0,
334
+ contentBase64: runtime.toString("base64"),
335
+ },
287
336
  {
288
337
  path: "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json",
289
338
  sha256: runtimeSha,
@@ -309,15 +358,16 @@ test("offline sync ignores runtime-derived records from older peers", async () =
309
358
  },
310
359
  });
311
360
 
312
- assert.equal(pull.upserted, 2);
361
+ assert.equal(pull.upserted, 4);
313
362
  assert.equal(await readUtf8(root, "facts/a.md"), "alpha");
314
363
  assert.equal(await readUtf8(root, "assets/state/fact-hashes.txt"), "durable asset");
315
- await assert.rejects(
316
- () => readFile(path.join(root, "state", "buffer.json")),
317
- /ENOENT/,
364
+ assert.equal(await readUtf8(root, "state/buffer.json"), "legacy runtime");
365
+ assert.equal(
366
+ await readUtf8(root, "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json"),
367
+ "legacy runtime",
318
368
  );
319
369
  await assert.rejects(
320
- () => readFile(path.join(root, "namespaces", "generalist-project-origin-6ebeaa54", "state", "last_intent.json")),
370
+ () => readFile(path.join(root, "state", "buffer.json.tmp-123-456")),
321
371
  /ENOENT/,
322
372
  );
323
373
 
@@ -343,6 +393,17 @@ test("offline sync ignores runtime-derived records from older peers", async () =
343
393
  contentBase64: runtime.toString("base64"),
344
394
  },
345
395
  },
396
+ {
397
+ type: "upsert",
398
+ path: "state/buffer.json.tmp-123-456",
399
+ file: {
400
+ path: "state/buffer.json.tmp-123-456",
401
+ sha256: runtimeSha,
402
+ bytes: runtime.byteLength,
403
+ mtimeMs: 0,
404
+ contentBase64: runtime.toString("base64"),
405
+ },
406
+ },
346
407
  {
347
408
  type: "upsert",
348
409
  path: "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json",
@@ -380,15 +441,19 @@ test("offline sync ignores runtime-derived records from older peers", async () =
380
441
  },
381
442
  });
382
443
 
383
- assert.equal(push.appliedUpserts, 2);
444
+ assert.equal(push.appliedUpserts, 4);
384
445
  assert.equal(await readUtf8(remote, "facts/a.md"), "alpha");
385
446
  assert.equal(await readUtf8(remote, "assets/state/fact-hashes.txt"), "durable asset");
386
- await assert.rejects(
387
- () => readFile(path.join(remote, "state", "memory-lifecycle-ledger.jsonl")),
388
- /ENOENT/,
447
+ assert.equal(
448
+ await readUtf8(remote, "state/memory-lifecycle-ledger.jsonl"),
449
+ "legacy runtime",
450
+ );
451
+ assert.equal(
452
+ await readUtf8(remote, "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json"),
453
+ "legacy runtime",
389
454
  );
390
455
  await assert.rejects(
391
- () => readFile(path.join(remote, "namespaces", "generalist-project-origin-6ebeaa54", "state", "last_intent.json")),
456
+ () => readFile(path.join(remote, "state", "buffer.json.tmp-123-456")),
392
457
  /ENOENT/,
393
458
  );
394
459
  } finally {
@@ -426,6 +491,7 @@ test("offline sync reads bounded file content chunks with metadata", async () =>
426
491
  const root = await tempDir("remnic-offline-file-content");
427
492
  try {
428
493
  await write(root, "artifacts/large.txt", "alpha\nbeta\ngamma\n");
494
+ await write(root, "state/lcm.sqlite", "live db");
429
495
 
430
496
  const chunk = await readOfflineSyncFileContentChunk({
431
497
  root,
@@ -440,14 +506,199 @@ test("offline sync reads bounded file content chunks with metadata", async () =>
440
506
  assert.equal(chunk.content.toString("utf-8"), "beta\n");
441
507
  assert.equal(chunk.bytes, Buffer.byteLength("alpha\nbeta\ngamma\n"));
442
508
 
443
- await assert.rejects(
444
- () =>
445
- readOfflineSyncFileContentChunk({
446
- root,
447
- path: "state/lcm.sqlite",
448
- }),
449
- /offline sync file content path is excluded: state\/lcm\.sqlite/,
450
- );
509
+ const lcm = await readOfflineSyncFileContentChunk({
510
+ root,
511
+ path: "state/lcm.sqlite",
512
+ });
513
+ assert.equal(lcm.content.toString("utf-8"), "live db");
514
+ } finally {
515
+ await rm(root, { recursive: true, force: true });
516
+ }
517
+ });
518
+
519
+ test("offline sync applies chunked file content with base conflict checks", async () => {
520
+ const root = await tempDir("remnic-offline-file-content-apply");
521
+ try {
522
+ await write(root, "state/lcm.sqlite", "old");
523
+ const oldSha = createHash("sha256").update("old").digest("hex");
524
+ const next = Buffer.from("new durable sqlite content");
525
+ const nextSha = createHash("sha256").update(next).digest("hex");
526
+
527
+ const first = await applyOfflineSyncFileContentChunk({
528
+ root,
529
+ sourceId: "laptop",
530
+ path: "state/lcm.sqlite",
531
+ sha256: nextSha,
532
+ bytes: next.byteLength,
533
+ mtimeMs: 123,
534
+ offset: 0,
535
+ baseSha256: oldSha,
536
+ content: next.subarray(0, 8),
537
+ });
538
+ assert.equal(first.done, false);
539
+
540
+ const second = await applyOfflineSyncFileContentChunk({
541
+ root,
542
+ sourceId: "laptop",
543
+ path: "state/lcm.sqlite",
544
+ sha256: nextSha,
545
+ bytes: next.byteLength,
546
+ mtimeMs: 123,
547
+ offset: 8,
548
+ baseSha256: oldSha,
549
+ content: next.subarray(8),
550
+ });
551
+ assert.equal(second.done, true);
552
+ assert.equal(second.applied, true);
553
+ assert.equal(await readUtf8(root, "state/lcm.sqlite"), "new durable sqlite content");
554
+
555
+ const conflictContent = Buffer.from("conflicting local sqlite");
556
+ const conflictSha = createHash("sha256").update(conflictContent).digest("hex");
557
+ const conflict = await applyOfflineSyncFileContentChunk({
558
+ root,
559
+ sourceId: "laptop",
560
+ path: "state/lcm.sqlite",
561
+ sha256: conflictSha,
562
+ bytes: conflictContent.byteLength,
563
+ mtimeMs: 456,
564
+ offset: 0,
565
+ baseSha256: oldSha,
566
+ content: conflictContent,
567
+ });
568
+ assert.equal(conflict.done, true);
569
+ assert.equal(conflict.applied, false);
570
+ assert.equal(conflict.conflict?.reason, "remote_changed_for_local_update");
571
+ assert.equal(await readUtf8(root, "state/lcm.sqlite"), "new durable sqlite content");
572
+ } finally {
573
+ await rm(root, { recursive: true, force: true });
574
+ }
575
+ });
576
+
577
+ test("offline sync stages chunked uploads through storage hooks", async () => {
578
+ const root = await tempDir("remnic-offline-file-content-hooks");
579
+ const encode = (content: Buffer) => Buffer.from(`ENC:${content.toString("base64")}`);
580
+ const decode = (content: Buffer) => {
581
+ const text = content.toString("utf-8");
582
+ return text.startsWith("ENC:") ? Buffer.from(text.slice(4), "base64") : content;
583
+ };
584
+ const readHook = async ({ filePath }: { filePath: string }) => decode(await readFile(filePath));
585
+ let stagingWrites = 0;
586
+ let mutationWrites = 0;
587
+ const writeStagingHook = async ({ filePath, content }: { filePath: string; content: Buffer }) => {
588
+ stagingWrites += 1;
589
+ await mkdir(path.dirname(filePath), { recursive: true });
590
+ await writeFile(filePath, encode(content));
591
+ };
592
+ const writeHook = async ({ filePath, content }: { filePath: string; content: Buffer }) => {
593
+ mutationWrites += 1;
594
+ await mkdir(path.dirname(filePath), { recursive: true });
595
+ await writeFile(filePath, encode(content));
596
+ };
597
+ const writeChunksHook = async ({
598
+ filePath,
599
+ chunks,
600
+ }: {
601
+ filePath: string;
602
+ chunks: AsyncIterable<Buffer>;
603
+ }) => {
604
+ const content: Buffer[] = [];
605
+ for await (const chunk of chunks) content.push(chunk);
606
+ await writeHook({ filePath, content: Buffer.concat(content) });
607
+ };
608
+
609
+ try {
610
+ await write(root, "state/lcm.sqlite", "old");
611
+ const oldSha = createHash("sha256").update("old").digest("hex");
612
+ const next = Buffer.from("new durable sqlite content");
613
+ const nextSha = createHash("sha256").update(next).digest("hex");
614
+
615
+ const first = await applyOfflineSyncFileContentChunk({
616
+ root,
617
+ sourceId: "laptop",
618
+ path: "state/lcm.sqlite",
619
+ sha256: nextSha,
620
+ bytes: next.byteLength,
621
+ mtimeMs: 123,
622
+ offset: 0,
623
+ baseSha256: oldSha,
624
+ content: next.subarray(0, 8),
625
+ readFile: readHook,
626
+ writeFile: writeHook,
627
+ writeStagingFile: writeStagingHook,
628
+ writeFileChunks: writeChunksHook,
629
+ });
630
+ assert.equal(first.done, false);
631
+ assert.equal(stagingWrites, 1);
632
+ assert.equal(mutationWrites, 0);
633
+ const uploadEntries = await readdir(path.join(root, ".offline-sync", "uploads"));
634
+ assert.equal(uploadEntries.length, 1);
635
+ const uploadChunkEntries = await readdir(path.join(root, ".offline-sync", "uploads", uploadEntries[0]));
636
+ assert.deepEqual(uploadChunkEntries, ["00000000000000000000.part"]);
637
+ const rawUpload = await readFile(path.join(
638
+ root,
639
+ ".offline-sync",
640
+ "uploads",
641
+ uploadEntries[0],
642
+ uploadChunkEntries[0],
643
+ ));
644
+ assert.match(rawUpload.toString("utf-8"), /^ENC:/);
645
+ assert.equal(rawUpload.includes(next.subarray(0, 8)), false);
646
+
647
+ const second = await applyOfflineSyncFileContentChunk({
648
+ root,
649
+ sourceId: "laptop",
650
+ path: "state/lcm.sqlite",
651
+ sha256: nextSha,
652
+ bytes: next.byteLength,
653
+ mtimeMs: 123,
654
+ offset: 8,
655
+ baseSha256: oldSha,
656
+ content: next.subarray(8),
657
+ readFile: readHook,
658
+ writeFile: writeHook,
659
+ writeStagingFile: writeStagingHook,
660
+ writeFileChunks: writeChunksHook,
661
+ });
662
+ assert.equal(second.applied, true);
663
+ assert.equal(stagingWrites, 2);
664
+ assert.equal(mutationWrites, 1);
665
+ assert.equal((await readdir(path.join(root, ".offline-sync", "uploads"))).length, 0);
666
+ const rawTarget = await readFile(path.join(root, "state/lcm.sqlite"));
667
+ assert.match(rawTarget.toString("utf-8"), /^ENC:/);
668
+ assert.equal(decode(rawTarget).toString("utf-8"), "new durable sqlite content");
669
+ } finally {
670
+ await rm(root, { recursive: true, force: true });
671
+ }
672
+ });
673
+
674
+ test("offline sync prunes stale staged uploads when starting a new upload", async () => {
675
+ const root = await tempDir("remnic-offline-file-content-prune");
676
+ try {
677
+ const staleKey = `${"a".repeat(64)}.part`;
678
+ const staleDir = path.join(root, ".offline-sync", "uploads", staleKey);
679
+ await mkdir(staleDir, { recursive: true });
680
+ await writeFile(path.join(staleDir, "00000000000000000000.part"), "abandoned");
681
+ const staleTime = new Date(Date.now() - 25 * 60 * 60 * 1000);
682
+ await utimes(path.join(staleDir, "00000000000000000000.part"), staleTime, staleTime);
683
+ await utimes(staleDir, staleTime, staleTime);
684
+
685
+ const next = Buffer.from("new durable sqlite content");
686
+ const nextSha = createHash("sha256").update(next).digest("hex");
687
+ const first = await applyOfflineSyncFileContentChunk({
688
+ root,
689
+ sourceId: "laptop",
690
+ path: "state/lcm.sqlite",
691
+ sha256: nextSha,
692
+ bytes: next.byteLength,
693
+ mtimeMs: 123,
694
+ offset: 0,
695
+ content: next.subarray(0, 8),
696
+ });
697
+
698
+ assert.equal(first.done, false);
699
+ const uploadEntries = await readdir(path.join(root, ".offline-sync", "uploads"));
700
+ assert.equal(uploadEntries.includes(staleKey), false);
701
+ assert.equal(uploadEntries.length, 1);
451
702
  } finally {
452
703
  await rm(root, { recursive: true, force: true });
453
704
  }
@@ -526,6 +777,32 @@ test("offline changeset only carries content for changed local files", async ()
526
777
  }
527
778
  });
528
779
 
780
+ test("offline changeset can exclude directly pushed large files without reading their content", async () => {
781
+ const local = await tempDir("remnic-offline-changeset-exclude");
782
+ try {
783
+ await write(local, "state/lcm.sqlite", "before");
784
+ await write(local, "facts/small.md", "before");
785
+ const base = await buildOfflineSyncSnapshot({
786
+ root: local,
787
+ sourceId: "remote",
788
+ includeContent: false,
789
+ });
790
+
791
+ await write(local, "state/lcm.sqlite", "after large");
792
+ await write(local, "facts/small.md", "after small");
793
+ const changeset = await buildOfflineSyncChangeset({
794
+ root: local,
795
+ sourceId: "laptop",
796
+ baseFiles: base.files,
797
+ excludePaths: ["state/lcm.sqlite"],
798
+ });
799
+
800
+ assert.deepEqual(changeset.changes.map((change) => change.path), ["facts/small.md"]);
801
+ } finally {
802
+ await rm(local, { recursive: true, force: true });
803
+ }
804
+ });
805
+
529
806
  test("offline pull accepts metadata-only snapshots when files are unchanged", async () => {
530
807
  const remote = await tempDir("remnic-offline-metadata-remote");
531
808
  const local = await tempDir("remnic-offline-metadata-local");
@@ -960,6 +1237,45 @@ test("offline sync applies and snapshots through secure storage hooks", async ()
960
1237
  "secret fact",
961
1238
  );
962
1239
  assert.equal(snapshot.files[0]?.bytes, Buffer.byteLength("secret fact"));
1240
+
1241
+ const sqlite = Buffer.from("streamed durable sqlite content");
1242
+ const sqliteSha = createHash("sha256").update(sqlite).digest("hex");
1243
+ const first = await applyOfflineSyncFileContentChunk({
1244
+ root,
1245
+ sourceId: "laptop",
1246
+ path: "state/lcm.sqlite",
1247
+ sha256: sqliteSha,
1248
+ bytes: sqlite.byteLength,
1249
+ mtimeMs: 321,
1250
+ offset: 0,
1251
+ content: sqlite.subarray(0, 8),
1252
+ readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
1253
+ writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
1254
+ writeStagingFile: async ({ filePath, content }) => storage.writeOfflineSyncStagingFile(filePath, content),
1255
+ writeFileChunks: async ({ filePath, chunks }) => storage.writeOfflineSyncFileChunks(filePath, chunks),
1256
+ });
1257
+ assert.equal(first.done, false);
1258
+ const second = await applyOfflineSyncFileContentChunk({
1259
+ root,
1260
+ sourceId: "laptop",
1261
+ path: "state/lcm.sqlite",
1262
+ sha256: sqliteSha,
1263
+ bytes: sqlite.byteLength,
1264
+ mtimeMs: 321,
1265
+ offset: 8,
1266
+ content: sqlite.subarray(8),
1267
+ readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
1268
+ writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
1269
+ writeStagingFile: async ({ filePath, content }) => storage.writeOfflineSyncStagingFile(filePath, content),
1270
+ writeFileChunks: async ({ filePath, chunks }) => storage.writeOfflineSyncFileChunks(filePath, chunks),
1271
+ });
1272
+ assert.equal(second.applied, true);
1273
+ const rawSqlite = await readFile(path.join(root, "state", "lcm.sqlite"));
1274
+ assert.equal(isEncryptedFile(rawSqlite), true);
1275
+ assert.equal(
1276
+ (await storage.readOfflineSyncFile(path.join(root, "state", "lcm.sqlite"))).toString("utf8"),
1277
+ "streamed durable sqlite content",
1278
+ );
963
1279
  } finally {
964
1280
  await rm(root, { recursive: true, force: true });
965
1281
  await rm(source, { recursive: true, force: true });
@@ -984,7 +1300,7 @@ test("offline storage writes invalidate fact hash readiness for rebuild", async
984
1300
 
985
1301
  assert.equal(
986
1302
  sourceChangeset.changes.some((change) => change.path.startsWith("state/fact-hashes")),
987
- false,
1303
+ true,
988
1304
  );
989
1305
 
990
1306
  const factChangeset = {