@remnic/core 9.3.649 → 9.3.651

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 (176) hide show
  1. package/dist/access-cli.js +36 -35
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.d.ts +2 -2
  4. package/dist/access-http.js +16 -16
  5. package/dist/access-mcp.d.ts +2 -2
  6. package/dist/access-mcp.js +15 -15
  7. package/dist/access-schema.js +3 -3
  8. package/dist/{access-service-DFXIlGvZ.d.ts → access-service-DIZRHQ7Q.d.ts} +255 -2
  9. package/dist/access-service.d.ts +2 -2
  10. package/dist/access-service.js +13 -13
  11. package/dist/{auto-sync-54QQHOG5.js → auto-sync-5CJBJMPZ.js} +5 -5
  12. package/dist/bootstrap.d.ts +1 -1
  13. package/dist/briefing.js +3 -3
  14. package/dist/calibration.js +2 -2
  15. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  16. package/dist/causal-consolidation.js +6 -6
  17. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  18. package/dist/{chunk-OAZ5MFUB.js → chunk-3XGWCZ63.js} +45 -28
  19. package/dist/chunk-3XGWCZ63.js.map +1 -0
  20. package/dist/{chunk-QKE4LHNR.js → chunk-4HYSMH7D.js} +2 -2
  21. package/dist/{chunk-NMIOW7XG.js → chunk-4PTKFBST.js} +2 -2
  22. package/dist/{chunk-DDRNDPX4.js → chunk-4SKKVWLQ.js} +2 -2
  23. package/dist/chunk-5FOCXX5E.js +34 -0
  24. package/dist/chunk-5FOCXX5E.js.map +1 -0
  25. package/dist/{chunk-XUGVP7ZU.js → chunk-5WSDHTBO.js} +166 -47
  26. package/dist/chunk-5WSDHTBO.js.map +1 -0
  27. package/dist/{chunk-WPCCNSWO.js → chunk-6UKL6IXM.js} +4 -4
  28. package/dist/{chunk-DB5A3NHS.js → chunk-7LWRCOP7.js} +9 -2
  29. package/dist/chunk-7LWRCOP7.js.map +1 -0
  30. package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
  31. package/dist/{chunk-4BISW7RX.js → chunk-AJE7FJVE.js} +2 -2
  32. package/dist/{chunk-ZXWAQFDE.js → chunk-CFOCZPIQ.js} +2 -2
  33. package/dist/{chunk-NT5TINK5.js → chunk-DHGSZ3UD.js} +2 -2
  34. package/dist/{chunk-OTC2KOZ2.js → chunk-EHQLDFSH.js} +2 -2
  35. package/dist/{chunk-AMACWKM4.js → chunk-IJHLC5CH.js} +2 -2
  36. package/dist/{chunk-OR7R6M5Z.js → chunk-IVYSVAC6.js} +2 -2
  37. package/dist/{chunk-UMKPSD35.js → chunk-JF7SFXTG.js} +2 -2
  38. package/dist/{chunk-MCYT2RNT.js → chunk-KJDKZVF3.js} +3 -3
  39. package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
  40. package/dist/{chunk-PQFUUXWK.js → chunk-KWM33SPU.js} +2 -2
  41. package/dist/{chunk-A3BS64GV.js → chunk-LCC5EZTT.js} +4 -4
  42. package/dist/{chunk-ZT6R3WR3.js → chunk-LFTLXOFX.js} +4 -4
  43. package/dist/{chunk-CNRZ6WJU.js → chunk-MF32AL7N.js} +5 -5
  44. package/dist/{chunk-6GIKAUTN.js → chunk-MMJANTJX.js} +33 -2
  45. package/dist/{chunk-6GIKAUTN.js.map → chunk-MMJANTJX.js.map} +1 -1
  46. package/dist/{chunk-D6WVJIS3.js → chunk-ORGWWNJG.js} +2 -2
  47. package/dist/{chunk-Z3PZRDLW.js → chunk-PRQXUSQV.js} +2 -2
  48. package/dist/{chunk-VWT3F4IV.js → chunk-PS3SYNHP.js} +12 -4
  49. package/dist/chunk-PS3SYNHP.js.map +1 -0
  50. package/dist/{chunk-IMWFHBG2.js → chunk-QWRC7GIO.js} +2 -2
  51. package/dist/{chunk-FQYFMIKG.js → chunk-RKN5J4RO.js} +26 -26
  52. package/dist/{chunk-FUXV6HSO.js → chunk-RSS2KWN6.js} +5 -5
  53. package/dist/{chunk-U3GQ33JC.js → chunk-SLTKP5WJ.js} +2 -2
  54. package/dist/{chunk-5ETA6OAS.js → chunk-SLYD3AH4.js} +617 -89
  55. package/dist/chunk-SLYD3AH4.js.map +1 -0
  56. package/dist/{chunk-6NKAQ74D.js → chunk-UU6MVCJ6.js} +1 -1
  57. package/dist/chunk-UU6MVCJ6.js.map +1 -0
  58. package/dist/{chunk-WEPMT6SC.js → chunk-V25ZAOSB.js} +5 -5
  59. package/dist/{chunk-UMTG2BN2.js → chunk-V4UDXYGG.js} +2 -2
  60. package/dist/{chunk-RRRCNIPK.js → chunk-WJK75OCH.js} +4 -4
  61. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  62. package/dist/{chunk-OZKZ2TRP.js → chunk-XBIACVCO.js} +9 -2
  63. package/dist/chunk-XBIACVCO.js.map +1 -0
  64. package/dist/{chunk-ALUZN7BE.js → chunk-XMN6MMTU.js} +2 -2
  65. package/dist/{chunk-A4BTPHIN.js → chunk-Y7NWBBHV.js} +6 -6
  66. package/dist/{chunk-M75TBFKQ.js → chunk-Z2OXSMZK.js} +2 -2
  67. package/dist/{cli-DrL2Nv4j.d.ts → cli-BG4ybtJr.d.ts} +2 -2
  68. package/dist/cli.d.ts +3 -3
  69. package/dist/cli.js +31 -31
  70. package/dist/compounding/engine.js +3 -3
  71. package/dist/connectors/codex-materialize-runner.js +3 -3
  72. package/dist/connectors/index.js +3 -3
  73. package/dist/entity-retrieval.js +3 -3
  74. package/dist/event-order-recall.js +1 -1
  75. package/dist/explicit-capture.d.ts +1 -1
  76. package/dist/explicit-cue-recall.d.ts +7 -0
  77. package/dist/explicit-cue-recall.js +2 -1
  78. package/dist/extraction-judge.js +3 -3
  79. package/dist/extraction.js +3 -3
  80. package/dist/fallback-llm.js +2 -2
  81. package/dist/focused-list-recall.d.ts +6 -0
  82. package/dist/focused-list-recall.js +2 -1
  83. package/dist/index.d.ts +4 -4
  84. package/dist/index.js +84 -83
  85. package/dist/index.js.map +1 -1
  86. package/dist/lcm/engine.js +2 -2
  87. package/dist/lcm/index.js +5 -5
  88. package/dist/lcm-fallback-read.d.ts +71 -0
  89. package/dist/lcm-fallback-read.js +10 -0
  90. package/dist/lcm-fallback-read.js.map +1 -0
  91. package/dist/maintenance/memory-governance.js +3 -3
  92. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  93. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  94. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  95. package/dist/namespaces/migrate.js +7 -7
  96. package/dist/namespaces/search.js +3 -3
  97. package/dist/namespaces/storage.js +3 -3
  98. package/dist/operator-toolkit.js +9 -9
  99. package/dist/{orchestrator-DEQW9j0Z.d.ts → orchestrator-CX-oqwJq.d.ts} +58 -0
  100. package/dist/orchestrator.d.ts +1 -1
  101. package/dist/orchestrator.js +30 -29
  102. package/dist/recall-planner-llm.js +2 -2
  103. package/dist/response-guidance-recall.d.ts +6 -0
  104. package/dist/response-guidance-recall.js +2 -1
  105. package/dist/schemas.d.ts +22 -22
  106. package/dist/search/factory.js +2 -2
  107. package/dist/search/index.js +4 -4
  108. package/dist/semantic-consolidation.js +4 -4
  109. package/dist/semantic-rule-promotion.js +3 -3
  110. package/dist/semantic-rule-verifier.js +3 -3
  111. package/dist/storage.js +2 -2
  112. package/dist/summarizer.js +3 -3
  113. package/dist/targeted-fact-recall.d.ts +6 -0
  114. package/dist/targeted-fact-recall.js +2 -1
  115. package/dist/transfer/backup.js +2 -2
  116. package/dist/transfer/capsule-export.js +2 -2
  117. package/dist/transfer/capsule-import.js +2 -2
  118. package/dist/transfer/import-sqlite.js +2 -2
  119. package/dist/transfer/types.d.ts +12 -12
  120. package/dist/verified-recall.js +3 -3
  121. package/package.json +1 -1
  122. package/src/access-service-lcm-forgery.test.ts +410 -0
  123. package/src/access-service-observe-lcm-parity.test.ts +1397 -0
  124. package/src/access-service-observe-scope.test.ts +599 -0
  125. package/src/access-service-raw-excerpt-read-gate.test.ts +443 -0
  126. package/src/access-service.ts +1270 -113
  127. package/src/coding/coding-namespace.test.ts +44 -0
  128. package/src/coding/coding-namespace.ts +163 -0
  129. package/src/event-order-recall.ts +8 -0
  130. package/src/explicit-cue-recall.ts +70 -29
  131. package/src/focused-list-recall.ts +23 -1
  132. package/src/lcm-fallback-read.ts +113 -0
  133. package/src/orchestrator.ts +331 -26
  134. package/src/response-guidance-recall.ts +21 -1
  135. package/src/targeted-fact-recall.ts +24 -3
  136. package/dist/chunk-5ETA6OAS.js.map +0 -1
  137. package/dist/chunk-6NKAQ74D.js.map +0 -1
  138. package/dist/chunk-DB5A3NHS.js.map +0 -1
  139. package/dist/chunk-OAZ5MFUB.js.map +0 -1
  140. package/dist/chunk-OZKZ2TRP.js.map +0 -1
  141. package/dist/chunk-VWT3F4IV.js.map +0 -1
  142. package/dist/chunk-XUGVP7ZU.js.map +0 -1
  143. /package/dist/{auto-sync-54QQHOG5.js.map → auto-sync-5CJBJMPZ.js.map} +0 -0
  144. /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  145. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  146. /package/dist/{chunk-QKE4LHNR.js.map → chunk-4HYSMH7D.js.map} +0 -0
  147. /package/dist/{chunk-NMIOW7XG.js.map → chunk-4PTKFBST.js.map} +0 -0
  148. /package/dist/{chunk-DDRNDPX4.js.map → chunk-4SKKVWLQ.js.map} +0 -0
  149. /package/dist/{chunk-WPCCNSWO.js.map → chunk-6UKL6IXM.js.map} +0 -0
  150. /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
  151. /package/dist/{chunk-4BISW7RX.js.map → chunk-AJE7FJVE.js.map} +0 -0
  152. /package/dist/{chunk-ZXWAQFDE.js.map → chunk-CFOCZPIQ.js.map} +0 -0
  153. /package/dist/{chunk-NT5TINK5.js.map → chunk-DHGSZ3UD.js.map} +0 -0
  154. /package/dist/{chunk-OTC2KOZ2.js.map → chunk-EHQLDFSH.js.map} +0 -0
  155. /package/dist/{chunk-AMACWKM4.js.map → chunk-IJHLC5CH.js.map} +0 -0
  156. /package/dist/{chunk-OR7R6M5Z.js.map → chunk-IVYSVAC6.js.map} +0 -0
  157. /package/dist/{chunk-UMKPSD35.js.map → chunk-JF7SFXTG.js.map} +0 -0
  158. /package/dist/{chunk-MCYT2RNT.js.map → chunk-KJDKZVF3.js.map} +0 -0
  159. /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
  160. /package/dist/{chunk-PQFUUXWK.js.map → chunk-KWM33SPU.js.map} +0 -0
  161. /package/dist/{chunk-A3BS64GV.js.map → chunk-LCC5EZTT.js.map} +0 -0
  162. /package/dist/{chunk-ZT6R3WR3.js.map → chunk-LFTLXOFX.js.map} +0 -0
  163. /package/dist/{chunk-CNRZ6WJU.js.map → chunk-MF32AL7N.js.map} +0 -0
  164. /package/dist/{chunk-D6WVJIS3.js.map → chunk-ORGWWNJG.js.map} +0 -0
  165. /package/dist/{chunk-Z3PZRDLW.js.map → chunk-PRQXUSQV.js.map} +0 -0
  166. /package/dist/{chunk-IMWFHBG2.js.map → chunk-QWRC7GIO.js.map} +0 -0
  167. /package/dist/{chunk-FQYFMIKG.js.map → chunk-RKN5J4RO.js.map} +0 -0
  168. /package/dist/{chunk-FUXV6HSO.js.map → chunk-RSS2KWN6.js.map} +0 -0
  169. /package/dist/{chunk-U3GQ33JC.js.map → chunk-SLTKP5WJ.js.map} +0 -0
  170. /package/dist/{chunk-WEPMT6SC.js.map → chunk-V25ZAOSB.js.map} +0 -0
  171. /package/dist/{chunk-UMTG2BN2.js.map → chunk-V4UDXYGG.js.map} +0 -0
  172. /package/dist/{chunk-RRRCNIPK.js.map → chunk-WJK75OCH.js.map} +0 -0
  173. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  174. /package/dist/{chunk-ALUZN7BE.js.map → chunk-XMN6MMTU.js.map} +0 -0
  175. /package/dist/{chunk-A4BTPHIN.js.map → chunk-Y7NWBBHV.js.map} +0 -0
  176. /package/dist/{chunk-M75TBFKQ.js.map → chunk-Z2OXSMZK.js.map} +0 -0
