@remnic/core 9.17.0 → 9.17.1

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 (45) hide show
  1. package/dist/access-admin-ops-surface.js +2 -2
  2. package/dist/access-boundary.js +2 -2
  3. package/dist/access-cli.js +8 -8
  4. package/dist/access-http.js +6 -6
  5. package/dist/access-identity-continuity-surface.js +2 -2
  6. package/dist/access-lcm-surface.js +2 -2
  7. package/dist/access-mcp.js +5 -5
  8. package/dist/access-namespace-preflight.js +2 -2
  9. package/dist/access-observe-write-surface.js +2 -2
  10. package/dist/access-operations-batch.js +3 -3
  11. package/dist/access-operations.d.ts +3 -3
  12. package/dist/access-operations.js +4 -4
  13. package/dist/access-recall-concurrency.js +2 -2
  14. package/dist/access-recall-response.js +2 -2
  15. package/dist/access-recall-surface.js +2 -2
  16. package/dist/access-schema.d.ts +76 -76
  17. package/dist/access-service.js +2 -2
  18. package/dist/{chunk-UH7VNQ7O.js → chunk-52EFQMQC.js} +2 -2
  19. package/dist/{chunk-GEDBTSYU.js → chunk-7O2C2OOF.js} +4 -4
  20. package/dist/{chunk-OXT5TQDS.js → chunk-B6P3QDBF.js} +2 -2
  21. package/dist/{chunk-TUNI4GB3.js → chunk-BGBPDAIN.js} +2 -2
  22. package/dist/{chunk-TXXP7WAM.js → chunk-KQED7RCV.js} +4 -4
  23. package/dist/{chunk-M6TZQDTX.js → chunk-VZC7ORJE.js} +2 -2
  24. package/dist/{chunk-ANGUOE2N.js → chunk-WIUYMCPL.js} +2 -2
  25. package/dist/{chunk-DETNBTZS.js → chunk-XSBUROEB.js} +20 -4
  26. package/dist/chunk-XSBUROEB.js.map +1 -0
  27. package/dist/cli.js +7 -7
  28. package/dist/index.d.ts +404 -404
  29. package/dist/index.js +8 -8
  30. package/dist/offline-sync-impression-drain.js +1 -1
  31. package/dist/orchestrator.js +8 -8
  32. package/dist/schemas.d.ts +76 -76
  33. package/dist/shared-context/manager.d.ts +8 -8
  34. package/dist/transfer/types.d.ts +66 -66
  35. package/package.json +2 -2
  36. package/src/offline-sync-impression-drain.test.ts +54 -0
  37. package/src/offline-sync-impression-drain.ts +35 -3
  38. package/dist/chunk-DETNBTZS.js.map +0 -1
  39. /package/dist/{chunk-UH7VNQ7O.js.map → chunk-52EFQMQC.js.map} +0 -0
  40. /package/dist/{chunk-GEDBTSYU.js.map → chunk-7O2C2OOF.js.map} +0 -0
  41. /package/dist/{chunk-OXT5TQDS.js.map → chunk-B6P3QDBF.js.map} +0 -0
  42. /package/dist/{chunk-TUNI4GB3.js.map → chunk-BGBPDAIN.js.map} +0 -0
  43. /package/dist/{chunk-TXXP7WAM.js.map → chunk-KQED7RCV.js.map} +0 -0
  44. /package/dist/{chunk-M6TZQDTX.js.map → chunk-VZC7ORJE.js.map} +0 -0
  45. /package/dist/{chunk-ANGUOE2N.js.map → chunk-WIUYMCPL.js.map} +0 -0
@@ -20,26 +20,26 @@ declare const ExportManifestV1Schema: z.ZodObject<{
20
20
  bytes: number;
21
21
  }>, "many">;
22
22
  }, "strip", z.ZodTypeAny, {
23
- schemaVersion: 1;
23
+ format: "openclaw-engram-export";
24
24
  createdAt: string;
25
+ pluginVersion: string;
25
26
  files: {
26
27
  path: string;
27
28
  sha256: string;
28
29
  bytes: number;
29
30
  }[];
30
- format: "openclaw-engram-export";
31
- pluginVersion: string;
31
+ schemaVersion: 1;
32
32
  includesTranscripts: boolean;
33
33
  }, {
34
- schemaVersion: 1;
34
+ format: "openclaw-engram-export";
35
35
  createdAt: string;
36
+ pluginVersion: string;
36
37
  files: {
37
38
  path: string;
38
39
  sha256: string;
39
40
  bytes: number;
40
41
  }[];
41
- format: "openclaw-engram-export";
42
- pluginVersion: string;
42
+ schemaVersion: 1;
43
43
  includesTranscripts: boolean;
44
44
  }>;
