@remnic/core 9.3.753 → 9.3.754

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 (160) hide show
  1. package/dist/access-admin-ops-surface.d.ts +1 -1
  2. package/dist/access-admin-ops-surface.js +26 -26
  3. package/dist/access-boundary.d.ts +1 -1
  4. package/dist/access-boundary.js +27 -27
  5. package/dist/access-cli.js +75 -75
  6. package/dist/access-http.d.ts +1 -1
  7. package/dist/access-http.js +34 -34
  8. package/dist/access-lcm-surface.d.ts +188 -0
  9. package/dist/access-lcm-surface.js +128 -0
  10. package/dist/access-lcm-surface.js.map +1 -0
  11. package/dist/access-mcp.d.ts +1 -1
  12. package/dist/access-mcp.js +31 -31
  13. package/dist/access-operations-batch.js +28 -28
  14. package/dist/access-operations.d.ts +5 -5
  15. package/dist/access-operations.js +30 -30
  16. package/dist/access-recall-surface.d.ts +1 -1
  17. package/dist/access-recall-surface.js +26 -26
  18. package/dist/access-schema.d.ts +74 -74
  19. package/dist/access-schema.js +6 -6
  20. package/dist/{access-service-MitduesD.d.ts → access-service-DKRV1zxh.d.ts} +4 -64
  21. package/dist/access-service.d.ts +1 -1
  22. package/dist/access-service.js +26 -26
  23. package/dist/access-surface-catalog.d.ts +1 -1
  24. package/dist/adapters/index.js +4 -4
  25. package/dist/adapters/registry.js +2 -2
  26. package/dist/{auto-sync-IEBKPGAM.js → auto-sync-G4YKBPOU.js} +5 -5
  27. package/dist/briefing.js +7 -7
  28. package/dist/calibration.js +2 -2
  29. package/dist/causal-consolidation.js +8 -8
  30. package/dist/{chunk-HAZVVOYT.js → chunk-24463PIV.js} +2 -2
  31. package/dist/{chunk-C3IHRWIC.js → chunk-2MHCWXX6.js} +2 -2
  32. package/dist/{chunk-GWGWBK5Y.js → chunk-2MR3MFQB.js} +7 -7
  33. package/dist/{chunk-YTU7WKYN.js → chunk-2OZ2JEUP.js} +2 -2
  34. package/dist/{chunk-AU7Q3LSC.js → chunk-2QSZNTDO.js} +4 -4
  35. package/dist/{chunk-EHYB325U.js → chunk-3DOD6ICJ.js} +2 -2
  36. package/dist/{chunk-KY4WOZ3P.js → chunk-4DVYRPLL.js} +42 -42
  37. package/dist/{chunk-VUA4S7P6.js → chunk-5J4OL7TJ.js} +4 -4
  38. package/dist/{chunk-EEO4YWQ7.js → chunk-7AL5WNPW.js} +2 -2
  39. package/dist/{chunk-BWOEYBRB.js → chunk-7CAJLUNW.js} +2 -2
  40. package/dist/{chunk-JES2O5Q6.js → chunk-BERWPOXU.js} +4 -4
  41. package/dist/{chunk-3HGZX26P.js → chunk-BQ6J6P3K.js} +8 -8
  42. package/dist/{chunk-XOPLMSVV.js → chunk-CJ276MDI.js} +2 -2
  43. package/dist/{chunk-TVISHYL5.js → chunk-COZEVVXZ.js} +2 -2
  44. package/dist/{chunk-ZIFPFKB2.js → chunk-CXCZ4LAI.js} +2 -2
  45. package/dist/{chunk-OUXPVWTZ.js → chunk-FLVL4L3L.js} +5 -5
  46. package/dist/{chunk-LZ73Y56P.js → chunk-G7LCG56O.js} +4 -4
  47. package/dist/{chunk-E3MRYDUQ.js → chunk-GAH5755F.js} +2 -2
  48. package/dist/{chunk-JGPURIEH.js → chunk-GPFXPC2V.js} +2 -2
  49. package/dist/{chunk-3ZIKDH6C.js → chunk-JBO6UBIE.js} +4 -4
  50. package/dist/{chunk-YNBEHUZH.js → chunk-KURUULFF.js} +2 -2
  51. package/dist/{chunk-ZECLLIMX.js → chunk-L4AS7TIG.js} +4998 -4950
  52. package/dist/chunk-L4AS7TIG.js.map +1 -0
  53. package/dist/{chunk-LQ6JI4VH.js → chunk-LD53WPMU.js} +4 -4
  54. package/dist/{chunk-QIRZI3C3.js → chunk-OH66EBCS.js} +2 -2
  55. package/dist/{chunk-OZMBT7N3.js → chunk-PJQMISPD.js} +2 -2
  56. package/dist/{chunk-PN5AMCK2.js → chunk-PYVZKEZZ.js} +53 -53
  57. package/dist/{chunk-DCWIQFNA.js → chunk-T422SYM6.js} +5 -5
  58. package/dist/{chunk-5CEJH5ZN.js → chunk-TAPSNIMA.js} +4 -4
  59. package/dist/{chunk-3NN73CM7.js → chunk-TH7WMHVK.js} +2 -2
  60. package/dist/{chunk-34THARMH.js → chunk-THZ4WR5L.js} +10 -10
  61. package/dist/{chunk-E6LENGYV.js → chunk-TIFF32QK.js} +2 -2
  62. package/dist/{chunk-RNWWAS5V.js → chunk-TJATYB3F.js} +11 -11
  63. package/dist/{chunk-KYHGLDFU.js → chunk-TXALJRYO.js} +6 -6
  64. package/dist/{chunk-DKIZ4BUQ.js → chunk-U2SMR57B.js} +7 -7
  65. package/dist/{chunk-VVSV2QVY.js → chunk-V4C4SFZB.js} +3 -3
  66. package/dist/{chunk-AXBRVBT6.js → chunk-VSU2NZGC.js} +2 -2
  67. package/dist/{chunk-N3VE7OCY.js → chunk-WS6GLJ7P.js} +2 -2
  68. package/dist/{chunk-ITLJATQI.js → chunk-WUVUTOUV.js} +2 -2
  69. package/dist/{chunk-WRFKZEO6.js → chunk-YQGHW5ML.js} +4 -4
  70. package/dist/{chunk-243P7YYV.js → chunk-YRBXBHU7.js} +2 -2
  71. package/dist/{chunk-ZMHDSIEX.js → chunk-ZJMSTFOR.js} +4 -4
  72. package/dist/{cli-DqonYBZ0.d.ts → cli-D-xw8Sp1.d.ts} +1 -1
  73. package/dist/cli.d.ts +2 -2
  74. package/dist/cli.js +55 -55
  75. package/dist/compounding/engine.js +7 -7
  76. package/dist/connectors/codex-materialize-runner.js +7 -7
  77. package/dist/connectors/index.js +7 -7
  78. package/dist/conversation-index/backend.js +2 -2
  79. package/dist/entity-retrieval.js +7 -7
  80. package/dist/extraction-faithfulness.js +3 -3
  81. package/dist/extraction-judge.js +3 -3
  82. package/dist/extraction.js +3 -3
  83. package/dist/fallback-llm.js +2 -2
  84. package/dist/index.d.ts +2 -2
  85. package/dist/index.js +149 -149
  86. package/dist/lcm/engine.js +2 -2
  87. package/dist/lcm/index.js +4 -4
  88. package/dist/maintenance/memory-governance.js +7 -7
  89. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  90. package/dist/maintenance/rebuild-memory-projection.js +8 -8
  91. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  92. package/dist/namespaces/migrate.js +17 -17
  93. package/dist/namespaces/search.js +9 -9
  94. package/dist/namespaces/storage.js +7 -7
  95. package/dist/operator-toolkit.js +21 -21
  96. package/dist/orchestration/maintenance.js +9 -9
  97. package/dist/orchestrator.js +75 -75
  98. package/dist/recall-planner-llm.js +2 -2
  99. package/dist/schemas.d.ts +90 -90
  100. package/dist/search/factory.js +8 -8
  101. package/dist/search/index.js +13 -13
  102. package/dist/search/lancedb-backend.js +2 -2
  103. package/dist/search/meilisearch-backend.js +2 -2
  104. package/dist/search/orama-backend.js +2 -2
  105. package/dist/semantic-consolidation.js +8 -8
  106. package/dist/semantic-rule-promotion.js +7 -7
  107. package/dist/semantic-rule-verifier.js +7 -7
  108. package/dist/shared-context/manager.d.ts +8 -8
  109. package/dist/storage.js +6 -6
  110. package/dist/summarizer.js +3 -3
  111. package/dist/transfer/autodetect.js +1 -1
  112. package/dist/transfer/backup.js +1 -1
  113. package/dist/transfer/capsule-export.js +2 -2
  114. package/dist/transfer/types.d.ts +66 -66
  115. package/dist/verified-recall.js +7 -7
  116. package/package.json +2 -2
  117. package/src/access-lcm-surface.ts +517 -0
  118. package/src/access-service.ts +21 -417
  119. package/dist/chunk-ZECLLIMX.js.map +0 -1
  120. /package/dist/{auto-sync-IEBKPGAM.js.map → auto-sync-G4YKBPOU.js.map} +0 -0
  121. /package/dist/{chunk-HAZVVOYT.js.map → chunk-24463PIV.js.map} +0 -0
  122. /package/dist/{chunk-C3IHRWIC.js.map → chunk-2MHCWXX6.js.map} +0 -0
  123. /package/dist/{chunk-GWGWBK5Y.js.map → chunk-2MR3MFQB.js.map} +0 -0
  124. /package/dist/{chunk-YTU7WKYN.js.map → chunk-2OZ2JEUP.js.map} +0 -0
  125. /package/dist/{chunk-AU7Q3LSC.js.map → chunk-2QSZNTDO.js.map} +0 -0
  126. /package/dist/{chunk-EHYB325U.js.map → chunk-3DOD6ICJ.js.map} +0 -0
  127. /package/dist/{chunk-KY4WOZ3P.js.map → chunk-4DVYRPLL.js.map} +0 -0
  128. /package/dist/{chunk-VUA4S7P6.js.map → chunk-5J4OL7TJ.js.map} +0 -0
  129. /package/dist/{chunk-EEO4YWQ7.js.map → chunk-7AL5WNPW.js.map} +0 -0
  130. /package/dist/{chunk-BWOEYBRB.js.map → chunk-7CAJLUNW.js.map} +0 -0
  131. /package/dist/{chunk-JES2O5Q6.js.map → chunk-BERWPOXU.js.map} +0 -0
  132. /package/dist/{chunk-3HGZX26P.js.map → chunk-BQ6J6P3K.js.map} +0 -0
  133. /package/dist/{chunk-XOPLMSVV.js.map → chunk-CJ276MDI.js.map} +0 -0
  134. /package/dist/{chunk-TVISHYL5.js.map → chunk-COZEVVXZ.js.map} +0 -0
  135. /package/dist/{chunk-ZIFPFKB2.js.map → chunk-CXCZ4LAI.js.map} +0 -0
  136. /package/dist/{chunk-OUXPVWTZ.js.map → chunk-FLVL4L3L.js.map} +0 -0
  137. /package/dist/{chunk-LZ73Y56P.js.map → chunk-G7LCG56O.js.map} +0 -0
  138. /package/dist/{chunk-E3MRYDUQ.js.map → chunk-GAH5755F.js.map} +0 -0
  139. /package/dist/{chunk-JGPURIEH.js.map → chunk-GPFXPC2V.js.map} +0 -0
  140. /package/dist/{chunk-3ZIKDH6C.js.map → chunk-JBO6UBIE.js.map} +0 -0
  141. /package/dist/{chunk-YNBEHUZH.js.map → chunk-KURUULFF.js.map} +0 -0
  142. /package/dist/{chunk-LQ6JI4VH.js.map → chunk-LD53WPMU.js.map} +0 -0
  143. /package/dist/{chunk-QIRZI3C3.js.map → chunk-OH66EBCS.js.map} +0 -0
  144. /package/dist/{chunk-OZMBT7N3.js.map → chunk-PJQMISPD.js.map} +0 -0
  145. /package/dist/{chunk-PN5AMCK2.js.map → chunk-PYVZKEZZ.js.map} +0 -0
  146. /package/dist/{chunk-DCWIQFNA.js.map → chunk-T422SYM6.js.map} +0 -0
  147. /package/dist/{chunk-5CEJH5ZN.js.map → chunk-TAPSNIMA.js.map} +0 -0
  148. /package/dist/{chunk-3NN73CM7.js.map → chunk-TH7WMHVK.js.map} +0 -0
  149. /package/dist/{chunk-34THARMH.js.map → chunk-THZ4WR5L.js.map} +0 -0
  150. /package/dist/{chunk-E6LENGYV.js.map → chunk-TIFF32QK.js.map} +0 -0
  151. /package/dist/{chunk-RNWWAS5V.js.map → chunk-TJATYB3F.js.map} +0 -0
  152. /package/dist/{chunk-KYHGLDFU.js.map → chunk-TXALJRYO.js.map} +0 -0
  153. /package/dist/{chunk-DKIZ4BUQ.js.map → chunk-U2SMR57B.js.map} +0 -0
  154. /package/dist/{chunk-VVSV2QVY.js.map → chunk-V4C4SFZB.js.map} +0 -0
  155. /package/dist/{chunk-AXBRVBT6.js.map → chunk-VSU2NZGC.js.map} +0 -0
  156. /package/dist/{chunk-N3VE7OCY.js.map → chunk-WS6GLJ7P.js.map} +0 -0
  157. /package/dist/{chunk-ITLJATQI.js.map → chunk-WUVUTOUV.js.map} +0 -0
  158. /package/dist/{chunk-WRFKZEO6.js.map → chunk-YQGHW5ML.js.map} +0 -0
  159. /package/dist/{chunk-243P7YYV.js.map → chunk-YRBXBHU7.js.map} +0 -0
  160. /package/dist/{chunk-ZMHDSIEX.js.map → chunk-ZJMSTFOR.js.map} +0 -0