@@ -0,0 +1,443 @@
1
+ /**
2
+ * #1505 thread 2f7: the `disclosure: "raw"` excerpt path MUST pass through the
3
+ * SAME read-authorization gate as normal recall + `lcmSearch` + the in-prompt
4
+ * LCM sections — NOT `snapshot.namespace` (the effective WRITE/overlay
5
+ * namespace).
6
+ *
7
+ * The defect class: a project-scoped session whose principal can WRITE but not
8
+ * READ its self base (or whose `defaultRecallNamespaces` omits `self`) archives
9
+ * LCM rows under the `<principal>-project-*` overlay key (the write key). When
10
+ * `recall({ disclosure: "raw" })` derived the raw-excerpt LCM `session_id` from
11
+ * `snapshot.namespace`, it prefixed the lookup with that overlay namespace and
12
+ * surfaced `<principal>-project-*` transcript rows that normal recall and
13
+ * `lcmSearch` intentionally EXCLUDE for that reader — a cross-tenant read leak.
14
+ *
15
+ * After the fix the raw-excerpt lookup routes through
16
+ * `resolveRawExcerptReadNamespace` → `resolveLcmReadNamespace(..., "read")`,
17
+ * which honours the overlay only when the principal SELF base is in the readable
18
+ * recall set. When it is not, the lookup falls back to the default store (raw
19
+ * sessionKey) exactly like normal recall + `lcmSearch`.
20
+ *
21
+ * These tests exercise the private `executeRecall` directly (the budget /
22
+ * idempotency wrapper is orthogonal to the namespace gate) and assert the
23
+ * `session_id` that reaches the LCM engine's `searchContextFull`.
24
+ *
25
+ * All fixtures are synthetic — no real user data.
26
+ */
27
+ import assert from "node:assert/strict";
28
+ import test from "node:test";
29
+
30
+ import { EngramAccessService } from "./access-service.js";
31
+ import { CrossNamespaceBudget } from "./cross-namespace-budget.js";
32
+ import { Orchestrator } from "./orchestrator.js";
33
+ import type { LastRecallSnapshot } from "./recall-state.js";
34
+ import {
35
+ combineNamespaces,
36
+ lcmSessionKeyForNamespace,
37
+ projectNamespaceName,
38
+ projectTagProjectId,
39
+ } from "./coding/coding-namespace.js";
40
+ import type { StorageManager } from "./storage.js";
41
+ import type { CodingContext, PluginConfig } from "./types.js";
42
+
43
+ interface RawExcerptProbe {
44
+ service: EngramAccessService;
45
+ /** session_id values that reached the LCM engine's `searchContextFull`. */
46
+ searchSessionIds: Array<string | undefined>;
47
+ contexts: Map<string, CodingContext>;
48
+ }
49
+
50
+ /**
51
+ * Build a service whose orchestrator stub:
52
+ * - delegates principal / overlay resolution to the REAL Orchestrator
53
+ * prototype so the read gate is exercised exactly as production does it,
54
+ * - records the `session_id` the raw-excerpt LCM lookup prefixes,
55
+ * - returns a fixed `lastRecall` snapshot whose `namespace` is the WRITE/overlay
56
+ * namespace (simulating what `observe` wrote), with NO result paths so the
57
+ * test isolates the raw-excerpt session_id.
58
+ */
59
+ function makeRawExcerptProbe(options: {
60
+ config: Partial<PluginConfig>;
61
+ snapshotNamespace: string;
62
+ sessionContext?: CodingContext;
63
+ sessionKey: string;
64
+ }): RawExcerptProbe {
65
+ const searchSessionIds: Array<string | undefined> = [];
66
+ const contexts = new Map<string, CodingContext>();
67
+ if (options.sessionContext) {
68
+ contexts.set(options.sessionKey, options.sessionContext);
69
+ }
70
+
71
+ const config = {
72
+ namespacesEnabled: true,
73
+ defaultNamespace: "default",
74
+ sharedNamespace: "shared",
75
+ namespacePolicies: [],
76
+ defaultRecallNamespaces: ["self", "shared"],
77
+ codingMode: { projectScope: true },
78
+ memoryDir: "/synthetic/remnic-raw-excerpt-read-gate",
79
+ objectiveStateMemoryEnabled: false,
80
+ objectiveStateSnapshotWritesEnabled: false,
81
+ principalFromSessionKeyMode: "prefix",
82
+ principalFromSessionKeyRules: [],
83
+ recallCrossNamespaceBudgetEnabled: false,
84
+ ...options.config,
85
+ } as unknown as PluginConfig;
86
+
87
+ const snapshot: LastRecallSnapshot = {
88
+ sessionKey: options.sessionKey,
89
+ recordedAt: new Date().toISOString(),
90
+ queryHash: "hash",
91
+ queryLen: 5,
92
+ memoryIds: [],
93
+ namespace: options.snapshotNamespace,
94
+ recallNamespaces: [options.snapshotNamespace],
95
+ resultPaths: [],
96
+ };
97
+
98
+ const storage = {
99
+ dir: "/synthetic/remnic-raw-excerpt-read-gate/store",
100
+ async readMemoryByPath() {
101
+ return null;
102
+ },
103
+ async getMemoryById() {
104
+ return null;
105
+ },
106
+ } as unknown as StorageManager;
107
+
108
+ const service = Object.create(
109
+ EngramAccessService.prototype,
110
+ ) as EngramAccessService;
111
+
112
+ const orch = {
113
+ config,
114
+ getCodingContextForSession: (sk: string | undefined) =>
115
+ (sk ? contexts.get(sk) : null) ?? null,
116
+ setCodingContextForSession: (sk: string, ctx: CodingContext | null) => {
117
+ if (ctx === null) contexts.delete(sk);
118
+ else contexts.set(sk, ctx);
119
+ },
120
+ applyCodingNamespaceOverlay: (sk: string | undefined, base: string) =>
121
+ Orchestrator.prototype.applyCodingNamespaceOverlay.call(orch, sk, base),
122
+ resolvePrincipal: (sk?: string) =>
123
+ Orchestrator.prototype.resolvePrincipal.call(orch, sk),
124
+ resolveSelfNamespace: (sk?: string) =>
125
+ Orchestrator.prototype.resolveSelfNamespace.call(orch, sk),
126
+ async getStorage() {
127
+ return storage;
128
+ },
129
+ lastRecall: new Map<string, LastRecallSnapshot>([
130
+ [options.sessionKey, snapshot],
131
+ ]),
132
+ async recall() {
133
+ return "";
134
+ },
135
+ lcmEngine: {
136
+ enabled: true,
137
+ searchContextFull: async (
138
+ _query: string,
139
+ _limit: number,
140
+ sessionId?: string,
141
+ ) => {
142
+ searchSessionIds.push(sessionId);
143
+ return [];
144
+ },
145
+ },
146
+ } as unknown as Orchestrator;
147
+
148
+ (service as unknown as { orchestrator: Orchestrator }).orchestrator = orch;
149
+
150
+ // `executeRecall` consults the cross-namespace budget; `Object.create` skips
151
+ // the constructor that builds the real limiter, so install one. Budget is
152
+ // disabled in config, so it never denies — orthogonal to the namespace gate.
153
+ (service as unknown as { budget: CrossNamespaceBudget }).budget =
154
+ new CrossNamespaceBudget({
155
+ enabled: false,
156
+ windowMs: 60_000,
157
+ softLimit: 10,
158
+ hardLimit: 30,
159
+ });
160
+
161
+ return { service, searchSessionIds, contexts };
162
+ }
163
+
164
+ type ExecuteRecallInternals = {
165
+ executeRecall: (request: unknown) => Promise<unknown>;
166
+ };
167
+
168
+ const SESSION_KEY = "pi-geek:abc123";
169
+ const PROJECT_TAG = "Blend/Supply";
170
+
171
+ function overlayNamespace(): string {
172
+ return combineNamespaces(
173
+ "pi-geek",
174
+ projectNamespaceName(projectTagProjectId(PROJECT_TAG)),
175
+ );
176
+ }
177
+
178
+ function sessionContext(): CodingContext {
179
+ return {
180
+ projectId: projectTagProjectId(PROJECT_TAG),
181
+ branch: null,
182
+ rootPath: projectTagProjectId(PROJECT_TAG),
183
+ defaultBranch: null,
184
+ };
185
+ }
186
+
187
+ test("#1505 thread 2f7: WRITE-only / self-unreadable principal ⇒ raw excerpts fall back to the default store (no overlay prefix)", async () => {
188
+ // Self namespace EXISTS and is WRITABLE by pi-geek, but NOT readable by it
189
+ // (only `other` may read). An unqualified project-scoped observe archived LCM
190
+ // under the `<pi-geek>-project-*` overlay key. The read gate
191
+ // (`recallNamespacesForPrincipal`) excludes that overlay for pi-geek, so raw
192
+ // disclosure MUST fall back to the default store — the raw sessionKey — never
193
+ // the overlay key.
194
+ const probe = makeRawExcerptProbe({
195
+ config: {
196
+ namespacePolicies: [
197
+ { name: "pi-geek", readPrincipals: ["other"], writePrincipals: ["pi-geek"] },
198
+ ],
199
+ principalFromSessionKeyMode: "prefix",
200
+ principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
201
+ },
202
+ snapshotNamespace: overlayNamespace(),
203
+ sessionContext: sessionContext(),
204
+ sessionKey: SESSION_KEY,
205
+ });
206
+
207
+ await (probe.service as unknown as ExecuteRecallInternals).executeRecall({
208
+ query: "what database are we using?",
209
+ sessionKey: SESSION_KEY,
210
+ authenticatedPrincipal: "pi-geek",
211
+ disclosure: "raw",
212
+ });
213
+
214
+ assert.equal(probe.searchSessionIds.length, 1, "raw excerpt lookup must run once");
215
+ // FAIL-BEFORE: previously prefixed with `snapshot.namespace` (the overlay),
216
+ // i.e. `${overlayNamespace()}:${SESSION_KEY}`. PASS-AFTER: gated read namespace
217
+ // collapses to the default store ⇒ the raw sessionKey.
218
+ assert.equal(
219
+ probe.searchSessionIds[0],
220
+ SESSION_KEY,
221
+ "raw excerpts must NOT prefix with the unreadable overlay namespace",
222
+ );
223
+ });
224
+
225
+ test("#1505 thread 2f7: defaultRecallNamespaces omits 'self' ⇒ raw excerpts fall back to the default store", async () => {
226
+ // pi-geek may both read AND write its self base, but the operator's
227
+ // `defaultRecallNamespaces` omits `self`, so normal recall + lcmSearch never
228
+ // surface overlay rows for this reader. Raw disclosure must match.
229
+ const probe = makeRawExcerptProbe({
230
+ config: {
231
+ defaultRecallNamespaces: ["shared"],
232
+ namespacePolicies: [
233
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
234
+ ],
235
+ principalFromSessionKeyMode: "prefix",
236
+ principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
237
+ },
238
+ snapshotNamespace: overlayNamespace(),
239
+ sessionContext: sessionContext(),
240
+ sessionKey: SESSION_KEY,
241
+ });
242
+
243
+ await (probe.service as unknown as ExecuteRecallInternals).executeRecall({
244
+ query: "what database are we using?",
245
+ sessionKey: SESSION_KEY,
246
+ authenticatedPrincipal: "pi-geek",
247
+ disclosure: "raw",
248
+ });
249
+
250
+ assert.equal(probe.searchSessionIds.length, 1);
251
+ assert.equal(
252
+ probe.searchSessionIds[0],
253
+ SESSION_KEY,
254
+ "self-omitted recall set ⇒ raw excerpts fall back to the default store",
255
+ );
256
+ });
257
+
258
+ test("#1505 thread 2f7 (positive): overlay IS readable ⇒ raw disclosure includes the overlay rows", async () => {
259
+ // pi-geek may read its self base AND `defaultRecallNamespaces` includes
260
+ // `self`, so the overlay is in the readable recall set. Raw disclosure MUST
261
+ // continue to prefix with the overlay key so the session finds its own
262
+ // project-scoped transcript rows (no regression for the normal case).
263
+ const probe = makeRawExcerptProbe({
264
+ config: {
265
+ defaultRecallNamespaces: ["self", "shared"],
266
+ namespacePolicies: [
267
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
268
+ ],
269
+ principalFromSessionKeyMode: "prefix",
270
+ principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
271
+ },
272
+ snapshotNamespace: overlayNamespace(),
273
+ sessionContext: sessionContext(),
274
+ sessionKey: SESSION_KEY,
275
+ });
276
+
277
+ await (probe.service as unknown as ExecuteRecallInternals).executeRecall({
278
+ query: "what database are we using?",
279
+ sessionKey: SESSION_KEY,
280
+ authenticatedPrincipal: "pi-geek",
281
+ disclosure: "raw",
282
+ });
283
+
284
+ assert.equal(probe.searchSessionIds.length, 1);
285
+ assert.equal(
286
+ probe.searchSessionIds[0],
287
+ lcmSessionKeyForNamespace(overlayNamespace(), SESSION_KEY, "default"),
288
+ "readable overlay ⇒ raw disclosure keeps the overlay prefix",
289
+ );
290
+ });
291
+
292
+ test("#1505 thread NBHWz (codex P2): restrictive `default` READ policy + readable self ⇒ raw excerpts read the self/recall-authorized namespace (no `not readable: default` throw)", async () => {
293
+ // The root defect: the raw-excerpt path PRE-authorized
294
+ // `undefined ⇒ config.defaultNamespace` via `resolveReadableNamespace` BEFORE
295
+ // computing the LCM excerpt key. Under a deployment whose `default` namespace
296
+ // has a RESTRICTIVE read policy (pi-geek may NOT read `default`) but where
297
+ // pi-geek's self namespace IS readable, normal recall still succeeds via
298
+ // `recallNamespacesForPrincipal`, yet `disclosure: "raw"` threw `namespace is
299
+ // not readable: default` before serialization.
300
+ //
301
+ // FAIL-BEFORE: `executeRecall({ disclosure: "raw" })` throws `namespace is not
302
+ // readable: default`. PASS-AFTER: the fallback comes from the already
303
+ // read-authorized recall namespace set, so the raw lookup runs and prefixes
304
+ // its LCM session_id with the readable self namespace (no pre-auth of default).
305
+ const probe = makeRawExcerptProbe({
306
+ config: {
307
+ // RESTRICTIVE default: pi-geek may NOT read `default`.
308
+ namespacePolicies: [
309
+ { name: "default", readPrincipals: [], writePrincipals: [] },
310
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
311
+ ],
312
+ // self IS in the recall set and IS readable, so the overlay resolves and
313
+ // the read gate keeps it (no pre-auth of the denied default).
314
+ defaultRecallNamespaces: ["self", "shared"],
315
+ codingMode: { projectScope: true, branchScope: false, globalFallback: true },
316
+ principalFromSessionKeyMode: "prefix",
317
+ principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
318
+ },
319
+ // snapshot.namespace records the overlay (the write target observe used);
320
+ // the read gate independently derives the readable overlay.
321
+ snapshotNamespace: overlayNamespace(),
322
+ sessionContext: sessionContext(),
323
+ sessionKey: SESSION_KEY,
324
+ });
325
+
326
+ await (probe.service as unknown as ExecuteRecallInternals).executeRecall({
327
+ query: "what database are we using?",
328
+ sessionKey: SESSION_KEY,
329
+ authenticatedPrincipal: "pi-geek",
330
+ disclosure: "raw",
331
+ });
332
+
333
+ assert.ok(
334
+ probe.searchSessionIds.length >= 1,
335
+ "raw excerpt lookup must run (NOT throw `not readable: default`)",
336
+ );
337
+ // The readable self overlay is honoured ⇒ the PRIMARY LCM session_id is
338
+ // prefixed with it, matching what normal recall + `lcmSearch` search for this
339
+ // principal. The premature `default` read-auth (which would have thrown) is
340
+ // gone. (The fallback-unification may append project/root read-fallback keys
341
+ // after the primary; the primary overlay key is what matters here.)
342
+ assert.equal(
343
+ probe.searchSessionIds[0],
344
+ lcmSessionKeyForNamespace(overlayNamespace(), SESSION_KEY, "default"),
345
+ "raw excerpts must read the recall-authorized self overlay, not pre-authorize the denied default",
346
+ );
347
+ // No queried key may be the bare default-store key (the unprefixed raw
348
+ // sessionKey) — that would mean the read gate fell back to the DENIED default
349
+ // store. Every searched key must be namespace-prefixed with an AUTHORIZED
350
+ // namespace the principal may read (the `pi-geek` self base or its
351
+ // `pi-geek-project-*` overlay), matching what normal recall + `lcmSearch`
352
+ // search.
353
+ // #1495 P1: the namespaced key is sentinel-framed (`\x1f<ns>\x1f<sessionKey>`),
354
+ // so parse the namespace out of the frame and assert it is an authorized
355
+ // pi-geek namespace (the self base or its `pi-geek-project-*` overlay).
356
+ const SENTINEL = "\u001f";
357
+ for (const id of probe.searchSessionIds) {
358
+ assert.notEqual(
359
+ id,
360
+ SESSION_KEY,
361
+ "raw-excerpt LCM keys must NOT fall back to the bare default store (the denied default)",
362
+ );
363
+ assert.ok(
364
+ typeof id === "string" && id.startsWith(SENTINEL),
365
+ `every raw-excerpt LCM key must be the sentinel-framed namespaced key, got ${String(id)}`,
366
+ );
367
+ const framedNs = (id as string).slice(SENTINEL.length).split(SENTINEL)[0]!;
368
+ assert.ok(
369
+ framedNs.startsWith("pi-geek"),
370
+ `every raw-excerpt LCM key must be framed with an authorized pi-geek namespace, got ${String(id)}`,
371
+ );
372
+ }
373
+ });
374
+
375
+ test("#1505 thread NBHWz (codex P2): no readable LCM namespace ⇒ raw excerpts are EMPTY (no throw, no fallback to unreadable default)", async () => {
376
+ // alice authenticates but her policy denies reading `default`, `shared` is not
377
+ // in the recall set, and she has NO readable self namespace
378
+ // (`defaultRecallNamespaces` omits `self` AND her self base is unreadable). No
379
+ // readable LCM namespace exists for an implicit raw recall.
380
+ //
381
+ // FAIL-BEFORE: throws `namespace is not readable: default`. PASS-AFTER: the
382
+ // raw-excerpt lookup is suppressed (returns EMPTY) — `searchContextFull` is
383
+ // never called — so raw recall degrades gracefully instead of throwing.
384
+ const probe = makeRawExcerptProbe({
385
+ config: {
386
+ namespacePolicies: [
387
+ { name: "default", readPrincipals: [], writePrincipals: [] },
388
+ // alice can WRITE but NOT read her self namespace, and self is omitted
389
+ // from the recall set ⇒ nothing readable to fall back to.
390
+ { name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
391
+ ],
392
+ // `shared` deliberately not granted either (default policy denies, no
393
+ // shared policy ⇒ canReadNamespace(alice, "shared") is true by the
394
+ // default-or-shared fallback). Omit shared from the recall set so it is not
395
+ // a fallback.
396
+ defaultRecallNamespaces: [],
397
+ codingMode: { projectScope: false, branchScope: false, globalFallback: true },
398
+ principalFromSessionKeyMode: "prefix",
399
+ principalFromSessionKeyRules: [],
400
+ },
401
+ snapshotNamespace: "default",
402
+ sessionKey: SESSION_KEY,
403
+ });
404
+
405
+ // Must NOT throw.
406
+ await (probe.service as unknown as ExecuteRecallInternals).executeRecall({
407
+ query: "what database are we using?",
408
+ sessionKey: SESSION_KEY,
409
+ authenticatedPrincipal: "alice",
410
+ disclosure: "raw",
411
+ });
412
+
413
+ assert.equal(
414
+ probe.searchSessionIds.length,
415
+ 0,
416
+ "no readable LCM namespace ⇒ raw excerpts must be EMPTY (searchContextFull never called), not throw",
417
+ );
418
+ });
419
+
420
+ test("#1505 thread 2f7 (single-store regression): namespaces disabled ⇒ raw excerpts use the raw sessionKey", async () => {
421
+ // Byte-for-byte single-user behavior: no namespaces, no overlay, raw key.
422
+ const probe = makeRawExcerptProbe({
423
+ config: {
424
+ namespacesEnabled: false,
425
+ codingMode: {
426
+ projectScope: false,
427
+ branchScope: false,
428
+ globalFallback: true,
429
+ },
430
+ },
431
+ snapshotNamespace: "default",
432
+ sessionKey: SESSION_KEY,
433
+ });
434
+
435
+ await (probe.service as unknown as ExecuteRecallInternals).executeRecall({
436
+ query: "what database are we using?",
437
+ sessionKey: SESSION_KEY,
438
+ disclosure: "raw",
439
+ });
440
+
441
+ assert.equal(probe.searchSessionIds.length, 1);
442
+ assert.equal(probe.searchSessionIds[0], SESSION_KEY);
443
+ });