@remnic/core 9.3.706 → 9.3.707

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/schemas.d.ts CHANGED
@@ -290,12 +290,12 @@ declare const EntityMentionSchema: z.ZodObject<{
290
290
  title: z.ZodString;
291
291
  facts: z.ZodArray<z.ZodString, "many">;
292
292
  }, "strip", z.ZodTypeAny, {
293
- key: string;
294
293
  title: string;
294
+ key: string;
295
295
  facts: string[];
296
296
  }, {
297
- key: string;
298
297
  title: string;
298
+ key: string;
299
299
  facts: string[];
300
300
  }>, "many">>>;
301
301
  }, "strip", z.ZodTypeAny, {
@@ -303,8 +303,8 @@ declare const EntityMentionSchema: z.ZodObject<{
303
303
  name: string;
304
304
  facts: string[];
305
305
  structuredSections?: {
306
- key: string;
307
306
  title: string;
307
+ key: string;
308
308
  facts: string[];
309
309
  }[] | null | undefined;
310
310
  promptedByQuestion?: string | null | undefined;
@@ -313,8 +313,8 @@ declare const EntityMentionSchema: z.ZodObject<{
313
313
  name: string;
314
314
  facts: string[];
315
315
  structuredSections?: {
316
- key: string;
317
316
  title: string;
317
+ key: string;
318
318
  facts: string[];
319
319
  }[] | null | undefined;
320
320
  promptedByQuestion?: string | null | undefined;
@@ -614,12 +614,12 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
614
614
  title: z.ZodString;
615
615
  facts: z.ZodArray<z.ZodString, "many">;
616
616
  }, "strip", z.ZodTypeAny, {
617
- key: string;
618
617
  title: string;
618
+ key: string;
619
619
  facts: string[];
620
620
  }, {
621
- key: string;
622
621
  title: string;
622
+ key: string;
623
623
  facts: string[];
624
624
  }>, "many">>>;
625
625
  }, "strip", z.ZodTypeAny, {
@@ -627,8 +627,8 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
627
627
  name: string;
628
628
  facts: string[];
629
629
  structuredSections?: {
630
- key: string;
631
630
  title: string;
631
+ key: string;
632
632
  facts: string[];
633
633
  }[] | null | undefined;
634
634
  promptedByQuestion?: string | null | undefined;
@@ -637,8 +637,8 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
637
637
  name: string;
638
638
  facts: string[];
639
639
  structuredSections?: {
640
- key: string;
641
640
  title: string;
641
+ key: string;
642
642
  facts: string[];
643
643
  }[] | null | undefined;
644
644
  promptedByQuestion?: string | null | undefined;
@@ -698,8 +698,8 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
698
698
  name: string;
699
699
  facts: string[];
700
700
  structuredSections?: {
701
- key: string;
702
701
  title: string;
702
+ key: string;
703
703
  facts: string[];
704
704
  }[] | null | undefined;
705
705
  promptedByQuestion?: string | null | undefined;
@@ -750,8 +750,8 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
750
750
  name: string;
751
751
  facts: string[];
752
752
  structuredSections?: {
753
- key: string;
754
753
  title: string;
754
+ key: string;
755
755
  facts: string[];
756
756
  }[] | null | undefined;
757
757
  promptedByQuestion?: string | null | undefined;
@@ -1003,12 +1003,12 @@ declare const ExtractionResultSchema: z.ZodObject<{
1003
1003
  title: z.ZodString;
1004
1004
  facts: z.ZodArray<z.ZodString, "many">;
1005
1005
  }, "strip", z.ZodTypeAny, {
1006
- key: string;
1007
1006
  title: string;
1007
+ key: string;
1008
1008
  facts: string[];
1009
1009
  }, {
1010
- key: string;
1011
1010
  title: string;
1011
+ key: string;
1012
1012
  facts: string[];
1013
1013
  }>, "many">>>;
1014
1014
  }, "strip", z.ZodTypeAny, {
@@ -1016,8 +1016,8 @@ declare const ExtractionResultSchema: z.ZodObject<{
1016
1016
  name: string;
1017
1017
  facts: string[];
1018
1018
  structuredSections?: {
1019
- key: string;
1020
1019
  title: string;
1020
+ key: string;
1021
1021
  facts: string[];
1022
1022
  }[] | null | undefined;
1023
1023
  promptedByQuestion?: string | null | undefined;
@@ -1026,8 +1026,8 @@ declare const ExtractionResultSchema: z.ZodObject<{
1026
1026
  name: string;
1027
1027
  facts: string[];
1028
1028
  structuredSections?: {
1029
- key: string;
1030
1029
  title: string;
1030
+ key: string;
1031
1031
  facts: string[];
1032
1032
  }[] | null | undefined;
1033
1033
  promptedByQuestion?: string | null | undefined;
@@ -1106,8 +1106,8 @@ declare const ExtractionResultSchema: z.ZodObject<{
1106
1106
  name: string;
1107
1107
  facts: string[];
1108
1108
  structuredSections?: {
1109
- key: string;
1110
1109
  title: string;
1110
+ key: string;
1111
1111
  facts: string[];
1112
1112
  }[] | null | undefined;
1113
1113
  promptedByQuestion?: string | null | undefined;
@@ -1164,8 +1164,8 @@ declare const ExtractionResultSchema: z.ZodObject<{
1164
1164
  name: string;
1165
1165
  facts: string[];
1166
1166
  structuredSections?: {
1167
- key: string;
1168
1167
  title: string;
1168
+ key: string;
1169
1169
  facts: string[];
1170
1170
  }[] | null | undefined;
1171
1171
  promptedByQuestion?: string | null | undefined;
@@ -1229,12 +1229,12 @@ declare const ConsolidationResultSchema: z.ZodObject<{
1229
1229
  title: z.ZodString;
1230
1230
  facts: z.ZodArray<z.ZodString, "many">;
1231
1231
  }, "strip", z.ZodTypeAny, {
1232
- key: string;
1233
1232
  title: string;
1233
+ key: string;
1234
1234
  facts: string[];
1235
1235
  }, {
1236
- key: string;
1237
1236
  title: string;
1237
+ key: string;
1238
1238
  facts: string[];
1239
1239
  }>, "many">>>;
1240
1240
  }, "strip", z.ZodTypeAny, {
@@ -1242,8 +1242,8 @@ declare const ConsolidationResultSchema: z.ZodObject<{
1242
1242
  name: string;
1243
1243
  facts: string[];
1244
1244
  structuredSections?: {
1245
- key: string;
1246
1245
  title: string;
1246
+ key: string;
1247
1247
  facts: string[];
1248
1248
  }[] | null | undefined;
1249
1249
  promptedByQuestion?: string | null | undefined;
@@ -1252,8 +1252,8 @@ declare const ConsolidationResultSchema: z.ZodObject<{
1252
1252
  name: string;
1253
1253
  facts: string[];
1254
1254
  structuredSections?: {
1255
- key: string;
1256
1255
  title: string;
1256
+ key: string;
1257
1257
  facts: string[];
1258
1258
  }[] | null | undefined;
1259
1259
  promptedByQuestion?: string | null | undefined;
@@ -1272,8 +1272,8 @@ declare const ConsolidationResultSchema: z.ZodObject<{
1272
1272
  name: string;
1273
1273
  facts: string[];
1274
1274
  structuredSections?: {
1275
- key: string;
1276
1275
  title: string;
1276
+ key: string;
1277
1277
  facts: string[];
1278
1278
  }[] | null | undefined;
1279
1279
  promptedByQuestion?: string | null | undefined;
@@ -1292,8 +1292,8 @@ declare const ConsolidationResultSchema: z.ZodObject<{
1292
1292
  name: string;
1293
1293
  facts: string[];
1294
1294
  structuredSections?: {
1295
- key: string;
1296
1295
  title: string;
1296
+ key: string;
1297
1297
  facts: string[];
1298
1298
  }[] | null | undefined;
1299
1299
  promptedByQuestion?: string | null | undefined;
@@ -313,13 +313,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
313
313
  peerProfiles: boolean;
314
314
  }>;
315
315
  }, "strip", z.ZodTypeAny, {
316
+ schemaVersion: string;
316
317
  includes: {
317
318
  procedural: boolean;
318
319
  taxonomy: boolean;
319
320
  identityAnchors: boolean;
320
321
  peerProfiles: boolean;
321
322
  };
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;
337
338
  includes: {
338
339
  procedural: boolean;
339
340
  taxonomy: boolean;
340
341
  identityAnchors: boolean;
341
342
  peerProfiles: boolean;
342
343
  };
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;
467
468
  includes: {
468
469
  procedural: boolean;
469
470
  taxonomy: boolean;
470
471
  identityAnchors: boolean;
471
472
  peerProfiles: boolean;
472
473
  };
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;
488
489
  includes: {
489
490
  procedural: boolean;
490
491
  taxonomy: boolean;
491
492
  identityAnchors: boolean;
492
493
  peerProfiles: boolean;
493
494
  };
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;
521
522
  includes: {
522
523
  procedural: boolean;
523
524
  taxonomy: boolean;
524
525
  identityAnchors: boolean;
525
526
  peerProfiles: boolean;
526
527
  };
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;
554
555
  includes: {
555
556
  procedural: boolean;
556
557
  taxonomy: boolean;
557
558
  identityAnchors: boolean;
558
559
  peerProfiles: boolean;
559
560
  };
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;
686
687
  includes: {
687
688
  procedural: boolean;
688
689
  taxonomy: boolean;
689
690
  identityAnchors: boolean;
690
691
  peerProfiles: boolean;
691
692
  };
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;
707
708
  includes: {
708
709
  procedural: boolean;
709
710
  taxonomy: boolean;
710
711
  identityAnchors: boolean;
711
712
  peerProfiles: boolean;
712
713
  };
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;
740
741
  includes: {
741
742
  procedural: boolean;
742
743
  taxonomy: boolean;
743
744
  identityAnchors: boolean;
744
745
  peerProfiles: boolean;
745
746
  };
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;
773
774
  includes: {
774
775
  procedural: boolean;
775
776
  taxonomy: boolean;
776
777
  identityAnchors: boolean;
777
778
  peerProfiles: boolean;
778
779
  };
779
- schemaVersion: string;
780
780
  id: string;
781
781
  description: string;
782
782
  version: string;
@@ -819,13 +819,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
819
819
  pluginVersion: string;
820
820
  includesTranscripts: boolean;
821
821
  capsule: {
822
+ schemaVersion: string;
822
823
  includes: {
823
824
  procedural: boolean;
824
825
  taxonomy: boolean;
825
826
  identityAnchors: boolean;
826
827
  peerProfiles: boolean;
827
828
  };
828
- schemaVersion: string;
829
829
  id: string;
830
830
  description: string;
831
831
  version: string;
@@ -858,13 +858,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
858
858
  pluginVersion: string;
859
859
  includesTranscripts: boolean;
860
860
  capsule: {
861
+ schemaVersion: string;
861
862
  includes: {
862
863
  procedural: boolean;
863
864
  taxonomy: boolean;
864
865
  identityAnchors: boolean;
865
866
  peerProfiles: boolean;
866
867
  };
867
- schemaVersion: string;
868
868
  id: string;
869
869
  description: string;
870
870
  version: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.706",
3
+ "version": "9.3.707",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -2966,7 +2966,7 @@
2966
2966
  "core"
2967
2967
  ],
2968
2968
  "peerDependencies": {
2969
- "@remnic/coding-graph": "^9.3.706"
2969
+ "@remnic/coding-graph": "^9.3.707"
2970
2970
  },
2971
2971
  "peerDependenciesMeta": {
2972
2972
  "@remnic/coding-graph": {
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Issue #1492 — Remnic Coding mode inconsistencies.
3
+ *
4
+ * Cross-subsystem characterization of the three coding-mode drifts an external
5
+ * reporter found when wiring per-repo/project namespace isolation for a shared
6
+ * multi-user Remnic deployment. Each reported symptom was fixed by a dedicated
7
+ * child issue; this file is the single #1492-named anchor that ties them
8
+ * together so a regression in ANY of the three subsystems is caught and
9
+ * attributed here.
10
+ *
11
+ * Symptom A (observe handler ignored coding context)
12
+ * → fixed by #1495: observe resolves ONE effective scope plan
13
+ * (`resolveMemoryScopePlan`) whose `writeNamespace` is byte-identical to
14
+ * what `memory_store`/`suggestion_submit` resolve via
15
+ * `resolveCodingScopedWriteNamespace`, so observed turns and explicit
16
+ * writes on the same project-scoped session land in the SAME store a
17
+ * same-session recall reads. Deep plan-level parity is guarded by
18
+ * `access-service-observe-scope.test.ts` (#1495). Here we pin the
19
+ * coding-namespace derivation invariant the drift violated: the scoped
20
+ * namespace is built by exactly ONE `combineNamespaces(principal, overlay)`
21
+ * and is stable/idempotent — never a second ad-hoc combination that could
22
+ * drift between the write and read paths.
23
+ *
24
+ * Symptom B (transcript/summarizer session-key parser conflated sessions)
25
+ * → fixed by #1496: arbitrary session keys (`pi-geek:abc123`, `pi:abc123`,
26
+ * `myuser:abc123`) are first-class `session/<hash>` identities — never the
27
+ * legacy `other/default` bucket — for BOTH transcript.ts and summarizer.ts.
28
+ *
29
+ * Symptom C (collectActiveMemoryPaths scanned only 4 of 12+ category dirs)
30
+ * → fixed by #1497: the QMD-unavailable filesystem fallback recall scans
31
+ * `RECALL_FALLBACK_DIRS` (every recall category dir), so a memory written
32
+ * to e.g. `preferences/` is found when QMD is disabled/missing/unhealthy.
33
+ *
34
+ * The architectural gaps the reporter also called out — the maintenance
35
+ * pipeline being blind to dynamic project-scoped namespaces (#1500), and the
36
+ * admin/webui not surfacing coding context (#1502) — were tracked by their own
37
+ * issues and are both CLOSED.
38
+ */
39
+
40
+ import assert from "node:assert/strict";
41
+ import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
42
+ import type { Dirent } from "node:fs";
43
+ import os from "node:os";
44
+ import path from "node:path";
45
+ import test from "node:test";
46
+
47
+ import {
48
+ combineNamespaces,
49
+ projectNamespaceName,
50
+ projectTagProjectId,
51
+ resolveCodingNamespaceOverlay,
52
+ } from "./coding/coding-namespace.js";
53
+ import {
54
+ SESSION_CHANNEL_TYPE,
55
+ parseSessionIdentity,
56
+ sessionStoragePaths,
57
+ } from "./session-identity.js";
58
+ import { HourlySummarizer } from "./summarizer.js";
59
+ import type { HourlySummary, PluginConfig } from "./types.js";
60
+ import { StorageManager } from "./storage.js";
61
+ import { RECALL_FALLBACK_DIRS } from "./utils/category-dir.js";
62
+
63
+ /** Recursively collect the immediate child directory names under `root` that
64
+ * contain at least one file, into `out`. Used to verify the summarizer wrote
65
+ * each session's summary into its OWN dir (no conflation). */
66
+ async function walkCollectDirs(root: string, out: Set<string>): Promise<void> {
67
+ let entries: Dirent[];
68
+ try {
69
+ entries = await readdir(root, { withFileTypes: true });
70
+ } catch {
71
+ return;
72
+ }
73
+ for (const entry of entries) {
74
+ if (!entry.isDirectory()) continue;
75
+ const child = path.join(root, entry.name);
76
+ let childEntries: Dirent[];
77
+ try {
78
+ childEntries = await readdir(child, { withFileTypes: true });
79
+ } catch {
80
+ continue;
81
+ }
82
+ if (childEntries.some((e) => e.isFile())) {
83
+ out.add(entry.name);
84
+ } else {
85
+ await walkCollectDirs(child, out);
86
+ }
87
+ }
88
+ }
89
+
90
+ // ─── Symptom A: coding-namespace derivation is stable across write/read ────
91
+
92
+ test("#1492 Symptom A: a project-scoped session derives ONE stable overlay namespace — the write/read convergence point", () => {
93
+ // The reporter's scenario: user "pi-geek" in project "Blend/Supply". Both the
94
+ // observe write path and the recall read path must resolve the SAME scoped
95
+ // namespace. They do so by combining the principal base with the project
96
+ // overlay through exactly ONE helper (combineNamespaces); a second ad-hoc
97
+ // combination is what caused the original drift.
98
+ const principal = "pi-geek";
99
+ const projectId = projectTagProjectId("Blend/Supply");
100
+ const codingContext = {
101
+ projectId,
102
+ branch: null,
103
+ rootPath: projectId,
104
+ defaultBranch: null,
105
+ };
106
+ const overlay = resolveCodingNamespaceOverlay(
107
+ codingContext,
108
+ { projectScope: true, branchScope: false, globalFallback: false },
109
+ "default",
110
+ );
111
+ assert.ok(overlay, "projectScope=true must yield a coding overlay");
112
+
113
+ const scopedWrite = combineNamespaces(principal, overlay.namespace);
114
+ const scopedRead = combineNamespaces(principal, overlay.namespace);
115
+
116
+ // Write and read converge on the identical scoped namespace.
117
+ assert.equal(scopedRead, scopedWrite);
118
+ // It is well-formed: principal base + project overlay, not the flat default.
119
+ assert.equal(scopedWrite, combineNamespaces(principal, projectNamespaceName(projectId)));
120
+ assert.ok(
121
+ scopedWrite.startsWith(`${principal}-`),
122
+ "scoped namespace must be principal-prefixed, preserving per-user isolation",
123
+ );
124
+ assert.notEqual(scopedWrite, principal, "must layer the project overlay onto the principal base");
125
+ assert.notEqual(scopedWrite, "default", "must not collapse to the flat default namespace");
126
+ // Two principals in the same repo get distinct scoped namespaces.
127
+ const other = combineNamespaces("pi-friend", overlay.namespace);
128
+ assert.notEqual(scopedWrite, other, "per-user isolation: distinct principals stay distinct");
129
+ });
130
+
131
+ test("#1492 Symptom A: combineNamespaces is idempotent under re-derivation (no double-combination drift)", () => {
132
+ // A read path that (incorrectly) re-combined an already-scoped namespace with
133
+ // the overlay would produce `pi-friend-project-x-project-x` and miss the data.
134
+ // Guard the single-combination contract: combineNamespaces joins base+overlay
135
+ // with `-`, so re-combining must change the result (drift detector).
136
+ const principal = "pi-friend";
137
+ const overlayNs = projectNamespaceName(projectTagProjectId("Remnic"));
138
+ const once = combineNamespaces(principal, overlayNs);
139
+ const twice = combineNamespaces(once, overlayNs);
140
+ assert.notEqual(once, twice, "re-combining a scoped namespace must change it (drift detector)");
141
+ assert.ok(once.startsWith(`${principal}-`), "scoped namespace is principal-prefixed");
142
+ assert.ok(!once.includes(`${overlayNs}-${overlayNs}`), "no duplicated overlay fragment");
143
+ });
144
+
145
+ // ─── Symptom B: arbitrary session keys never conflate ──────────────────────
146
+
147
+ test("#1492 Symptom B: arbitrary session keys get distinct transcript AND summarizer paths, never other/default", async () => {
148
+ // Exact keys from the issue body. Pre-fix these all collapsed into
149
+ // transcripts/other/default/*.jsonl AND summaries/other/default/*.md,
150
+ // conflating every non-agent-prefixed session in BOTH subsystems.
151
+ const keys = ["pi-geek:abc123", "pi:abc123", "myuser:abc123", "pi-friend:def456"];
152
+
153
+ // ── Transcript path (sessionStoragePaths, consumed by transcript.ts) ──
154
+ const transcriptDirs = new Set<string>();
155
+ for (const key of keys) {
156
+ const id = parseSessionIdentity(key);
157
+ assert.equal(id.legacy, false, `${key} must not be treated as a legacy agent: key`);
158
+ assert.equal(id.channelType, SESSION_CHANNEL_TYPE, `${key} channelType must be "session"`);
159
+ assert.notEqual(id.channelType, "other", `${key} must not fall back to "other"`);
160
+ assert.notEqual(id.channelId, "default", `${key} must not fall back to "default"`);
161
+ const paths = sessionStoragePaths(key);
162
+ assert.match(paths.dir, /^session\/[0-9a-f]{16}$/, `${key} transcript dir must be session/<hash>`);
163
+ transcriptDirs.add(paths.dir);
164
+ }
165
+ assert.equal(transcriptDirs.size, keys.length, "each session key must map to a distinct transcript dir");
166
+
167
+ // ── Summarizer path: exercise the REAL HourlySummarizer.saveSummary() so a
168
+ // regression in summarizer.ts itself (reverting to the old parsed-session
169
+ // bucket) is caught here, not just a regression in session-identity.ts.
170
+ // Pre-fix the summarizer derived its dir from the same brittle split(':')
171
+ // parser; this round-trip proves the written summary files land in
172
+ // distinct per-session dirs (no other/default conflation). ──
173
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "engram-1492-sum-"));
174
+ try {
175
+ const config = {
176
+ memoryDir,
177
+ localLlmEnabled: false,
178
+ localLlmFallback: false,
179
+ localLlmUrl: "http://localhost:1234/v1",
180
+ localLlmModel: "local-model",
181
+ } as PluginConfig;
182
+ const summarizer = new HourlySummarizer(config);
183
+
184
+ const summary: HourlySummary = {
185
+ hour: "2026-06-28T14:00:00.000Z",
186
+ sessionKey: "", // set per key below
187
+ bullets: ["worked on shared memory pool"],
188
+ turnCount: 2,
189
+ generatedAt: "2026-06-28T14:05:00.000Z",
190
+ };
191
+
192
+ for (const key of keys) {
193
+ await summarizer.saveSummary({ ...summary, sessionKey: key });
194
+ }
195
+
196
+ // Walk summaries/hourly/ and collect the distinct per-session dirs that
197
+ // actually received a file. Each must be the encoded session key, never
198
+ // "other" or "default".
199
+ const hourlyRoot = path.join(memoryDir, "summaries", "hourly");
200
+ const sessionDirs = new Set<string>();
201
+ await walkCollectDirs(hourlyRoot, sessionDirs);
202
+ assert.ok(sessionDirs.size >= keys.length, "each session key must produce a distinct summary dir");
203
+ for (const dir of sessionDirs) {
204
+ assert.notEqual(dir, "other", `summary dir "${dir}" must not be the legacy "other" bucket`);
205
+ assert.notEqual(dir, "default", `summary dir "${dir}" must not be the legacy "default" bucket`);
206
+ }
207
+ } finally {
208
+ await rm(memoryDir, { recursive: true, force: true });
209
+ }
210
+ });
211
+
212
+ test("#1492 Symptom B: legacy agent: keys keep their readable identity (no regression from the fix)", () => {
213
+ // The fix must not break the legacy 5-part agent:<id>:discord:channel:<chan> shape.
214
+ const id = parseSessionIdentity("agent:generalist:discord:channel:998877");
215
+ assert.equal(id.legacy, true);
216
+ assert.equal(id.channelType, "discord");
217
+ assert.equal(id.channelId, "998877");
218
+ });
219
+
220
+ // ─── Symptom C: fallback recall reads EVERY category dir ───────────────────
221
+
222
+ function memoryFile(id: string, category: string, content: string): string {
223
+ const now = new Date().toISOString();
224
+ return [
225
+ "---",
226
+ `id: ${id}`,
227
+ `category: ${category}`,
228
+ `created: ${now}`,
229
+ `updated: ${now}`,
230
+ "confidence: 0.9",
231
+ "importance: 5",
232
+ "tags: []",
233
+ "---",
234
+ "",
235
+ content,
236
+ "",
237
+ ].join("\n");
238
+ }
239
+
240
+ async function makeStorage(prefix = "engram-1492-"): Promise<{
241
+ storage: StorageManager;
242
+ baseDir: string;
243
+ cleanup: () => Promise<void>;
244
+ }> {
245
+ const baseDir = await mkdtemp(path.join(os.tmpdir(), prefix));
246
+ const storage = new StorageManager(baseDir);
247
+ await storage.ensureDirectories();
248
+ StorageManager.clearAllStaticCaches();
249
+ storage.invalidateAllMemoriesCacheForDir();
250
+ return {
251
+ storage,
252
+ baseDir,
253
+ cleanup: async () => {
254
+ StorageManager.clearAllStaticCaches();
255
+ await rm(baseDir, { recursive: true, force: true });
256
+ },
257
+ };
258
+ }
259
+
260
+ test("#1492 Symptom C: a memory in a non-legacy-4 category (preferences/) is found by fallback disk recall", async () => {
261
+ // The reporter found collectActiveMemoryPaths scanned only
262
+ // facts/procedures/reasoning-traces/corrections. A memory written to
263
+ // preferences/ was invisible to the QMD-unavailable fallback, so recall
264
+ // returned empty even though the file was on disk.
265
+ const { storage, baseDir, cleanup } = await makeStorage();
266
+ try {
267
+ const preferencesDir = path.join(baseDir, "preferences");
268
+ await mkdir(preferencesDir, { recursive: true });
269
+ await writeFile(
270
+ path.join(preferencesDir, "pref-1.md"),
271
+ memoryFile("pref-1", "preference", "Prefer tabs over spaces in this repo."),
272
+ "utf-8",
273
+ );
274
+ storage.invalidateAllMemoriesCacheForDir();
275
+
276
+ const memories = await storage.readAllMemories();
277
+ const ids = memories.map((m) => m.frontmatter?.id ?? "");
278
+ assert.ok(
279
+ ids.includes("pref-1"),
280
+ "fallback disk recall must read preferences/ — the 4-dir scan bug is fixed",
281
+ );
282
+ } finally {
283
+ await cleanup();
284
+ }
285
+ });
286
+
287
+ test("#1492 Symptom C: RECALL_FALLBACK_DIRS covers the categories the issue named as missed", () => {
288
+ // The issue enumerated questions/, entities/, decisions/, preferences/,
289
+ // commitments/, principles/, skills/, relationships/, moments/, rules/,
290
+ // artifacts/ as directories the system writes to but the old 4-dir scan
291
+ // missed. Assert the recall-memory categories among them are now covered.
292
+ // (questions/ is intentionally a non-memory queue dir; artifacts/ and
293
+ // entities/ are non-category content dirs — see category-dir.ts. The recall
294
+ // *memory* categories must all be present.)
295
+ const requiredRecallCategories = [
296
+ "decisions",
297
+ "preferences",
298
+ "commitments",
299
+ "principles",
300
+ "skills",
301
+ "relationships",
302
+ "moments",
303
+ "rules",
304
+ ];
305
+ for (const dir of requiredRecallCategories) {
306
+ assert.ok(
307
+ RECALL_FALLBACK_DIRS.includes(dir),
308
+ `RECALL_FALLBACK_DIRS must include ${dir}/ (missed by the pre-fix 4-dir scan)`,
309
+ );
310
+ }
311
+ // The legacy four are still present.
312
+ for (const dir of ["facts", "procedures", "reasoning-traces", "corrections"]) {
313
+ assert.ok(RECALL_FALLBACK_DIRS.includes(dir), `legacy recall dir ${dir}/ must remain covered`);
314
+ }
315
+ });