@remnic/core 9.3.655 → 9.3.656

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 (247) hide show
  1. package/dist/access-cli.js +22 -22
  2. package/dist/access-http.d.ts +4 -4
  3. package/dist/access-http.js +10 -10
  4. package/dist/access-mcp.d.ts +4 -4
  5. package/dist/access-mcp.js +9 -9
  6. package/dist/access-schema.d.ts +10 -10
  7. package/dist/{access-service-BEJvriUt.d.ts → access-service-D_nbpexW.d.ts} +33 -2
  8. package/dist/access-service.d.ts +4 -4
  9. package/dist/access-service.js +8 -8
  10. package/dist/action-confidence.d.ts +1 -1
  11. package/dist/active-memory-bridge.d.ts +1 -1
  12. package/dist/active-recall.d.ts +1 -1
  13. package/dist/active-recall.js +1 -1
  14. package/dist/behavior-learner.d.ts +1 -1
  15. package/dist/behavior-signals.d.ts +1 -1
  16. package/dist/bootstrap.d.ts +3 -3
  17. package/dist/briefing.d.ts +1 -1
  18. package/dist/briefing.js +3 -3
  19. package/dist/buffer-surprise-report.d.ts +1 -1
  20. package/dist/buffer.d.ts +1 -1
  21. package/dist/calibration.d.ts +1 -1
  22. package/dist/causal-behavior.d.ts +1 -1
  23. package/dist/causal-consolidation.d.ts +1 -1
  24. package/dist/causal-consolidation.js +4 -4
  25. package/dist/{chunk-PVE7KSQP.js → chunk-2BD7DG37.js} +2 -2
  26. package/dist/{chunk-54LOUIBE.js → chunk-2MXEVL75.js} +2 -2
  27. package/dist/{chunk-55ZMNKMQ.js → chunk-4UL7VPTD.js} +276 -57
  28. package/dist/chunk-4UL7VPTD.js.map +1 -0
  29. package/dist/{chunk-COVZLGMR.js → chunk-54XF2FY7.js} +17 -17
  30. package/dist/{chunk-UYNFWZWG.js → chunk-AGJKWOKV.js} +2 -2
  31. package/dist/{chunk-TDZSSJV4.js → chunk-AZBV4RRY.js} +1 -1
  32. package/dist/chunk-AZBV4RRY.js.map +1 -0
  33. package/dist/{chunk-KOI765XP.js → chunk-CTAV55JM.js} +241 -1
  34. package/dist/chunk-CTAV55JM.js.map +1 -0
  35. package/dist/{chunk-A3Y37UWI.js → chunk-DIBWFCLA.js} +3 -3
  36. package/dist/{chunk-QDVQ4AN2.js → chunk-DR67OK4E.js} +5 -5
  37. package/dist/{chunk-XBIACVCO.js → chunk-EC2AYKRX.js} +2 -2
  38. package/dist/{chunk-IQ53ZSXV.js → chunk-GCYFUTUC.js} +2 -2
  39. package/dist/{chunk-YYN3LIYA.js → chunk-GSHW5VVD.js} +5 -5
  40. package/dist/chunk-GYSYLGNE.js +650 -0
  41. package/dist/chunk-GYSYLGNE.js.map +1 -0
  42. package/dist/{chunk-NRBGRZW4.js → chunk-IOZ5WBWD.js} +2 -2
  43. package/dist/{chunk-NCSJKK23.js → chunk-JSVFEHLL.js} +7 -5
  44. package/dist/chunk-JSVFEHLL.js.map +1 -0
  45. package/dist/{chunk-7LWRCOP7.js → chunk-LZTFCAKE.js} +2 -2
  46. package/dist/{chunk-TEO46GMM.js → chunk-NXCK7DO7.js} +2 -2
  47. package/dist/{chunk-XOFXKASO.js → chunk-PEPHBH2W.js} +2 -2
  48. package/dist/{chunk-WDTUYOLS.js → chunk-QZRKNA5F.js} +2 -2
  49. package/dist/{chunk-PS3SYNHP.js → chunk-R5DB26G6.js} +2 -2
  50. package/dist/{chunk-5QD3QD76.js → chunk-RDW5G6DO.js} +659 -123
  51. package/dist/chunk-RDW5G6DO.js.map +1 -0
  52. package/dist/{chunk-BGKXTVNG.js → chunk-SWDHVH2P.js} +2 -2
  53. package/dist/{chunk-67G4T7KI.js → chunk-SXYCVRLK.js} +3 -3
  54. package/dist/{chunk-UCEABZZN.js → chunk-TFFZUFEP.js} +7 -5
  55. package/dist/chunk-TFFZUFEP.js.map +1 -0
  56. package/dist/{chunk-UCEDY5M7.js → chunk-TIJYQXDI.js} +2 -2
  57. package/dist/{chunk-2RCGZ67B.js → chunk-VAEAGTEQ.js} +3 -3
  58. package/dist/{chunk-XRKQOQLY.js → chunk-WIKMCJUR.js} +2 -2
  59. package/dist/{chunk-KZZ4YAEC.js → chunk-WWMHAMAY.js} +2 -2
  60. package/dist/{chunk-OKW6F5S5.js → chunk-YEZHZCUO.js} +4 -4
  61. package/dist/{chunk-5FOCXX5E.js → chunk-YVVQUAOO.js} +3 -3
  62. package/dist/{chunk-5FOCXX5E.js.map → chunk-YVVQUAOO.js.map} +1 -1
  63. package/dist/{chunk-3XGWCZ63.js → chunk-YXLT4EMM.js} +2 -2
  64. package/dist/{chunk-PTMJ2FH2.js → chunk-Z6UDTNY6.js} +2 -2
  65. package/dist/{cli-BGahB_d3.d.ts → cli-aYxSuPvP.d.ts} +3 -3
  66. package/dist/cli.d.ts +5 -5
  67. package/dist/cli.js +22 -22
  68. package/dist/compounding/engine.d.ts +1 -1
  69. package/dist/compounding/engine.js +3 -3
  70. package/dist/compounding/preference-consolidator.d.ts +1 -1
  71. package/dist/compression-optimizer.d.ts +1 -1
  72. package/dist/config.d.ts +1 -1
  73. package/dist/config.js +1 -1
  74. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  75. package/dist/connectors/codex-materialize-runner.js +3 -3
  76. package/dist/connectors/codex-materialize.d.ts +1 -1
  77. package/dist/connectors/index.d.ts +1 -1
  78. package/dist/connectors/index.js +3 -3
  79. package/dist/consolidation-provenance-check.d.ts +1 -1
  80. package/dist/consolidation-undo.d.ts +1 -1
  81. package/dist/contradiction/index.d.ts +1 -1
  82. package/dist/conversation-index/backend.d.ts +1 -1
  83. package/dist/conversation-index/chunker.d.ts +1 -1
  84. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  85. package/dist/conversation-index/indexer.d.ts +1 -1
  86. package/dist/conversation-index/search.d.ts +1 -1
  87. package/dist/day-summary.d.ts +1 -1
  88. package/dist/delinearize.d.ts +1 -1
  89. package/dist/direct-answer-wiring.d.ts +1 -1
  90. package/dist/direct-answer.d.ts +1 -1
  91. package/dist/embedding-fallback.d.ts +1 -1
  92. package/dist/enrichment/index.d.ts +1 -1
  93. package/dist/entity-retrieval.d.ts +1 -1
  94. package/dist/entity-retrieval.js +3 -3
  95. package/dist/entity-schema.d.ts +1 -1
  96. package/dist/explicit-capture.d.ts +3 -3
  97. package/dist/explicit-cue-recall.js +2 -2
  98. package/dist/extraction-judge-telemetry.d.ts +1 -1
  99. package/dist/extraction-judge-training.d.ts +1 -1
  100. package/dist/extraction-judge.d.ts +1 -1
  101. package/dist/extraction.d.ts +1 -1
  102. package/dist/fallback-llm.d.ts +1 -1
  103. package/dist/focused-list-recall.js +2 -2
  104. package/dist/identity-continuity.d.ts +1 -1
  105. package/dist/importance.d.ts +1 -1
  106. package/dist/index.d.ts +121 -121
  107. package/dist/index.js +32 -32
  108. package/dist/intent.d.ts +1 -1
  109. package/dist/lcm/engine.d.ts +1 -1
  110. package/dist/lcm/index.d.ts +1 -1
  111. package/dist/lcm/tools.d.ts +1 -1
  112. package/dist/lcm-fallback-read.js +1 -1
  113. package/dist/lifecycle.d.ts +1 -1
  114. package/dist/live-connectors-runner.d.ts +1 -1
  115. package/dist/local-llm.d.ts +1 -1
  116. package/dist/maintenance/memory-governance.d.ts +1 -1
  117. package/dist/maintenance/memory-governance.js +3 -3
  118. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  119. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  120. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  121. package/dist/memory-action-policy.d.ts +1 -1
  122. package/dist/memory-cache.d.ts +1 -1
  123. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  124. package/dist/memory-projection-store.d.ts +1 -1
  125. package/dist/memory-provenance.d.ts +1 -1
  126. package/dist/memory-worth-outcomes.d.ts +1 -1
  127. package/dist/models-json.d.ts +1 -1
  128. package/dist/namespaces/migrate.d.ts +1 -1
  129. package/dist/namespaces/migrate.js +4 -4
  130. package/dist/namespaces/principal.d.ts +1 -1
  131. package/dist/namespaces/search.d.ts +1 -1
  132. package/dist/namespaces/storage.d.ts +1 -1
  133. package/dist/namespaces/storage.js +3 -3
  134. package/dist/native-knowledge.d.ts +1 -1
  135. package/dist/operator-toolkit.d.ts +1 -1
  136. package/dist/operator-toolkit.js +7 -7
  137. package/dist/{orchestrator-BgzZlWxH.d.ts → orchestrator-D1wcmPNj.d.ts} +8 -2
  138. package/dist/orchestrator.d.ts +3 -3
  139. package/dist/orchestrator.js +18 -18
  140. package/dist/patterns-cli.d.ts +1 -1
  141. package/dist/policy-runtime.d.ts +1 -1
  142. package/dist/qmd-recall-cache.d.ts +1 -1
  143. package/dist/qmd.d.ts +1 -1
  144. package/dist/recall-disclosure-escalation.d.ts +1 -1
  145. package/dist/recall-explain-renderer.d.ts +1 -1
  146. package/dist/recall-explain-renderer.js +3 -3
  147. package/dist/recall-planner-llm.d.ts +1 -1
  148. package/dist/recall-state.d.ts +1 -1
  149. package/dist/recall-tag-filter.d.ts +1 -1
  150. package/dist/recall-xray-cli.d.ts +1 -1
  151. package/dist/recall-xray-cli.js +4 -4
  152. package/dist/recall-xray-renderer.d.ts +1 -1
  153. package/dist/recall-xray-renderer.js +3 -3
  154. package/dist/recall-xray.d.ts +1 -1
  155. package/dist/recall-xray.js +2 -2
  156. package/dist/resolve-auth-token.d.ts +1 -1
  157. package/dist/response-guidance-recall.js +2 -2
  158. package/dist/resume-bundles.js +2 -2
  159. package/dist/retrieval-agents.d.ts +1 -1
  160. package/dist/retrieval-tiers.d.ts +1 -1
  161. package/dist/routing/engine.d.ts +1 -1
  162. package/dist/routing/store.d.ts +1 -1
  163. package/dist/search/embed-helper.d.ts +1 -1
  164. package/dist/search/factory.d.ts +1 -1
  165. package/dist/search/index.d.ts +1 -1
  166. package/dist/search/lancedb-backend.d.ts +1 -1
  167. package/dist/search/meilisearch-backend.d.ts +1 -1
  168. package/dist/search/noop-backend.d.ts +1 -1
  169. package/dist/search/orama-backend.d.ts +1 -1
  170. package/dist/search/port.d.ts +1 -1
  171. package/dist/search/remote-backend.d.ts +1 -1
  172. package/dist/{semantic-consolidation-Z8d_uMq8.d.ts → semantic-consolidation-MWOdNtSE.d.ts} +1 -1
  173. package/dist/semantic-consolidation.d.ts +2 -2
  174. package/dist/semantic-consolidation.js +4 -4
  175. package/dist/semantic-rule-promotion.js +3 -3
  176. package/dist/semantic-rule-verifier.d.ts +3 -2
  177. package/dist/semantic-rule-verifier.js +5 -3
  178. package/dist/session-observer-bands.d.ts +1 -1
  179. package/dist/session-observer-state.d.ts +1 -1
  180. package/dist/shared-context/manager.d.ts +1 -1
  181. package/dist/signal.d.ts +1 -1
  182. package/dist/storage.d.ts +1 -1
  183. package/dist/storage.js +2 -2
  184. package/dist/summarizer.d.ts +1 -1
  185. package/dist/summary-snapshot.d.ts +1 -1
  186. package/dist/targeted-fact-recall.js +2 -2
  187. package/dist/temporal-supersession.d.ts +1 -1
  188. package/dist/temporal-validity.d.ts +1 -1
  189. package/dist/threading.d.ts +1 -1
  190. package/dist/tier-migration.d.ts +1 -1
  191. package/dist/tier-routing.d.ts +1 -1
  192. package/dist/topics.d.ts +1 -1
  193. package/dist/transcript.d.ts +1 -1
  194. package/dist/{types-2OPlQWJG.d.ts → types-CgcCpUrf.d.ts} +39 -1
  195. package/dist/types.d.ts +1 -1
  196. package/dist/types.js +1 -1
  197. package/dist/utility-runtime.d.ts +1 -1
  198. package/dist/verified-recall.d.ts +2 -1
  199. package/dist/verified-recall.js +5 -3
  200. package/package.json +1 -1
  201. package/src/access-service-observe-lcm-parity.test.ts +86 -1
  202. package/src/access-service-observe-scope.test.ts +283 -1
  203. package/src/access-service-raw-excerpt-read-gate.test.ts +53 -0
  204. package/src/access-service.ts +391 -93
  205. package/src/coding/coding-namespace.ts +0 -3
  206. package/src/config.ts +282 -0
  207. package/src/lcm-fallback-read.ts +2 -6
  208. package/src/namespaces/scope-profiles.test.ts +1074 -0
  209. package/src/namespaces/scope-profiles.ts +456 -0
  210. package/src/orchestrator-flush.test.ts +142 -0
  211. package/src/orchestrator-source-attribution.test.ts +73 -0
  212. package/src/orchestrator.ts +835 -163
  213. package/src/semantic-rule-verifier.ts +13 -6
  214. package/src/types.ts +52 -0
  215. package/src/verified-recall.ts +10 -6
  216. package/dist/chunk-55ZMNKMQ.js.map +0 -1
  217. package/dist/chunk-5QD3QD76.js.map +0 -1
  218. package/dist/chunk-KOI765XP.js.map +0 -1
  219. package/dist/chunk-MMJANTJX.js +0 -339
  220. package/dist/chunk-MMJANTJX.js.map +0 -1
  221. package/dist/chunk-NCSJKK23.js.map +0 -1
  222. package/dist/chunk-TDZSSJV4.js.map +0 -1
  223. package/dist/chunk-UCEABZZN.js.map +0 -1
  224. /package/dist/{chunk-PVE7KSQP.js.map → chunk-2BD7DG37.js.map} +0 -0
  225. /package/dist/{chunk-54LOUIBE.js.map → chunk-2MXEVL75.js.map} +0 -0
  226. /package/dist/{chunk-COVZLGMR.js.map → chunk-54XF2FY7.js.map} +0 -0
  227. /package/dist/{chunk-UYNFWZWG.js.map → chunk-AGJKWOKV.js.map} +0 -0
  228. /package/dist/{chunk-A3Y37UWI.js.map → chunk-DIBWFCLA.js.map} +0 -0
  229. /package/dist/{chunk-QDVQ4AN2.js.map → chunk-DR67OK4E.js.map} +0 -0
  230. /package/dist/{chunk-XBIACVCO.js.map → chunk-EC2AYKRX.js.map} +0 -0
  231. /package/dist/{chunk-IQ53ZSXV.js.map → chunk-GCYFUTUC.js.map} +0 -0
  232. /package/dist/{chunk-YYN3LIYA.js.map → chunk-GSHW5VVD.js.map} +0 -0
  233. /package/dist/{chunk-NRBGRZW4.js.map → chunk-IOZ5WBWD.js.map} +0 -0
  234. /package/dist/{chunk-7LWRCOP7.js.map → chunk-LZTFCAKE.js.map} +0 -0
  235. /package/dist/{chunk-TEO46GMM.js.map → chunk-NXCK7DO7.js.map} +0 -0
  236. /package/dist/{chunk-XOFXKASO.js.map → chunk-PEPHBH2W.js.map} +0 -0
  237. /package/dist/{chunk-WDTUYOLS.js.map → chunk-QZRKNA5F.js.map} +0 -0
  238. /package/dist/{chunk-PS3SYNHP.js.map → chunk-R5DB26G6.js.map} +0 -0
  239. /package/dist/{chunk-BGKXTVNG.js.map → chunk-SWDHVH2P.js.map} +0 -0
  240. /package/dist/{chunk-67G4T7KI.js.map → chunk-SXYCVRLK.js.map} +0 -0
  241. /package/dist/{chunk-UCEDY5M7.js.map → chunk-TIJYQXDI.js.map} +0 -0
  242. /package/dist/{chunk-2RCGZ67B.js.map → chunk-VAEAGTEQ.js.map} +0 -0
  243. /package/dist/{chunk-XRKQOQLY.js.map → chunk-WIKMCJUR.js.map} +0 -0
  244. /package/dist/{chunk-KZZ4YAEC.js.map → chunk-WWMHAMAY.js.map} +0 -0
  245. /package/dist/{chunk-OKW6F5S5.js.map → chunk-YEZHZCUO.js.map} +0 -0
  246. /package/dist/{chunk-3XGWCZ63.js.map → chunk-YXLT4EMM.js.map} +0 -0
  247. /package/dist/{chunk-PTMJ2FH2.js.map → chunk-Z6UDTNY6.js.map} +0 -0