@@ -0,0 +1,517 @@
1
+ /**
2
+ * Access LCM read-surface (extracted from access-service.ts; god-file
3
+ * decomposition, #1526 playbook: verbatim move + live selfDeps wiring).
4
+ *
5
+ * Owns the LCM archive read path of the access layer: lcmSearch and the
6
+ * read-authorization resolution trio (LCM read namespace, raw-excerpt
7
+ * read namespace, and the ordered read-authorized LCM session-id set,
8
+ * #1495/#1505 read gates).
9
+ */
10
+
11
+ import { resolveNamespaceCapabilities } from "./capabilities.js";
12
+ import { combineNamespaces, lcmSessionKeyForNamespace, resolveCodingNamespaceOverlay } from "./coding/coding-namespace.js";
13
+ import { log } from "./logger.js";
14
+ import { canWriteNamespace, defaultNamespaceForPrincipal, recallNamespacesForPrincipal } from "./namespaces/principal.js";
15
+ import type { Orchestrator } from "./orchestrator.js";
16
+ import {
17
+ EngramAccessInputError,
18
+ type EngramAccessLcmSearchRequest,
19
+ type EngramAccessLcmSearchResponse,
20
+ } from "./access-service.js";
21
+
22
+ export interface AccessLcmSurfaceDeps {
23
+ lcmSessionIdsForNamespaces(namespaces: string[], sessionKey: string): string[];
24
+ readonly orchestrator: Orchestrator;
25
+ resolveImplicitLcmReadFallbackNamespace(
26
+ principal: string | undefined,
27
+ ): string | undefined;
28
+ resolveLcmReadNamespace(
29
+ explicitNamespace: string | undefined,
30
+ resolvedNamespace: string,
31
+ sessionKeyForOverlay: string | undefined,
32
+ authenticatedPrincipal: string | undefined,
33
+ purpose?: "read" | "write",
34
+ ): string;
35
+ resolveLcmReadSessionIds(
36
+ explicitNamespace: string | undefined,
37
+ resolvedNamespace: string,
38
+ sessionKey: string,
39
+ authenticatedPrincipal: string | undefined,
40
+ ): string[];
41
+ resolveLcmReadSessionKey(
42
+ explicitNamespace: string | undefined,
43
+ resolvedNamespace: string,
44
+ sessionKey: string,
45
+ authenticatedPrincipal: string | undefined,
46
+ purpose?: "read" | "write",
47
+ ): string;
48
+ resolveReadableNamespace(namespace: string | undefined, principal?: string): string;
49
+ resolveRequestPrincipal(sessionKey: string | undefined, authenticatedPrincipal?: string): string | undefined;
50
+ resolveScopeProfileLcmReadNamespaces(
51
+ sessionKey: string | undefined,
52
+ authenticatedPrincipal: string | undefined,
53
+ ): string[] | null;
54
+ }
55
+
56
+ export class AccessLcmSurface {
57
+ constructor(
58
+ private readonly deps: AccessLcmSurfaceDeps,
59
+ ) {}
60
+
61
+ async lcmSearch(request: EngramAccessLcmSearchRequest): Promise<EngramAccessLcmSearchResponse> {
62
+ if (!request.query || typeof request.query !== "string" || request.query.trim().length === 0) {
63
+ throw new EngramAccessInputError("query is required and must be a non-empty string");
64
+ }
65
+
66
+ const principal = this.deps.resolveRequestPrincipal(request.sessionKey, request.authenticatedPrincipal);
67
+ const hasExplicitNamespace =
68
+ typeof request.namespace === "string" &&
69
+ request.namespace.trim().length > 0;
70
+ // Resolve the readable base namespace WITHOUT pre-authorizing `default`
71
+ // (#1505 thread NBHWz). An EXPLICIT namespace is still authorized strictly
72
+ // via `resolveReadableNamespace` (explicit reads must pass the ACL — throws
73
+ // on an unreadable explicit namespace). For an IMPLICIT read, derive the
74
+ // fallback from the ALREADY read-authorized recall namespace set instead of
75
+ // read-authorizing `config.defaultNamespace`: under a restrictive `default`
76
+ // READ policy where the principal's self namespace is readable, normal recall
77
+ // still succeeds via `recallNamespacesForPrincipal`, so `lcmSearch` must too
78
+ // (the same defect class the raw-excerpt path fixes). `undefined` ⇒ no
79
+ // readable LCM namespace exists, so return NO rows rather than throwing.
80
+ const profileLcmReadNamespaces = hasExplicitNamespace
81
+ ? null
82
+ : this.deps.resolveScopeProfileLcmReadNamespaces(
83
+ request.sessionKey,
84
+ request.authenticatedPrincipal,
85
+ );
86
+ const namespace = hasExplicitNamespace
87
+ ? this.deps.resolveReadableNamespace(request.namespace, principal)
88
+ : profileLcmReadNamespaces !== null
89
+ ? profileLcmReadNamespaces[0]
90
+ : this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
91
+
92
+ if (!this.deps.orchestrator.lcmEngine || !this.deps.orchestrator.lcmEngine.enabled) {
93
+ return {
94
+ query: request.query,
95
+ namespace: namespace ?? this.deps.orchestrator.config.defaultNamespace,
96
+ results: [],
97
+ count: 0,
98
+ lcmEnabled: false,
99
+ };
100
+ }
101
+
102
+ // An active scope profile with no readable layers is authoritative: search
103
+ // no legacy/default LCM keys instead of falling back around the profile.
104
+ if (profileLcmReadNamespaces !== null && profileLcmReadNamespaces.length === 0) {
105
+ return {
106
+ query: request.query,
107
+ namespace: this.deps.orchestrator.config.defaultNamespace,
108
+ results: [],
109
+ count: 0,
110
+ lcmEnabled: true,
111
+ };
112
+ }
113
+
114
+ // No readable LCM namespace for an IMPLICIT read (restrictive `default` READ
115
+ // policy, no readable overlay/self) ⇒ return NO rows instead of pre-
116
+ // authorizing the denied default (#1505 thread NBHWz). Normal recall still
117
+ // succeeds through the readable self namespace; LCM search degrades to empty.
118
+ if (namespace === undefined) {
119
+ return {
120
+ query: request.query,
121
+ namespace: this.deps.orchestrator.config.defaultNamespace,
122
+ results: [],
123
+ count: 0,
124
+ lcmEnabled: true,
125
+ };
126
+ }
127
+
128
+ const limit = Math.max(1, Math.min(request.limit ?? 10, 100));
129
+ // Route the LCM read session_id AND prefix through the SAME overlay-aware
130
+ // namespace `observe`'s write key and compaction use (#1505 round 3). A
131
+ // project-scoped `observe` with no explicit namespace archived under the
132
+ // coding-overlay namespace; deriving the prefix only from the readable
133
+ // `namespace` (as before) would search the raw key and miss those turns.
134
+ // The effective namespace is resolved ONCE from the real session
135
+ // (`request.sessionKey`) — the prefix is a search fragment with no bound
136
+ // coding context, so it inherits the same namespace. Collapses to the raw key
137
+ // for single-store / no-overlay / explicit-default flows (existing behavior).
138
+ const lcmReadNamespace = profileLcmReadNamespaces !== null
139
+ ? profileLcmReadNamespaces[0] ?? this.deps.orchestrator.config.defaultNamespace
140
+ : this.deps.resolveLcmReadNamespace(
141
+ request.namespace,
142
+ namespace,
143
+ request.sessionKey,
144
+ request.authenticatedPrincipal,
145
+ );
146
+ // Ordered, read-authorized LCM read key SET for a concrete `sessionKey`
147
+ // (#1505 fallback unification + #1501 scope profiles). A branch-scoped
148
+ // session whose rows were archived at project/root scope is found by querying
149
+ // the primary overlay key first, then each fallback. When a scope profile is
150
+ // active, the profile's expanded read namespace set is authoritative,
151
+ // including the empty set.
152
+ const lcmSessionKeyIds = request.sessionKey
153
+ ? profileLcmReadNamespaces !== null
154
+ ? this.deps.lcmSessionIdsForNamespaces(
155
+ profileLcmReadNamespaces,
156
+ request.sessionKey,
157
+ )
158
+ : this.deps.resolveLcmReadSessionIds(
159
+ request.namespace,
160
+ namespace,
161
+ request.sessionKey,
162
+ request.authenticatedPrincipal,
163
+ )
164
+ : [undefined];
165
+ const lcmSessionPrefixes = request.sessionPrefix
166
+ ? profileLcmReadNamespaces !== null && !request.sessionKey
167
+ ? this.deps.lcmSessionIdsForNamespaces(
168
+ profileLcmReadNamespaces,
169
+ request.sessionPrefix,
170
+ )
171
+ : [
172
+ lcmSessionKeyForNamespace(
173
+ lcmReadNamespace,
174
+ request.sessionPrefix,
175
+ this.deps.orchestrator.config.defaultNamespace,
176
+ ) ?? request.sessionPrefix,
177
+ ]
178
+ : [undefined];
179
+ // SECURITY (#1495 P1 + codex P1 r2 "Require a scoped LCM filter before
180
+ // archive searches"): a sessionless, prefixless `lcmSearch` issues
181
+ // `searchContextFull(query, limit, undefined, undefined)`, an archive-wide
182
+ // FTS scan over EVERY `session_id`, including the sentinel-framed
183
+ // `<ns>`-scoped overlay/tenant rows. The LCM archive is keyed by the
184
+ // `session_id` STRING and is NOT partitioned by namespace, so an unscoped
185
+ // scan CANNOT be constrained to the caller's authorized namespace — neither
186
+ // an explicit `namespace` nor a readable `default` confines its results to
187
+ // rows the caller may read. The scan is therefore safe ONLY in single-store
188
+ // mode (namespaces disabled, one shared archive owned by the caller). When
189
+ // namespaces are ENABLED, an unscoped `lcmSearch` (no `sessionKey` AND no
190
+ // `sessionPrefix`) must be SUPPRESSED — return EMPTY — regardless of an
191
+ // explicit namespace or default-readability, so a caller authorized for
192
+ // `default` (or for one explicit namespace) cannot read other namespaces'
193
+ // transcript rows via the archive-wide scan (cross-tenant read leak). A
194
+ // SCOPED call (sessionKey or sessionPrefix present) is unaffected: it carries
195
+ // a namespace-framed `session_id` / prefix filter that already constrains the
196
+ // search to the caller's authorized, read-gated namespace.
197
+ const hasScopedSession =
198
+ (typeof request.sessionKey === "string" &&
199
+ request.sessionKey.length > 0) ||
200
+ lcmSessionPrefixes.some((prefix) => typeof prefix === "string" && prefix.length > 0);
201
+ if (!hasScopedSession && resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true) {
202
+ return {
203
+ query: request.query,
204
+ namespace,
205
+ results: [],
206
+ count: 0,
207
+ lcmEnabled: true,
208
+ };
209
+ }
210
+ // Query each LCM read key in order, merging + deduping rows (by
211
+ // sessionId+turnIndex) and preserving first-seen order, capped at `limit`.
212
+ // Use allSettled so one corrupt/failed namespace key cannot discard sibling
213
+ // results from other authorized profile namespaces.
214
+ const seenRows = new Set<string>();
215
+ const results: Array<{ sessionId: string; content: string; turnIndex: number }> = [];
216
+ const lcmSearches: Array<{
217
+ key: string | undefined;
218
+ prefix: string | undefined;
219
+ promise: Promise<Array<{ session_id: string; content: string; turn_index: number }>>;
220
+ }> = [];
221
+ for (const lcmSessionKey of lcmSessionKeyIds) {
222
+ for (const lcmSessionPrefix of lcmSessionPrefixes) {
223
+ lcmSearches.push({
224
+ key: lcmSessionKey,
225
+ prefix: lcmSessionPrefix,
226
+ promise: this.deps.orchestrator.lcmEngine.searchContextFull(
227
+ request.query,
228
+ limit,
229
+ lcmSessionKey,
230
+ lcmSessionPrefix,
231
+ ) as Promise<Array<{ session_id: string; content: string; turn_index: number }>>,
232
+ });
233
+ }
234
+ }
235
+ const settledSearches = await Promise.allSettled(
236
+ lcmSearches.map((search) => search.promise),
237
+ );
238
+ for (let i = 0; i < settledSearches.length; i += 1) {
239
+ if (results.length >= limit) break;
240
+ const settled = settledSearches[i];
241
+ if (!settled || settled.status === "rejected") {
242
+ const failed = lcmSearches[i];
243
+ log.warn(
244
+ `lcmSearch: failed for key=${failed?.key ?? "<none>"} prefix=${failed?.prefix ?? "<none>"}: ${settled?.status === "rejected" ? settled.reason : "missing result"}`,
245
+ );
246
+ continue;
247
+ }
248
+ for (const r of settled.value) {
249
+ const dedupeKey = `${r.session_id}\0${r.turn_index}`;
250
+ if (seenRows.has(dedupeKey)) continue;
251
+ seenRows.add(dedupeKey);
252
+ results.push({
253
+ sessionId: r.session_id,
254
+ content: r.content,
255
+ turnIndex: r.turn_index,
256
+ });
257
+ if (results.length >= limit) break;
258
+ }
259
+ }
260
+
261
+ return {
262
+ query: request.query,
263
+ namespace,
264
+ results,
265
+ count: results.length,
266
+ lcmEnabled: true,
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Resolve the effective LCM NAMESPACE a same-session operation must prefix
272
+ * with (the namespace half of {@link resolveLcmReadSessionKey}). Split out so
273
+ * `lcmSearch` can apply ONE namespace to BOTH its `sessionKey` and its
274
+ * `sessionPrefix` — the prefix is a search fragment, not a real session, so its
275
+ * own coding context can't be looked up; it must inherit the namespace resolved
276
+ * from the real session (`sessionKeyForOverlay`).
277
+ *
278
+ * `purpose` selects the AUTHORIZATION gate applied before honouring the
279
+ * coding overlay (#1505 round 3 + round 4, codex P2):
280
+ *
281
+ * - `"read"` (`lcmSearch` / raw-excerpt recall): the overlay rows are only
282
+ * visible when the principal SELF base is in the READABLE RECALL SET — the
283
+ * same gate the orchestrator's `lcmReadNamespaceForSession` and the recall
284
+ * namespace set use (`recallNamespacesForPrincipal`, gated by both
285
+ * `defaultRecallNamespaces.includes("self")` AND `canReadNamespace`). A
286
+ * caller that passed the default read check must NOT receive
287
+ * `<principal>-project-*` rows the policy never granted (cross-tenant read
288
+ * leak). When the self base is not readable, keep the just-authorized
289
+ * namespace (collapses to the raw key on the default store).
290
+ *
291
+ * - `"write"` (`lcmCompactionFlush` / `lcmCompactionRecord`): these are
292
+ * write/maintenance operations on the SAME queue `observe` just wrote, so
293
+ * the gate must mirror observe's WRITE authorization (`canWriteNamespace`
294
+ * on the self base), NOT readability. A principal that can WRITE but not
295
+ * READ its self namespace (or whose `defaultRecallNamespaces` omits `self`)
296
+ * archived under the overlay key via `observe`; gating compaction by
297
+ * readability would fall back to the default/raw key and leave that queue
298
+ * never flushed/recorded (round-4 codex P2). Write-authorized ⇒ overlay
299
+ * key, matching the observe write key (rule 42 read/write parity; rule 39
300
+ * identical gates across paths).
301
+ */
302
+ resolveLcmReadNamespace(
303
+ explicitNamespace: string | undefined,
304
+ resolvedNamespace: string,
305
+ sessionKeyForOverlay: string | undefined,
306
+ authenticatedPrincipal: string | undefined,
307
+ purpose: "read" | "write" = "read",
308
+ ): string {
309
+ const hasExplicitNamespace =
310
+ typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
311
+ if (hasExplicitNamespace) return resolvedNamespace;
312
+ // Mirror observe's write resolution: use the coding-overlay namespace when
313
+ // one applies, else the default store. NOT the principal self base — an
314
+ // unqualified observe archives under the default store, so a self-base prefix
315
+ // here would target a queue observe never wrote to (#1495).
316
+ const principal = this.deps.resolveRequestPrincipal(
317
+ sessionKeyForOverlay,
318
+ authenticatedPrincipal,
319
+ );
320
+ const base = defaultNamespaceForPrincipal(principal, this.deps.orchestrator.config);
321
+ const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
322
+ sessionKeyForOverlay,
323
+ base,
324
+ );
325
+ // No overlay → the default store (raw sessionKey), as before.
326
+ if (overlaid === base) return this.deps.orchestrator.config.defaultNamespace;
327
+ // Overlay applied. Authorize access to the principal's `<principal>-project-*`
328
+ // overlay base before switching the LCM key to it. The gate differs by
329
+ // operation purpose (see the doc comment): reads use the readable-recall-set
330
+ // gate (no cross-tenant read leak), writes use observe's write authorization
331
+ // (so compaction targets the same overlay queue observe wrote to).
332
+ const authorized =
333
+ purpose === "write"
334
+ ? canWriteNamespace(principal, base, this.deps.orchestrator.config)
335
+ : recallNamespacesForPrincipal(
336
+ principal,
337
+ this.deps.orchestrator.config,
338
+ ).includes(base);
339
+ if (authorized) return overlaid;
340
+ // Unauthorized overlay base. For READS, collapse to the DEFAULT STORE (the
341
+ // raw sessionKey) EXACTLY like the orchestrator's `lcmReadNamespaceForSession`
342
+ // (rule 39 / 42) — NOT the caller's `resolvedNamespace`, which for an implicit
343
+ // read can be a readable recall namespace (e.g. `shared`). Returning that
344
+ // would prefix LCM reads with `shared:sessionKey` while in-prompt recall uses
345
+ // the raw `sessionKey`, diverging `lcmSearch`/raw disclosure from orchestrator
346
+ // LCM reads (cursor "LCM read gate wrong fallback"). For an explicit read the
347
+ // method already returned at the top, so this only affects implicit reads.
348
+ // The (currently unused) write purpose preserves its prior `resolvedNamespace`
349
+ // fallback for backward compatibility.
350
+ if (purpose === "read") return this.deps.orchestrator.config.defaultNamespace;
351
+ return resolvedNamespace;
352
+ }
353
+
354
+ /**
355
+ * Resolve the namespace the raw-disclosure excerpt lookup
356
+ * ({@link fetchRawExcerpts}) must prefix its LCM `session_id` with (#1505
357
+ * thread 2f7). Raw disclosure reads the SAME LCM archive `lcmSearch` and the
358
+ * in-prompt LCM sections read, so it MUST pass through the identical
359
+ * read-authorization gate — NOT `snapshot.namespace`, which records the
360
+ * effective WRITE/overlay namespace (`<principal>-project-*`) even when the
361
+ * principal can WRITE but not READ its self base (or `defaultRecallNamespaces`
362
+ * omits `self`). Routing through `resolveLcmReadNamespace(..., "read")` makes
363
+ * raw disclosure fall back to the default store exactly like normal recall +
364
+ * `lcmSearch`, so it never attaches overlay transcript rows the read gate
365
+ * excludes (cross-tenant read leak). Collapses to the default store / raw
366
+ * sessionKey for single-store / no-overlay / explicit-default flows, so
367
+ * single-user recall is byte-for-byte unchanged.
368
+ *
369
+ * Returns `undefined` when NO readable LCM namespace exists for an IMPLICIT
370
+ * (no explicit `namespace`) raw recall — i.e. a restrictive `default` READ
371
+ * policy denies the principal `default` AND no overlay/self namespace is
372
+ * readable. In that case the caller emits NO excerpts rather than throwing
373
+ * `namespace is not readable: default` (#1505 thread NBHWz): normal recall
374
+ * still succeeds via `recallNamespacesForPrincipal`, so `disclosure: "raw"`
375
+ * must degrade gracefully (empty excerpts), never pre-authorize `default`.
376
+ *
377
+ * IMPLICIT-namespace fallback selection derives from the ALREADY
378
+ * read-authorized recall namespace set (`recallNamespacesForPrincipal` +
379
+ * `canReadNamespace`) — the principal's self base when it is in the readable
380
+ * recall set, else `config.defaultNamespace` ONLY when the principal may read
381
+ * it. It NEVER pre-authorizes `default`. An EXPLICIT `namespace` is still
382
+ * authorized strictly via `resolveReadableNamespace` (explicit reads must pass
383
+ * the ACL — no behavior change).
384
+ */
385
+ resolveRawExcerptReadNamespace(
386
+ explicitNamespace: string | undefined,
387
+ sessionKey: string | undefined,
388
+ authenticatedPrincipal: string | undefined,
389
+ ): string | undefined {
390
+ const principal = this.deps.resolveRequestPrincipal(
391
+ sessionKey,
392
+ authenticatedPrincipal,
393
+ );
394
+ const hasExplicitNamespace =
395
+ typeof explicitNamespace === "string" &&
396
+ explicitNamespace.trim().length > 0;
397
+ if (hasExplicitNamespace) {
398
+ // Explicit reads must pass the ACL — authorize strictly, exactly as
399
+ // `lcmSearch` does (throws on an unreadable explicit namespace).
400
+ const resolvedNamespace = this.deps.resolveReadableNamespace(
401
+ explicitNamespace,
402
+ principal,
403
+ );
404
+ return this.deps.resolveLcmReadNamespace(
405
+ explicitNamespace,
406
+ resolvedNamespace,
407
+ sessionKey,
408
+ authenticatedPrincipal,
409
+ "read",
410
+ );
411
+ }
412
+ // IMPLICIT raw recall: an active scope profile owns the same LCM read
413
+ // namespace set used by recall and lcmSearch. Return the first profile
414
+ // namespace as the legacy raw-excerpt namespace hint; the concrete ordered
415
+ // key set is still produced by resolveLcmReadSessionIds(), which also treats
416
+ // an empty profile read set as authoritative.
417
+ const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
418
+ sessionKey,
419
+ authenticatedPrincipal,
420
+ );
421
+ if (profileReadNamespaces !== null) return profileReadNamespaces[0];
422
+
423
+ // Otherwise derive the read fallback from the ALREADY read-authorized recall
424
+ // namespace set — NEVER pre-authorize `default` (#1505 thread NBHWz). When
425
+ // namespaces are disabled the default store is the only namespace and is
426
+ // always readable (byte-for-byte single-user path).
427
+ const fallbackNamespace =
428
+ this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
429
+ // No readable LCM namespace at all ⇒ no excerpts (caller short-circuits).
430
+ if (fallbackNamespace === undefined) return undefined;
431
+ return this.deps.resolveLcmReadNamespace(
432
+ explicitNamespace,
433
+ fallbackNamespace,
434
+ sessionKey,
435
+ authenticatedPrincipal,
436
+ "read",
437
+ );
438
+ }
439
+
440
+ resolveLcmReadSessionIds(
441
+ explicitNamespace: string | undefined,
442
+ resolvedNamespace: string,
443
+ sessionKey: string,
444
+ authenticatedPrincipal: string | undefined,
445
+ ): string[] {
446
+ const primary = this.deps.resolveLcmReadSessionKey(
447
+ explicitNamespace,
448
+ resolvedNamespace,
449
+ sessionKey,
450
+ authenticatedPrincipal,
451
+ "read",
452
+ );
453
+ const hasExplicitNamespace =
454
+ typeof explicitNamespace === "string" &&
455
+ explicitNamespace.trim().length > 0;
456
+ // Explicit namespace → no overlay fallbacks (the overlay never applies to an
457
+ // explicit read). Single key, unchanged.
458
+ if (hasExplicitNamespace) return [primary];
459
+
460
+ const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
461
+ sessionKey,
462
+ authenticatedPrincipal,
463
+ );
464
+ if (profileReadNamespaces !== null) {
465
+ return this.deps.lcmSessionIdsForNamespaces(profileReadNamespaces, sessionKey);
466
+ }
467
+
468
+ const principal = this.deps.resolveRequestPrincipal(
469
+ sessionKey,
470
+ authenticatedPrincipal,
471
+ );
472
+ const base = defaultNamespaceForPrincipal(
473
+ principal,
474
+ this.deps.orchestrator.config,
475
+ );
476
+ const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
477
+ sessionKey,
478
+ base,
479
+ );
480
+ // No overlay → single default-store key, unchanged.
481
+ if (overlaid === base) return [primary];
482
+ // Overlay present but self base unreadable → the "read" gate already
483
+ // collapsed `primary` to the default store; do NOT add overlay fallbacks
484
+ // (they would be unauthorized `<principal>-project-*` keys). Single key.
485
+ const selfReadableInRecall = recallNamespacesForPrincipal(
486
+ principal,
487
+ this.deps.orchestrator.config,
488
+ ).includes(base);
489
+ if (!selfReadableInRecall) return [primary];
490
+ // Self base readable → overlay rows authorized. Append one LCM key per coding
491
+ // read fallback (project → root), combined with the principal base for
492
+ // isolation — the SAME ordered set the orchestrator recall path searches.
493
+ const overlay = resolveCodingNamespaceOverlay(
494
+ this.deps.orchestrator.getCodingContextForSession(sessionKey),
495
+ this.deps.orchestrator.config.codingMode,
496
+ this.deps.orchestrator.config.defaultNamespace,
497
+ );
498
+ const fallbackNamespaces = (overlay?.readFallbacks ?? []).map((fallback) =>
499
+ combineNamespaces(base, fallback),
500
+ );
501
+ const out = [primary];
502
+ const seen = new Set<string>([primary]);
503
+ for (const ns of fallbackNamespaces) {
504
+ const key =
505
+ lcmSessionKeyForNamespace(
506
+ ns,
507
+ sessionKey,
508
+ this.deps.orchestrator.config.defaultNamespace,
509
+ ) ?? sessionKey;
510
+ if (!seen.has(key)) {
511
+ seen.add(key);
512
+ out.push(key);
513
+ }
514
+ }
515
+ return out;
516
+ }
517
+ }