@remnic/core 1.1.22 → 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.
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-TLM762GT.js → chunk-2WIPXV3Y.js} +2 -2
  14. package/dist/{chunk-QOHBYVZG.js → chunk-3F24QTRI.js} +2 -2
  15. package/dist/{chunk-5IQC4OG6.js → chunk-4H6DURG6.js} +2 -2
  16. package/dist/{chunk-NOQ74SJN.js → chunk-7D6O46PF.js} +2 -2
  17. package/dist/{chunk-VMQRBXJ5.js → chunk-7E7SZRPP.js} +2 -2
  18. package/dist/{chunk-7Q2P774N.js → chunk-F33CJ5CH.js} +13 -3
  19. package/dist/chunk-F33CJ5CH.js.map +1 -0
  20. package/dist/{chunk-26OQECWH.js → chunk-FHXVW3L4.js} +4 -4
  21. package/dist/{chunk-UA6OCL6S.js → chunk-HWF42K6J.js} +103 -4
  22. package/dist/chunk-HWF42K6J.js.map +1 -0
  23. package/dist/{chunk-FSODDMR2.js → chunk-IANK6Y5W.js} +2 -2
  24. package/dist/{chunk-FF46Q3SN.js → chunk-JKXFF3NT.js} +356 -32
  25. package/dist/chunk-JKXFF3NT.js.map +1 -0
  26. package/dist/{chunk-NGPO6S3M.js → chunk-MM5EBZVW.js} +42 -5
  27. package/dist/chunk-MM5EBZVW.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-SZKCBLS5.js → chunk-PUXCIHRL.js} +2 -2
  32. package/dist/{chunk-2IRT26RZ.js → chunk-QYHQ2JHL.js} +2 -2
  33. package/dist/{chunk-LDJANWTK.js → chunk-RA73CTVY.js} +12 -12
  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 +26 -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 +36 -1
  58. package/dist/offline-sync.js +4 -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 +176 -0
  72. package/src/access-http.ts +116 -0
  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 +2 -0
  76. package/src/offline-sync.test.ts +395 -79
  77. package/src/offline-sync.ts +471 -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-TLM762GT.js.map → chunk-2WIPXV3Y.js.map} +0 -0
  87. /package/dist/{chunk-QOHBYVZG.js.map → chunk-3F24QTRI.js.map} +0 -0
  88. /package/dist/{chunk-5IQC4OG6.js.map → chunk-4H6DURG6.js.map} +0 -0
  89. /package/dist/{chunk-NOQ74SJN.js.map → chunk-7D6O46PF.js.map} +0 -0
  90. /package/dist/{chunk-VMQRBXJ5.js.map → chunk-7E7SZRPP.js.map} +0 -0
  91. /package/dist/{chunk-26OQECWH.js.map → chunk-FHXVW3L4.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-SZKCBLS5.js.map → chunk-PUXCIHRL.js.map} +0 -0
  95. /package/dist/{chunk-2IRT26RZ.js.map → chunk-QYHQ2JHL.js.map} +0 -0
  96. /package/dist/{chunk-LDJANWTK.js.map → chunk-RA73CTVY.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
@@ -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;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  searchVerifiedEpisodes
3
- } from "./chunk-5IQC4OG6.js";
3
+ } from "./chunk-4H6DURG6.js";
4
4
  import "./chunk-NZL6GGQE.js";
5
- import "./chunk-7Q2P774N.js";
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-SH5S7XYD.js";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "1.1.22",
3
+ "version": "1.1.23",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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 = {
@@ -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
+ });
@@ -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,