45
45
  type ExportManifestV1 = z.infer<typeof ExportManifestV1Schema>;
@@ -75,26 +75,26 @@ declare const ExportBundleV1Schema: z.ZodObject<{
75
75
  bytes: number;
76
76
  }>, "many">;
77
77
  }, "strip", z.ZodTypeAny, {
78
- schemaVersion: 1;
78
+ format: "openclaw-engram-export";
79
79
  createdAt: string;
80
+ pluginVersion: string;
80
81
  files: {
81
82
  path: string;
82
83
  sha256: string;
83
84
  bytes: number;
84
85
  }[];
85
- format: "openclaw-engram-export";
86
- pluginVersion: string;
86
+ schemaVersion: 1;
87
87
  includesTranscripts: boolean;
88
88
  }, {
89
- schemaVersion: 1;
89
+ format: "openclaw-engram-export";
90
90
  createdAt: string;
91
+ pluginVersion: string;
91
92
  files: {
92
93
  path: string;
93
94
  sha256: string;
94
95
  bytes: number;
95
96
  }[];
96
- format: "openclaw-engram-export";
97
- pluginVersion: string;
97
+ schemaVersion: 1;
98
98
  includesTranscripts: boolean;
99
99
  }>;
100
100
  records: z.ZodArray<z.ZodObject<{
@@ -109,15 +109,15 @@ declare const ExportBundleV1Schema: z.ZodObject<{
109
109
  }>, "many">;
110
110
  }, "strip", z.ZodTypeAny, {
111
111
  manifest: {
112
- schemaVersion: 1;
112
+ format: "openclaw-engram-export";
113
113
  createdAt: string;
114
+ pluginVersion: string;
114
115
  files: {
115
116
  path: string;
116
117
  sha256: string;
117
118
  bytes: number;
118
119
  }[];
119
- format: "openclaw-engram-export";
120
- pluginVersion: string;
120
+ schemaVersion: 1;
121
121
  includesTranscripts: boolean;
122
122
  };
123
123
  records: {
@@ -126,15 +126,15 @@ declare const ExportBundleV1Schema: z.ZodObject<{
126
126
  }[];
127
127
  }, {
128
128
  manifest: {
129
- schemaVersion: 1;
129
+ format: "openclaw-engram-export";
130
130
  createdAt: string;
131
+ pluginVersion: string;
131
132
  files: {
132
133
  path: string;
133
134
  sha256: string;
134
135
  bytes: number;
135
136
  }[];
136
- format: "openclaw-engram-export";
137
- pluginVersion: string;
137
+ schemaVersion: 1;
138
138
  includesTranscripts: boolean;
139
139
  };
140
140
  records: {
@@ -313,16 +313,16 @@ 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
+ description: string;
323
323
  id: string;
324
+ schemaVersion: string;
324
325
  version: string;
325
- description: string;
326
326
  parentCapsule: string | null;
327
327
  parent: {
328
328
  version: string;
@@ -334,16 +334,16 @@ 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
+ description: string;
344
344
  id: string;
345
+ schemaVersion: string;
345
346
  version: string;
346
- description: string;
347
347
  parentCapsule: string | null;
348
348
  retrievalPolicy: {
349
349
  tierWeights: Record<string, number>;
@@ -464,16 +464,16 @@ 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
+ description: string;
474
474
  id: string;
475
+ schemaVersion: string;
475
476
  version: string;
476
- description: string;
477
477
  parentCapsule: string | null;
478
478
  parent: {
479
479
  version: string;
@@ -485,16 +485,16 @@ 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
+ description: string;
495
495
  id: string;
496
+ schemaVersion: string;
496
497
  version: string;
497
- description: string;
498
498
  parentCapsule: string | null;
499
499
  retrievalPolicy: {
500
500
  tierWeights: Record<string, number>;
@@ -507,27 +507,25 @@ declare const ExportManifestV2Schema: z.ZodObject<{
507
507
  } | null | undefined;
508
508
  }>;
509
509
  }, "strip", z.ZodTypeAny, {
510
- schemaVersion: 2;
510
+ format: "openclaw-engram-export";
511
511
  createdAt: string;
512
+ pluginVersion: string;
512
513
  files: {
513
514
  path: string;
514
515
  sha256: string;
515
516
  bytes: number;
516
517
  }[];
517
- format: "openclaw-engram-export";
518
- pluginVersion: string;
519
- includesTranscripts: boolean;
520
518
  capsule: {
521
- schemaVersion: string;
522
519
  includes: {
523
520
  procedural: boolean;
524
521
  taxonomy: boolean;
525
522
  identityAnchors: boolean;
526
523
  peerProfiles: boolean;
527
524
  };
525
+ description: string;
528
526
  id: string;
527
+ schemaVersion: string;
529
528
  version: string;
530
- description: string;
531
529
  parentCapsule: string | null;
532
530
  parent: {
533
531
  version: string;
@@ -539,28 +537,28 @@ declare const ExportManifestV2Schema: z.ZodObject<{
539
537
  directAnswerEnabled: boolean;
540
538
  };
541
539
  };
542
- }, {
543
540
  schemaVersion: 2;
541
+ includesTranscripts: boolean;
542
+ }, {
543
+ format: "openclaw-engram-export";
544
544
  createdAt: string;
545
+ pluginVersion: string;
545
546
  files: {
546
547
  path: string;
547
548
  sha256: string;
548
549
  bytes: number;
549
550
  }[];
550
- format: "openclaw-engram-export";
551
- pluginVersion: string;
552
- includesTranscripts: boolean;
553
551
  capsule: {
554
- schemaVersion: string;
555
552
  includes: {
556
553
  procedural: boolean;
557
554
  taxonomy: boolean;
558
555
  identityAnchors: boolean;
559
556
  peerProfiles: boolean;
560
557
  };
558
+ description: string;
561
559
  id: string;
560
+ schemaVersion: string;
562
561
  version: string;
563
- description: string;
564
562
  parentCapsule: string | null;
565
563
  retrievalPolicy: {
566
564
  tierWeights: Record<string, number>;
@@ -572,6 +570,8 @@ declare const ExportManifestV2Schema: z.ZodObject<{
572
570
  forkRoot: string;
573
571
  } | null | undefined;
574
572
  };
573
+ schemaVersion: 2;
574
+ includesTranscripts: boolean;
575
575
  }>;
576
576
  type ExportManifestV2 = z.infer<typeof ExportManifestV2Schema>;
577
577
  declare const ExportBundleV2Schema: z.ZodObject<{
@@ -683,16 +683,16 @@ 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
+ description: string;
693
693
  id: string;
694
+ schemaVersion: string;
694
695
  version: string;
695
- description: string;
696
696
  parentCapsule: string | null;
697
697
  parent: {
698
698
  version: string;
@@ -704,16 +704,16 @@ 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
+ description: string;
714
714
  id: string;
715
+ schemaVersion: string;
715
716
  version: string;
716
- description: string;
717
717
  parentCapsule: string | null;
718
718
  retrievalPolicy: {
719
719
  tierWeights: Record<string, number>;
@@ -726,27 +726,25 @@ declare const ExportBundleV2Schema: z.ZodObject<{
726
726
  } | null | undefined;
727
727
  }>;
728
728
  }, "strip", z.ZodTypeAny, {
729
- schemaVersion: 2;
729
+ format: "openclaw-engram-export";
730
730
  createdAt: string;
731
+ pluginVersion: string;
731
732
  files: {
732
733
  path: string;
733
734
  sha256: string;
734
735
  bytes: number;
735
736
  }[];
736
- format: "openclaw-engram-export";
737
- pluginVersion: string;
738
- includesTranscripts: boolean;
739
737
  capsule: {
740
- schemaVersion: string;
741
738
  includes: {
742
739
  procedural: boolean;
743
740
  taxonomy: boolean;
744
741
  identityAnchors: boolean;
745
742
  peerProfiles: boolean;
746
743
  };
744
+ description: string;
747
745
  id: string;
746
+ schemaVersion: string;
748
747
  version: string;
749
- description: string;
750
748
  parentCapsule: string | null;
751
749
  parent: {
752
750
  version: string;
@@ -758,28 +756,28 @@ declare const ExportBundleV2Schema: z.ZodObject<{
758
756
  directAnswerEnabled: boolean;
759
757
  };
760
758
  };
761
- }, {
762
759
  schemaVersion: 2;
760
+ includesTranscripts: boolean;
761
+ }, {
762
+ format: "openclaw-engram-export";
763
763
  createdAt: string;
764
+ pluginVersion: string;
764
765
  files: {
765
766
  path: string;
766
767
  sha256: string;
767
768
  bytes: number;
768
769
  }[];
769
- format: "openclaw-engram-export";
770
- pluginVersion: string;
771
- includesTranscripts: boolean;
772
770
  capsule: {
773
- schemaVersion: string;
774
771
  includes: {
775
772
  procedural: boolean;
776
773
  taxonomy: boolean;
777
774
  identityAnchors: boolean;
778
775
  peerProfiles: boolean;
779
776
  };
777
+ description: string;
780
778
  id: string;
779
+ schemaVersion: string;
781
780
  version: string;
782
- description: string;
783
781
  parentCapsule: string | null;
784
782
  retrievalPolicy: {
785
783
  tierWeights: Record<string, number>;
@@ -791,6 +789,8 @@ declare const ExportBundleV2Schema: z.ZodObject<{
791
789
  forkRoot: string;
792
790
  } | null | undefined;
793
791
  };
792
+ schemaVersion: 2;
793
+ includesTranscripts: boolean;
794
794
  }>;
795
795
  records: z.ZodArray<z.ZodObject<{
796
796
  path: z.ZodString;
@@ -804,27 +804,25 @@ declare const ExportBundleV2Schema: z.ZodObject<{
804
804
  }>, "many">;
805
805
  }, "strip", z.ZodTypeAny, {
806
806
  manifest: {
807
- schemaVersion: 2;
807
+ format: "openclaw-engram-export";
808
808
  createdAt: string;
809
+ pluginVersion: string;
809
810
  files: {
810
811
  path: string;
811
812
  sha256: string;
812
813
  bytes: number;
813
814
  }[];
814
- format: "openclaw-engram-export";
815
- pluginVersion: string;
816
- includesTranscripts: boolean;
817
815
  capsule: {
818
- schemaVersion: string;
819
816
  includes: {
820
817
  procedural: boolean;
821
818
  taxonomy: boolean;
822
819
  identityAnchors: boolean;
823
820
  peerProfiles: boolean;
824
821
  };
822
+ description: string;
825
823
  id: string;
824
+ schemaVersion: string;
826
825
  version: string;
827
- description: string;
828
826
  parentCapsule: string | null;
829
827
  parent: {
830
828
  version: string;
@@ -836,6 +834,8 @@ declare const ExportBundleV2Schema: z.ZodObject<{
836
834
  directAnswerEnabled: boolean;
837
835
  };
838
836
  };
837
+ schemaVersion: 2;
838
+ includesTranscripts: boolean;
839
839
  };
840
840
  records: {
841
841
  content: string;
@@ -843,27 +843,25 @@ declare const ExportBundleV2Schema: z.ZodObject<{
843
843
  }[];
844
844
  }, {
845
845
  manifest: {
846
- schemaVersion: 2;
846
+ format: "openclaw-engram-export";
847
847
  createdAt: string;
848
+ pluginVersion: string;
848
849
  files: {
849
850
  path: string;
850
851
  sha256: string;
851
852
  bytes: number;
852
853
  }[];
853
- format: "openclaw-engram-export";
854
- pluginVersion: string;
855
- includesTranscripts: boolean;
856
854
  capsule: {
857
- schemaVersion: string;
858
855
  includes: {
859
856
  procedural: boolean;
860
857
  taxonomy: boolean;
861
858
  identityAnchors: boolean;
862
859
  peerProfiles: boolean;
863
860
  };
861
+ description: string;
864
862
  id: string;
863
+ schemaVersion: string;
865
864
  version: string;
866
- description: string;
867
865
  parentCapsule: string | null;
868
866
  retrievalPolicy: {
869
867
  tierWeights: Record<string, number>;
@@ -875,6 +873,8 @@ declare const ExportBundleV2Schema: z.ZodObject<{
875
873
  forkRoot: string;
876
874
  } | null | undefined;
877
875
  };
876
+ schemaVersion: 2;
877
+ includesTranscripts: boolean;
878
878
  };
879
879
  records: {
880
880
  content: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.17.0",
3
+ "version": "9.17.1",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3045,7 +3045,7 @@
3045
3045
  "core"
3046
3046
  ],
3047
3047
  "peerDependencies": {
3048
- "@remnic/coding-graph": "^9.17.0"
3048
+ "@remnic/coding-graph": "^9.17.1"
3049
3049
  },
3050
3050
  "peerDependenciesMeta": {
3051
3051
  "@remnic/coding-graph": {
@@ -8,7 +8,9 @@ import {
8
8
  drainPendingImpressionsForOfflineSync,
9
9
  drainPendingLifecycleForOfflineSync,
10
10
  getOfflineSyncStorage,
11
+ listOfflineSyncNamespaces,
11
12
  } from "./offline-sync-impression-drain.js";
13
+ import { namespaceIdentityToken } from "./namespaces/identity.js";
12
14
  import { isEncryptedFile } from "./secure-store/secure-fs.js";
13
15
  import { StorageManager } from "./storage.js";
14
16
 
@@ -314,3 +316,55 @@ test("getOfflineSyncStorage never double-folds the root ledger when it is listed
314
316
 
315
317
  assert.deepEqual(lifecycleDrains, ["root", "team"], "root ledger is folded exactly once");
316
318
  });
319
+
320
+ test("listOfflineSyncNamespaces decodes canonical token dirs and skips names the router would reject", async () => {
321
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-ns-enum-"));
322
+ try {
323
+ // A project namespace whose canonical token dir `ns-<hex>` exceeds the
324
+ // 64-char route-namespace cap: `getStorage` on the RAW basename throws
325
+ // `unsafe namespace` and 500s the whole root snapshot-stream (the regression).
326
+ const longName = "generalist-project-origin-6d1ad1f2";
327
+ const longToken = namespaceIdentityToken(longName); // ns-<hex>, 71 chars
328
+ assert.ok(longToken.length > 64, "token dir must exceed the route-namespace cap");
329
+ // A namespace whose DECODED name itself exceeds the 64-char route cap:
330
+ // decoding succeeds but isSafeRouteNamespace must still reject it.
331
+ const overCapName = `generalist-project-tag-${"x".repeat(60)}`;
332
+ const overCapToken = namespaceIdentityToken(overCapName);
333
+ assert.ok(overCapName.length > 64, "decoded name must exceed the route cap");
334
+ for (const dir of [longToken, overCapToken, "blend", "ns-default"]) {
335
+ await mkdir(path.join(root, "namespaces", dir, "state"), { recursive: true });
336
+ await writeFile(
337
+ path.join(root, "namespaces", dir, LIFECYCLE_LEDGER_REL),
338
+ "",
339
+ "utf-8",
340
+ );
341
+ }
342
+ const host = {
343
+ config: {
344
+ memoryDir: root,
345
+ defaultNamespace: "generalist",
346
+ sharedNamespace: "shared",
347
+ namespacePolicies: [],
348
+ },
349
+ namespaceCatalog: { listNamespaces: async () => [] },
350
+ } as unknown as Parameters<typeof listOfflineSyncNamespaces>[0];
351
+
352
+ const names = new Set(await listOfflineSyncNamespaces(host));
353
+
354
+ // The canonical token dir is decoded back to its namespace NAME...
355
+ assert.ok(names.has(longName), "canonical token dir must be decoded to its namespace name");
356
+ // ...never surfaced as the raw >64-char token that getStorage would reject.
357
+ assert.ok(!names.has(longToken), "raw over-cap token must never be enumerated as a namespace");
358
+ // A decoded name that is ITSELF over the route cap is skipped entirely
359
+ // (the isSafeRouteNamespace rejection branch).
360
+ assert.ok(!names.has(overCapName), "over-cap decoded name must be skipped");
361
+ assert.ok(!names.has(overCapToken), "over-cap raw token must be skipped");
362
+ // A raw plain-named dir is used verbatim.
363
+ assert.ok(names.has("blend"), "plain namespace dir must be enumerated verbatim");
364
+ // The literal ns-default token decodes to the EMPTY default identity and
365
+ // must map to the configured default namespace, not be skipped as "".
366
+ assert.ok(names.has("generalist"), "ns-default dir maps to the configured default namespace");
367
+ } finally {
368
+ await rm(root, { recursive: true, force: true });
369
+ }
370
+ });
@@ -12,6 +12,8 @@ import { isErrnoCode } from "./utils/errno.js";
12
12
  import { assertPathInsideRoot, pathIsInside } from "./utils/path-containment.js";
13
13
  import { displayErrorDetail } from "./runtime/better-sqlite.js";
14
14
  import { getConfiguredNamespaces } from "./scopes/scope-plan.js";
15
+ import { isSafeRouteNamespace } from "./routing/engine.js";
16
+ import { namespaceIdentityFromToken } from "./namespaces/identity.js";
15
17
  import type { PluginConfig } from "./types.js";
16
18
 
17
19
  type PendingImpressionDrain = () => Promise<{ pendingDeferred: boolean }>;
@@ -89,7 +91,17 @@ export async function getOfflineSyncStorage<T extends PendingLifecycleDrain & {
89
91
  if (path.resolve(storage.dir) === memoryRoot && orchestrator.listOfflineSyncNamespaces) {
90
92
  const drained = new Set<string>([memoryRoot]);
91
93
  for (const ns of await orchestrator.listOfflineSyncNamespaces()) {
92
- const nsStorage = await orchestrator.getStorage(ns);
94
+ // A single unresolvable namespace (e.g. a legacy canonical token dir whose
95
+ // name exceeds the route-namespace length cap) must not abort the whole root
96
+ // snapshot — degrade to draining the rest. listOfflineSyncNamespaces already
97
+ // normalizes token dirs, so this is defense-in-depth.
98
+ let nsStorage: T;
99
+ try {
100
+ nsStorage = await orchestrator.getStorage(ns);
101
+ } catch (err) {
102
+ log.debug(`offline-sync root drain: skipping namespace ${ns} (non-fatal): ${err}`);
103
+ continue;
104
+ }
93
105
  const nsDir = path.resolve(nsStorage.dir);
94
106
  if (drained.has(nsDir)) continue;
95
107
  drained.add(nsDir);
@@ -114,8 +126,28 @@ export async function listOfflineSyncNamespaces(host: OfflineSyncNamespaceHost):
114
126
  try {
115
127
  for (const ledgerPath of await offlineSyncLifecycleLedgerPaths(host.config.memoryDir)) {
116
128
  const namespaceDir = path.dirname(path.dirname(ledgerPath));
117
- if (path.basename(path.dirname(namespaceDir)) === "namespaces") {
118
- names.add(path.basename(namespaceDir));
129
+ if (path.basename(path.dirname(namespaceDir)) !== "namespaces") continue;
130
+ // A namespace is stored under its canonical token dir `ns-<hex>`. Decode the
131
+ // token back to the namespace NAME so the drain fanout resolves the SAME
132
+ // storage the token dir belongs to (the catalog rebuild scanner decodes the
133
+ // same way, catalog.ts finishRebuild). A raw dir already named as a plain
134
+ // namespace is used verbatim. Skip any name the router would reject — e.g. a
135
+ // canonical token dir whose decoded-or-raw name exceeds isSafeRouteNamespace's
136
+ // 64-char cap — because getStorage() would throw `unsafe namespace` and 500
137
+ // the whole root snapshot-stream (the offline-sync regression this fixes).
138
+ const dirName = path.basename(namespaceDir);
139
+ // namespaceIdentityFromToken returns "" for the ns-default token (the
140
+ // default-namespace identity) — map that to the configured default
141
+ // namespace instead of skipping the directory as an unsafe empty name.
142
+ const decoded = namespaceIdentityFromToken(dirName);
143
+ const name =
144
+ decoded === null
145
+ ? dirName
146
+ : decoded.length > 0
147
+ ? decoded
148
+ : host.config.defaultNamespace;
149
+ if (isSafeRouteNamespace(name)) {
150
+ names.add(name);
119
151
  }
120
152
  }
121
153
  } catch {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/offline-sync-impression-drain.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { lstat, realpath } from \"node:fs/promises\";\nimport {\n drainPendingLifecycleForSyncOrThrow,\n drainPendingLifecycleLedgerForSync,\n type LifecyclePendingIo,\n} from \"./storage/memory-lifecycle-ledger-access.js\";\nimport { writeFileAtomically } from \"./maintenance/atomic-file.js\";\nimport { log } from \"./logger.js\";\nimport { isErrnoCode } from \"./utils/errno.js\";\nimport { assertPathInsideRoot, pathIsInside } from \"./utils/path-containment.js\";\nimport { displayErrorDetail } from \"./runtime/better-sqlite.js\";\nimport { getConfiguredNamespaces } from \"./scopes/scope-plan.js\";\nimport type { PluginConfig } from \"./types.js\";\n\ntype PendingImpressionDrain = () => Promise<{ pendingDeferred: boolean }>;\n\ntype PendingLifecycleDrain = {\n drainPendingMemoryLifecycleEventsForSync(): Promise<{ folded: boolean; pendingDeferred: boolean }>;\n};\n\ntype LifecycleDrainAtPath = (\n ledgerPath: string,\n) => Promise<{ folded: boolean; pendingDeferred: boolean }>;\n\ntype PendingImpressionHost = {\n drainPendingRecallImpressions(): Promise<{ pendingDeferred: boolean }>;\n};\n/**\n * Fold pending recall-impression spills before building an offline-sync snapshot.\n * A deferred or failed drain must abort the snapshot rather than silently omit\n * durable rows from the sync payload.\n */\nexport async function drainPendingImpressionsForOfflineSync(\n host: PendingImpressionDrain,\n): Promise<void> {\n const maxAttempts = 3;\n let lastError: unknown;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const result = await host();\n if (!result.pendingDeferred) return;\n lastError = undefined;\n } catch (error) {\n lastError = error;\n }\n }\n const detail = lastError\n ? `: ${displayErrorDetail(lastError) || \"unknown error\"}`\n : \" (rotation lock held by a peer)\";\n throw new Error(\n `offline-sync impression drain could not fold pending recall impressions after ${maxAttempts} attempts${detail}; aborting snapshot so the pending rows are not silently excluded (#2033)`,\n );\n}\n\n\nexport async function getOfflineSyncStorage<T extends PendingLifecycleDrain & { dir: string }>(\n orchestrator: PendingImpressionHost & {\n config: { memoryDir: string };\n getStorage(namespace: string): Promise<T>;\n listOfflineSyncNamespaces?(): Promise<string[]>;\n },\n namespace: string,\n): Promise<T> {\n const storage = await orchestrator.getStorage(namespace);\n const memoryRoot = path.resolve(orchestrator.config.memoryDir);\n // Recall impressions are global: the orchestrator's single LastRecallStore\n // roots at config.memoryDir, so the active recall_impressions.jsonl lives at\n // <memoryDir>/state/. Fold the global pending queue ONLY when THIS snapshot's\n // root actually contains that active file - a root snapshot. A namespace\n // snapshot (storage.dir under namespaces/<token>/) cannot carry it, so\n // draining there would empty the durable pending queue into a file this\n // snapshot omits and strand the row until a root sync; leave it pending for\n // the root sync instead (#2033).\n const impressionsFile = path.join(memoryRoot, \"state\", \"recall_impressions.jsonl\");\n if (pathIsInside(path.resolve(storage.dir), impressionsFile)) {\n await drainPendingImpressionsForOfflineSync(() => orchestrator.drainPendingRecallImpressions());\n }\n await drainPendingLifecycleForSyncOrThrow(() => storage.drainPendingMemoryLifecycleEventsForSync());\n // Root snapshot: buildOfflineSyncSnapshot walks `namespaces/<ns>/state/` for\n // every namespace under the memory root and INCLUDES each namespace's active\n // lifecycle ledger, but the per-namespace pending spill dirs are offline-sync\n // EXCLUDED. Draining only the root ledger above would let a root snapshot omit\n // append-only rows still sitting in a namespace's spill queue, so fold every\n // namespace ledger too - each through its OWN namespace storage so secure-store\n // AAD stays namespace-scoped (#2033, matching the CLI all-ledger drain). A\n // namespace snapshot folds only its own ledger, already done above.\n if (path.resolve(storage.dir) === memoryRoot && orchestrator.listOfflineSyncNamespaces) {\n const drained = new Set<string>([memoryRoot]);\n for (const ns of await orchestrator.listOfflineSyncNamespaces()) {\n const nsStorage = await orchestrator.getStorage(ns);\n const nsDir = path.resolve(nsStorage.dir);\n if (drained.has(nsDir)) continue;\n drained.add(nsDir);\n await drainPendingLifecycleForSyncOrThrow(() => nsStorage.drainPendingMemoryLifecycleEventsForSync());\n }\n }\n return storage;\n}\n\ntype OfflineSyncNamespaceHost = {\n config: PluginConfig;\n namespaceCatalog: { listNamespaces(): Promise<Array<{ namespace: string }>> };\n};\n\n/**\n * Every namespace whose lifecycle ledger a ROOT offline snapshot enumerates.\n * Configured namespaces plus catalog-discovered ones; the catalog is queried\n * best-effort so a catalog read failure still drains the configured set (#2033).\n */\nexport async function listOfflineSyncNamespaces(host: OfflineSyncNamespaceHost): Promise<string[]> {\n const names = new Set<string>(getConfiguredNamespaces(host.config));\n try {\n for (const ledgerPath of await offlineSyncLifecycleLedgerPaths(host.config.memoryDir)) {\n const namespaceDir = path.dirname(path.dirname(ledgerPath));\n if (path.basename(path.dirname(namespaceDir)) === \"namespaces\") {\n names.add(path.basename(namespaceDir));\n }\n }\n } catch {\n // best-effort: a filesystem scan failure must not block configured/catalog namespaces\n }\n try {\n for (const record of await host.namespaceCatalog.listNamespaces()) {\n names.add(record.namespace);\n }\n } catch {\n // best-effort: fall back to the configured and filesystem namespace sets\n }\n return [...names];\n}\n\n/**\n * Resolve the offline-sync storage for a snapshot, folding pending spills first.\n * For a root snapshot this also folds every namespace lifecycle ledger the\n * snapshot will enumerate, so append-only rows in a namespace spill queue are\n * never omitted (#2033).\n */\nexport function offlineSyncStorageForSnapshot<T extends PendingLifecycleDrain & { dir: string }>(\n host: PendingImpressionHost &\n OfflineSyncNamespaceHost & {\n getStorage(namespace: string): Promise<T>;\n },\n namespace: string,\n): Promise<T> {\n return getOfflineSyncStorage(\n {\n config: { memoryDir: host.config.memoryDir },\n drainPendingRecallImpressions: () => host.drainPendingRecallImpressions(),\n getStorage: (ns: string) => host.getStorage(ns),\n listOfflineSyncNamespaces: () => listOfflineSyncNamespaces(host),\n },\n namespace,\n );\n}\n\nconst LIFECYCLE_LEDGER_FILE = \"memory-lifecycle-ledger.jsonl\";\n\n/**\n * Plaintext filesystem IO for a standalone (CLI) offline-sync lifecycle drain.\n * A standalone offline cache is plaintext - exactly like its LastRecallStore\n * impression drain - so spills read/append as UTF-8. `writeSecure` keeps the\n * atomic temp+rename the pending contract requires (#2033); a pure drain never\n * spills a new event, but the interface demands an atomic writer.\n */\nfunction plaintextLifecyclePendingIo(): LifecyclePendingIo {\n return {\n writeSecure: async (filePath, payload) => {\n await writeFileAtomically(filePath, payload);\n },\n readSecure: (filePath) => fs.promises.readFile(filePath, \"utf8\"),\n };\n}\n\n/**\n * Lifecycle ledger paths under a standalone memory dir: the root `state/` ledger\n * plus every per-namespace `namespaces/<ns>/state/` ledger (#2033). Offline sync\n * pushes the whole memory dir, so every namespace's ledger can carry pending\n * spills that must be folded first.\n */\nasync function offlineSyncLifecycleLedgerPaths(memoryDir: string): Promise<string[]> {\n const paths = [path.join(memoryDir, \"state\", LIFECYCLE_LEDGER_FILE)];\n const namespacesBase = path.join(memoryDir, \"namespaces\");\n // Symlink/traversal containment (#2033): a symlinked <memoryDir>/namespaces\n // (or a symlinked child) must not redirect the pre-sync drain - and the\n // ledger appends/mkdirs it drives - outside memoryDir. Resolve the memory\n // root and the scan base through realpath, reject a symlinked/escaping/\n // non-directory base, and skip any symlinked/escaping child, mirroring the\n // lifecycle-compaction scanner (orchestration/maintenance.ts) and the\n // memory-store walkers (utils/path-containment). A rejected base is\n // non-fatal: drain the root ledger only, never through the poisoned link.\n let memoryDirReal: string;\n try {\n memoryDirReal = await realpath(memoryDir);\n const baseStat = await lstat(namespacesBase);\n if (baseStat.isSymbolicLink() || !baseStat.isDirectory()) {\n throw new Error(\"namespaces base is a symlink or not a directory\");\n }\n assertPathInsideRoot(memoryDirReal, await realpath(namespacesBase), namespacesBase);\n } catch (err) {\n if (!isErrnoCode(err, \"ENOENT\")) {\n log.debug(`offline-sync lifecycle drain: namespaces base rejected (non-fatal): ${err}`);\n }\n return paths;\n }\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(namespacesBase, { withFileTypes: true });\n } catch (err) {\n if (!isErrnoCode(err, \"ENOENT\")) {\n log.debug(`offline-sync lifecycle drain: namespaces dir scan failed (non-fatal): ${err}`);\n }\n return paths;\n }\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n if (!entry.isDirectory() || entry.isSymbolicLink()) continue;\n const childPath = path.join(namespacesBase, entry.name);\n try {\n assertPathInsideRoot(memoryDirReal, await realpath(childPath), childPath);\n } catch (err) {\n log.debug(`offline-sync lifecycle drain: skipping out-of-root namespace dir ${childPath}: ${err}`);\n continue;\n }\n paths.push(path.join(childPath, \"state\", LIFECYCLE_LEDGER_FILE));\n }\n return paths;\n}\n\n/**\n * Fold pending memory-lifecycle spills into each active ledger under `memoryDir`\n * before a standalone (CLI) offline-sync snapshot (#2033). Mirrors the\n * access-service snapshot entrypoints, which drain lifecycle pending first: the\n * default sync exclude globs keep `memory-lifecycle-ledger.jsonl.pending.d` out\n * of the push, so an append-only row (promotion/import/explicit capture) that\n * spilled while the ledger lock was held would be silently dropped if the local\n * node were discarded before maintenance folds it. A deferred drain (lock held\n * by a peer) or a repeatedly failing fold ABORTS via\n * {@link drainPendingLifecycleForSyncOrThrow} rather than building/pushing a\n * snapshot that omits durable rows. Fast no-op per ledger with no pending dir.\n */\nexport async function drainPendingLifecycleForOfflineSync(\n memoryDir: string,\n drainAtPath: LifecycleDrainAtPath = (ledgerPath) => {\n const io = plaintextLifecyclePendingIo();\n const stateDir = path.dirname(ledgerPath);\n return drainPendingLifecycleLedgerForSync(\n ledgerPath,\n io,\n (payload) => fs.promises.appendFile(ledgerPath, payload),\n async () => {\n await fs.promises.mkdir(stateDir, { recursive: true });\n },\n );\n },\n): Promise<void> {\n for (const ledgerPath of await offlineSyncLifecycleLedgerPaths(memoryDir)) {\n await drainPendingLifecycleForSyncOrThrow(() => drainAtPath(ledgerPath));\n }\n}\n"],"mappings":"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","names":[]}