@@ -56,6 +56,12 @@ import {
56
56
  } from "./memory-lifecycle-ledger-utils.js";
57
57
  import { getMemoryProjectionPath } from "./memory-projection-store.js";
58
58
  import { canReadNamespace, canWriteNamespace, defaultNamespaceForPrincipal, recallNamespacesForPrincipal, resolvePrincipal } from "./namespaces/principal.js";
59
+ import {
60
+ expandScopeProfileReadNamespaces,
61
+ resolveScopeProfilePlan,
62
+ type ScopeProfileLayerResolution,
63
+ type ScopeProfilePromotionResolution,
64
+ } from "./namespaces/scope-profiles.js";
59
65
  import { namespaceIdentityFromToken } from "./namespaces/identity.js";
60
66
  import { namespaceCollectionName } from "./namespaces/search.js";
61
67
  import { SecureStoreLockedError } from "./secure-store/index.js";
@@ -888,6 +894,14 @@ export interface MemoryScopePlan {
888
894
  objectiveStateNamespace: string;
889
895
  /** Namespaces a same-session recall would read (cheap subset). */
890
896
  readNamespaces: string[];
897
+ /** Active scope profile id, when `defaultScopeProfile` is configured. */
898
+ scopeProfile?: string;
899
+ /** Resolved profile layer that supplied `writeNamespace`. */
900
+ writeLayer?: string;
901
+ /** Resolved profile layers in the active profile contract. */
902
+ layers?: ScopeProfileLayerResolution[];
903
+ /** Authorized promotion targets from the active profile contract. */
904
+ promotionTargets?: ScopeProfilePromotionResolution[];
891
905
  /** Whether the coding overlay changed the base namespace. */
892
906
  codingOverlayApplied: boolean;
893
907
  /** Non-fatal diagnostics surfaced during resolution. */
@@ -966,6 +980,10 @@ export interface EngramAccessScopeDebug {
966
980
  codingOverlayApplied: boolean;
967
981
  /** Namespaces a same-session recall would read, when cheap to compute. */
968
982
  readNamespaces?: string[];
983
+ scopeProfile?: string;
984
+ writeLayer?: string;
985
+ layers?: ScopeProfileLayerResolution[];
986
+ promotionTargets?: ScopeProfilePromotionResolution[];
969
987
  }
970
988
 
971
989
  export interface EngramAccessObserveResponse {
@@ -1398,17 +1416,45 @@ export class EngramAccessService {
1398
1416
  // `default-project-*` that its own recall never searches (Codex review).
1399
1417
  const hasSession =
1400
1418
  typeof request.sessionKey === "string" && request.sessionKey.length > 0;
1419
+ const codingContext =
1420
+ hasSession &&
1421
+ this.orchestrator.config.namespacesEnabled &&
1422
+ this.orchestrator.config.codingMode?.projectScope
1423
+ ? this.orchestrator.getCodingContextForSession(request.sessionKey) ??
1424
+ (await this.resolveCodingContextFromOptions(request))
1425
+ : null;
1401
1426
  const overlay =
1402
1427
  hasSession &&
1403
1428
  this.orchestrator.config.namespacesEnabled &&
1404
1429
  this.orchestrator.config.codingMode?.projectScope
1405
1430
  ? resolveCodingNamespaceOverlay(
1406
- this.orchestrator.getCodingContextForSession(request.sessionKey) ??
1407
- (await this.resolveCodingContextFromOptions(request)),
1431
+ codingContext,
1408
1432
  this.orchestrator.config.codingMode,
1409
1433
  this.orchestrator.config.defaultNamespace,
1410
1434
  )
1411
1435
  : null;
1436
+ const principal = this.resolveRequestPrincipal(
1437
+ request.sessionKey,
1438
+ request.authenticatedPrincipal,
1439
+ );
1440
+ const profilePlan = resolveScopeProfilePlan({
1441
+ config: this.orchestrator.config,
1442
+ principal,
1443
+ codingContext,
1444
+ codingOverlay: overlay,
1445
+ });
1446
+ if (profilePlan) {
1447
+ const selectedLayer = profilePlan.layers.find((layer) => layer.id === profilePlan.writeLayer);
1448
+ const writeNamespaceReadable =
1449
+ profilePlan.writeNamespace.length > 0 &&
1450
+ profilePlan.readNamespaces.includes(profilePlan.writeNamespace);
1451
+ if (!selectedLayer?.writable || !writeNamespaceReadable) {
1452
+ throw new EngramAccessInputError(
1453
+ `scope profile ${profilePlan.profileId} has no writable layer inside the profile read stack for principal ${principal ?? "anonymous"}`,
1454
+ );
1455
+ }
1456
+ return profilePlan.writeNamespace;
1457
+ }
1412
1458
  if (!overlay) {
1413
1459
  // No coding overlay → unqualified write stays on config.defaultNamespace,
1414
1460
  // exactly the pre-#1434 behavior (auth-checked, like the legacy path).
@@ -1422,10 +1468,6 @@ export class EngramAccessService {
1422
1468
  // recall/observe/buffer-flush use. The result is a principal-owned
1423
1469
  // `project-*` sub-namespace derived from this authorized base, so it needs
1424
1470
  // no separate write policy.
1425
- const principal = this.resolveRequestPrincipal(
1426
- request.sessionKey,
1427
- request.authenticatedPrincipal,
1428
- );
1429
1471
  const base = defaultNamespaceForPrincipal(principal, this.orchestrator.config);
1430
1472
  if (!canWriteNamespace(principal, base, this.orchestrator.config)) {
1431
1473
  throw new EngramAccessInputError(`namespace is not writable: ${base}`);
@@ -1565,6 +1607,66 @@ export class EngramAccessService {
1565
1607
  baseNamespace,
1566
1608
  );
1567
1609
  const codingOverlayApplied = overlaidBase !== baseNamespace;
1610
+ const codingOverlay = overlayEligible
1611
+ ? resolveCodingNamespaceOverlay(
1612
+ attachedContext,
1613
+ this.orchestrator.config.codingMode,
1614
+ this.orchestrator.config.defaultNamespace,
1615
+ )
1616
+ : null;
1617
+ const profilePlan = resolveScopeProfilePlan({
1618
+ config: this.orchestrator.config,
1619
+ principal,
1620
+ codingContext: attachedContext,
1621
+ codingOverlay,
1622
+ });
1623
+ if (profilePlan) {
1624
+ const selectedLayer = profilePlan.layers.find((layer) => layer.id === profilePlan.writeLayer);
1625
+ const writeNamespaceReadable =
1626
+ profilePlan.writeNamespace.length > 0 &&
1627
+ profilePlan.readNamespaces.includes(profilePlan.writeNamespace);
1628
+ if (!selectedLayer?.writable || !writeNamespaceReadable) {
1629
+ clearSeededContext();
1630
+ throw new EngramAccessInputError(
1631
+ `scope profile ${profilePlan.profileId} has no writable layer inside the profile read stack for principal ${principal ?? "anonymous"}`,
1632
+ );
1633
+ }
1634
+ const legacyRecallNamespaces = Array.isArray(this.orchestrator.config.defaultRecallNamespaces)
1635
+ ? recallNamespacesForPrincipal(principal, this.orchestrator.config)
1636
+ : [];
1637
+ const expandedReadNamespaces = expandScopeProfileReadNamespaces({
1638
+ profilePlan,
1639
+ principalSelfNamespace: profilePlan.baseNamespace,
1640
+ config: this.orchestrator.config,
1641
+ principal,
1642
+ codingOverlay,
1643
+ legacyRecallNamespaces,
1644
+ });
1645
+ const readNamespaces = expandedReadNamespaces;
1646
+ const profileCodingOverlayApplied = Boolean(
1647
+ codingOverlay &&
1648
+ profilePlan.layers.some(
1649
+ (layer) =>
1650
+ (layer.id === "userProject" || layer.id === "teamProject") &&
1651
+ layer.readable &&
1652
+ layer.namespace &&
1653
+ readNamespaces.includes(layer.namespace),
1654
+ ),
1655
+ );
1656
+ return {
1657
+ principal,
1658
+ baseNamespace: profilePlan.baseNamespace,
1659
+ writeNamespace: profilePlan.writeNamespace,
1660
+ objectiveStateNamespace: profilePlan.writeNamespace,
1661
+ readNamespaces,
1662
+ scopeProfile: profilePlan.profileId,
1663
+ writeLayer: profilePlan.writeLayer,
1664
+ layers: profilePlan.layers,
1665
+ promotionTargets: profilePlan.promotionTargets,
1666
+ codingOverlayApplied: profileCodingOverlayApplied,
1667
+ warnings: [...warnings, ...profilePlan.warnings],
1668
+ };
1669
+ }
1568
1670
 
1569
1671
  if (!codingOverlayApplied) {
1570
1672
  // No overlay → the LCM/extraction/response write namespace mirrors the
@@ -1640,12 +1742,7 @@ export class EngramAccessService {
1640
1742
  // matches what a same-session recall searches. Resolved through the pure
1641
1743
  // overlay helper to enumerate fallbacks; the write namespace itself already
1642
1744
  // came from `applyCodingNamespaceOverlay` so the two agree.
1643
- const overlay = resolveCodingNamespaceOverlay(
1644
- attachedContext,
1645
- this.orchestrator.config.codingMode,
1646
- this.orchestrator.config.defaultNamespace,
1647
- );
1648
- for (const fallback of overlay?.readFallbacks ?? []) {
1745
+ for (const fallback of codingOverlay?.readFallbacks ?? []) {
1649
1746
  const ns = combineNamespaces(baseNamespace, fallback);
1650
1747
  if (!readNamespaces.includes(ns)) readNamespaces.push(ns);
1651
1748
  }
@@ -1660,6 +1757,19 @@ export class EngramAccessService {
1660
1757
  };
1661
1758
  }
1662
1759
 
1760
+ private legacyResponseNamespaceForScope(scope: MemoryScopePlan): string {
1761
+ if (scope.explicitNamespace) return scope.writeNamespace;
1762
+ // Legacy overlay compatibility only applies to the principal-owned
1763
+ // user-project layer. Hosted profile layers such as teamProject are not the
1764
+ // old overlay response shape; reporting default there hides the real write.
1765
+ if (scope.scopeProfile && scope.writeLayer !== "userProject") {
1766
+ return scope.writeNamespace;
1767
+ }
1768
+ return scope.codingOverlayApplied
1769
+ ? this.orchestrator.config.defaultNamespace
1770
+ : scope.writeNamespace;
1771
+ }
1772
+
1663
1773
  private async objectiveStateStoreLocationForNamespace(namespace: string): Promise<{
1664
1774
  memoryDir: string;
1665
1775
  objectiveStateStoreDir?: string;
@@ -1723,8 +1833,30 @@ export class EngramAccessService {
1723
1833
  );
1724
1834
  }
1725
1835
 
1726
- return recallNamespacesForPrincipal(principal, this.orchestrator.config)
1727
- .filter((ns) => canReadNamespace(principal, ns, this.orchestrator.config));
1836
+ const legacyRecallNamespaces = recallNamespacesForPrincipal(
1837
+ principal,
1838
+ this.orchestrator.config,
1839
+ );
1840
+ const profilePlan = resolveScopeProfilePlan({
1841
+ config: this.orchestrator.config,
1842
+ principal,
1843
+ codingContext: null,
1844
+ codingOverlay: null,
1845
+ });
1846
+ const namespaces = profilePlan
1847
+ ? expandScopeProfileReadNamespaces({
1848
+ profilePlan,
1849
+ principalSelfNamespace: profilePlan.baseNamespace,
1850
+ config: this.orchestrator.config,
1851
+ principal,
1852
+ codingOverlay: null,
1853
+ legacyRecallNamespaces,
1854
+ })
1855
+ : legacyRecallNamespaces;
1856
+ if (profilePlan) return namespaces;
1857
+ return namespaces.filter((ns) =>
1858
+ canReadNamespace(principal, ns, this.orchestrator.config),
1859
+ );
1728
1860
  }
1729
1861
 
1730
1862
  private resolveAllReadableConfiguredNamespaces(principal: string): string[] {
@@ -1752,8 +1884,18 @@ export class EngramAccessService {
1752
1884
  return namespaces;
1753
1885
  }
1754
1886
 
1887
+ const activeScopeProfilePlan = collectionPrincipal
1888
+ ? resolveScopeProfilePlan({
1889
+ config: this.orchestrator.config,
1890
+ principal: collectionPrincipal,
1891
+ codingContext: null,
1892
+ codingOverlay: null,
1893
+ })
1894
+ : null;
1755
1895
  const candidates = collectionPrincipal
1756
- ? this.resolveAllReadableConfiguredNamespaces(collectionPrincipal)
1896
+ ? activeScopeProfilePlan
1897
+ ? namespaces
1898
+ : this.resolveAllReadableConfiguredNamespaces(collectionPrincipal)
1757
1899
  : namespaces;
1758
1900
  const matchedNamespaces = candidates.filter((namespace) => {
1759
1901
  const canonical = namespaceCollectionName(baseCollection, namespace, {
@@ -1867,7 +2009,7 @@ export class EngramAccessService {
1867
2009
  ...(options.rawExcerptNamespace
1868
2010
  ? { rawExcerptNamespace: options.rawExcerptNamespace }
1869
2011
  : {}),
1870
- ...(options.rawExcerptSessionIds
2012
+ ...(options.rawExcerptSessionIds !== undefined
1871
2013
  ? { rawExcerptSessionIds: options.rawExcerptSessionIds }
1872
2014
  : {}),
1873
2015
  ...(options.rawExcerptsSuppressed
@@ -2093,7 +2235,7 @@ export class EngramAccessService {
2093
2235
  ? { sessionKey: rawContext.sessionKey }
2094
2236
  : {}),
2095
2237
  namespace: rawContext.rawExcerptNamespace ?? namespace,
2096
- ...(rawContext.rawExcerptSessionIds
2238
+ ...(rawContext.rawExcerptSessionIds !== undefined
2097
2239
  ? { lcmSessionIds: rawContext.rawExcerptSessionIds }
2098
2240
  : {}),
2099
2241
  }
@@ -2241,7 +2383,7 @@ export class EngramAccessService {
2241
2383
  ? `${context.namespace}:${context.sessionKey}`
2242
2384
  : context.sessionKey;
2243
2385
  const lcmSessionIds =
2244
- context.lcmSessionIds && context.lcmSessionIds.length > 0
2386
+ context.lcmSessionIds !== undefined
2245
2387
  ? context.lcmSessionIds
2246
2388
  : [legacyKey];
2247
2389
  // Cap the excerpt fanout so recall responses stay bounded. Five matches
@@ -2252,14 +2394,15 @@ export class EngramAccessService {
2252
2394
  const limit = 5;
2253
2395
  const seenRows = new Set<string>();
2254
2396
  const excerpts: NonNullable<EngramAccessMemorySummary["rawExcerpts"]> = [];
2255
- for (const lcmSessionKey of lcmSessionIds) {
2397
+ const settledRows = await Promise.allSettled(
2398
+ lcmSessionIds.map(async (lcmSessionKey) =>
2399
+ lcm.searchContextFull(context.query, limit, lcmSessionKey),
2400
+ ),
2401
+ );
2402
+ for (const result of settledRows) {
2256
2403
  if (excerpts.length >= limit) break;
2257
- const rows = await lcm.searchContextFull(
2258
- context.query,
2259
- limit,
2260
- lcmSessionKey,
2261
- );
2262
- for (const r of rows) {
2404
+ if (result.status !== "fulfilled") continue;
2405
+ for (const r of result.value) {
2263
2406
  const dedupeKey = `${r.session_id} ${r.turn_index}`;
2264
2407
  if (seenRows.has(dedupeKey)) continue;
2265
2408
  seenRows.add(dedupeKey);
@@ -3048,6 +3191,29 @@ export class EngramAccessService {
3048
3191
  }
3049
3192
  const principal = maybePrincipal ?? "default";
3050
3193
  const principalNamespace = defaultNamespaceForPrincipal(principal, this.orchestrator.config);
3194
+ const profileCodingContext =
3195
+ request.sessionKey && typeof this.orchestrator.getCodingContextForSession === "function"
3196
+ ? this.orchestrator.getCodingContextForSession(request.sessionKey)
3197
+ : null;
3198
+ const profileCodingOverlay =
3199
+ !namespaceOverride &&
3200
+ profileCodingContext &&
3201
+ this.orchestrator.config.namespacesEnabled &&
3202
+ this.orchestrator.config.codingMode?.projectScope
3203
+ ? resolveCodingNamespaceOverlay(
3204
+ profileCodingContext,
3205
+ this.orchestrator.config.codingMode,
3206
+ this.orchestrator.config.defaultNamespace,
3207
+ )
3208
+ : null;
3209
+ const profilePlan = namespaceOverride
3210
+ ? null
3211
+ : resolveScopeProfilePlan({
3212
+ config: this.orchestrator.config,
3213
+ principal,
3214
+ codingContext: profileCodingContext,
3215
+ codingOverlay: profileCodingOverlay,
3216
+ });
3051
3217
  // Skip budget checks for modes that never perform a cross-namespace read.
3052
3218
  const modeSkipsBudget = mode === "no_recall";
3053
3219
  // Derive the full set of namespaces the orchestrator will actually search.
@@ -3056,14 +3222,22 @@ export class EngramAccessService {
3056
3222
  // against every cross-namespace entry in the effective set so that omitting
3057
3223
  // `namespace` cannot bypass the limiter (Cursor/Codex review feedback).
3058
3224
  //
3059
- // NOTE: coding overlays (branch/project scope) are resolved inside
3060
- // orchestrator.recall() AFTER this check. The access-service does not
3061
- // duplicate that resolution here to avoid tight coupling. Coding-overlay
3062
- // namespaces are a second-layer defense covered by the anomaly detector
3063
- // (PR 5/5 of issue #565).
3225
+ const legacyRecallNamespaces = Array.isArray(this.orchestrator.config.defaultRecallNamespaces)
3226
+ ? recallNamespacesForPrincipal(principal, this.orchestrator.config)
3227
+ : [];
3064
3228
  const effectiveNamespaces = namespaceOverride
3065
3229
  ? [namespaceOverride]
3066
- : recallNamespacesForPrincipal(principal, this.orchestrator.config);
3230
+ : profilePlan
3231
+ ? expandScopeProfileReadNamespaces({
3232
+ profilePlan,
3233
+ principalSelfNamespace: profilePlan.baseNamespace,
3234
+ config: this.orchestrator.config,
3235
+ principal,
3236
+ codingOverlay: profileCodingOverlay,
3237
+ legacyRecallNamespaces,
3238
+ })
3239
+ : legacyRecallNamespaces;
3240
+ const budgetPrincipalNamespace = profilePlan?.baseNamespace ?? principalNamespace;
3067
3241
  let budgetDecision: BudgetDecision;
3068
3242
  let recordBudgetAfterSuccess = false;
3069
3243
  if (modeSkipsBudget) {
@@ -3091,7 +3265,7 @@ export class EngramAccessService {
3091
3265
  for (const ns of effectiveNamespaces) {
3092
3266
  const peek = this.budget.peek({
3093
3267
  principal,
3094
- principalNamespace,
3268
+ principalNamespace: budgetPrincipalNamespace,
3095
3269
  queryNamespace: ns,
3096
3270
  });
3097
3271
  if (peek.reason !== "allowed-same-namespace") {
@@ -3286,7 +3460,7 @@ export class EngramAccessService {
3286
3460
  query,
3287
3461
  sessionKey: trimmedSessionKey,
3288
3462
  ...(rawExcerptNamespace ? { rawExcerptNamespace } : {}),
3289
- ...(rawExcerptSessionIds ? { rawExcerptSessionIds } : {}),
3463
+ ...(rawExcerptSessionIds !== undefined ? { rawExcerptSessionIds } : {}),
3290
3464
  ...(rawExcerptsSuppressed ? { rawExcerptsSuppressed } : {}),
3291
3465
  });
3292
3466
 
@@ -3829,7 +4003,7 @@ export class EngramAccessService {
3829
4003
  ...(rawExcerptNamespace
3830
4004
  ? { namespace: rawExcerptNamespace }
3831
4005
  : {}),
3832
- ...(rawExcerptSessionIds
4006
+ ...(rawExcerptSessionIds !== undefined
3833
4007
  ? { lcmSessionIds: rawExcerptSessionIds }
3834
4008
  : {}),
3835
4009
  })
@@ -3981,7 +4155,7 @@ export class EngramAccessService {
3981
4155
  ...(xrayRawExcerptNamespace
3982
4156
  ? { rawExcerptNamespace: xrayRawExcerptNamespace }
3983
4157
  : {}),
3984
- ...(xrayRawExcerptSessionIds
4158
+ ...(xrayRawExcerptSessionIds !== undefined
3985
4159
  ? { rawExcerptSessionIds: xrayRawExcerptSessionIds }
3986
4160
  : {}),
3987
4161
  ...(xrayRawExcerptsSuppressed
@@ -5165,15 +5339,11 @@ export class EngramAccessService {
5165
5339
  // the single authorization point (rule 22 / 39); the legacy field must reuse it
5166
5340
  // and never re-authorize. Pre-#1495 semantics were exactly
5167
5341
  // `resolveWritableNamespace(request.namespace)` (overlay-agnostic): the explicit
5168
- // namespace when supplied, else `config.defaultNamespace`. `scope.explicitNamespace`
5169
- // carries the authorized explicit value; the no-overlay implicit
5170
- // `scope.writeNamespace` IS `config.defaultNamespace`, so an unqualified observe
5171
- // stays byte-for-byte identical to the legacy response.
5172
- const namespace = scope.explicitNamespace
5173
- ? scope.writeNamespace
5174
- : scope.codingOverlayApplied
5175
- ? this.orchestrator.config.defaultNamespace
5176
- : scope.writeNamespace;
5342
+ // namespace when supplied, else `config.defaultNamespace` for user-project
5343
+ // coding overlays. Hosted scope-profile layers such as `teamProject` report
5344
+ // their effective profile write namespace because there is no legacy
5345
+ // overlay-compatible base namespace for those writes.
5346
+ const namespace = this.legacyResponseNamespaceForScope(scope);
5177
5347
  const shouldWriteObjectiveState =
5178
5348
  this.orchestrator.config.objectiveStateMemoryEnabled === true &&
5179
5349
  this.orchestrator.config.objectiveStateSnapshotWritesEnabled === true;
@@ -5339,6 +5509,10 @@ export class EngramAccessService {
5339
5509
  writeNamespace: scope.writeNamespace,
5340
5510
  codingOverlayApplied: scope.codingOverlayApplied,
5341
5511
  readNamespaces: scope.readNamespaces,
5512
+ scopeProfile: scope.scopeProfile,
5513
+ writeLayer: scope.writeLayer,
5514
+ layers: scope.layers,
5515
+ promotionTargets: scope.promotionTargets,
5342
5516
  },
5343
5517
  lcmArchived,
5344
5518
  extractionQueued,
@@ -5364,9 +5538,17 @@ export class EngramAccessService {
5364
5538
  // still succeeds via `recallNamespacesForPrincipal`, so `lcmSearch` must too
5365
5539
  // (the same defect class the raw-excerpt path fixes). `undefined` ⇒ no
5366
5540
  // readable LCM namespace exists, so return NO rows rather than throwing.
5541
+ const profileLcmReadNamespaces = hasExplicitNamespace
5542
+ ? null
5543
+ : this.resolveScopeProfileLcmReadNamespaces(
5544
+ request.sessionKey,
5545
+ request.authenticatedPrincipal,
5546
+ );
5367
5547
  const namespace = hasExplicitNamespace
5368
5548
  ? this.resolveReadableNamespace(request.namespace, principal)
5369
- : this.resolveImplicitLcmReadFallbackNamespace(principal);
5549
+ : profileLcmReadNamespaces !== null
5550
+ ? profileLcmReadNamespaces[0]
5551
+ : this.resolveImplicitLcmReadFallbackNamespace(principal);
5370
5552
 
5371
5553
  if (!this.orchestrator.lcmEngine || !this.orchestrator.lcmEngine.enabled) {
5372
5554
  return {
@@ -5378,6 +5560,18 @@ export class EngramAccessService {
5378
5560
  };
5379
5561
  }
5380
5562
 
5563
+ // An active scope profile with no readable layers is authoritative: search
5564
+ // no legacy/default LCM keys instead of falling back around the profile.
5565
+ if (profileLcmReadNamespaces !== null && profileLcmReadNamespaces.length === 0) {
5566
+ return {
5567
+ query: request.query,
5568
+ namespace: this.orchestrator.config.defaultNamespace,
5569
+ results: [],
5570
+ count: 0,
5571
+ lcmEnabled: true,
5572
+ };
5573
+ }
5574
+
5381
5575
  // No readable LCM namespace for an IMPLICIT read (restrictive `default` READ
5382
5576
  // policy, no readable overlay/self) ⇒ return NO rows instead of pre-
5383
5577
  // authorizing the denied default (#1505 thread NBHWz). Normal recall still
@@ -5402,34 +5596,47 @@ export class EngramAccessService {
5402
5596
  // (`request.sessionKey`) — the prefix is a search fragment with no bound
5403
5597
  // coding context, so it inherits the same namespace. Collapses to the raw key
5404
5598
  // for single-store / no-overlay / explicit-default flows (existing behavior).
5405
- const lcmReadNamespace = this.resolveLcmReadNamespace(
5406
- request.namespace,
5407
- namespace,
5408
- request.sessionKey,
5409
- request.authenticatedPrincipal,
5410
- );
5411
- // Ordered, read-authorized LCM read key SET for a concrete `sessionKey`
5412
- // (#1505 fallback unification). A branch-scoped session whose rows were
5413
- // archived at project/root scope is found by querying the primary overlay key
5414
- // first, then each coding read fallback — exactly as the orchestrator recall
5415
- // path does. Collapses to a single key for explicit-namespace / no-overlay /
5416
- // unreadable-self flows. The `sessionPrefix` search fragment stays on the
5417
- // primary overlay namespace (its own coding context can't be looked up).
5418
- const lcmSessionKeyIds = request.sessionKey
5419
- ? this.resolveLcmReadSessionIds(
5599
+ const lcmReadNamespace = profileLcmReadNamespaces !== null
5600
+ ? profileLcmReadNamespaces[0] ?? this.orchestrator.config.defaultNamespace
5601
+ : this.resolveLcmReadNamespace(
5420
5602
  request.namespace,
5421
5603
  namespace,
5422
5604
  request.sessionKey,
5423
5605
  request.authenticatedPrincipal,
5424
- )
5606
+ );
5607
+ // Ordered, read-authorized LCM read key SET for a concrete `sessionKey`
5608
+ // (#1505 fallback unification + #1501 scope profiles). A branch-scoped
5609
+ // session whose rows were archived at project/root scope is found by querying
5610
+ // the primary overlay key first, then each fallback. When a scope profile is
5611
+ // active, the profile's expanded read namespace set is authoritative,
5612
+ // including the empty set.
5613
+ const lcmSessionKeyIds = request.sessionKey
5614
+ ? profileLcmReadNamespaces !== null
5615
+ ? this.lcmSessionIdsForNamespaces(
5616
+ profileLcmReadNamespaces,
5617
+ request.sessionKey,
5618
+ )
5619
+ : this.resolveLcmReadSessionIds(
5620
+ request.namespace,
5621
+ namespace,
5622
+ request.sessionKey,
5623
+ request.authenticatedPrincipal,
5624
+ )
5625
+ : [undefined];
5626
+ const lcmSessionPrefixes = request.sessionPrefix
5627
+ ? profileLcmReadNamespaces !== null && !request.sessionKey
5628
+ ? this.lcmSessionIdsForNamespaces(
5629
+ profileLcmReadNamespaces,
5630
+ request.sessionPrefix,
5631
+ )
5632
+ : [
5633
+ lcmSessionKeyForNamespace(
5634
+ lcmReadNamespace,
5635
+ request.sessionPrefix,
5636
+ this.orchestrator.config.defaultNamespace,
5637
+ ) ?? request.sessionPrefix,
5638
+ ]
5425
5639
  : [undefined];
5426
- const lcmSessionPrefix = request.sessionPrefix
5427
- ? lcmSessionKeyForNamespace(
5428
- lcmReadNamespace,
5429
- request.sessionPrefix,
5430
- this.orchestrator.config.defaultNamespace,
5431
- ) ?? request.sessionPrefix
5432
- : request.sessionPrefix;
5433
5640
  // SECURITY (#1495 P1 + codex P1 r2 "Require a scoped LCM filter before
5434
5641
  // archive searches"): a sessionless, prefixless `lcmSearch` issues
5435
5642
  // `searchContextFull(query, limit, undefined, undefined)`, an archive-wide
@@ -5451,7 +5658,7 @@ export class EngramAccessService {
5451
5658
  const hasScopedSession =
5452
5659
  (typeof request.sessionKey === "string" &&
5453
5660
  request.sessionKey.length > 0) ||
5454
- (typeof lcmSessionPrefix === "string" && lcmSessionPrefix.length > 0);
5661
+ lcmSessionPrefixes.some((prefix) => typeof prefix === "string" && prefix.length > 0);
5455
5662
  if (!hasScopedSession && this.orchestrator.config.namespacesEnabled === true) {
5456
5663
  return {
5457
5664
  query: request.query,
@@ -5463,18 +5670,44 @@ export class EngramAccessService {
5463
5670
  }
5464
5671
  // Query each LCM read key in order, merging + deduping rows (by
5465
5672
  // sessionId+turnIndex) and preserving first-seen order, capped at `limit`.
5673
+ // Use allSettled so one corrupt/failed namespace key cannot discard sibling
5674
+ // results from other authorized profile namespaces.
5466
5675
  const seenRows = new Set<string>();
5467
5676
  const results: Array<{ sessionId: string; content: string; turnIndex: number }> = [];
5677
+ const lcmSearches: Array<{
5678
+ key: string | undefined;
5679
+ prefix: string | undefined;
5680
+ promise: Promise<Array<{ session_id: string; content: string; turn_index: number }>>;
5681
+ }> = [];
5468
5682
  for (const lcmSessionKey of lcmSessionKeyIds) {
5683
+ for (const lcmSessionPrefix of lcmSessionPrefixes) {
5684
+ lcmSearches.push({
5685
+ key: lcmSessionKey,
5686
+ prefix: lcmSessionPrefix,
5687
+ promise: this.orchestrator.lcmEngine.searchContextFull(
5688
+ request.query,
5689
+ limit,
5690
+ lcmSessionKey,
5691
+ lcmSessionPrefix,
5692
+ ) as Promise<Array<{ session_id: string; content: string; turn_index: number }>>,
5693
+ });
5694
+ }
5695
+ }
5696
+ const settledSearches = await Promise.allSettled(
5697
+ lcmSearches.map((search) => search.promise),
5698
+ );
5699
+ for (let i = 0; i < settledSearches.length; i += 1) {
5469
5700
  if (results.length >= limit) break;
5470
- const rawResults = await this.orchestrator.lcmEngine.searchContextFull(
5471
- request.query,
5472
- limit,
5473
- lcmSessionKey,
5474
- lcmSessionPrefix,
5475
- );
5476
- for (const r of rawResults as Array<{ session_id: string; content: string; turn_index: number }>) {
5477
- const dedupeKey = `${r.session_id}${r.turn_index}`;
5701
+ const settled = settledSearches[i];
5702
+ if (!settled || settled.status === "rejected") {
5703
+ const failed = lcmSearches[i];
5704
+ log.warn(
5705
+ `lcmSearch: failed for key=${failed?.key ?? "<none>"} prefix=${failed?.prefix ?? "<none>"}: ${settled?.status === "rejected" ? settled.reason : "missing result"}`,
5706
+ );
5707
+ continue;
5708
+ }
5709
+ for (const r of settled.value) {
5710
+ const dedupeKey = `${r.session_id}\0${r.turn_index}`;
5478
5711
  if (seenRows.has(dedupeKey)) continue;
5479
5712
  seenRows.add(dedupeKey);
5480
5713
  results.push({
@@ -5659,10 +5892,21 @@ export class EngramAccessService {
5659
5892
  "read",
5660
5893
  );
5661
5894
  }
5662
- // IMPLICIT raw recall: derive the read fallback from the ALREADY
5663
- // read-authorized recall namespace set NEVER pre-authorize `default`
5664
- // (#1505 thread NBHWz). When namespaces are disabled the default store is the
5665
- // only namespace and is always readable (byte-for-byte single-user path).
5895
+ // IMPLICIT raw recall: an active scope profile owns the same LCM read
5896
+ // namespace set used by recall and lcmSearch. Return the first profile
5897
+ // namespace as the legacy raw-excerpt namespace hint; the concrete ordered
5898
+ // key set is still produced by resolveLcmReadSessionIds(), which also treats
5899
+ // an empty profile read set as authoritative.
5900
+ const profileReadNamespaces = this.resolveScopeProfileLcmReadNamespaces(
5901
+ sessionKey,
5902
+ authenticatedPrincipal,
5903
+ );
5904
+ if (profileReadNamespaces !== null) return profileReadNamespaces[0];
5905
+
5906
+ // Otherwise derive the read fallback from the ALREADY read-authorized recall
5907
+ // namespace set — NEVER pre-authorize `default` (#1505 thread NBHWz). When
5908
+ // namespaces are disabled the default store is the only namespace and is
5909
+ // always readable (byte-for-byte single-user path).
5666
5910
  const fallbackNamespace =
5667
5911
  this.resolveImplicitLcmReadFallbackNamespace(principal);
5668
5912
  // No readable LCM namespace at all ⇒ no excerpts (caller short-circuits).
@@ -5789,6 +6033,59 @@ export class EngramAccessService {
5789
6033
  * `<principal>-project-*` key is ever searched for an unauthorized reader (no
5790
6034
  * cross-tenant read leak).
5791
6035
  */
6036
+ private resolveScopeProfileLcmReadNamespaces(
6037
+ sessionKey: string | undefined,
6038
+ authenticatedPrincipal: string | undefined,
6039
+ ): string[] | null {
6040
+ const config = this.orchestrator.config;
6041
+ const principal = this.resolveRequestPrincipal(sessionKey, authenticatedPrincipal);
6042
+ const codingContext = sessionKey
6043
+ ? this.orchestrator.getCodingContextForSession(sessionKey)
6044
+ : null;
6045
+ const codingOverlay = resolveCodingNamespaceOverlay(
6046
+ codingContext,
6047
+ config.codingMode,
6048
+ config.defaultNamespace,
6049
+ );
6050
+ const profilePlan = resolveScopeProfilePlan({
6051
+ config,
6052
+ principal,
6053
+ codingContext,
6054
+ codingOverlay,
6055
+ });
6056
+ if (!profilePlan) return null;
6057
+ const principalSelfNamespace = defaultNamespaceForPrincipal(principal, config);
6058
+ const legacyRecallNamespaces = Array.isArray(config.defaultRecallNamespaces)
6059
+ ? recallNamespacesForPrincipal(principal, config)
6060
+ : [];
6061
+ return expandScopeProfileReadNamespaces({
6062
+ profilePlan,
6063
+ principalSelfNamespace: profilePlan.baseNamespace,
6064
+ config,
6065
+ principal,
6066
+ codingOverlay,
6067
+ legacyRecallNamespaces,
6068
+ });
6069
+ }
6070
+
6071
+ private lcmSessionIdsForNamespaces(namespaces: string[], sessionKey: string): string[] {
6072
+ const out: string[] = [];
6073
+ const seen = new Set<string>();
6074
+ for (const namespace of namespaces) {
6075
+ const key =
6076
+ lcmSessionKeyForNamespace(
6077
+ namespace,
6078
+ sessionKey,
6079
+ this.orchestrator.config.defaultNamespace,
6080
+ ) ?? sessionKey;
6081
+ if (!seen.has(key)) {
6082
+ seen.add(key);
6083
+ out.push(key);
6084
+ }
6085
+ }
6086
+ return out;
6087
+ }
6088
+
5792
6089
  private resolveLcmReadSessionIds(
5793
6090
  explicitNamespace: string | undefined,
5794
6091
  resolvedNamespace: string,
@@ -5809,6 +6106,14 @@ export class EngramAccessService {
5809
6106
  // explicit read). Single key, unchanged.
5810
6107
  if (hasExplicitNamespace) return [primary];
5811
6108
 
6109
+ const profileReadNamespaces = this.resolveScopeProfileLcmReadNamespaces(
6110
+ sessionKey,
6111
+ authenticatedPrincipal,
6112
+ );
6113
+ if (profileReadNamespaces !== null) {
6114
+ return this.lcmSessionIdsForNamespaces(profileReadNamespaces, sessionKey);
6115
+ }
6116
+
5812
6117
  const principal = this.resolveRequestPrincipal(
5813
6118
  sessionKey,
5814
6119
  authenticatedPrincipal,
@@ -5883,13 +6188,10 @@ export class EngramAccessService {
5883
6188
  // `resolveWritableNamespace(request.namespace)` (overlay-agnostic) — the
5884
6189
  // authorized explicit namespace when supplied, else `config.defaultNamespace`.
5885
6190
  // DERIVED from the scope plan (NOT a second auth pass, #1505 thread jvO):
5886
- // explicit ⇒ writeNamespace; coding overlay ⇒ defaultNamespace; no overlay ⇒
5887
- // writeNamespace (== defaultNamespace). Identical to observe's legacy field.
5888
- const namespace = scope.explicitNamespace
5889
- ? scope.writeNamespace
5890
- : scope.codingOverlayApplied
5891
- ? this.orchestrator.config.defaultNamespace
5892
- : scope.writeNamespace;
6191
+ // explicit ⇒ writeNamespace; user-project coding overlay ⇒ defaultNamespace;
6192
+ // non-user scope-profile layer/no overlay ⇒ writeNamespace. Identical to
6193
+ // observe's legacy field.
6194
+ const namespace = this.legacyResponseNamespaceForScope(scope);
5893
6195
  if (!this.orchestrator.lcmEngine || !this.orchestrator.lcmEngine.enabled) {
5894
6196
  return {
5895
6197
  enabled: false,
@@ -5948,11 +6250,7 @@ export class EngramAccessService {
5948
6250
  // overlay write and never throws `not writable: default` for a validly scoped
5949
6251
  // observe's queue.
5950
6252
  const scope = await this.resolveMemoryScopePlan(request);
5951
- const namespace = scope.explicitNamespace
5952
- ? scope.writeNamespace
5953
- : scope.codingOverlayApplied
5954
- ? this.orchestrator.config.defaultNamespace
5955
- : scope.writeNamespace;
6253
+ const namespace = this.legacyResponseNamespaceForScope(scope);
5956
6254
  if (!this.orchestrator.lcmEngine || !this.orchestrator.lcmEngine.enabled) {
5957
6255
  return {
5958
6256
  enabled: false,