@remnic/core 1.1.21 → 1.1.23
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 +15 -15
- package/dist/access-http.d.ts +9 -1
- package/dist/access-http.js +9 -9
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +8 -8
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-DT9L2DW4.d.ts → access-service-CEyV8XJ5.d.ts} +19 -2
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +6 -6
- package/dist/briefing.js +3 -3
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-YO3AZEE5.js → chunk-25YQM6XW.js} +3 -3
- package/dist/{chunk-TLM762GT.js → chunk-2WIPXV3Y.js} +2 -2
- package/dist/{chunk-QOHBYVZG.js → chunk-3F24QTRI.js} +2 -2
- package/dist/{chunk-5IQC4OG6.js → chunk-4H6DURG6.js} +2 -2
- package/dist/{chunk-NOQ74SJN.js → chunk-7D6O46PF.js} +2 -2
- package/dist/{chunk-SLKSC522.js → chunk-7E7SZRPP.js} +2 -2
- package/dist/{chunk-7Q2P774N.js → chunk-F33CJ5CH.js} +13 -3
- package/dist/chunk-F33CJ5CH.js.map +1 -0
- package/dist/{chunk-APW7AQOJ.js → chunk-FHXVW3L4.js} +4 -4
- package/dist/{chunk-PFFKUJM2.js → chunk-HWF42K6J.js} +103 -4
- package/dist/chunk-HWF42K6J.js.map +1 -0
- package/dist/{chunk-FSODDMR2.js → chunk-IANK6Y5W.js} +2 -2
- package/dist/{chunk-BYYIIXIJ.js → chunk-JKXFF3NT.js} +361 -29
- package/dist/chunk-JKXFF3NT.js.map +1 -0
- package/dist/{chunk-P7F6DJPA.js → chunk-MM5EBZVW.js} +42 -5
- package/dist/chunk-MM5EBZVW.js.map +1 -0
- package/dist/{chunk-GGCJ253V.js → chunk-MVAOT247.js} +8 -8
- package/dist/{chunk-SH5S7XYD.js → chunk-MXFBBHJU.js} +72 -2
- package/dist/chunk-MXFBBHJU.js.map +1 -0
- package/dist/{chunk-SZKCBLS5.js → chunk-PUXCIHRL.js} +2 -2
- package/dist/{chunk-2IRT26RZ.js → chunk-QYHQ2JHL.js} +2 -2
- package/dist/{chunk-73DAPA62.js → chunk-RA73CTVY.js} +12 -12
- package/dist/{chunk-CN4P6SVA.js → chunk-RCZRL5BE.js} +2 -2
- package/dist/{chunk-SGIXDVSF.js → chunk-S27EXIHY.js} +2 -2
- package/dist/{chunk-5ML4TH3E.js → chunk-TFORLO3O.js} +4 -4
- package/dist/{chunk-TOFUTKQN.js → chunk-TR4DK5OH.js} +2 -2
- package/dist/{chunk-6ORWKANA.js → chunk-VYU7PXUS.js} +2 -2
- package/dist/{chunk-FFU4GMST.js → chunk-WNARATI3.js} +2 -2
- package/dist/{chunk-KSFBM6TV.js → chunk-YITUHONZ.js} +2 -2
- package/dist/{cli-BN0CkYzI.d.ts → cli-BguVmIwO.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +18 -18
- package/dist/compounding/engine.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/entity-retrieval.js +3 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +26 -24
- package/dist/index.js.map +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/storage.js +3 -3
- package/dist/offline-sync.d.ts +36 -1
- package/dist/offline-sync.js +4 -2
- package/dist/operator-toolkit.js +6 -6
- package/dist/orchestrator.js +11 -11
- package/dist/schemas.d.ts +22 -22
- package/dist/secure-store/index.js +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/access-http.test.ts +176 -0
- package/src/access-http.ts +116 -0
- package/src/access-service-offline-file-content.test.ts +37 -0
- package/src/access-service.ts +70 -0
- package/src/index.ts +2 -0
- package/src/offline-sync.test.ts +448 -64
- package/src/offline-sync.ts +477 -29
- package/src/secure-store/secure-fs.ts +84 -3
- package/src/storage.ts +12 -0
- package/dist/chunk-7Q2P774N.js.map +0 -1
- package/dist/chunk-BYYIIXIJ.js.map +0 -1
- package/dist/chunk-P7F6DJPA.js.map +0 -1
- package/dist/chunk-PFFKUJM2.js.map +0 -1
- package/dist/chunk-SH5S7XYD.js.map +0 -1
- /package/dist/{chunk-YO3AZEE5.js.map → chunk-25YQM6XW.js.map} +0 -0
- /package/dist/{chunk-TLM762GT.js.map → chunk-2WIPXV3Y.js.map} +0 -0
- /package/dist/{chunk-QOHBYVZG.js.map → chunk-3F24QTRI.js.map} +0 -0
- /package/dist/{chunk-5IQC4OG6.js.map → chunk-4H6DURG6.js.map} +0 -0
- /package/dist/{chunk-NOQ74SJN.js.map → chunk-7D6O46PF.js.map} +0 -0
- /package/dist/{chunk-SLKSC522.js.map → chunk-7E7SZRPP.js.map} +0 -0
- /package/dist/{chunk-APW7AQOJ.js.map → chunk-FHXVW3L4.js.map} +0 -0
- /package/dist/{chunk-FSODDMR2.js.map → chunk-IANK6Y5W.js.map} +0 -0
- /package/dist/{chunk-GGCJ253V.js.map → chunk-MVAOT247.js.map} +0 -0
- /package/dist/{chunk-SZKCBLS5.js.map → chunk-PUXCIHRL.js.map} +0 -0
- /package/dist/{chunk-2IRT26RZ.js.map → chunk-QYHQ2JHL.js.map} +0 -0
- /package/dist/{chunk-73DAPA62.js.map → chunk-RA73CTVY.js.map} +0 -0
- /package/dist/{chunk-CN4P6SVA.js.map → chunk-RCZRL5BE.js.map} +0 -0
- /package/dist/{chunk-SGIXDVSF.js.map → chunk-S27EXIHY.js.map} +0 -0
- /package/dist/{chunk-5ML4TH3E.js.map → chunk-TFORLO3O.js.map} +0 -0
- /package/dist/{chunk-TOFUTKQN.js.map → chunk-TR4DK5OH.js.map} +0 -0
- /package/dist/{chunk-6ORWKANA.js.map → chunk-VYU7PXUS.js.map} +0 -0
- /package/dist/{chunk-FFU4GMST.js.map → chunk-WNARATI3.js.map} +0 -0
- /package/dist/{chunk-KSFBM6TV.js.map → chunk-YITUHONZ.js.map} +0 -0
package/dist/transfer/types.d.ts
CHANGED
|
@@ -313,13 +313,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
|
|
|
313
313
|
peerProfiles: boolean;
|
|
314
314
|
}>;
|
|
315
315
|
}, "strip", z.ZodTypeAny, {
|
|
316
|
-
schemaVersion: string;
|
|
317
316
|
includes: {
|
|
318
317
|
procedural: boolean;
|
|
319
318
|
taxonomy: boolean;
|
|
320
319
|
identityAnchors: boolean;
|
|
321
320
|
peerProfiles: boolean;
|
|
322
321
|
};
|
|
322
|
+
schemaVersion: string;
|
|
323
323
|
id: string;
|
|
324
324
|
description: string;
|
|
325
325
|
version: string;
|
|
@@ -334,13 +334,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
|
|
|
334
334
|
directAnswerEnabled: boolean;
|
|
335
335
|
};
|
|
336
336
|
}, {
|
|
337
|
-
schemaVersion: string;
|
|
338
337
|
includes: {
|
|
339
338
|
procedural: boolean;
|
|
340
339
|
taxonomy: boolean;
|
|
341
340
|
identityAnchors: boolean;
|
|
342
341
|
peerProfiles: boolean;
|
|
343
342
|
};
|
|
343
|
+
schemaVersion: string;
|
|
344
344
|
id: string;
|
|
345
345
|
description: string;
|
|
346
346
|
version: string;
|
|
@@ -464,13 +464,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
|
|
|
464
464
|
peerProfiles: boolean;
|
|
465
465
|
}>;
|
|
466
466
|
}, "strip", z.ZodTypeAny, {
|
|
467
|
-
schemaVersion: string;
|
|
468
467
|
includes: {
|
|
469
468
|
procedural: boolean;
|
|
470
469
|
taxonomy: boolean;
|
|
471
470
|
identityAnchors: boolean;
|
|
472
471
|
peerProfiles: boolean;
|
|
473
472
|
};
|
|
473
|
+
schemaVersion: string;
|
|
474
474
|
id: string;
|
|
475
475
|
description: string;
|
|
476
476
|
version: string;
|
|
@@ -485,13 +485,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
|
|
|
485
485
|
directAnswerEnabled: boolean;
|
|
486
486
|
};
|
|
487
487
|
}, {
|
|
488
|
-
schemaVersion: string;
|
|
489
488
|
includes: {
|
|
490
489
|
procedural: boolean;
|
|
491
490
|
taxonomy: boolean;
|
|
492
491
|
identityAnchors: boolean;
|
|
493
492
|
peerProfiles: boolean;
|
|
494
493
|
};
|
|
494
|
+
schemaVersion: string;
|
|
495
495
|
id: string;
|
|
496
496
|
description: string;
|
|
497
497
|
version: string;
|
|
@@ -518,13 +518,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
|
|
|
518
518
|
pluginVersion: string;
|
|
519
519
|
includesTranscripts: boolean;
|
|
520
520
|
capsule: {
|
|
521
|
-
schemaVersion: string;
|
|
522
521
|
includes: {
|
|
523
522
|
procedural: boolean;
|
|
524
523
|
taxonomy: boolean;
|
|
525
524
|
identityAnchors: boolean;
|
|
526
525
|
peerProfiles: boolean;
|
|
527
526
|
};
|
|
527
|
+
schemaVersion: string;
|
|
528
528
|
id: string;
|
|
529
529
|
description: string;
|
|
530
530
|
version: string;
|
|
@@ -551,13 +551,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
|
|
|
551
551
|
pluginVersion: string;
|
|
552
552
|
includesTranscripts: boolean;
|
|
553
553
|
capsule: {
|
|
554
|
-
schemaVersion: string;
|
|
555
554
|
includes: {
|
|
556
555
|
procedural: boolean;
|
|
557
556
|
taxonomy: boolean;
|
|
558
557
|
identityAnchors: boolean;
|
|
559
558
|
peerProfiles: boolean;
|
|
560
559
|
};
|
|
560
|
+
schemaVersion: string;
|
|
561
561
|
id: string;
|
|
562
562
|
description: string;
|
|
563
563
|
version: string;
|
|
@@ -683,13 +683,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
683
683
|
peerProfiles: boolean;
|
|
684
684
|
}>;
|
|
685
685
|
}, "strip", z.ZodTypeAny, {
|
|
686
|
-
schemaVersion: string;
|
|
687
686
|
includes: {
|
|
688
687
|
procedural: boolean;
|
|
689
688
|
taxonomy: boolean;
|
|
690
689
|
identityAnchors: boolean;
|
|
691
690
|
peerProfiles: boolean;
|
|
692
691
|
};
|
|
692
|
+
schemaVersion: string;
|
|
693
693
|
id: string;
|
|
694
694
|
description: string;
|
|
695
695
|
version: string;
|
|
@@ -704,13 +704,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
704
704
|
directAnswerEnabled: boolean;
|
|
705
705
|
};
|
|
706
706
|
}, {
|
|
707
|
-
schemaVersion: string;
|
|
708
707
|
includes: {
|
|
709
708
|
procedural: boolean;
|
|
710
709
|
taxonomy: boolean;
|
|
711
710
|
identityAnchors: boolean;
|
|
712
711
|
peerProfiles: boolean;
|
|
713
712
|
};
|
|
713
|
+
schemaVersion: string;
|
|
714
714
|
id: string;
|
|
715
715
|
description: string;
|
|
716
716
|
version: string;
|
|
@@ -737,13 +737,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
737
737
|
pluginVersion: string;
|
|
738
738
|
includesTranscripts: boolean;
|
|
739
739
|
capsule: {
|
|
740
|
-
schemaVersion: string;
|
|
741
740
|
includes: {
|
|
742
741
|
procedural: boolean;
|
|
743
742
|
taxonomy: boolean;
|
|
744
743
|
identityAnchors: boolean;
|
|
745
744
|
peerProfiles: boolean;
|
|
746
745
|
};
|
|
746
|
+
schemaVersion: string;
|
|
747
747
|
id: string;
|
|
748
748
|
description: string;
|
|
749
749
|
version: string;
|
|
@@ -770,13 +770,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
770
770
|
pluginVersion: string;
|
|
771
771
|
includesTranscripts: boolean;
|
|
772
772
|
capsule: {
|
|
773
|
-
schemaVersion: string;
|
|
774
773
|
includes: {
|
|
775
774
|
procedural: boolean;
|
|
776
775
|
taxonomy: boolean;
|
|
777
776
|
identityAnchors: boolean;
|
|
778
777
|
peerProfiles: boolean;
|
|
779
778
|
};
|
|
779
|
+
schemaVersion: string;
|
|
780
780
|
id: string;
|
|
781
781
|
description: string;
|
|
782
782
|
version: string;
|
|
@@ -815,13 +815,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
815
815
|
pluginVersion: string;
|
|
816
816
|
includesTranscripts: boolean;
|
|
817
817
|
capsule: {
|
|
818
|
-
schemaVersion: string;
|
|
819
818
|
includes: {
|
|
820
819
|
procedural: boolean;
|
|
821
820
|
taxonomy: boolean;
|
|
822
821
|
identityAnchors: boolean;
|
|
823
822
|
peerProfiles: boolean;
|
|
824
823
|
};
|
|
824
|
+
schemaVersion: string;
|
|
825
825
|
id: string;
|
|
826
826
|
description: string;
|
|
827
827
|
version: string;
|
|
@@ -854,13 +854,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
|
|
|
854
854
|
pluginVersion: string;
|
|
855
855
|
includesTranscripts: boolean;
|
|
856
856
|
capsule: {
|
|
857
|
-
schemaVersion: string;
|
|
858
857
|
includes: {
|
|
859
858
|
procedural: boolean;
|
|
860
859
|
taxonomy: boolean;
|
|
861
860
|
identityAnchors: boolean;
|
|
862
861
|
peerProfiles: boolean;
|
|
863
862
|
};
|
|
863
|
+
schemaVersion: string;
|
|
864
864
|
id: string;
|
|
865
865
|
description: string;
|
|
866
866
|
version: string;
|
package/dist/verified-recall.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
searchVerifiedEpisodes
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-4H6DURG6.js";
|
|
4
4
|
import "./chunk-NZL6GGQE.js";
|
|
5
|
-
import "./chunk-
|
|
5
|
+
import "./chunk-F33CJ5CH.js";
|
|
6
6
|
import "./chunk-5UZXUTVO.js";
|
|
7
7
|
import "./chunk-NN2DKE4T.js";
|
|
8
8
|
import "./chunk-Q7P4WJDP.js";
|
|
@@ -19,7 +19,7 @@ import "./chunk-FAAFWE4G.js";
|
|
|
19
19
|
import "./chunk-DT5TVLJE.js";
|
|
20
20
|
import "./chunk-4DJQYKMN.js";
|
|
21
21
|
import "./chunk-2ODBA7MQ.js";
|
|
22
|
-
import "./chunk-
|
|
22
|
+
import "./chunk-MXFBBHJU.js";
|
|
23
23
|
import "./chunk-A6XUJE5D.js";
|
|
24
24
|
import "./chunk-P7FMDTKL.js";
|
|
25
25
|
import "./chunk-PZ5AY32C.js";
|
package/package.json
CHANGED
package/src/access-http.test.ts
CHANGED
|
@@ -449,6 +449,182 @@ test("HTTP offline file-content forwards range options and returns binary metada
|
|
|
449
449
|
}
|
|
450
450
|
});
|
|
451
451
|
|
|
452
|
+
test("HTTP offline apply-file-content forwards binary chunks and metadata", async () => {
|
|
453
|
+
const calls: Array<{
|
|
454
|
+
namespace: string | undefined;
|
|
455
|
+
principal: string | undefined;
|
|
456
|
+
includeTranscripts: boolean | undefined;
|
|
457
|
+
sourceId: string;
|
|
458
|
+
path: string;
|
|
459
|
+
sha256: string;
|
|
460
|
+
bytes: number;
|
|
461
|
+
mtimeMs: number;
|
|
462
|
+
offset: number | undefined;
|
|
463
|
+
baseSha256: string | undefined;
|
|
464
|
+
content: string;
|
|
465
|
+
}> = [];
|
|
466
|
+
const service = {
|
|
467
|
+
offlineSyncApplyFileContent: async (options: {
|
|
468
|
+
namespace?: string;
|
|
469
|
+
principal?: string;
|
|
470
|
+
includeTranscripts?: boolean;
|
|
471
|
+
sourceId: string;
|
|
472
|
+
path: string;
|
|
473
|
+
sha256: string;
|
|
474
|
+
bytes: number;
|
|
475
|
+
mtimeMs: number;
|
|
476
|
+
offset?: number;
|
|
477
|
+
baseSha256?: string;
|
|
478
|
+
content: Buffer;
|
|
479
|
+
}) => {
|
|
480
|
+
calls.push({
|
|
481
|
+
namespace: options.namespace,
|
|
482
|
+
principal: options.principal,
|
|
483
|
+
includeTranscripts: options.includeTranscripts,
|
|
484
|
+
sourceId: options.sourceId,
|
|
485
|
+
path: options.path,
|
|
486
|
+
sha256: options.sha256,
|
|
487
|
+
bytes: options.bytes,
|
|
488
|
+
mtimeMs: options.mtimeMs,
|
|
489
|
+
offset: options.offset,
|
|
490
|
+
baseSha256: options.baseSha256,
|
|
491
|
+
content: options.content.toString("utf-8"),
|
|
492
|
+
});
|
|
493
|
+
return {
|
|
494
|
+
namespace: options.namespace ?? "default",
|
|
495
|
+
path: options.path,
|
|
496
|
+
sha256: options.sha256,
|
|
497
|
+
bytes: options.bytes,
|
|
498
|
+
mtimeMs: options.mtimeMs,
|
|
499
|
+
offset: options.offset ?? 0,
|
|
500
|
+
chunkBytes: options.content.length,
|
|
501
|
+
done: true,
|
|
502
|
+
applied: true,
|
|
503
|
+
skipped: false,
|
|
504
|
+
};
|
|
505
|
+
},
|
|
506
|
+
} as unknown as EngramAccessService;
|
|
507
|
+
const server = new EngramAccessHttpServer({
|
|
508
|
+
service,
|
|
509
|
+
port: 0,
|
|
510
|
+
authToken: "test-token",
|
|
511
|
+
principal: "writer",
|
|
512
|
+
adminConsoleEnabled: false,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const status = await server.start();
|
|
516
|
+
try {
|
|
517
|
+
const response = await fetch(
|
|
518
|
+
`http://127.0.0.1:${status.port}/remnic/v1/offline-sync/apply-file-content?namespace=team`,
|
|
519
|
+
{
|
|
520
|
+
method: "POST",
|
|
521
|
+
headers: {
|
|
522
|
+
authorization: "Bearer test-token",
|
|
523
|
+
"content-type": "application/octet-stream",
|
|
524
|
+
"x-remnic-include-transcripts": "false",
|
|
525
|
+
"x-remnic-source-id": encodeURIComponent("laptop:test"),
|
|
526
|
+
"x-remnic-file-path": encodeURIComponent("state/lcm.sqlite"),
|
|
527
|
+
"x-remnic-file-sha256": "b".repeat(64),
|
|
528
|
+
"x-remnic-file-bytes": "5",
|
|
529
|
+
"x-remnic-file-mtime-ms": "1234",
|
|
530
|
+
"x-remnic-chunk-offset": "8",
|
|
531
|
+
"x-remnic-base-sha256": "a".repeat(64),
|
|
532
|
+
},
|
|
533
|
+
body: Buffer.from("hello"),
|
|
534
|
+
},
|
|
535
|
+
);
|
|
536
|
+
const body = await response.json() as { namespace?: string; applied?: boolean; chunkBytes?: number };
|
|
537
|
+
|
|
538
|
+
assert.equal(response.status, 200);
|
|
539
|
+
assert.equal(body.namespace, "team");
|
|
540
|
+
assert.equal(body.applied, true);
|
|
541
|
+
assert.equal(body.chunkBytes, 5);
|
|
542
|
+
assert.deepEqual(calls, [{
|
|
543
|
+
namespace: "team",
|
|
544
|
+
principal: "writer",
|
|
545
|
+
includeTranscripts: false,
|
|
546
|
+
sourceId: "laptop:test",
|
|
547
|
+
path: "state/lcm.sqlite",
|
|
548
|
+
sha256: "b".repeat(64),
|
|
549
|
+
bytes: 5,
|
|
550
|
+
mtimeMs: 1234,
|
|
551
|
+
offset: 8,
|
|
552
|
+
baseSha256: "a".repeat(64),
|
|
553
|
+
content: "hello",
|
|
554
|
+
}]);
|
|
555
|
+
} finally {
|
|
556
|
+
await server.stop();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("HTTP offline apply-file-content rate limits accepted chunks", async () => {
|
|
561
|
+
let calls = 0;
|
|
562
|
+
const service = {
|
|
563
|
+
offlineSyncApplyFileContent: async (options: {
|
|
564
|
+
namespace?: string;
|
|
565
|
+
path: string;
|
|
566
|
+
sha256: string;
|
|
567
|
+
bytes: number;
|
|
568
|
+
mtimeMs: number;
|
|
569
|
+
offset?: number;
|
|
570
|
+
content: Buffer;
|
|
571
|
+
}) => {
|
|
572
|
+
calls += 1;
|
|
573
|
+
return {
|
|
574
|
+
namespace: options.namespace ?? "default",
|
|
575
|
+
path: options.path,
|
|
576
|
+
sha256: options.sha256,
|
|
577
|
+
bytes: options.bytes,
|
|
578
|
+
mtimeMs: options.mtimeMs,
|
|
579
|
+
offset: options.offset ?? 0,
|
|
580
|
+
chunkBytes: options.content.length,
|
|
581
|
+
done: true,
|
|
582
|
+
applied: true,
|
|
583
|
+
skipped: false,
|
|
584
|
+
};
|
|
585
|
+
},
|
|
586
|
+
} as unknown as EngramAccessService;
|
|
587
|
+
const server = new EngramAccessHttpServer({
|
|
588
|
+
service,
|
|
589
|
+
port: 0,
|
|
590
|
+
authToken: "test-token",
|
|
591
|
+
principal: "writer",
|
|
592
|
+
adminConsoleEnabled: false,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const status = await server.start();
|
|
596
|
+
try {
|
|
597
|
+
let lastStatus = 0;
|
|
598
|
+
for (let i = 0; i < 31; i += 1) {
|
|
599
|
+
const response = await fetch(
|
|
600
|
+
`http://127.0.0.1:${status.port}/remnic/v1/offline-sync/apply-file-content?namespace=team`,
|
|
601
|
+
{
|
|
602
|
+
method: "POST",
|
|
603
|
+
headers: {
|
|
604
|
+
authorization: "Bearer test-token",
|
|
605
|
+
"content-type": "application/octet-stream",
|
|
606
|
+
"x-remnic-source-id": encodeURIComponent("laptop:test"),
|
|
607
|
+
"x-remnic-file-path": encodeURIComponent(`state/file-${i}.bin`),
|
|
608
|
+
"x-remnic-file-sha256": "b".repeat(64),
|
|
609
|
+
"x-remnic-file-bytes": "5",
|
|
610
|
+
"x-remnic-file-mtime-ms": "1234",
|
|
611
|
+
"x-remnic-chunk-offset": "0",
|
|
612
|
+
},
|
|
613
|
+
body: new Blob([new Uint8Array(Buffer.from("hello"))]),
|
|
614
|
+
},
|
|
615
|
+
);
|
|
616
|
+
lastStatus = response.status;
|
|
617
|
+
if (!response.ok) break;
|
|
618
|
+
await response.arrayBuffer();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
assert.equal(lastStatus, 429);
|
|
622
|
+
assert.equal(calls, 30);
|
|
623
|
+
} finally {
|
|
624
|
+
await server.stop();
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
452
628
|
test("HTTP offline snapshot rejects invalid boolean query values", async () => {
|
|
453
629
|
let calls = 0;
|
|
454
630
|
const service = {
|
package/src/access-http.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { log } from "./logger.js";
|
|
|
9
9
|
import { EngramAccessInputError, type EngramAccessService } from "./access-service.js";
|
|
10
10
|
import { EngramMcpServer } from "./access-mcp.js";
|
|
11
11
|
import { validateRequest, type SchemaName, type SchemaTypeFor } from "./access-schema.js";
|
|
12
|
+
import { OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES } from "./offline-sync.js";
|
|
12
13
|
import type { RecallDisclosure, RecallPlanMode } from "./types.js";
|
|
13
14
|
import { isRecallDisclosure } from "./types.js";
|
|
14
15
|
import { isTrustZoneName, type TrustZoneName, type TrustZoneRecordKind, type TrustZoneSourceClass } from "./trust-zones.js";
|
|
@@ -669,6 +670,44 @@ export class EngramAccessHttpServer {
|
|
|
669
670
|
return;
|
|
670
671
|
}
|
|
671
672
|
|
|
673
|
+
if (
|
|
674
|
+
req.method === "POST" &&
|
|
675
|
+
(
|
|
676
|
+
pathname === "/engram/v1/offline-sync/apply-file-content" ||
|
|
677
|
+
pathname === "/remnic/v1/offline-sync/apply-file-content"
|
|
678
|
+
)
|
|
679
|
+
) {
|
|
680
|
+
const namespaceParam = parsed.searchParams.get("namespace");
|
|
681
|
+
const bytes = this.readRequiredIntegerHeader(req, "x-remnic-file-bytes");
|
|
682
|
+
const offset = this.readOptionalIntegerHeader(req, "x-remnic-chunk-offset") ?? 0;
|
|
683
|
+
const startsUpload = offset === 0;
|
|
684
|
+
if (startsUpload) this.ensureWriteRateLimitAvailable();
|
|
685
|
+
const content = await this.readBinaryBody(req, OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES);
|
|
686
|
+
const result = await this.service.offlineSyncApplyFileContent({
|
|
687
|
+
namespace: this.resolveNamespace(
|
|
688
|
+
req,
|
|
689
|
+
namespaceParam && namespaceParam.length > 0 ? namespaceParam : undefined,
|
|
690
|
+
),
|
|
691
|
+
principal: this.resolveRequestPrincipal(req),
|
|
692
|
+
includeTranscripts: this.parseOptionalBooleanHeader(
|
|
693
|
+
req,
|
|
694
|
+
"x-remnic-include-transcripts",
|
|
695
|
+
true,
|
|
696
|
+
),
|
|
697
|
+
sourceId: this.readRequiredDecodedHeader(req, "x-remnic-source-id"),
|
|
698
|
+
path: this.readRequiredDecodedHeader(req, "x-remnic-file-path"),
|
|
699
|
+
sha256: this.readRequiredHeader(req, "x-remnic-file-sha256"),
|
|
700
|
+
bytes,
|
|
701
|
+
mtimeMs: this.readRequiredNumberHeader(req, "x-remnic-file-mtime-ms"),
|
|
702
|
+
offset,
|
|
703
|
+
baseSha256: this.readOptionalHeader(req, "x-remnic-base-sha256"),
|
|
704
|
+
content,
|
|
705
|
+
});
|
|
706
|
+
if (startsUpload) this.recordWriteRateLimitHit();
|
|
707
|
+
this.respondJson(res, 200, result);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
672
711
|
if (
|
|
673
712
|
req.method === "POST" &&
|
|
674
713
|
(pathname === "/engram/v1/offline-sync/apply" || pathname === "/remnic/v1/offline-sync/apply")
|
|
@@ -1911,6 +1950,83 @@ export class EngramAccessHttpServer {
|
|
|
1911
1950
|
return parsed as Record<string, unknown>;
|
|
1912
1951
|
}
|
|
1913
1952
|
|
|
1953
|
+
private async readBinaryBody(req: IncomingMessage, maxBytes: number): Promise<Buffer> {
|
|
1954
|
+
const chunks: Buffer[] = [];
|
|
1955
|
+
let total = 0;
|
|
1956
|
+
for await (const chunk of req) {
|
|
1957
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1958
|
+
total += buffer.length;
|
|
1959
|
+
if (total > maxBytes) {
|
|
1960
|
+
throw new HttpError(413, "request_body_too_large", "request_body_too_large");
|
|
1961
|
+
}
|
|
1962
|
+
chunks.push(buffer);
|
|
1963
|
+
}
|
|
1964
|
+
return Buffer.concat(chunks, total);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
private readRequiredHeader(req: IncomingMessage, name: string): string {
|
|
1968
|
+
const value = this.readOptionalHeader(req, name);
|
|
1969
|
+
if (value === undefined || value.length === 0) {
|
|
1970
|
+
throw new EngramAccessInputError(`${name} header is required`);
|
|
1971
|
+
}
|
|
1972
|
+
return value;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
private readOptionalHeader(req: IncomingMessage, name: string): string | undefined {
|
|
1976
|
+
const raw = req.headers[name.toLowerCase()];
|
|
1977
|
+
if (Array.isArray(raw)) return raw[0]?.trim() || undefined;
|
|
1978
|
+
return raw?.trim() || undefined;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
private readRequiredDecodedHeader(req: IncomingMessage, name: string): string {
|
|
1982
|
+
const raw = this.readRequiredHeader(req, name);
|
|
1983
|
+
try {
|
|
1984
|
+
return decodeURIComponent(raw);
|
|
1985
|
+
} catch {
|
|
1986
|
+
throw new EngramAccessInputError(`${name} header is not valid percent-encoded input`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
private readRequiredIntegerHeader(req: IncomingMessage, name: string): number {
|
|
1991
|
+
const raw = this.readRequiredHeader(req, name);
|
|
1992
|
+
const parsed = Number(raw);
|
|
1993
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
1994
|
+
throw new EngramAccessInputError(`${name} header must be a non-negative integer`);
|
|
1995
|
+
}
|
|
1996
|
+
return parsed;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
private readOptionalIntegerHeader(req: IncomingMessage, name: string): number | undefined {
|
|
2000
|
+
const raw = this.readOptionalHeader(req, name);
|
|
2001
|
+
if (raw === undefined) return undefined;
|
|
2002
|
+
const parsed = Number(raw);
|
|
2003
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
2004
|
+
throw new EngramAccessInputError(`${name} header must be a non-negative integer`);
|
|
2005
|
+
}
|
|
2006
|
+
return parsed;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
private readRequiredNumberHeader(req: IncomingMessage, name: string): number {
|
|
2010
|
+
const raw = this.readRequiredHeader(req, name);
|
|
2011
|
+
const parsed = Number(raw);
|
|
2012
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
2013
|
+
throw new EngramAccessInputError(`${name} header must be a non-negative finite number`);
|
|
2014
|
+
}
|
|
2015
|
+
return parsed;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
private parseOptionalBooleanHeader(
|
|
2019
|
+
req: IncomingMessage,
|
|
2020
|
+
name: string,
|
|
2021
|
+
defaultValue: boolean,
|
|
2022
|
+
): boolean {
|
|
2023
|
+
const raw = this.readOptionalHeader(req, name);
|
|
2024
|
+
if (raw === undefined) return defaultValue;
|
|
2025
|
+
if (raw === "true") return true;
|
|
2026
|
+
if (raw === "false") return false;
|
|
2027
|
+
throw new EngramAccessInputError(`${name} header must be one of: true, false`);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1914
2030
|
private async readValidatedBody<S extends SchemaName>(req: IncomingMessage, schemaName: S): Promise<SchemaTypeFor<S>> {
|
|
1915
2031
|
const raw = await this.readJsonBody(req);
|
|
1916
2032
|
const result = validateRequest(schemaName, raw);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { EngramAccessInputError, EngramAccessService } from "./access-service.js";
|
|
5
|
+
|
|
6
|
+
function createOfflineService(): EngramAccessService {
|
|
7
|
+
return new EngramAccessService({
|
|
8
|
+
config: {
|
|
9
|
+
memoryDir: "/tmp/remnic-access-service-offline-file-content-test",
|
|
10
|
+
namespacesEnabled: false,
|
|
11
|
+
defaultNamespace: "global",
|
|
12
|
+
sharedNamespace: "shared",
|
|
13
|
+
},
|
|
14
|
+
getStorage: async () => ({
|
|
15
|
+
dir: "/tmp/remnic-access-service-offline-file-content-test",
|
|
16
|
+
}),
|
|
17
|
+
} as any);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("offline apply-file-content reports invalid metadata as input errors", async () => {
|
|
21
|
+
const service = createOfflineService();
|
|
22
|
+
await assert.rejects(
|
|
23
|
+
() => service.offlineSyncApplyFileContent({
|
|
24
|
+
includeTranscripts: true,
|
|
25
|
+
sourceId: "laptop",
|
|
26
|
+
path: "state/lcm.sqlite",
|
|
27
|
+
sha256: "not-a-sha",
|
|
28
|
+
bytes: 0,
|
|
29
|
+
mtimeMs: 0,
|
|
30
|
+
offset: 0,
|
|
31
|
+
content: Buffer.alloc(0),
|
|
32
|
+
}),
|
|
33
|
+
(error) =>
|
|
34
|
+
error instanceof EngramAccessInputError &&
|
|
35
|
+
/sha256 must be a 64-character sha256/.test(error.message),
|
|
36
|
+
);
|
|
37
|
+
});
|
package/src/access-service.ts
CHANGED
|
@@ -136,10 +136,12 @@ import {
|
|
|
136
136
|
type CapsuleListEntry,
|
|
137
137
|
} from "./capsule-cli.js";
|
|
138
138
|
import {
|
|
139
|
+
applyOfflineSyncFileContentChunk,
|
|
139
140
|
applyOfflineSyncChangeset,
|
|
140
141
|
buildOfflineSyncSnapshot,
|
|
141
142
|
buildOfflineSyncSnapshotForPaths,
|
|
142
143
|
readOfflineSyncFileContentChunk,
|
|
144
|
+
type OfflineSyncApplyFileContentChunkResult,
|
|
143
145
|
type OfflineSyncApplyChangesetResult,
|
|
144
146
|
type OfflineSyncFileContentChunk,
|
|
145
147
|
type OfflineSyncSnapshot,
|
|
@@ -635,6 +637,20 @@ export interface EngramAccessOfflineSyncFileContentRequest {
|
|
|
635
637
|
length?: number;
|
|
636
638
|
}
|
|
637
639
|
|
|
640
|
+
export interface EngramAccessOfflineSyncApplyFileContentRequest {
|
|
641
|
+
namespace?: string;
|
|
642
|
+
principal?: string;
|
|
643
|
+
includeTranscripts?: boolean;
|
|
644
|
+
sourceId: string;
|
|
645
|
+
path: string;
|
|
646
|
+
sha256: string;
|
|
647
|
+
bytes: number;
|
|
648
|
+
mtimeMs: number;
|
|
649
|
+
offset?: number;
|
|
650
|
+
baseSha256?: string;
|
|
651
|
+
content: Buffer;
|
|
652
|
+
}
|
|
653
|
+
|
|
638
654
|
export interface EngramAccessOfflineSyncApplyRequest {
|
|
639
655
|
namespace?: string;
|
|
640
656
|
principal?: string;
|
|
@@ -653,6 +669,10 @@ export interface EngramAccessOfflineSyncFileContentResponse extends OfflineSyncF
|
|
|
653
669
|
namespace: string;
|
|
654
670
|
}
|
|
655
671
|
|
|
672
|
+
export interface EngramAccessOfflineSyncApplyFileContentResponse extends OfflineSyncApplyFileContentChunkResult {
|
|
673
|
+
namespace: string;
|
|
674
|
+
}
|
|
675
|
+
|
|
656
676
|
export interface EngramAccessOfflineSyncApplyResponse extends OfflineSyncApplyChangesetResult {
|
|
657
677
|
namespace: string;
|
|
658
678
|
}
|
|
@@ -5660,6 +5680,56 @@ export class EngramAccessService {
|
|
|
5660
5680
|
}
|
|
5661
5681
|
}
|
|
5662
5682
|
|
|
5683
|
+
async offlineSyncApplyFileContent(
|
|
5684
|
+
options: EngramAccessOfflineSyncApplyFileContentRequest,
|
|
5685
|
+
): Promise<EngramAccessOfflineSyncApplyFileContentResponse> {
|
|
5686
|
+
const resolvedNamespace = this.resolveWritableNamespace(
|
|
5687
|
+
options.namespace,
|
|
5688
|
+
undefined,
|
|
5689
|
+
options.principal,
|
|
5690
|
+
);
|
|
5691
|
+
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
5692
|
+
try {
|
|
5693
|
+
const result = await applyOfflineSyncFileContentChunk({
|
|
5694
|
+
root: storage.dir,
|
|
5695
|
+
sourceId: options.sourceId,
|
|
5696
|
+
path: options.path,
|
|
5697
|
+
sha256: options.sha256,
|
|
5698
|
+
bytes: options.bytes,
|
|
5699
|
+
mtimeMs: options.mtimeMs,
|
|
5700
|
+
offset: options.offset,
|
|
5701
|
+
baseSha256: options.baseSha256,
|
|
5702
|
+
content: options.content,
|
|
5703
|
+
includeTranscripts: options.includeTranscripts !== false,
|
|
5704
|
+
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
5705
|
+
writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
|
|
5706
|
+
writeStagingFile: async ({ filePath, content }) => storage.writeOfflineSyncStagingFile(filePath, content),
|
|
5707
|
+
writeFileChunks: async ({ filePath, chunks }) => storage.writeOfflineSyncFileChunks(filePath, chunks),
|
|
5708
|
+
});
|
|
5709
|
+
return {
|
|
5710
|
+
namespace: resolvedNamespace,
|
|
5711
|
+
...result,
|
|
5712
|
+
};
|
|
5713
|
+
} catch (error) {
|
|
5714
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5715
|
+
if (
|
|
5716
|
+
message.startsWith("offline sync") ||
|
|
5717
|
+
message.startsWith("path:") ||
|
|
5718
|
+
message.startsWith("sourceId must ") ||
|
|
5719
|
+
message.startsWith("sha256 must ") ||
|
|
5720
|
+
message.startsWith("baseSha256 must ") ||
|
|
5721
|
+
message.startsWith("bytes must ") ||
|
|
5722
|
+
message.startsWith("mtimeMs must ") ||
|
|
5723
|
+
message.startsWith("offset must ") ||
|
|
5724
|
+
message.startsWith("content chunk ") ||
|
|
5725
|
+
message === "content must be a Buffer"
|
|
5726
|
+
) {
|
|
5727
|
+
throw new EngramAccessInputError(message);
|
|
5728
|
+
}
|
|
5729
|
+
throw error;
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
|
|
5663
5733
|
async offlineSyncApply(
|
|
5664
5734
|
options: EngramAccessOfflineSyncApplyRequest,
|
|
5665
5735
|
): Promise<EngramAccessOfflineSyncApplyResponse> {
|
package/src/index.ts
CHANGED
|
@@ -683,6 +683,7 @@ export {
|
|
|
683
683
|
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
684
684
|
OFFLINE_SYNC_SNAPSHOT_FORMAT,
|
|
685
685
|
OFFLINE_SYNC_STATE_VERSION,
|
|
686
|
+
applyOfflineSyncFileContentChunk,
|
|
686
687
|
applyOfflineSyncChangeset,
|
|
687
688
|
applyOfflineSyncSnapshot,
|
|
688
689
|
buildOfflineSyncChangeset,
|
|
@@ -698,6 +699,7 @@ export {
|
|
|
698
699
|
summarizeOfflineSyncChangeset,
|
|
699
700
|
summarizeOfflineSyncPendingChanges,
|
|
700
701
|
writeOfflineSyncState,
|
|
702
|
+
type OfflineSyncApplyFileContentChunkResult,
|
|
701
703
|
type OfflineSyncApplyChangesetResult,
|
|
702
704
|
type OfflineSyncApplySnapshotResult,
|
|
703
705
|
type OfflineSyncChange,
|