@remnic/core 1.1.14 → 1.1.16
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.
- package/dist/access-cli.js +34 -33
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +2 -1
- package/dist/access-http.js +15 -14
- package/dist/access-mcp.d.ts +2 -1
- package/dist/access-mcp.js +14 -13
- package/dist/access-schema.d.ts +36 -5
- package/dist/access-schema.js +9 -5
- package/dist/{access-service-DcCDmNYC.d.ts → access-service-DZXc7qwR.d.ts} +31 -1
- package/dist/access-service.d.ts +2 -1
- package/dist/access-service.js +12 -11
- package/dist/briefing.js +4 -4
- package/dist/causal-consolidation.js +5 -5
- package/dist/chunk-2OZ6GP27.js +832 -0
- package/dist/chunk-2OZ6GP27.js.map +1 -0
- package/dist/{chunk-VNO6ZJ35.js → chunk-2PRLKQAH.js} +5 -5
- package/dist/{chunk-EFJ3MQ4V.js → chunk-65HQPW6O.js} +2 -2
- package/dist/{chunk-A2XUIMJ3.js → chunk-66H2DZYB.js} +18 -2
- package/dist/chunk-66H2DZYB.js.map +1 -0
- package/dist/{chunk-GA454ALV.js → chunk-AAX3SUM3.js} +39 -39
- package/dist/{chunk-QQUAB63I.js → chunk-BEB4GUU5.js} +2 -2
- package/dist/{chunk-KUJVMMZQ.js → chunk-C7DGCHJE.js} +2 -2
- package/dist/{chunk-PR5FBTFU.js → chunk-CYFQJMUV.js} +5 -5
- package/dist/{chunk-KLAO5DGL.js → chunk-G7JBLD65.js} +3 -3
- package/dist/{chunk-CHEL3SKB.js → chunk-HJILHQOR.js} +27 -27
- package/dist/{chunk-ME6ESPZU.js → chunk-IG5VGHYB.js} +2 -2
- package/dist/{chunk-7AAT6G4Q.js → chunk-IOAY54RF.js} +57 -5
- package/dist/chunk-IOAY54RF.js.map +1 -0
- package/dist/{chunk-XVZ7B3HG.js → chunk-JFEH2LZM.js} +2 -2
- package/dist/{chunk-JLFA7DQG.js → chunk-M3AA636B.js} +2 -2
- package/dist/{chunk-P4NEIHUT.js → chunk-MS3ULOZF.js} +2 -2
- package/dist/{chunk-CQZRLNMV.js → chunk-MTYLGYOQ.js} +53 -4
- package/dist/chunk-MTYLGYOQ.js.map +1 -0
- package/dist/{chunk-7IASACLB.js → chunk-NOHC2L57.js} +2 -2
- package/dist/{chunk-6RVI47ZR.js → chunk-NTUNYIF7.js} +5 -5
- package/dist/{chunk-CK5NTM2S.js → chunk-OGROP7ZN.js} +2 -2
- package/dist/{chunk-MT25YHYH.js → chunk-OJRKZLZ4.js} +5 -5
- package/dist/{chunk-2F2W355T.js → chunk-QA2ZAPBU.js} +4 -4
- package/dist/{chunk-MC26UJIM.js → chunk-QLKBF3TI.js} +2 -2
- package/dist/{chunk-YNJHCGDT.js → chunk-SH5S7XYD.js} +8 -5
- package/dist/chunk-SH5S7XYD.js.map +1 -0
- package/dist/{chunk-WZYKANL3.js → chunk-SK42SSAN.js} +4 -4
- package/dist/{chunk-VW676BEI.js → chunk-V7WH7DEM.js} +2 -2
- package/dist/{chunk-PU63GXWS.js → chunk-W7DK3CYM.js} +2 -2
- package/dist/{chunk-TFO23QT4.js → chunk-XKLD5OK4.js} +4 -4
- package/dist/{chunk-M23FSH32.js → chunk-Y2YBRCEF.js} +79 -6
- package/dist/chunk-Y2YBRCEF.js.map +1 -0
- package/dist/{chunk-I5V2VDIW.js → chunk-YCVWX2NF.js} +2 -2
- package/dist/{chunk-UXHQAFNA.js → chunk-ZPXYWTN5.js} +4 -4
- package/dist/{chunk-GGKRUQOO.js → chunk-ZYVPLJ4T.js} +4 -4
- package/dist/{cli-D3VpkVwB.d.ts → cli-kVwab1_L.d.ts} +1 -1
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +35 -34
- package/dist/compounding/engine.js +4 -4
- package/dist/connectors/codex-materialize-runner.js +4 -4
- package/dist/connectors/index.js +4 -4
- package/dist/conversation-index/backend.js +2 -2
- package/dist/entity-retrieval.js +4 -4
- package/dist/index.d.ts +4 -3
- package/dist/index.js +92 -58
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +5 -5
- package/dist/maintenance/memory-governance.js +4 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/mcp-memory-inspector-app.d.ts +2 -1
- package/dist/namespaces/migrate.js +10 -10
- package/dist/namespaces/search.js +5 -5
- package/dist/namespaces/storage.js +4 -4
- package/dist/offline-sync.d.ts +145 -0
- package/dist/offline-sync.js +43 -0
- package/dist/offline-sync.js.map +1 -0
- package/dist/operator-toolkit.js +13 -13
- package/dist/orchestrator.js +24 -24
- package/dist/schemas.d.ts +22 -22
- package/dist/search/factory.js +4 -4
- package/dist/search/index.js +6 -6
- package/dist/secure-store/index.d.ts +1 -15
- package/dist/secure-store/index.js +2 -2
- package/dist/semantic-consolidation.js +5 -5
- package/dist/semantic-rule-promotion.js +4 -4
- package/dist/semantic-rule-verifier.js +4 -4
- package/dist/storage.d.ts +7 -0
- package/dist/storage.js +3 -3
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +4 -4
- package/dist/transfer/capsule-import.js +3 -3
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +4 -4
- package/package.json +1 -1
- package/src/access-http.test.ts +289 -0
- package/src/access-http.ts +69 -0
- package/src/access-schema.ts +30 -0
- package/src/access-service-namespace.test.ts +64 -1
- package/src/access-service.ts +120 -0
- package/src/index.ts +34 -0
- package/src/offline-sync.test.ts +646 -0
- package/src/offline-sync.ts +1087 -0
- package/src/secure-store/secure-fs.ts +14 -7
- package/src/storage.ts +59 -0
- package/dist/chunk-7AAT6G4Q.js.map +0 -1
- package/dist/chunk-A2XUIMJ3.js.map +0 -1
- package/dist/chunk-CQZRLNMV.js.map +0 -1
- package/dist/chunk-M23FSH32.js.map +0 -1
- package/dist/chunk-YNJHCGDT.js.map +0 -1
- /package/dist/{chunk-VNO6ZJ35.js.map → chunk-2PRLKQAH.js.map} +0 -0
- /package/dist/{chunk-EFJ3MQ4V.js.map → chunk-65HQPW6O.js.map} +0 -0
- /package/dist/{chunk-GA454ALV.js.map → chunk-AAX3SUM3.js.map} +0 -0
- /package/dist/{chunk-QQUAB63I.js.map → chunk-BEB4GUU5.js.map} +0 -0
- /package/dist/{chunk-KUJVMMZQ.js.map → chunk-C7DGCHJE.js.map} +0 -0
- /package/dist/{chunk-PR5FBTFU.js.map → chunk-CYFQJMUV.js.map} +0 -0
- /package/dist/{chunk-KLAO5DGL.js.map → chunk-G7JBLD65.js.map} +0 -0
- /package/dist/{chunk-CHEL3SKB.js.map → chunk-HJILHQOR.js.map} +0 -0
- /package/dist/{chunk-ME6ESPZU.js.map → chunk-IG5VGHYB.js.map} +0 -0
- /package/dist/{chunk-XVZ7B3HG.js.map → chunk-JFEH2LZM.js.map} +0 -0
- /package/dist/{chunk-JLFA7DQG.js.map → chunk-M3AA636B.js.map} +0 -0
- /package/dist/{chunk-P4NEIHUT.js.map → chunk-MS3ULOZF.js.map} +0 -0
- /package/dist/{chunk-7IASACLB.js.map → chunk-NOHC2L57.js.map} +0 -0
- /package/dist/{chunk-6RVI47ZR.js.map → chunk-NTUNYIF7.js.map} +0 -0
- /package/dist/{chunk-CK5NTM2S.js.map → chunk-OGROP7ZN.js.map} +0 -0
- /package/dist/{chunk-MT25YHYH.js.map → chunk-OJRKZLZ4.js.map} +0 -0
- /package/dist/{chunk-2F2W355T.js.map → chunk-QA2ZAPBU.js.map} +0 -0
- /package/dist/{chunk-MC26UJIM.js.map → chunk-QLKBF3TI.js.map} +0 -0
- /package/dist/{chunk-WZYKANL3.js.map → chunk-SK42SSAN.js.map} +0 -0
- /package/dist/{chunk-VW676BEI.js.map → chunk-V7WH7DEM.js.map} +0 -0
- /package/dist/{chunk-PU63GXWS.js.map → chunk-W7DK3CYM.js.map} +0 -0
- /package/dist/{chunk-TFO23QT4.js.map → chunk-XKLD5OK4.js.map} +0 -0
- /package/dist/{chunk-I5V2VDIW.js.map → chunk-YCVWX2NF.js.map} +0 -0
- /package/dist/{chunk-UXHQAFNA.js.map → chunk-ZPXYWTN5.js.map} +0 -0
- /package/dist/{chunk-GGKRUQOO.js.map → chunk-ZYVPLJ4T.js.map} +0 -0
package/src/access-http.test.ts
CHANGED
|
@@ -223,3 +223,292 @@ 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 files forwards namespace and requested paths", async () => {
|
|
290
|
+
const calls: Array<{
|
|
291
|
+
namespace: string | undefined;
|
|
292
|
+
principal: string | undefined;
|
|
293
|
+
includeTranscripts: boolean | undefined;
|
|
294
|
+
paths: string[];
|
|
295
|
+
}> = [];
|
|
296
|
+
const service = {
|
|
297
|
+
offlineSyncFiles: async (options: {
|
|
298
|
+
namespace?: string;
|
|
299
|
+
principal?: string;
|
|
300
|
+
includeTranscripts?: boolean;
|
|
301
|
+
paths: string[];
|
|
302
|
+
}) => {
|
|
303
|
+
calls.push({
|
|
304
|
+
namespace: options.namespace,
|
|
305
|
+
principal: options.principal,
|
|
306
|
+
includeTranscripts: options.includeTranscripts,
|
|
307
|
+
paths: options.paths,
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
namespace: options.namespace ?? "default",
|
|
311
|
+
format: "remnic.offline-sync.snapshot.v1",
|
|
312
|
+
schemaVersion: 1,
|
|
313
|
+
createdAt: new Date("2026-05-21T00:00:00Z").toISOString(),
|
|
314
|
+
sourceId: "remote:test",
|
|
315
|
+
includeTranscripts: options.includeTranscripts !== false,
|
|
316
|
+
files: [],
|
|
317
|
+
};
|
|
318
|
+
},
|
|
319
|
+
} as unknown as EngramAccessService;
|
|
320
|
+
const server = new EngramAccessHttpServer({
|
|
321
|
+
service,
|
|
322
|
+
port: 0,
|
|
323
|
+
authToken: "test-token",
|
|
324
|
+
principal: "reader",
|
|
325
|
+
adminConsoleEnabled: false,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const status = await server.start();
|
|
329
|
+
try {
|
|
330
|
+
const response = await fetch(
|
|
331
|
+
`http://127.0.0.1:${status.port}/remnic/v1/offline-sync/files`,
|
|
332
|
+
{
|
|
333
|
+
method: "POST",
|
|
334
|
+
headers: {
|
|
335
|
+
authorization: "Bearer test-token",
|
|
336
|
+
"content-type": "application/json",
|
|
337
|
+
},
|
|
338
|
+
body: JSON.stringify({
|
|
339
|
+
namespace: "team",
|
|
340
|
+
includeTranscripts: false,
|
|
341
|
+
paths: ["facts/a.md"],
|
|
342
|
+
}),
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
const body = await response.json() as { namespace?: string; includeTranscripts?: boolean; files?: unknown[] };
|
|
346
|
+
|
|
347
|
+
assert.equal(response.status, 200);
|
|
348
|
+
assert.equal(body.namespace, "team");
|
|
349
|
+
assert.equal(body.includeTranscripts, false);
|
|
350
|
+
assert.deepEqual(body.files, []);
|
|
351
|
+
assert.deepEqual(calls, [{
|
|
352
|
+
namespace: "team",
|
|
353
|
+
principal: "reader",
|
|
354
|
+
includeTranscripts: false,
|
|
355
|
+
paths: ["facts/a.md"],
|
|
356
|
+
}]);
|
|
357
|
+
} finally {
|
|
358
|
+
await server.stop();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("HTTP offline snapshot rejects invalid boolean query values", async () => {
|
|
363
|
+
let calls = 0;
|
|
364
|
+
const service = {
|
|
365
|
+
offlineSyncSnapshot: async () => {
|
|
366
|
+
calls += 1;
|
|
367
|
+
return {};
|
|
368
|
+
},
|
|
369
|
+
} as unknown as EngramAccessService;
|
|
370
|
+
const server = new EngramAccessHttpServer({
|
|
371
|
+
service,
|
|
372
|
+
port: 0,
|
|
373
|
+
authToken: "test-token",
|
|
374
|
+
principal: "reader",
|
|
375
|
+
adminConsoleEnabled: false,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const status = await server.start();
|
|
379
|
+
try {
|
|
380
|
+
const response = await fetch(
|
|
381
|
+
`http://127.0.0.1:${status.port}/engram/v1/offline-sync/snapshot?include_transcripts=maybe`,
|
|
382
|
+
{ headers: { authorization: "Bearer test-token" } },
|
|
383
|
+
);
|
|
384
|
+
const body = await response.json() as { error?: string; code?: string };
|
|
385
|
+
|
|
386
|
+
assert.equal(response.status, 400);
|
|
387
|
+
assert.match(body.error ?? "", /include_transcripts must be one of: true, false/);
|
|
388
|
+
assert.equal(body.code, "input_error");
|
|
389
|
+
assert.equal(calls, 0);
|
|
390
|
+
} finally {
|
|
391
|
+
await server.stop();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("HTTP offline apply validates and forwards changesets", async () => {
|
|
396
|
+
const calls: Array<{
|
|
397
|
+
namespace: string | undefined;
|
|
398
|
+
principal: string | undefined;
|
|
399
|
+
changeset: unknown;
|
|
400
|
+
}> = [];
|
|
401
|
+
const changeset = {
|
|
402
|
+
format: "remnic.offline-sync.changeset.v1",
|
|
403
|
+
schemaVersion: 1,
|
|
404
|
+
createdAt: new Date("2026-05-21T00:00:00Z").toISOString(),
|
|
405
|
+
sourceId: "laptop:test",
|
|
406
|
+
includeTranscripts: true,
|
|
407
|
+
changes: [],
|
|
408
|
+
};
|
|
409
|
+
const service = {
|
|
410
|
+
offlineSyncApply: async (options: {
|
|
411
|
+
namespace?: string;
|
|
412
|
+
principal?: string;
|
|
413
|
+
changeset: unknown;
|
|
414
|
+
}) => {
|
|
415
|
+
calls.push({
|
|
416
|
+
namespace: options.namespace,
|
|
417
|
+
principal: options.principal,
|
|
418
|
+
changeset: options.changeset,
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
namespace: options.namespace ?? "default",
|
|
422
|
+
appliedUpserts: 0,
|
|
423
|
+
appliedDeletes: 0,
|
|
424
|
+
skipped: 0,
|
|
425
|
+
conflicts: [],
|
|
426
|
+
currentFiles: [],
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
} as unknown as EngramAccessService;
|
|
430
|
+
const server = new EngramAccessHttpServer({
|
|
431
|
+
service,
|
|
432
|
+
port: 0,
|
|
433
|
+
authToken: "test-token",
|
|
434
|
+
principal: "writer",
|
|
435
|
+
adminConsoleEnabled: false,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const status = await server.start();
|
|
439
|
+
try {
|
|
440
|
+
const response = await fetch(`http://127.0.0.1:${status.port}/remnic/v1/offline-sync/apply`, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: {
|
|
443
|
+
authorization: "Bearer test-token",
|
|
444
|
+
"content-type": "application/json",
|
|
445
|
+
},
|
|
446
|
+
body: JSON.stringify({ namespace: "team", changeset }),
|
|
447
|
+
});
|
|
448
|
+
const body = await response.json() as { namespace?: string; appliedUpserts?: number };
|
|
449
|
+
|
|
450
|
+
assert.equal(response.status, 200);
|
|
451
|
+
assert.equal(body.namespace, "team");
|
|
452
|
+
assert.equal(body.appliedUpserts, 0);
|
|
453
|
+
assert.deepEqual(calls, [{
|
|
454
|
+
namespace: "team",
|
|
455
|
+
principal: "writer",
|
|
456
|
+
changeset,
|
|
457
|
+
}]);
|
|
458
|
+
} finally {
|
|
459
|
+
await server.stop();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("HTTP offline apply requires a changeset", async () => {
|
|
464
|
+
let calls = 0;
|
|
465
|
+
const service = {
|
|
466
|
+
offlineSyncApply: async () => {
|
|
467
|
+
calls += 1;
|
|
468
|
+
return {};
|
|
469
|
+
},
|
|
470
|
+
} as unknown as EngramAccessService;
|
|
471
|
+
const server = new EngramAccessHttpServer({
|
|
472
|
+
service,
|
|
473
|
+
port: 0,
|
|
474
|
+
authToken: "test-token",
|
|
475
|
+
principal: "writer",
|
|
476
|
+
adminConsoleEnabled: false,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const status = await server.start();
|
|
480
|
+
try {
|
|
481
|
+
const response = await fetch(`http://127.0.0.1:${status.port}/engram/v1/offline-sync/apply`, {
|
|
482
|
+
method: "POST",
|
|
483
|
+
headers: {
|
|
484
|
+
authorization: "Bearer test-token",
|
|
485
|
+
"content-type": "application/json",
|
|
486
|
+
},
|
|
487
|
+
body: JSON.stringify({ namespace: "team" }),
|
|
488
|
+
});
|
|
489
|
+
const body = await response.json() as { code?: string; details?: Array<{ field?: string; message?: string }> };
|
|
490
|
+
|
|
491
|
+
assert.equal(response.status, 400);
|
|
492
|
+
assert.equal(body.code, "validation_error");
|
|
493
|
+
assert.equal(body.details?.[0]?.field, "changeset");
|
|
494
|
+
assert.equal(calls, 0);
|
|
495
|
+
|
|
496
|
+
const nullResponse = await fetch(`http://127.0.0.1:${status.port}/engram/v1/offline-sync/apply`, {
|
|
497
|
+
method: "POST",
|
|
498
|
+
headers: {
|
|
499
|
+
authorization: "Bearer test-token",
|
|
500
|
+
"content-type": "application/json",
|
|
501
|
+
},
|
|
502
|
+
body: JSON.stringify({ namespace: "team", changeset: null }),
|
|
503
|
+
});
|
|
504
|
+
const nullBody = await nullResponse.json() as { code?: string; details?: Array<{ field?: string; message?: string }> };
|
|
505
|
+
|
|
506
|
+
assert.equal(nullResponse.status, 400);
|
|
507
|
+
assert.equal(nullBody.code, "validation_error");
|
|
508
|
+
assert.equal(nullBody.details?.[0]?.field, "changeset");
|
|
509
|
+
assert.equal(nullBody.details?.[0]?.message, "changeset is required");
|
|
510
|
+
assert.equal(calls, 0);
|
|
511
|
+
} finally {
|
|
512
|
+
await server.stop();
|
|
513
|
+
}
|
|
514
|
+
});
|
package/src/access-http.ts
CHANGED
|
@@ -588,6 +588,75 @@ 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/files" || pathname === "/remnic/v1/offline-sync/files")
|
|
632
|
+
) {
|
|
633
|
+
const body = await this.readValidatedBody(req, "offlineSyncFiles");
|
|
634
|
+
const result = await this.service.offlineSyncFiles({
|
|
635
|
+
namespace: this.resolveNamespace(req, body.namespace),
|
|
636
|
+
principal: this.resolveRequestPrincipal(req),
|
|
637
|
+
includeTranscripts: body.includeTranscripts,
|
|
638
|
+
paths: body.paths,
|
|
639
|
+
});
|
|
640
|
+
this.respondJson(res, 200, result);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (
|
|
645
|
+
req.method === "POST" &&
|
|
646
|
+
(pathname === "/engram/v1/offline-sync/apply" || pathname === "/remnic/v1/offline-sync/apply")
|
|
647
|
+
) {
|
|
648
|
+
const body = await this.readValidatedBody(req, "offlineSyncApply");
|
|
649
|
+
this.ensureWriteRateLimitAvailable();
|
|
650
|
+
const result = await this.service.offlineSyncApply({
|
|
651
|
+
namespace: this.resolveNamespace(req, body.namespace),
|
|
652
|
+
principal: this.resolveRequestPrincipal(req),
|
|
653
|
+
changeset: body.changeset,
|
|
654
|
+
});
|
|
655
|
+
this.recordWriteRateLimitHit();
|
|
656
|
+
this.respondJson(res, 200, result);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
591
660
|
if (req.method === "POST" && pathname === "/engram/v1/recall/explain") {
|
|
592
661
|
const body = await this.readValidatedBody(req, "recallExplain");
|
|
593
662
|
const response = await this.service.recallExplain({
|
package/src/access-schema.ts
CHANGED
|
@@ -364,6 +364,28 @@ 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
|
+
|
|
381
|
+
export const offlineSyncFilesRequestSchema = z.object({
|
|
382
|
+
namespace: namespaceSchema,
|
|
383
|
+
includeTranscripts: z.boolean().optional(),
|
|
384
|
+
paths: z
|
|
385
|
+
.array(z.string().trim().min(1, "path must be non-empty").max(4096))
|
|
386
|
+
.max(5000, "paths must contain 5000 or fewer entries"),
|
|
387
|
+
});
|
|
388
|
+
|
|
367
389
|
// ---------------------------------------------------------------------------
|
|
368
390
|
// Action confidence
|
|
369
391
|
// ---------------------------------------------------------------------------
|
|
@@ -429,6 +451,8 @@ export type DaySummaryRequest = z.infer<typeof daySummaryRequestSchema>;
|
|
|
429
451
|
export type CapsuleExportRequest = z.infer<typeof capsuleExportRequestSchema>;
|
|
430
452
|
export type CapsuleImportRequest = z.infer<typeof capsuleImportRequestSchema>;
|
|
431
453
|
export type CapsuleListRequest = z.infer<typeof capsuleListRequestSchema>;
|
|
454
|
+
export type OfflineSyncApplyRequest = z.infer<typeof offlineSyncApplyRequestSchema>;
|
|
455
|
+
export type OfflineSyncFilesRequest = z.infer<typeof offlineSyncFilesRequestSchema>;
|
|
432
456
|
export type ActionConfidenceRequest = z.infer<typeof actionConfidenceRequestSchema>;
|
|
433
457
|
|
|
434
458
|
// ---------------------------------------------------------------------------
|
|
@@ -452,6 +476,8 @@ export type SchemaName =
|
|
|
452
476
|
| "capsuleExport"
|
|
453
477
|
| "capsuleImport"
|
|
454
478
|
| "capsuleList"
|
|
479
|
+
| "offlineSyncFiles"
|
|
480
|
+
| "offlineSyncApply"
|
|
455
481
|
| "actionConfidence";
|
|
456
482
|
|
|
457
483
|
export type SchemaTypeFor<N extends SchemaName> =
|
|
@@ -471,6 +497,8 @@ export type SchemaTypeFor<N extends SchemaName> =
|
|
|
471
497
|
: N extends "capsuleExport" ? CapsuleExportRequest
|
|
472
498
|
: N extends "capsuleImport" ? CapsuleImportRequest
|
|
473
499
|
: N extends "capsuleList" ? CapsuleListRequest
|
|
500
|
+
: N extends "offlineSyncFiles" ? OfflineSyncFilesRequest
|
|
501
|
+
: N extends "offlineSyncApply" ? OfflineSyncApplyRequest
|
|
474
502
|
: N extends "actionConfidence" ? ActionConfidenceRequest
|
|
475
503
|
: never;
|
|
476
504
|
|
|
@@ -491,6 +519,8 @@ const schemas: Record<SchemaName, z.ZodTypeAny> = {
|
|
|
491
519
|
capsuleExport: capsuleExportRequestSchema,
|
|
492
520
|
capsuleImport: capsuleImportRequestSchema,
|
|
493
521
|
capsuleList: capsuleListRequestSchema,
|
|
522
|
+
offlineSyncFiles: offlineSyncFilesRequestSchema,
|
|
523
|
+
offlineSyncApply: offlineSyncApplyRequestSchema,
|
|
494
524
|
actionConfidence: actionConfidenceRequestSchema,
|
|
495
525
|
};
|
|
496
526
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm, symlink } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import test from "node:test";
|
|
3
6
|
|
|
4
|
-
import { EngramAccessService } from "./access-service.js";
|
|
7
|
+
import { EngramAccessInputError, EngramAccessService } from "./access-service.js";
|
|
5
8
|
import type { StorageManager } from "./storage.js";
|
|
6
9
|
import type { PluginConfig } from "./types.js";
|
|
7
10
|
|
|
@@ -121,3 +124,63 @@ test("memoryBrowse resolves namespace storage for read principals", async () =>
|
|
|
121
124
|
assert.equal(result.count, 0);
|
|
122
125
|
assert.deepEqual(getStorageCalls, ["team"]);
|
|
123
126
|
});
|
|
127
|
+
|
|
128
|
+
test("offlineSyncFiles reports invalid requested paths as input errors", async () => {
|
|
129
|
+
const { service } = makeService();
|
|
130
|
+
(service as unknown as {
|
|
131
|
+
orchestrator: {
|
|
132
|
+
config: PluginConfig;
|
|
133
|
+
getStorage(namespace: string): Promise<StorageManager>;
|
|
134
|
+
};
|
|
135
|
+
}).orchestrator.getStorage = async () => ({
|
|
136
|
+
dir: os.tmpdir(),
|
|
137
|
+
async readOfflineSyncFile() {
|
|
138
|
+
throw new Error("should not read invalid paths");
|
|
139
|
+
},
|
|
140
|
+
} as unknown as StorageManager);
|
|
141
|
+
|
|
142
|
+
await assert.rejects(
|
|
143
|
+
() =>
|
|
144
|
+
service.offlineSyncFiles({
|
|
145
|
+
namespace: "team",
|
|
146
|
+
principal: "reader",
|
|
147
|
+
paths: ["../escape"],
|
|
148
|
+
}),
|
|
149
|
+
(error: unknown) =>
|
|
150
|
+
error instanceof EngramAccessInputError &&
|
|
151
|
+
/paths\[\]: record path contains unsafe segments/.test(error.message),
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("offlineSyncFiles reports symlink requested paths as input errors", async () => {
|
|
156
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-files-symlink-"));
|
|
157
|
+
try {
|
|
158
|
+
await symlink("/tmp", path.join(root, "linked"));
|
|
159
|
+
const { service } = makeService();
|
|
160
|
+
(service as unknown as {
|
|
161
|
+
orchestrator: {
|
|
162
|
+
config: PluginConfig;
|
|
163
|
+
getStorage(namespace: string): Promise<StorageManager>;
|
|
164
|
+
};
|
|
165
|
+
}).orchestrator.getStorage = async () => ({
|
|
166
|
+
dir: root,
|
|
167
|
+
async readOfflineSyncFile() {
|
|
168
|
+
throw new Error("should not read symlink paths");
|
|
169
|
+
},
|
|
170
|
+
} as unknown as StorageManager);
|
|
171
|
+
|
|
172
|
+
await assert.rejects(
|
|
173
|
+
() =>
|
|
174
|
+
service.offlineSyncFiles({
|
|
175
|
+
namespace: "team",
|
|
176
|
+
principal: "reader",
|
|
177
|
+
paths: ["linked"],
|
|
178
|
+
}),
|
|
179
|
+
(error: unknown) =>
|
|
180
|
+
error instanceof EngramAccessInputError &&
|
|
181
|
+
/buildOfflineSyncSnapshotForPaths: record path targets a symlink/.test(error.message),
|
|
182
|
+
);
|
|
183
|
+
} finally {
|
|
184
|
+
await rm(root, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
package/src/access-service.ts
CHANGED
|
@@ -135,6 +135,13 @@ import {
|
|
|
135
135
|
defaultCapsulesDir,
|
|
136
136
|
type CapsuleListEntry,
|
|
137
137
|
} from "./capsule-cli.js";
|
|
138
|
+
import {
|
|
139
|
+
applyOfflineSyncChangeset,
|
|
140
|
+
buildOfflineSyncSnapshot,
|
|
141
|
+
buildOfflineSyncSnapshotForPaths,
|
|
142
|
+
type OfflineSyncApplyChangesetResult,
|
|
143
|
+
type OfflineSyncSnapshot,
|
|
144
|
+
} from "./offline-sync.js";
|
|
138
145
|
import {
|
|
139
146
|
evaluateActionConfidence,
|
|
140
147
|
type ActionConfidenceInput,
|
|
@@ -603,6 +610,38 @@ export interface EngramAccessCapsuleListResponse {
|
|
|
603
610
|
capsules: CapsuleListEntry[];
|
|
604
611
|
}
|
|
605
612
|
|
|
613
|
+
export interface EngramAccessOfflineSyncSnapshotRequest {
|
|
614
|
+
namespace?: string;
|
|
615
|
+
principal?: string;
|
|
616
|
+
includeTranscripts?: boolean;
|
|
617
|
+
includeContent?: boolean;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export interface EngramAccessOfflineSyncFilesRequest {
|
|
621
|
+
namespace?: string;
|
|
622
|
+
principal?: string;
|
|
623
|
+
includeTranscripts?: boolean;
|
|
624
|
+
paths: string[];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export interface EngramAccessOfflineSyncApplyRequest {
|
|
628
|
+
namespace?: string;
|
|
629
|
+
principal?: string;
|
|
630
|
+
changeset: unknown;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export interface EngramAccessOfflineSyncSnapshotResponse extends OfflineSyncSnapshot {
|
|
634
|
+
namespace: string;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export interface EngramAccessOfflineSyncFilesResponse extends OfflineSyncSnapshot {
|
|
638
|
+
namespace: string;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export interface EngramAccessOfflineSyncApplyResponse extends OfflineSyncApplyChangesetResult {
|
|
642
|
+
namespace: string;
|
|
643
|
+
}
|
|
644
|
+
|
|
606
645
|
export type EngramAccessActionConfidenceRequest = ActionConfidenceInput;
|
|
607
646
|
export type EngramAccessActionConfidenceResponse = ActionConfidenceResult;
|
|
608
647
|
|
|
@@ -5522,6 +5561,87 @@ export class EngramAccessService {
|
|
|
5522
5561
|
return { namespace: resolvedNamespace, capsulesDir, capsules };
|
|
5523
5562
|
}
|
|
5524
5563
|
|
|
5564
|
+
async offlineSyncSnapshot(
|
|
5565
|
+
options: EngramAccessOfflineSyncSnapshotRequest = {},
|
|
5566
|
+
): Promise<EngramAccessOfflineSyncSnapshotResponse> {
|
|
5567
|
+
const resolvedNamespace = this.resolveReadableNamespace(options.namespace, options.principal);
|
|
5568
|
+
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
5569
|
+
const storageHash = createHash("sha256").update(storage.dir).digest("hex").slice(0, 16);
|
|
5570
|
+
const snapshot = await buildOfflineSyncSnapshot({
|
|
5571
|
+
root: storage.dir,
|
|
5572
|
+
sourceId: `remnic:${resolvedNamespace}:${storageHash}`,
|
|
5573
|
+
includeContent: options.includeContent !== false,
|
|
5574
|
+
includeTranscripts: options.includeTranscripts !== false,
|
|
5575
|
+
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
5576
|
+
});
|
|
5577
|
+
return {
|
|
5578
|
+
namespace: resolvedNamespace,
|
|
5579
|
+
...snapshot,
|
|
5580
|
+
};
|
|
5581
|
+
}
|
|
5582
|
+
|
|
5583
|
+
async offlineSyncFiles(
|
|
5584
|
+
options: EngramAccessOfflineSyncFilesRequest,
|
|
5585
|
+
): Promise<EngramAccessOfflineSyncFilesResponse> {
|
|
5586
|
+
const resolvedNamespace = this.resolveReadableNamespace(options.namespace, options.principal);
|
|
5587
|
+
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
5588
|
+
const storageHash = createHash("sha256").update(storage.dir).digest("hex").slice(0, 16);
|
|
5589
|
+
try {
|
|
5590
|
+
const snapshot = await buildOfflineSyncSnapshotForPaths({
|
|
5591
|
+
root: storage.dir,
|
|
5592
|
+
sourceId: `remnic:${resolvedNamespace}:${storageHash}`,
|
|
5593
|
+
paths: options.paths,
|
|
5594
|
+
includeContent: true,
|
|
5595
|
+
includeTranscripts: options.includeTranscripts !== false,
|
|
5596
|
+
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
5597
|
+
});
|
|
5598
|
+
return {
|
|
5599
|
+
namespace: resolvedNamespace,
|
|
5600
|
+
...snapshot,
|
|
5601
|
+
};
|
|
5602
|
+
} catch (error) {
|
|
5603
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5604
|
+
if (
|
|
5605
|
+
message.startsWith("paths[]:") ||
|
|
5606
|
+
message.startsWith("buildOfflineSyncSnapshotForPaths: record path ") ||
|
|
5607
|
+
message.startsWith("offline sync snapshot path is excluded:")
|
|
5608
|
+
) {
|
|
5609
|
+
throw new EngramAccessInputError(message);
|
|
5610
|
+
}
|
|
5611
|
+
throw error;
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
|
|
5615
|
+
async offlineSyncApply(
|
|
5616
|
+
options: EngramAccessOfflineSyncApplyRequest,
|
|
5617
|
+
): Promise<EngramAccessOfflineSyncApplyResponse> {
|
|
5618
|
+
const resolvedNamespace = this.resolveWritableNamespace(
|
|
5619
|
+
options.namespace,
|
|
5620
|
+
undefined,
|
|
5621
|
+
options.principal,
|
|
5622
|
+
);
|
|
5623
|
+
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
5624
|
+
try {
|
|
5625
|
+
const result = await applyOfflineSyncChangeset({
|
|
5626
|
+
root: storage.dir,
|
|
5627
|
+
changeset: options.changeset,
|
|
5628
|
+
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
5629
|
+
writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
|
|
5630
|
+
deleteFile: async ({ filePath }) => storage.deleteOfflineSyncFile(filePath),
|
|
5631
|
+
});
|
|
5632
|
+
return {
|
|
5633
|
+
namespace: resolvedNamespace,
|
|
5634
|
+
...result,
|
|
5635
|
+
};
|
|
5636
|
+
} catch (error) {
|
|
5637
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5638
|
+
if (message.startsWith("offline sync")) {
|
|
5639
|
+
throw new EngramAccessInputError(message);
|
|
5640
|
+
}
|
|
5641
|
+
throw error;
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
|
|
5525
5645
|
// ── Dreams pipeline telemetry surfaces (issue #678 PR 3+4) ──────────────
|
|
5526
5646
|
|
|
5527
5647
|
/**
|
package/src/index.ts
CHANGED
|
@@ -674,6 +674,40 @@ 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
|
+
buildOfflineSyncSnapshotForPaths,
|
|
690
|
+
defaultOfflineSyncStatePath,
|
|
691
|
+
fileStatesFromSnapshot,
|
|
692
|
+
normalizeOfflineSyncChangeset,
|
|
693
|
+
normalizeOfflineSyncSnapshot,
|
|
694
|
+
offlineSyncStateFromSnapshot,
|
|
695
|
+
readOfflineSyncState,
|
|
696
|
+
summarizeOfflineSyncChangeset,
|
|
697
|
+
writeOfflineSyncState,
|
|
698
|
+
type OfflineSyncApplyChangesetResult,
|
|
699
|
+
type OfflineSyncApplySnapshotResult,
|
|
700
|
+
type OfflineSyncChange,
|
|
701
|
+
type OfflineSyncChangeset,
|
|
702
|
+
type OfflineSyncConflict,
|
|
703
|
+
type OfflineSyncFileRecord,
|
|
704
|
+
type OfflineSyncFileState,
|
|
705
|
+
type OfflineSyncFileTarget,
|
|
706
|
+
type OfflineSyncFileWriteTarget,
|
|
707
|
+
type OfflineSyncSnapshot,
|
|
708
|
+
type OfflineSyncState,
|
|
709
|
+
} from "./offline-sync.js";
|
|
710
|
+
|
|
677
711
|
// ---------------------------------------------------------------------------
|
|
678
712
|
// Memory Extension Host (#382)
|
|
679
713
|
// ---------------------------------------------------------------------------
|