@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,1397 @@
1
+ /**
2
+ * #1505 thread 2 (P1): LCM read/write/compaction key PARITY.
3
+ *
4
+ * `observe` archives LCM / structured history under
5
+ * `${effectiveNamespace}:${sessionKey}` (the scope-plan write namespace). The
6
+ * LCM archive filters strictly by `session_id`, so a same-session reader and
7
+ * the compaction flush/record path MUST derive the EXACT same key, or a
8
+ * project-scoped session misses its own compressed-history / structured /
9
+ * targeted-fact evidence (and compaction flushes the wrong queue).
10
+ *
11
+ * This suite proves the key the WRITER (`observe`) produces equals:
12
+ * 1. the key the orchestrator recall READERS compute
13
+ * (`resolveSelfNamespace(sessionKey)` → `lcmSessionKeyForNamespace`), and
14
+ * 2. the key the compaction flush/record path computes.
15
+ * across the scenario matrix:
16
+ * (a) explicit namespace
17
+ * (b) auto-scoped via cwd (git repo)
18
+ * (c) auto-scoped via projectTag
19
+ * (d) no overlay (projectScope:false / namespacesEnabled:false) — must stay
20
+ * byte-for-byte the raw sessionKey (single-user regression guard).
21
+ *
22
+ * It also unit-tests the shared `lcmSessionKeyForNamespace` encoder so the
23
+ * write/read contract is pinned at the boundary.
24
+ */
25
+ import assert from "node:assert/strict";
26
+ import { execFileSync } from "node:child_process";
27
+ import { mkdtempSync, rmSync } from "node:fs";
28
+ import { tmpdir } from "node:os";
29
+ import { join } from "node:path";
30
+ import test from "node:test";
31
+
32
+ import { EngramAccessService } from "./access-service.js";
33
+ import { Orchestrator } from "./orchestrator.js";
34
+ import type { EngramAccessObserveRequest } from "./access-service.js";
35
+ import {
36
+ combineNamespaces,
37
+ lcmSessionKeyForNamespace,
38
+ projectNamespaceName,
39
+ projectTagProjectId,
40
+ } from "./coding/coding-namespace.js";
41
+ import { resolveGitContext } from "./coding/git-context.js";
42
+ import { defaultNamespaceForPrincipal } from "./namespaces/principal.js";
43
+ import type { CodingContext, PluginConfig } from "./types.js";
44
+
45
+ interface ParityProbe {
46
+ orch: Orchestrator;
47
+ contexts: Map<string, CodingContext>;
48
+ lcmWriteKeys: string[];
49
+ compactionFlushKeys: string[];
50
+ compactionRecordKeys: string[];
51
+ searchSessionIds: Array<string | undefined>;
52
+ searchSessionPrefixes: Array<string | undefined>;
53
+ extractionCalls: Array<{
54
+ sessionKeys: string[];
55
+ writeNamespaceOverride?: string;
56
+ principalOverride?: string;
57
+ }>;
58
+ }
59
+
60
+ /**
61
+ * Build an orchestrator stub that (a) records the LCM archival key `observe`
62
+ * writes under, (b) records the LCM key the compaction flush/record path
63
+ * targets, and (c) delegates `resolveSelfNamespace` / `resolvePrincipal` /
64
+ * `applyCodingNamespaceOverlay` to the REAL Orchestrator prototype so the
65
+ * reader-side namespace resolution is exercised exactly as production does it.
66
+ */
67
+ function makeParityProbe(overrides: Partial<PluginConfig> = {}): ParityProbe {
68
+ const contexts = new Map<string, CodingContext>();
69
+ const lcmWriteKeys: string[] = [];
70
+ const compactionFlushKeys: string[] = [];
71
+ const compactionRecordKeys: string[] = [];
72
+ const searchSessionIds: Array<string | undefined> = [];
73
+ const searchSessionPrefixes: Array<string | undefined> = [];
74
+ const extractionCalls: ParityProbe["extractionCalls"] = [];
75
+
76
+ const config = {
77
+ namespacesEnabled: true,
78
+ defaultNamespace: "default",
79
+ sharedNamespace: "shared",
80
+ namespacePolicies: [],
81
+ // Production default. The #1505 round-3 read-authorization gate consults
82
+ // `recallNamespacesForPrincipal`, which reads `defaultRecallNamespaces`;
83
+ // omitting it would throw. Per-test overrides can still narrow it.
84
+ defaultRecallNamespaces: ["self", "shared"],
85
+ codingMode: { projectScope: true },
86
+ memoryDir: "/synthetic/remnic-observe-lcm-parity",
87
+ // LCM-only test: keep objective-state off so the storage router is not hit.
88
+ objectiveStateMemoryEnabled: false,
89
+ objectiveStateSnapshotWritesEnabled: false,
90
+ principalFromSessionKeyMode: "prefix",
91
+ principalFromSessionKeyRules: [],
92
+ recallCrossNamespaceBudgetEnabled: false,
93
+ recallCrossNamespaceBudgetWindowMs: 60_000,
94
+ recallCrossNamespaceBudgetSoftLimit: 10,
95
+ recallCrossNamespaceBudgetHardLimit: 30,
96
+ ...overrides,
97
+ } as unknown as PluginConfig;
98
+
99
+ const orch = {
100
+ config,
101
+ getCodingContextForSession: (sk: string | undefined) =>
102
+ (sk ? contexts.get(sk) : null) ?? null,
103
+ setCodingContextForSession: (sk: string, ctx: CodingContext | null) => {
104
+ if (ctx === null) contexts.delete(sk);
105
+ else contexts.set(sk, ctx);
106
+ },
107
+ applyCodingNamespaceOverlay: (sk: string | undefined, base: string) =>
108
+ Orchestrator.prototype.applyCodingNamespaceOverlay.call(orch, sk, base),
109
+ resolvePrincipal: (sk?: string) =>
110
+ Orchestrator.prototype.resolvePrincipal.call(orch, sk),
111
+ resolveSelfNamespace: (sk?: string) =>
112
+ Orchestrator.prototype.resolveSelfNamespace.call(orch, sk),
113
+ lcmEngine: {
114
+ enabled: true,
115
+ enqueueObserveMessages: (sessionKey: string) => {
116
+ lcmWriteKeys.push(sessionKey);
117
+ },
118
+ waitForSessionObserveIdle: async (_sessionKey: string) => {},
119
+ preCompactionFlush: async (sessionKey: string) => {
120
+ compactionFlushKeys.push(sessionKey);
121
+ },
122
+ recordCompaction: async (
123
+ sessionKey: string,
124
+ _before: number,
125
+ _after: number,
126
+ ) => {
127
+ compactionRecordKeys.push(sessionKey);
128
+ },
129
+ searchContextFull: async (
130
+ _query: string,
131
+ _limit: number,
132
+ sessionId?: string,
133
+ sessionPrefix?: string,
134
+ ) => {
135
+ searchSessionIds.push(sessionId);
136
+ searchSessionPrefixes.push(sessionPrefix);
137
+ return [];
138
+ },
139
+ },
140
+ // Capture extraction routing/identity so the provenance-principal tests can
141
+ // assert what `observe` threads into `ingestReplayBatch`.
142
+ ingestReplayBatch: async (
143
+ turns: Array<{ sessionKey: string }>,
144
+ options: { writeNamespaceOverride?: string; principalOverride?: string } = {},
145
+ ) => {
146
+ extractionCalls.push({
147
+ sessionKeys: turns.map((t) => t.sessionKey),
148
+ writeNamespaceOverride: options.writeNamespaceOverride,
149
+ principalOverride: options.principalOverride,
150
+ });
151
+ },
152
+ } as unknown as Orchestrator;
153
+
154
+ return {
155
+ orch,
156
+ contexts,
157
+ lcmWriteKeys,
158
+ compactionFlushKeys,
159
+ compactionRecordKeys,
160
+ searchSessionIds,
161
+ searchSessionPrefixes,
162
+ extractionCalls,
163
+ };
164
+ }
165
+
166
+ function observeRequest(
167
+ overrides: Partial<EngramAccessObserveRequest>,
168
+ ): EngramAccessObserveRequest {
169
+ return {
170
+ sessionKey: "pi-geek:abc123",
171
+ // skipExtraction keeps this an LCM-only round-trip.
172
+ skipExtraction: true,
173
+ messages: [
174
+ { role: "user", content: "what database are we using?" },
175
+ { role: "assistant", content: "we use postgres for the primary store" },
176
+ ],
177
+ ...overrides,
178
+ } as EngramAccessObserveRequest;
179
+ }
180
+
181
+ function withSelfPolicyPrefix(principal: string): Partial<PluginConfig> {
182
+ return {
183
+ namespacePolicies: [
184
+ { name: principal, readPrincipals: [principal], writePrincipals: [principal] },
185
+ ],
186
+ principalFromSessionKeyMode: "prefix",
187
+ principalFromSessionKeyRules: [{ match: `${principal}:`, principal }],
188
+ } as Partial<PluginConfig>;
189
+ }
190
+
191
+ /**
192
+ * Reproduce EXACTLY what `orchestrator.recallInternal` computes for the LCM
193
+ * reader session_id of a same-session bare recall (no explicit namespace
194
+ * override): the coding-overlay namespace when one applies, else the default
195
+ * store (NOT the principal self base — that would prefix a namespace an
196
+ * unqualified observe never wrote to), then encode through the shared helper.
197
+ * This is the same rule as the orchestrator's private
198
+ * `lcmReadNamespaceForSession`.
199
+ */
200
+ function readerLcmKey(probe: ParityProbe, sessionKey: string): string {
201
+ const principal = (
202
+ probe.orch as unknown as { resolvePrincipal: (sk?: string) => string | undefined }
203
+ ).resolvePrincipal(sessionKey);
204
+ const base = defaultNamespaceForPrincipal(principal, probe.orch.config);
205
+ const overlaid = (
206
+ probe.orch as unknown as {
207
+ applyCodingNamespaceOverlay: (sk: string | undefined, base: string) => string;
208
+ }
209
+ ).applyCodingNamespaceOverlay(sessionKey, base);
210
+ const effectiveNamespace =
211
+ overlaid !== base ? overlaid : probe.orch.config.defaultNamespace;
212
+ return (
213
+ lcmSessionKeyForNamespace(
214
+ effectiveNamespace,
215
+ sessionKey,
216
+ probe.orch.config.defaultNamespace,
217
+ ) ?? sessionKey
218
+ );
219
+ }
220
+
221
+ // #1495 P1: reserved structural sentinel framing the namespaced LCM key
222
+ // (`\x1f<namespace>\x1f<sessionKey>`). Kept in sync with
223
+ // `coding-namespace.ts:LCM_NS_SENTINEL`. U+001F cannot occur in a route
224
+ // namespace (`[A-Za-z0-9._-]`) nor any legitimate session key, so the
225
+ // namespaced and default key-spaces are provably disjoint and an overlay id is
226
+ // unforgeable from a caller-controlled raw default-store sessionKey.
227
+ const LCM_NS_SENTINEL = "\u001f";
228
+
229
+ /**
230
+ * Encode the expected namespaced LCM `session_id` exactly as production does —
231
+ * via the shared `lcmSessionKeyForNamespace` helper — so these parity
232
+ * assertions stay shape-agnostic and never re-hardcode the `:`-join the #1495
233
+ * P1 fix removed (CLAUDE.md rule 22: never fork the encoding).
234
+ */
235
+ function encodeNs(namespace: string, sessionKey: string): string {
236
+ return lcmSessionKeyForNamespace(namespace, sessionKey, "default") ?? sessionKey;
237
+ }
238
+
239
+ /**
240
+ * Assert a namespaced LCM write key (new #1495 P1 encoding
241
+ * `\x1f<overlayNs>\x1f<sessionKey>`) was archived under an overlay namespace
242
+ * whose name begins with `overlayPrefix` (e.g. `alice-`, `pi-geek-`). Replaces
243
+ * the pre-#1495 `writeKey.startsWith("<principal>-")`, which no longer holds now
244
+ * that the key is sentinel-framed.
245
+ */
246
+ function assertOverlayWriteKey(writeKey: string, overlayPrefix: string): void {
247
+ assert.ok(
248
+ writeKey.startsWith(LCM_NS_SENTINEL),
249
+ `write key must be sentinel-framed, got ${JSON.stringify(writeKey)}`,
250
+ );
251
+ const overlayNs = writeKey.slice(LCM_NS_SENTINEL.length).split(LCM_NS_SENTINEL)[0]!;
252
+ assert.ok(
253
+ overlayNs.startsWith(overlayPrefix),
254
+ `overlay namespace must start with ${overlayPrefix}, got ${overlayNs} (key ${JSON.stringify(writeKey)})`,
255
+ );
256
+ }
257
+
258
+ test("#1505 thread 2 helper: write/read encoding agrees and collapses to raw key on the default store", () => {
259
+ // Non-default namespace ⇒ sentinel-framed, NOT `${ns}:${sessionKey}` (#1495 P1
260
+ // unforgeable encoding).
261
+ assert.equal(
262
+ lcmSessionKeyForNamespace("acme", "sk", "default"),
263
+ `${LCM_NS_SENTINEL}acme${LCM_NS_SENTINEL}sk`,
264
+ );
265
+ // Default namespace ⇒ raw key (single-store byte-for-byte).
266
+ assert.equal(lcmSessionKeyForNamespace("default", "sk", "default"), "sk");
267
+ // Undefined namespace ⇒ raw key.
268
+ assert.equal(lcmSessionKeyForNamespace(undefined, "sk", "default"), "sk");
269
+ // Empty namespace ⇒ raw key.
270
+ assert.equal(lcmSessionKeyForNamespace("", "sk", "default"), "sk");
271
+ // Missing sessionKey is passed through unchanged (recall's `?? "default"`
272
+ // fallback handles the undefined case downstream).
273
+ assert.equal(lcmSessionKeyForNamespace("acme", undefined, "default"), undefined);
274
+
275
+ // #1495 P1 UNFORGEABILITY: a default-store raw sessionKey can NEVER reproduce
276
+ // another namespace's encoded id, so a forged default read cannot collide with
277
+ // an overlay write key.
278
+ const overlayKey = lcmSessionKeyForNamespace("acme", "sk", "default");
279
+ // The classic forgery: a default-store caller passing the OLD `${ns}:${sk}`
280
+ // string. Under the new encoding the default path returns it verbatim (it does
281
+ // not start with the sentinel), which is disjoint from the overlay key-space.
282
+ const forgedDefaultKey = lcmSessionKeyForNamespace("default", "acme:sk", "default");
283
+ assert.equal(forgedDefaultKey, "acme:sk");
284
+ assert.notEqual(
285
+ forgedDefaultKey,
286
+ overlayKey,
287
+ "a forged `${ns}:${sk}` default key must NOT equal the overlay encoded id",
288
+ );
289
+ // Even a default sessionKey that itself begins with the sentinel is escaped so
290
+ // it can never equal an overlay key (whose 2nd char is a namespace char).
291
+ const sentinelLeadingDefault = lcmSessionKeyForNamespace(
292
+ "default",
293
+ `${LCM_NS_SENTINEL}acme${LCM_NS_SENTINEL}sk`,
294
+ "default",
295
+ );
296
+ assert.notEqual(
297
+ sentinelLeadingDefault,
298
+ overlayKey,
299
+ "a sentinel-leading default key must be escaped disjoint from the overlay key-space",
300
+ );
301
+ });
302
+
303
+ test("#1505 thread 2 (c) projectTag: observe LCM write key == recall reader key == compaction keys", async () => {
304
+ const probe = makeParityProbe(withSelfPolicyPrefix("pi-geek"));
305
+ const service = new EngramAccessService(probe.orch);
306
+
307
+ const res = await service.observe(
308
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
309
+ );
310
+
311
+ const expectedNs = combineNamespaces(
312
+ "pi-geek",
313
+ projectNamespaceName(projectTagProjectId("Blend/Supply")),
314
+ );
315
+ const expectedKey = encodeNs(expectedNs, "pi-geek:abc123");
316
+ assert.equal(res.effectiveNamespace, expectedNs);
317
+ assert.notEqual(expectedNs, "default", "overlay must change the namespace");
318
+
319
+ // WRITE key.
320
+ assert.equal(probe.lcmWriteKeys.length, 1);
321
+ assert.equal(probe.lcmWriteKeys[0], expectedKey, "LCM write key");
322
+
323
+ // READ key (observe attached the coding context, so the reader resolves the
324
+ // SAME overlay namespace as the writer).
325
+ assert.equal(
326
+ readerLcmKey(probe, "pi-geek:abc123"),
327
+ expectedKey,
328
+ "recall reader LCM key must equal the observe write key",
329
+ );
330
+
331
+ // COMPACTION keys (no explicit namespace ⇒ overlay applied from session ctx).
332
+ await service.lcmCompactionFlush({ sessionKey: "pi-geek:abc123" });
333
+ await service.lcmCompactionRecord({
334
+ sessionKey: "pi-geek:abc123",
335
+ tokensBefore: 100,
336
+ tokensAfter: 10,
337
+ });
338
+ assert.equal(probe.compactionFlushKeys[0], expectedKey, "flush key");
339
+ assert.equal(probe.compactionRecordKeys[0], expectedKey, "record key");
340
+ });
341
+
342
+ test("#1505 thread 2 (b) cwd git repo: observe LCM write key == recall reader key == compaction keys", async () => {
343
+ const repoDir = mkdtempSync(join(tmpdir(), "remnic-lcm-parity-git-"));
344
+ const git = (...args: string[]) =>
345
+ execFileSync("git", args, { cwd: repoDir, stdio: "pipe" });
346
+ git("init", "-q");
347
+ git("config", "user.email", "test@example.com");
348
+ git("config", "user.name", "Test");
349
+ try {
350
+ const gitCtx = await resolveGitContext(repoDir);
351
+ assert.ok(gitCtx, "synthetic repo must resolve a git context");
352
+
353
+ const probe = makeParityProbe(withSelfPolicyPrefix("pi-geek"));
354
+ const service = new EngramAccessService(probe.orch);
355
+
356
+ const res = await service.observe(
357
+ observeRequest({ sessionKey: "pi-geek:cwd1", cwd: repoDir }),
358
+ );
359
+ const expectedNs = combineNamespaces(
360
+ "pi-geek",
361
+ projectNamespaceName(gitCtx!.projectId),
362
+ );
363
+ const expectedKey = encodeNs(expectedNs, "pi-geek:cwd1");
364
+
365
+ assert.equal(res.effectiveNamespace, expectedNs);
366
+ assert.equal(probe.lcmWriteKeys[0], expectedKey, "LCM write key");
367
+ assert.equal(
368
+ readerLcmKey(probe, "pi-geek:cwd1"),
369
+ expectedKey,
370
+ "recall reader LCM key must equal the observe write key",
371
+ );
372
+
373
+ await service.lcmCompactionFlush({ sessionKey: "pi-geek:cwd1" });
374
+ await service.lcmCompactionRecord({
375
+ sessionKey: "pi-geek:cwd1",
376
+ tokensBefore: 100,
377
+ tokensAfter: 10,
378
+ });
379
+ assert.equal(probe.compactionFlushKeys[0], expectedKey, "flush key");
380
+ assert.equal(probe.compactionRecordKeys[0], expectedKey, "record key");
381
+ } finally {
382
+ rmSync(repoDir, { recursive: true, force: true });
383
+ }
384
+ });
385
+
386
+ test("#1505 thread 2 (a) explicit namespace: write key prefixed; compaction agrees", async () => {
387
+ const probe = makeParityProbe({
388
+ namespacePolicies: [
389
+ { name: "team", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
390
+ ],
391
+ principalFromSessionKeyMode: "prefix",
392
+ principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
393
+ } as Partial<PluginConfig>);
394
+ const service = new EngramAccessService(probe.orch);
395
+
396
+ const res = await service.observe(
397
+ observeRequest({ sessionKey: "pi-geek:abc123", namespace: "team" }),
398
+ );
399
+ assert.equal(res.effectiveNamespace, "team");
400
+ assert.equal(
401
+ probe.lcmWriteKeys[0],
402
+ encodeNs("team", "pi-geek:abc123"),
403
+ "LCM write key",
404
+ );
405
+
406
+ // A recall that wants the `team` namespace passes namespace=team; its reader
407
+ // key is built from that override and agrees with the write key.
408
+ assert.equal(
409
+ lcmSessionKeyForNamespace("team", "pi-geek:abc123", "default"),
410
+ encodeNs("team", "pi-geek:abc123"),
411
+ "explicit-namespace recall reader key matches the write key",
412
+ );
413
+
414
+ // Compaction with the same explicit namespace agrees.
415
+ await service.lcmCompactionFlush({
416
+ sessionKey: "pi-geek:abc123",
417
+ namespace: "team",
418
+ });
419
+ await service.lcmCompactionRecord({
420
+ sessionKey: "pi-geek:abc123",
421
+ namespace: "team",
422
+ tokensBefore: 100,
423
+ tokensAfter: 10,
424
+ });
425
+ assert.equal(
426
+ probe.compactionFlushKeys[0],
427
+ encodeNs("team", "pi-geek:abc123"),
428
+ "flush key",
429
+ );
430
+ assert.equal(
431
+ probe.compactionRecordKeys[0],
432
+ encodeNs("team", "pi-geek:abc123"),
433
+ "record key",
434
+ );
435
+ });
436
+
437
+ test("#1505 thread 2 (d) projectScope:false ⇒ raw sessionKey everywhere (single-user regression guard)", async () => {
438
+ const probe = makeParityProbe({
439
+ ...withSelfPolicyPrefix("pi-geek"),
440
+ codingMode: { projectScope: false },
441
+ } as Partial<PluginConfig>);
442
+ const service = new EngramAccessService(probe.orch);
443
+
444
+ const res = await service.observe(
445
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
446
+ );
447
+ // No overlay ⇒ effective namespace is the default store ⇒ raw key.
448
+ assert.equal(res.effectiveNamespace, "default");
449
+ assert.equal(probe.lcmWriteKeys[0], "pi-geek:abc123", "raw LCM write key");
450
+ assert.equal(
451
+ readerLcmKey(probe, "pi-geek:abc123"),
452
+ "pi-geek:abc123",
453
+ "reader key must be the raw sessionKey",
454
+ );
455
+
456
+ await service.lcmCompactionFlush({ sessionKey: "pi-geek:abc123" });
457
+ await service.lcmCompactionRecord({
458
+ sessionKey: "pi-geek:abc123",
459
+ tokensBefore: 100,
460
+ tokensAfter: 10,
461
+ });
462
+ assert.equal(probe.compactionFlushKeys[0], "pi-geek:abc123", "raw flush key");
463
+ assert.equal(probe.compactionRecordKeys[0], "pi-geek:abc123", "raw record key");
464
+ });
465
+
466
+ test("#1505 thread 2 (d) namespacesEnabled:false ⇒ raw sessionKey everywhere", async () => {
467
+ const probe = makeParityProbe({
468
+ namespacesEnabled: false,
469
+ } as Partial<PluginConfig>);
470
+ const service = new EngramAccessService(probe.orch);
471
+
472
+ const res = await service.observe(
473
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
474
+ );
475
+ assert.equal(res.effectiveNamespace, "default");
476
+ assert.equal(probe.lcmWriteKeys[0], "pi-geek:abc123", "raw LCM write key");
477
+ assert.equal(readerLcmKey(probe, "pi-geek:abc123"), "pi-geek:abc123");
478
+
479
+ await service.lcmCompactionFlush({ sessionKey: "pi-geek:abc123" });
480
+ assert.equal(probe.compactionFlushKeys[0], "pi-geek:abc123", "raw flush key");
481
+
482
+ // Defensive: even without a defaultNamespaceForPrincipal policy, the base is
483
+ // the default store when namespaces are disabled.
484
+ assert.equal(
485
+ defaultNamespaceForPrincipal("pi-geek", probe.orch.config),
486
+ "default",
487
+ );
488
+ });
489
+
490
+ test("#1505 thread 1: extraction provenance principal is the resolved principal (NOT default) for a project-scoped observe with an encoded-principal key", async () => {
491
+ // Identity-vs-routing separation. A project-scoped observe prefixes the LCM
492
+ // key with the overlay namespace (`pi-geek-project-...:pi-geek:abc123`). Before
493
+ // this fix, that prefixed key was ALSO fed to extraction as the turn
494
+ // sessionKey, so `resolvePrincipal` parsed `pi-geek-project-...` → no prefix
495
+ // rule match → `default`, mis-attributing provenance. The fix passes the
496
+ // ORIGINAL sessionKey (identity) plus principalOverride (the resolved
497
+ // principal) and writeNamespaceOverride (routing).
498
+ const probe = makeParityProbe(withSelfPolicyPrefix("pi-geek"));
499
+ const service = new EngramAccessService(probe.orch);
500
+
501
+ await service.observe(
502
+ observeRequest({
503
+ sessionKey: "pi-geek:abc123",
504
+ projectTag: "Blend/Supply",
505
+ skipExtraction: false, // exercise the extraction path
506
+ }),
507
+ );
508
+
509
+ const expectedNs = combineNamespaces(
510
+ "pi-geek",
511
+ projectNamespaceName(projectTagProjectId("Blend/Supply")),
512
+ );
513
+ assert.equal(probe.extractionCalls.length, 1);
514
+ // Provenance identity: the resolved principal, never a default parsed from the
515
+ // prefixed key.
516
+ assert.equal(
517
+ probe.extractionCalls[0].principalOverride,
518
+ "pi-geek",
519
+ "provenance principal must be the resolved principal, not default",
520
+ );
521
+ // The extraction turns carry the ORIGINAL session key (identity / threading).
522
+ assert.deepEqual(
523
+ new Set(probe.extractionCalls[0].sessionKeys),
524
+ new Set(["pi-geek:abc123"]),
525
+ "extraction turns must carry the ORIGINAL un-prefixed session key",
526
+ );
527
+ // Storage routing is pinned to the effective overlay namespace.
528
+ assert.equal(probe.extractionCalls[0].writeNamespaceOverride, expectedNs);
529
+ });
530
+
531
+ test("#1505 thread 1: provenance principal honors authenticatedPrincipal not encoded in the session key", async () => {
532
+ // alice authenticates at the transport layer but the raw sessionKey ("sess-1")
533
+ // encodes no principal. The scope plan resolves principal=alice (auth
534
+ // precedence), so extraction provenance must be pinned to alice — independent
535
+ // of what `resolvePrincipal("sess-1")` would parse (default).
536
+ const probe = makeParityProbe({
537
+ namespacePolicies: [
538
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
539
+ ],
540
+ principalFromSessionKeyMode: "prefix",
541
+ principalFromSessionKeyRules: [],
542
+ } as Partial<PluginConfig>);
543
+ const service = new EngramAccessService(probe.orch);
544
+
545
+ await service.observe(
546
+ observeRequest({
547
+ sessionKey: "sess-1",
548
+ authenticatedPrincipal: "alice",
549
+ skipExtraction: false,
550
+ }),
551
+ );
552
+
553
+ assert.equal(probe.extractionCalls.length, 1);
554
+ assert.equal(
555
+ probe.extractionCalls[0].principalOverride,
556
+ "alice",
557
+ "provenance principal must honor the authenticated principal",
558
+ );
559
+ assert.deepEqual(
560
+ new Set(probe.extractionCalls[0].sessionKeys),
561
+ new Set(["sess-1"]),
562
+ "extraction turns carry the original session key",
563
+ );
564
+ });
565
+
566
+ test("#1505 thread 2: LCM read namespace honors authenticatedPrincipal (write-under-alice ⇒ read-under-alice)", async () => {
567
+ // alice authenticates at the transport layer but is NOT encoded in the raw
568
+ // sessionKey ("sess-1"). With a PROJECT overlay, observe archives LCM under
569
+ // `combineNamespaces("alice", project):sess-1`. A same-session recall that
570
+ // supplies the SAME authenticated principal (principalOverride) must derive the
571
+ // base = alice so the overlay namespace — and thus the LCM read key — matches
572
+ // the write. Without the override, `lcmReadNamespaceForSession` derives the
573
+ // base from `resolvePrincipal("sess-1")` → default, so the overlay would be
574
+ // `combineNamespaces("default", project)` and the reader would MISS alice's
575
+ // evidence (the thread-2 bug).
576
+ // makeParityProbe defaults codingMode to { projectScope: true }; alice is NOT
577
+ // encoded in any prefix rule, so resolvePrincipal("sess-1") → default.
578
+ const probe = makeParityProbe({
579
+ namespacePolicies: [
580
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
581
+ ],
582
+ principalFromSessionKeyMode: "prefix",
583
+ principalFromSessionKeyRules: [],
584
+ } as Partial<PluginConfig>);
585
+ const service = new EngramAccessService(probe.orch);
586
+
587
+ // WRITE: alice observes sess-1 with a project tag → overlay applies on alice's
588
+ // self base. (No explicit namespace.)
589
+ await service.observe(
590
+ observeRequest({
591
+ sessionKey: "sess-1",
592
+ authenticatedPrincipal: "alice",
593
+ projectTag: "Blend/Supply",
594
+ }),
595
+ );
596
+ const writeKey = probe.lcmWriteKeys[0];
597
+ assertOverlayWriteKey(writeKey, "alice-"); // observe must archive under alice's overlay namespace, got ${writeKey}
598
+
599
+ // The orchestrator's real lcmReadNamespaceForSession (the stub delegates
600
+ // resolvePrincipal/overlay to the prototype).
601
+ const lcmReadNamespaceForSession = Orchestrator.prototype[
602
+ "lcmReadNamespaceForSession" as keyof Orchestrator
603
+ ] as unknown as (
604
+ this: Orchestrator,
605
+ sk?: string,
606
+ principalOverride?: string,
607
+ ) => string;
608
+
609
+ // Without the override, the base derives from resolvePrincipal("sess-1") →
610
+ // default, so the read namespace is the DEFAULT-based overlay (the bug).
611
+ const withoutOverride = lcmReadNamespaceForSession.call(probe.orch, "sess-1");
612
+ assert.ok(
613
+ !withoutOverride.startsWith("alice-"),
614
+ `without override the read base must NOT be alice (demonstrates the bug), got ${withoutOverride}`,
615
+ );
616
+
617
+ // With the authenticated principal override, the read base is alice → the read
618
+ // namespace matches the write namespace, so the read key matches the write key.
619
+ const withOverride = lcmReadNamespaceForSession.call(
620
+ probe.orch,
621
+ "sess-1",
622
+ "alice",
623
+ );
624
+ assert.equal(
625
+ encodeNs(withOverride, "sess-1"),
626
+ writeKey,
627
+ "authenticated principal override ⇒ read key matches the alice-prefixed write key",
628
+ );
629
+ });
630
+
631
+ test("#1505 thread 2 compaction regression: flush/record overlay-derived key matches observe (pre-fix used base only)", async () => {
632
+ // Before the fix, compaction flush/record built `${resolveWritableNamespace(
633
+ // request.namespace)}:${sessionKey}` which, with NO explicit namespace, is
634
+ // the BASE (config.defaultNamespace) — never the coding overlay. So an
635
+ // auto-scoped session flushed `pi-geek:abc123` (no overlay) while observe
636
+ // archived under `pi-geek-project-...:pi-geek:abc123`. This pins that they
637
+ // now agree.
638
+ const probe = makeParityProbe(withSelfPolicyPrefix("pi-geek"));
639
+ const service = new EngramAccessService(probe.orch);
640
+
641
+ await service.observe(
642
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
643
+ );
644
+ await service.lcmCompactionFlush({ sessionKey: "pi-geek:abc123" });
645
+
646
+ const observedWriteKey = probe.lcmWriteKeys[0];
647
+ assertOverlayWriteKey(observedWriteKey, "pi-geek-"); // observe must archive under the overlay namespace, got ${observedWriteKey}
648
+ assert.equal(
649
+ probe.compactionFlushKeys[0],
650
+ observedWriteKey,
651
+ "compaction flush must target the overlay key, not the base",
652
+ );
653
+ });
654
+
655
+ test("#1505 round 3: access lcmSearch routes the session_id through the SCOPED (overlay) key", async () => {
656
+ // cursor "LCM search misses overlay keys" / codex "Route access LCM search
657
+ // through the scoped key". A project-scoped observe (no explicit namespace)
658
+ // archives under `${overlayNs}:${sessionKey}` and binds the coding context to
659
+ // the session. A subsequent lcmSearch({ sessionKey }) with NO explicit
660
+ // namespace must search under the SAME overlay key — not the raw sessionKey —
661
+ // or it misses the turns just archived.
662
+ const probe = makeParityProbe(withSelfPolicyPrefix("pi-geek"));
663
+ const service = new EngramAccessService(probe.orch);
664
+
665
+ await service.observe(
666
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
667
+ );
668
+ const writeKey = probe.lcmWriteKeys[0];
669
+ // New #1495 P1 encoding: `\x1f<overlayNs>\x1f<sessionKey>`. Parse the overlay
670
+ // namespace out of the sentinel frame (the namespace is `\x1f`-free).
671
+ assert.ok(
672
+ writeKey.startsWith(LCM_NS_SENTINEL),
673
+ `observe must archive under the sentinel-framed overlay key, got ${JSON.stringify(writeKey)}`,
674
+ );
675
+ const overlayNs = writeKey.slice(LCM_NS_SENTINEL.length).split(LCM_NS_SENTINEL)[0]!;
676
+ assert.ok(
677
+ overlayNs.startsWith("pi-geek-"),
678
+ `overlay namespace must be pi-geek's project overlay, got ${overlayNs}`,
679
+ );
680
+
681
+ await service.lcmSearch({
682
+ query: "what database are we using?",
683
+ sessionKey: "pi-geek:abc123",
684
+ sessionPrefix: "pi-geek:",
685
+ });
686
+
687
+ assert.equal(
688
+ probe.searchSessionIds[0],
689
+ writeKey,
690
+ "lcmSearch session_id must be the overlay-scoped key, matching the write key",
691
+ );
692
+ assert.ok(
693
+ !probe.searchSessionIds.includes("pi-geek:abc123"),
694
+ `lcmSearch must NOT query the raw sessionKey; queried: ${JSON.stringify(probe.searchSessionIds)}`,
695
+ );
696
+ // The sessionPrefix is framed with the same overlay namespace (so it stays a
697
+ // valid LIKE-prefix of the overlay full keys).
698
+ assert.equal(
699
+ probe.searchSessionPrefixes[0],
700
+ encodeNs(overlayNs, "pi-geek:"),
701
+ "lcmSearch sessionPrefix must carry the overlay namespace too",
702
+ );
703
+ });
704
+
705
+ test("#1505 round 3: lcmSearch on a single-store / no-overlay session keeps the raw key (regression guard)", async () => {
706
+ const probe = makeParityProbe({
707
+ ...withSelfPolicyPrefix("pi-geek"),
708
+ codingMode: { projectScope: false },
709
+ } as Partial<PluginConfig>);
710
+ const service = new EngramAccessService(probe.orch);
711
+
712
+ await service.lcmSearch({
713
+ query: "anything",
714
+ sessionKey: "pi-geek:abc123",
715
+ });
716
+ assert.equal(
717
+ probe.searchSessionIds[0],
718
+ "pi-geek:abc123",
719
+ "no overlay ⇒ lcmSearch searches the raw sessionKey (byte-for-byte preserved)",
720
+ );
721
+ });
722
+
723
+ test("#1505 round 3: extraction is pinned to the resolved writeNamespace even when it equals the default store", async () => {
724
+ // codex "Pin default-store extraction writes too". An unqualified observe by a
725
+ // principal that HAS a self namespace resolves writeNamespace ==
726
+ // config.defaultNamespace (general path) but, left unpinned, runExtraction
727
+ // would fall back to defaultNamespaceForPrincipal(principal) == the SELF
728
+ // namespace — diverging from LCM/objective-state/response. With namespaces
729
+ // enabled, observe must pin writeNamespaceOverride = writeNamespace (= default)
730
+ // so every side effect lands in ONE namespace.
731
+ const probe = makeParityProbe({
732
+ ...withSelfPolicyPrefix("pi-geek"),
733
+ // No overlay so writeNamespace collapses to config.defaultNamespace.
734
+ codingMode: { projectScope: false },
735
+ } as Partial<PluginConfig>);
736
+ const service = new EngramAccessService(probe.orch);
737
+
738
+ await service.observe(
739
+ observeRequest({
740
+ sessionKey: "pi-geek:abc123",
741
+ skipExtraction: false, // exercise extraction
742
+ }),
743
+ );
744
+
745
+ assert.equal(probe.extractionCalls.length, 1);
746
+ assert.equal(
747
+ probe.extractionCalls[0].writeNamespaceOverride,
748
+ "default",
749
+ "extraction must be pinned to the default store, not left to fall back to the principal self namespace",
750
+ );
751
+ // Identity is still the original key + resolved principal.
752
+ assert.deepEqual(
753
+ new Set(probe.extractionCalls[0].sessionKeys),
754
+ new Set(["pi-geek:abc123"]),
755
+ );
756
+ assert.equal(probe.extractionCalls[0].principalOverride, "pi-geek");
757
+ });
758
+
759
+ // ──────────────────────────────────────────────────────────────────────────
760
+ // #1505 round 3 (codex P2): READ-AUTHORIZATION gating of the overlay LCM read
761
+ // key. The round-2 parity fix made LCM READS always substitute the principal
762
+ // self-overlay namespace. That bypassed the read-authorization / readable-
763
+ // recall-namespace gating the rest of recall honors, so a principal who can
764
+ // WRITE but NOT READ its self namespace (or whose `defaultRecallNamespaces`
765
+ // omits `self`) would have `<principal>-project-*` overlay rows injected into
766
+ // recall / returned by `lcmSearch` even though QMD/file recall excludes them
767
+ // (cross-tenant read leak). Both sites must gate the overlay substitution by
768
+ // the readable recall namespace set (rule 42 read/write parity; rule 48
769
+ // least-privilege).
770
+ // ──────────────────────────────────────────────────────────────────────────
771
+
772
+ test("#1505 round 3 thread 1: orchestrator LCM read key falls back to default when the principal can WRITE but not READ its self namespace", async () => {
773
+ // alice may WRITE her self namespace but NOT read it (readPrincipals omits
774
+ // alice). A project-scoped observe archives LCM under
775
+ // `alice-project-*:sess-1`, but a no-namespace recall by alice may NOT inject
776
+ // those overlay rows — QMD/file recall would exclude `alice` (unreadable), so
777
+ // the LCM read key MUST collapse to the default store too.
778
+ const probe = makeParityProbe({
779
+ namespacePolicies: [
780
+ // WRITE-only self policy: alice can write `alice` but cannot read it.
781
+ { name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
782
+ ],
783
+ principalFromSessionKeyMode: "prefix",
784
+ principalFromSessionKeyRules: [],
785
+ defaultRecallNamespaces: ["self", "shared"],
786
+ } as Partial<PluginConfig>);
787
+
788
+ // Bind a coding context to sess-1 so the overlay would apply on alice's base.
789
+ probe.contexts.set("sess-1", {
790
+ projectId: "blend-supply",
791
+ projectName: "Blend/Supply",
792
+ } as unknown as CodingContext);
793
+
794
+ const lcmReadNamespaceForSession = Orchestrator.prototype[
795
+ "lcmReadNamespaceForSession" as keyof Orchestrator
796
+ ] as unknown as (
797
+ this: Orchestrator,
798
+ sk?: string,
799
+ principalOverride?: string,
800
+ ) => string;
801
+
802
+ // With the authenticated principal = alice (NOT encoded in sess-1), the
803
+ // overlay base is `alice`. alice cannot READ `alice`, so the read namespace
804
+ // MUST fall back to the default store (NOT `alice-project-*`).
805
+ const readNs = lcmReadNamespaceForSession.call(probe.orch, "sess-1", "alice");
806
+ assert.equal(
807
+ readNs,
808
+ "default",
809
+ `unreadable self base ⇒ LCM read key must fall back to the default store, got ${readNs}`,
810
+ );
811
+ assert.ok(
812
+ !readNs.startsWith("alice-"),
813
+ `LCM read key must NOT inject alice's overlay rows when alice cannot read her self namespace, got ${readNs}`,
814
+ );
815
+ });
816
+
817
+ test("#1505 round 3 thread 1: orchestrator LCM read key falls back to default when defaultRecallNamespaces omits self", async () => {
818
+ // alice CAN read her self namespace, but `defaultRecallNamespaces` omits
819
+ // `self`, so QMD/file recall never includes `alice` for a no-namespace
820
+ // recall. The overlay LCM read key must mirror that exclusion.
821
+ const probe = makeParityProbe({
822
+ namespacePolicies: [
823
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
824
+ ],
825
+ principalFromSessionKeyMode: "prefix",
826
+ principalFromSessionKeyRules: [],
827
+ // Self deliberately omitted from the recall set.
828
+ defaultRecallNamespaces: ["shared"],
829
+ } as Partial<PluginConfig>);
830
+
831
+ probe.contexts.set("sess-1", {
832
+ projectId: "blend-supply",
833
+ projectName: "Blend/Supply",
834
+ } as unknown as CodingContext);
835
+
836
+ const lcmReadNamespaceForSession = Orchestrator.prototype[
837
+ "lcmReadNamespaceForSession" as keyof Orchestrator
838
+ ] as unknown as (
839
+ this: Orchestrator,
840
+ sk?: string,
841
+ principalOverride?: string,
842
+ ) => string;
843
+
844
+ const readNs = lcmReadNamespaceForSession.call(probe.orch, "sess-1", "alice");
845
+ assert.equal(
846
+ readNs,
847
+ "default",
848
+ `self omitted from defaultRecallNamespaces ⇒ overlay LCM read key must fall back to default, got ${readNs}`,
849
+ );
850
+ });
851
+
852
+ test("#1505 round 3 thread 1: orchestrator LCM read key still uses the overlay when self IS readable (round-2 positive case preserved)", async () => {
853
+ // pi-geek can read AND write its self namespace, and `self` is in the recall
854
+ // set, so the overlay LCM read key must STILL be the project-scoped overlay
855
+ // (the round-2 parity behavior must stay green).
856
+ const probe = makeParityProbe({
857
+ ...withSelfPolicyPrefix("pi-geek"),
858
+ defaultRecallNamespaces: ["self", "shared"],
859
+ } as Partial<PluginConfig>);
860
+
861
+ probe.contexts.set("pi-geek:abc123", {
862
+ projectId: "blend-supply",
863
+ projectName: "Blend/Supply",
864
+ } as unknown as CodingContext);
865
+
866
+ const lcmReadNamespaceForSession = Orchestrator.prototype[
867
+ "lcmReadNamespaceForSession" as keyof Orchestrator
868
+ ] as unknown as (this: Orchestrator, sk?: string) => string;
869
+
870
+ const readNs = lcmReadNamespaceForSession.call(probe.orch, "pi-geek:abc123");
871
+ assert.ok(
872
+ readNs.startsWith("pi-geek-"),
873
+ `readable self ⇒ overlay LCM read key must be the project overlay, got ${readNs}`,
874
+ );
875
+ assert.notEqual(readNs, "default");
876
+ });
877
+
878
+ test("#1505 round 3 thread 2: lcmSearch returns NO overlay rows when the principal cannot read its self base (authorized fallback)", async () => {
879
+ // alice authenticates and passes the `default` read check, but her policy does
880
+ // NOT permit reading her self/overlay base (readPrincipals omits alice). A
881
+ // coding context is bound to the session, so the overlay WOULD apply — but the
882
+ // read-authorization gate must keep the just-authorized (default) namespace,
883
+ // so lcmSearch queries the RAW sessionKey, NOT `alice-project-*`.
884
+ const probe = makeParityProbe({
885
+ namespacePolicies: [
886
+ { name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
887
+ ],
888
+ principalFromSessionKeyMode: "prefix",
889
+ principalFromSessionKeyRules: [],
890
+ defaultRecallNamespaces: ["self", "shared"],
891
+ } as Partial<PluginConfig>);
892
+ const service = new EngramAccessService(probe.orch);
893
+
894
+ probe.contexts.set("sess-1", {
895
+ projectId: "blend-supply",
896
+ projectName: "Blend/Supply",
897
+ } as unknown as CodingContext);
898
+
899
+ await service.lcmSearch({
900
+ query: "what database are we using?",
901
+ sessionKey: "sess-1",
902
+ sessionPrefix: "alice:",
903
+ authenticatedPrincipal: "alice",
904
+ });
905
+
906
+ assert.equal(
907
+ probe.searchSessionIds[0],
908
+ "sess-1",
909
+ `unauthorized overlay base ⇒ lcmSearch must query the raw sessionKey, got ${String(
910
+ probe.searchSessionIds[0],
911
+ )}`,
912
+ );
913
+ assert.ok(
914
+ !String(probe.searchSessionIds[0] ?? "").includes("alice-"),
915
+ `lcmSearch must NOT return alice-project-* rows to a caller who cannot read the alice namespace; queried: ${JSON.stringify(
916
+ probe.searchSessionIds,
917
+ )}`,
918
+ );
919
+ // The prefix must NOT carry the overlay namespace either.
920
+ assert.ok(
921
+ !String(probe.searchSessionPrefixes[0] ?? "").includes("alice-"),
922
+ `lcmSearch sessionPrefix must NOT carry the alice overlay namespace; got ${String(
923
+ probe.searchSessionPrefixes[0],
924
+ )}`,
925
+ );
926
+ });
927
+
928
+ test("#1505 round 4 thread (codex P2): compaction flush/record target the OVERLAY key even when the principal can WRITE but not READ its self base", async () => {
929
+ // The round-3 read-authorization gate is SHARED by lcmCompactionFlush/Record,
930
+ // but compaction is a WRITE/maintenance op on the queue observe just wrote.
931
+ // alice can WRITE her self namespace but NOT read it. observe archives under
932
+ // `alice-project-*:sess-1`; compaction must flush/record that SAME overlay key
933
+ // (gated by WRITE authorization), NOT fall back to the default/raw key — else
934
+ // the project-scoped LCM queue is never flushed/recorded.
935
+ const probe = makeParityProbe({
936
+ namespacePolicies: [
937
+ // Write-only self policy: alice can write `alice` but cannot read it.
938
+ { name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
939
+ ],
940
+ principalFromSessionKeyMode: "prefix",
941
+ principalFromSessionKeyRules: [],
942
+ // self deliberately omitted from the recall set too — read gate would fall
943
+ // back, but the WRITE gate must still honour the overlay.
944
+ defaultRecallNamespaces: ["shared"],
945
+ } as Partial<PluginConfig>);
946
+ const service = new EngramAccessService(probe.orch);
947
+
948
+ // WRITE: alice observes sess-1 with a project tag → overlay applies on alice's
949
+ // write-authorized self base.
950
+ await service.observe(
951
+ observeRequest({
952
+ sessionKey: "sess-1",
953
+ authenticatedPrincipal: "alice",
954
+ projectTag: "Blend/Supply",
955
+ }),
956
+ );
957
+ const writeKey = probe.lcmWriteKeys[0];
958
+ assertOverlayWriteKey(writeKey, "alice-"); // observe must archive under alice's overlay key, got ${writeKey}
959
+
960
+ await service.lcmCompactionFlush({
961
+ sessionKey: "sess-1",
962
+ authenticatedPrincipal: "alice",
963
+ });
964
+ await service.lcmCompactionRecord({
965
+ sessionKey: "sess-1",
966
+ authenticatedPrincipal: "alice",
967
+ tokensBefore: 100,
968
+ tokensAfter: 10,
969
+ });
970
+
971
+ assert.equal(
972
+ probe.compactionFlushKeys[0],
973
+ writeKey,
974
+ "compaction flush must target the overlay key (write-authorized), not the default/raw key",
975
+ );
976
+ assert.equal(
977
+ probe.compactionRecordKeys[0],
978
+ writeKey,
979
+ "compaction record must target the overlay key (write-authorized), not the default/raw key",
980
+ );
981
+ });
982
+
983
+ test("#1505 round 4 thread (codex P2): a read-only lcmSearch by the SAME write-only principal still does NOT leak the overlay rows (read vs write gate divergence)", async () => {
984
+ // Companion to the compaction-write-gate test: the SAME write-only, read-denied
985
+ // principal must STILL get the authorized fallback (raw key) on lcmSearch — the
986
+ // read gate and the write gate diverge by design.
987
+ const probe = makeParityProbe({
988
+ namespacePolicies: [
989
+ { name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
990
+ ],
991
+ principalFromSessionKeyMode: "prefix",
992
+ principalFromSessionKeyRules: [],
993
+ defaultRecallNamespaces: ["shared"],
994
+ } as Partial<PluginConfig>);
995
+ const service = new EngramAccessService(probe.orch);
996
+
997
+ probe.contexts.set("sess-1", {
998
+ projectId: "blend-supply",
999
+ projectName: "Blend/Supply",
1000
+ } as unknown as CodingContext);
1001
+
1002
+ await service.lcmSearch({
1003
+ query: "anything",
1004
+ sessionKey: "sess-1",
1005
+ authenticatedPrincipal: "alice",
1006
+ });
1007
+
1008
+ assert.equal(
1009
+ probe.searchSessionIds[0],
1010
+ "sess-1",
1011
+ "read gate: write-only/read-denied principal must NOT receive alice-project-* rows on lcmSearch",
1012
+ );
1013
+ });
1014
+
1015
+ test("#1505 round 3 thread 2: lcmSearch still routes through the overlay key when the principal CAN read its self base (round-2 positive case preserved)", async () => {
1016
+ // alice can read AND write her self namespace, so the authorized overlay LCM
1017
+ // read key is honored — lcmSearch routes the session_id through the overlay.
1018
+ const probe = makeParityProbe({
1019
+ namespacePolicies: [
1020
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
1021
+ ],
1022
+ principalFromSessionKeyMode: "prefix",
1023
+ principalFromSessionKeyRules: [],
1024
+ defaultRecallNamespaces: ["self", "shared"],
1025
+ } as Partial<PluginConfig>);
1026
+ const service = new EngramAccessService(probe.orch);
1027
+
1028
+ // Archive under alice's overlay (binds the coding context to the session).
1029
+ const writeRes = await service.observe(
1030
+ observeRequest({
1031
+ sessionKey: "sess-1",
1032
+ authenticatedPrincipal: "alice",
1033
+ projectTag: "Blend/Supply",
1034
+ }),
1035
+ );
1036
+ const writeKey = probe.lcmWriteKeys[0];
1037
+ assertOverlayWriteKey(writeKey, "alice-"); // observe must archive under alice's overlay key, got ${writeKey}
1038
+
1039
+ await service.lcmSearch({
1040
+ query: "what database are we using?",
1041
+ sessionKey: "sess-1",
1042
+ authenticatedPrincipal: "alice",
1043
+ });
1044
+
1045
+ assert.equal(
1046
+ probe.searchSessionIds[0],
1047
+ writeKey,
1048
+ "readable self ⇒ lcmSearch session_id must be the overlay-scoped key matching the write key",
1049
+ );
1050
+ assert.equal(
1051
+ writeRes.effectiveNamespace?.startsWith("alice-"),
1052
+ true,
1053
+ "observe effectiveNamespace must be the alice overlay (sanity)",
1054
+ );
1055
+ });
1056
+
1057
+ test("#1505 thread NBHWs (codex P2): restrictive `default` WRITE policy + writable self ⇒ compaction flush/record the OVERLAY queue (no `not writable: default` throw)", async () => {
1058
+ // The root defect: `lcmCompactionFlush`/`Record` PRE-authorized
1059
+ // `undefined ⇒ config.defaultNamespace` via `resolveWritableNamespace` BEFORE
1060
+ // the scoped write key was computed. Under a deployment whose `default`
1061
+ // namespace has a RESTRICTIVE write policy (alice may NOT write `default`) but
1062
+ // where alice CAN write her self/project overlay, `observe({ projectTag })`
1063
+ // succeeds and archives LCM under `alice-project-*:sess-1` — yet compaction
1064
+ // threw `namespace is not writable: default`, so the queue observe just wrote
1065
+ // could never be flushed or recorded.
1066
+ //
1067
+ // FAIL-BEFORE: both compaction calls throw `namespace is not writable:
1068
+ // default`. PASS-AFTER: compaction derives the namespace through the SAME
1069
+ // write-scoped plan/gate observe uses (`resolveMemoryScopePlan`), authorizes
1070
+ // the writable self base, and flushes/records the overlay key.
1071
+ const probe = makeParityProbe({
1072
+ namespacePolicies: [
1073
+ // RESTRICTIVE default: NO principal may write (or read) `default`.
1074
+ { name: "default", readPrincipals: [], writePrincipals: [] },
1075
+ // alice CAN write (and read) her self namespace.
1076
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
1077
+ ],
1078
+ principalFromSessionKeyMode: "prefix",
1079
+ principalFromSessionKeyRules: [],
1080
+ defaultRecallNamespaces: ["self", "shared"],
1081
+ } as Partial<PluginConfig>);
1082
+ const service = new EngramAccessService(probe.orch);
1083
+
1084
+ // observe succeeds under alice's writable self/project overlay even though
1085
+ // `default` is not writable.
1086
+ const observeRes = await service.observe(
1087
+ observeRequest({
1088
+ sessionKey: "sess-1",
1089
+ authenticatedPrincipal: "alice",
1090
+ projectTag: "Blend/Supply",
1091
+ }),
1092
+ );
1093
+ const writeKey = probe.lcmWriteKeys[0];
1094
+ assertOverlayWriteKey(writeKey, "alice-"); // observe must archive under alice's writable overlay key, got ${writeKey}
1095
+ assert.ok(
1096
+ observeRes.effectiveNamespace?.startsWith("alice-"),
1097
+ "observe effectiveNamespace must be the alice overlay (sanity)",
1098
+ );
1099
+
1100
+ // Compaction must NOT throw `not writable: default` and must target the
1101
+ // overlay queue observe wrote.
1102
+ const flushRes = await service.lcmCompactionFlush({
1103
+ sessionKey: "sess-1",
1104
+ authenticatedPrincipal: "alice",
1105
+ });
1106
+ const recordRes = await service.lcmCompactionRecord({
1107
+ sessionKey: "sess-1",
1108
+ authenticatedPrincipal: "alice",
1109
+ tokensBefore: 100,
1110
+ tokensAfter: 10,
1111
+ });
1112
+
1113
+ assert.equal(flushRes.flushed, true, "flush must succeed");
1114
+ assert.equal(recordRes.recorded, true, "record must succeed");
1115
+ assert.equal(
1116
+ probe.compactionFlushKeys[0],
1117
+ writeKey,
1118
+ "compaction flush must target the overlay key observe wrote, not the (unwritable) default key",
1119
+ );
1120
+ assert.equal(
1121
+ probe.compactionRecordKeys[0],
1122
+ writeKey,
1123
+ "compaction record must target the overlay key observe wrote, not the (unwritable) default key",
1124
+ );
1125
+ });
1126
+
1127
+ test("#1505 thread NBHWz (sweep): restrictive `default` READ policy + readable self ⇒ lcmSearch reads the self/recall-authorized overlay (no `not readable: default` throw)", async () => {
1128
+ // Convergence sweep: `lcmSearch` is the SAME defect class as the raw-excerpt
1129
+ // path — it pre-authorized `undefined ⇒ config.defaultNamespace` via
1130
+ // `resolveReadableNamespace` BEFORE deriving the scoped read namespace. Under a
1131
+ // restrictive `default` READ policy where pi-geek's self namespace IS readable,
1132
+ // normal recall succeeds via `recallNamespacesForPrincipal`, so `lcmSearch`
1133
+ // must too. FAIL-BEFORE: throws `namespace is not readable: default`.
1134
+ // PASS-AFTER: routes through the readable self overlay.
1135
+ const probe = makeParityProbe({
1136
+ namespacePolicies: [
1137
+ { name: "default", readPrincipals: [], writePrincipals: [] },
1138
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
1139
+ ],
1140
+ principalFromSessionKeyMode: "prefix",
1141
+ principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
1142
+ defaultRecallNamespaces: ["self", "shared"],
1143
+ } as Partial<PluginConfig>);
1144
+ const service = new EngramAccessService(probe.orch);
1145
+
1146
+ // observe archives under pi-geek's readable+writable project overlay.
1147
+ await service.observe(
1148
+ observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
1149
+ );
1150
+ const writeKey = probe.lcmWriteKeys[0];
1151
+ assertOverlayWriteKey(writeKey, "pi-geek-"); // observe must archive under pi-geek's overlay key, got ${writeKey}
1152
+
1153
+ // lcmSearch with NO explicit namespace must NOT throw `not readable: default`
1154
+ // and must route the session_id through the readable overlay key.
1155
+ const res = await service.lcmSearch({
1156
+ query: "what database are we using?",
1157
+ sessionKey: "pi-geek:abc123",
1158
+ });
1159
+ assert.equal(res.lcmEnabled, true);
1160
+ assert.equal(
1161
+ probe.searchSessionIds[0],
1162
+ writeKey,
1163
+ "lcmSearch must route through the readable self overlay key, not pre-authorize the denied default",
1164
+ );
1165
+ });
1166
+
1167
+ test("#1505 codex P1 'Don't treat any readable namespace as default LCM access': self EXCLUDED from recall + `shared` readable + `default` denied ⇒ lcmSearch SUPPRESSES (never the denied default store, never `shared:`)", async () => {
1168
+ // codex P1 on the round-7 head: my first NBHWz fix treated ANY readable recall
1169
+ // namespace (e.g. `shared`) as license to read the DEFAULT LCM store. But the
1170
+ // implicit LCM read can ONLY target the coding overlay (when the SELF base is
1171
+ // readable-in-recall) or the default store — never `shared`. So when the self
1172
+ // base is NOT readable-in-recall AND `default` is denied, there is NO
1173
+ // authorized LCM target: lcmSearch must SUPPRESS (empty), NOT fall back to the
1174
+ // denied default store (which, sessionless, would scan the whole archive).
1175
+ //
1176
+ // alice can WRITE her self base (so observe archives under the overlay) but
1177
+ // CANNOT read it; `self` is omitted from the recall set; only `shared` is
1178
+ // readable; `default` is restrictively unreadable.
1179
+ const probe = makeParityProbe({
1180
+ namespacePolicies: [
1181
+ // Restrictive default: alice may NOT read `default`.
1182
+ { name: "default", readPrincipals: [], writePrincipals: [] },
1183
+ // alice can WRITE but NOT read her self base.
1184
+ { name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
1185
+ ],
1186
+ principalFromSessionKeyMode: "prefix",
1187
+ principalFromSessionKeyRules: [],
1188
+ // `shared` is readable + in the recall set; `self` is omitted.
1189
+ defaultRecallNamespaces: ["shared"],
1190
+ } as Partial<PluginConfig>);
1191
+ const service = new EngramAccessService(probe.orch);
1192
+
1193
+ // Bind a coding context so the overlay WOULD apply on alice's base.
1194
+ probe.contexts.set("sess-1", {
1195
+ projectId: "blend-supply",
1196
+ projectName: "Blend/Supply",
1197
+ } as unknown as CodingContext);
1198
+
1199
+ const res = await service.lcmSearch({
1200
+ query: "what database are we using?",
1201
+ sessionKey: "sess-1",
1202
+ authenticatedPrincipal: "alice",
1203
+ });
1204
+
1205
+ assert.equal(res.lcmEnabled, true);
1206
+ assert.equal(res.count, 0, "no authorized LCM target ⇒ empty results");
1207
+ // SUPPRESS: self unreadable-in-recall AND default denied ⇒ no authorized LCM
1208
+ // target. `searchContextFull` must NEVER run — not against the denied default
1209
+ // store (raw key), not against `shared`, not against alice's unreadable
1210
+ // overlay.
1211
+ assert.equal(
1212
+ probe.searchSessionIds.length,
1213
+ 0,
1214
+ "lcmSearch must SUPPRESS (no searchContextFull) — never read the denied default store via a `shared`-only authorization",
1215
+ );
1216
+ });
1217
+
1218
+ test("#1505 cursor 'LCM read gate wrong fallback' (positive): self READABLE-in-recall but overlay-self gate denies ⇒ lcmSearch uses the raw sessionKey (default store), never `shared:`", async () => {
1219
+ // The complementary case to the codex P1 suppress: when the principal self base
1220
+ // IS readable-in-recall (so PROCEED is authorized) but `default` is restrictively
1221
+ // unreadable, the implicit LCM read still collapses to the DEFAULT STORE raw key
1222
+ // for a session whose overlay does not apply — EXACTLY like the orchestrator's
1223
+ // `lcmReadNamespaceForSession` — and NEVER an arbitrary readable recall namespace
1224
+ // (e.g. `shared`). Here NO coding context is bound, so no overlay applies and the
1225
+ // key is the raw sessionKey.
1226
+ const probe = makeParityProbe({
1227
+ namespacePolicies: [
1228
+ // Restrictive default: alice may NOT read `default`.
1229
+ { name: "default", readPrincipals: [], writePrincipals: [] },
1230
+ // alice CAN read AND write her self base.
1231
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
1232
+ ],
1233
+ principalFromSessionKeyMode: "prefix",
1234
+ principalFromSessionKeyRules: [],
1235
+ // self IS readable + in the recall set, so PROCEED is authorized.
1236
+ defaultRecallNamespaces: ["self", "shared"],
1237
+ } as Partial<PluginConfig>);
1238
+ const service = new EngramAccessService(probe.orch);
1239
+
1240
+ // No coding context bound ⇒ no overlay ⇒ the LCM key is the raw sessionKey.
1241
+ const res = await service.lcmSearch({
1242
+ query: "what database are we using?",
1243
+ sessionKey: "sess-1",
1244
+ authenticatedPrincipal: "alice",
1245
+ });
1246
+
1247
+ assert.equal(res.lcmEnabled, true);
1248
+ assert.equal(
1249
+ probe.searchSessionIds[0],
1250
+ "sess-1",
1251
+ "self-readable PROCEED + no overlay ⇒ lcmSearch queries the raw sessionKey (default store), matching the orchestrator",
1252
+ );
1253
+ for (const id of probe.searchSessionIds) {
1254
+ assert.ok(
1255
+ !String(id ?? "").startsWith("shared"),
1256
+ `lcmSearch must NOT prefix with the shared recall namespace; got ${String(id)}`,
1257
+ );
1258
+ }
1259
+ });
1260
+
1261
+ test("#1505 thread NBHWz (sweep): no readable LCM namespace ⇒ lcmSearch returns EMPTY (no `not readable: default` throw)", async () => {
1262
+ // Companion: when NO readable LCM namespace exists for an implicit lcmSearch
1263
+ // (restrictive default READ + unreadable self + self omitted from the recall
1264
+ // set), the search returns EMPTY rather than throwing — `searchContextFull` is
1265
+ // never called.
1266
+ const probe = makeParityProbe({
1267
+ namespacePolicies: [
1268
+ { name: "default", readPrincipals: [], writePrincipals: [] },
1269
+ { name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
1270
+ ],
1271
+ principalFromSessionKeyMode: "prefix",
1272
+ principalFromSessionKeyRules: [],
1273
+ defaultRecallNamespaces: [],
1274
+ } as Partial<PluginConfig>);
1275
+ const service = new EngramAccessService(probe.orch);
1276
+
1277
+ const res = await service.lcmSearch({
1278
+ query: "anything",
1279
+ sessionKey: "sess-1",
1280
+ authenticatedPrincipal: "alice",
1281
+ });
1282
+ assert.equal(res.lcmEnabled, true);
1283
+ assert.equal(res.count, 0, "no readable LCM namespace ⇒ empty results");
1284
+ assert.equal(
1285
+ probe.searchSessionIds.length,
1286
+ 0,
1287
+ "searchContextFull must NOT be called when no readable LCM namespace exists",
1288
+ );
1289
+ });
1290
+
1291
+ test("#1505 codex P1 (sessionless archive-scan guard): restrictive `default` READ + readable self but NO sessionKey/sessionPrefix ⇒ lcmSearch SUPPRESSES (no unbounded archive scan)", async () => {
1292
+ // codex P1 defense-in-depth: a sessionless, prefixless `lcmSearch` issues
1293
+ // `searchContextFull(query, limit, undefined, undefined)`, scanning the ENTIRE
1294
+ // LCM archive across every session/namespace. Under a restrictive `default`
1295
+ // READ policy (alice cannot read `default`), that scan exposes the denied
1296
+ // default store's rows. Even though alice's SELF base is readable (so the
1297
+ // implicit fallback PROCEEDs), with NO sessionKey the overlay cannot apply and
1298
+ // the read collapses to the default store — so the sessionless scan must be
1299
+ // SUPPRESSED.
1300
+ const probe = makeParityProbe({
1301
+ namespacePolicies: [
1302
+ { name: "default", readPrincipals: [], writePrincipals: [] },
1303
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
1304
+ ],
1305
+ principalFromSessionKeyMode: "prefix",
1306
+ principalFromSessionKeyRules: [],
1307
+ defaultRecallNamespaces: ["self", "shared"],
1308
+ } as Partial<PluginConfig>);
1309
+ const service = new EngramAccessService(probe.orch);
1310
+
1311
+ // NO sessionKey, NO sessionPrefix → would otherwise scan the whole archive.
1312
+ const res = await service.lcmSearch({
1313
+ query: "what database are we using?",
1314
+ authenticatedPrincipal: "alice",
1315
+ });
1316
+
1317
+ assert.equal(res.lcmEnabled, true);
1318
+ assert.equal(res.count, 0, "sessionless + denied default ⇒ empty results");
1319
+ assert.equal(
1320
+ probe.searchSessionIds.length,
1321
+ 0,
1322
+ "searchContextFull must NOT run an unbounded archive scan against the denied default store",
1323
+ );
1324
+ });
1325
+
1326
+ test("#1505 codex P1 (sessionless regression): namespaces DISABLED (single-store) + no sessionKey ⇒ lcmSearch still scans the archive (byte-for-byte prior behavior)", async () => {
1327
+ // Regression guard: in a single-store deployment (namespaces disabled, default
1328
+ // always readable), a sessionless `lcmSearch` keeps its prior unbounded
1329
+ // behavior — the P1 guard only fires when the principal cannot read the default
1330
+ // store (i.e. namespaces enabled + a restrictive default policy / unauthenticated
1331
+ // caller).
1332
+ const probe = makeParityProbe({
1333
+ namespacesEnabled: false,
1334
+ } as Partial<PluginConfig>);
1335
+ const service = new EngramAccessService(probe.orch);
1336
+
1337
+ await service.lcmSearch({ query: "anything" });
1338
+
1339
+ assert.equal(
1340
+ probe.searchSessionIds.length,
1341
+ 1,
1342
+ "namespaces disabled + sessionless ⇒ the archive search still runs (byte-for-byte prior behavior)",
1343
+ );
1344
+ assert.equal(
1345
+ probe.searchSessionIds[0],
1346
+ undefined,
1347
+ "sessionless search passes no session_id filter (archive-wide), unchanged",
1348
+ );
1349
+ });
1350
+
1351
+ test("#1505 thread NBHWs regression: restrictive `default` WRITE policy + no overlay (writable self) ⇒ compaction still authorizes the self base, no `not writable: default`", async () => {
1352
+ // Companion: even with projectScope OFF (no overlay), an implicit observe by a
1353
+ // principal that can write its self base archives under the default store ONLY
1354
+ // when objective-state writes are off; with objective-state writes enabled the
1355
+ // scope plan authorizes the self base. Here we keep objective-state off (the
1356
+ // probe default) and projectScope off, so the write namespace collapses to the
1357
+ // default store — but `default` is NOT writable. The scope plan's no-overlay
1358
+ // branch still collapses to `config.defaultNamespace` via
1359
+ // `resolveWritableNamespace(undefined)`, which DOES throw when default is
1360
+ // unwritable — matching observe EXACTLY (if observe can't write, there is no
1361
+ // queue to flush). This pins that compaction and observe agree on the throw.
1362
+ const probe = makeParityProbe({
1363
+ namespacePolicies: [
1364
+ { name: "default", readPrincipals: [], writePrincipals: [] },
1365
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
1366
+ ],
1367
+ principalFromSessionKeyMode: "prefix",
1368
+ principalFromSessionKeyRules: [],
1369
+ defaultRecallNamespaces: ["self", "shared"],
1370
+ codingMode: { projectScope: false, branchScope: false, globalFallback: true },
1371
+ } as Partial<PluginConfig>);
1372
+ const service = new EngramAccessService(probe.orch);
1373
+
1374
+ // No overlay ⇒ implicit write collapses to the (unwritable) default store, so
1375
+ // observe itself rejects. Compaction must reject identically (parity), NOT
1376
+ // succeed against a phantom queue.
1377
+ await assert.rejects(
1378
+ () =>
1379
+ service.observe(
1380
+ observeRequest({
1381
+ sessionKey: "sess-1",
1382
+ authenticatedPrincipal: "alice",
1383
+ }),
1384
+ ),
1385
+ /not writable: default/,
1386
+ "no-overlay implicit observe must reject on the unwritable default store",
1387
+ );
1388
+ await assert.rejects(
1389
+ () =>
1390
+ service.lcmCompactionFlush({
1391
+ sessionKey: "sess-1",
1392
+ authenticatedPrincipal: "alice",
1393
+ }),
1394
+ /not writable: default/,
1395
+ "compaction must reject identically to observe when the effective write target is the unwritable default store (parity)",
1396
+ );
1397
+ });