@remnic/core 9.3.649 → 9.3.650

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 (42) hide show
  1. package/dist/access-cli.js +3 -3
  2. package/dist/access-http.d.ts +2 -2
  3. package/dist/access-http.js +4 -4
  4. package/dist/access-mcp.d.ts +2 -2
  5. package/dist/access-mcp.js +3 -3
  6. package/dist/{access-service-DFXIlGvZ.d.ts → access-service-DIZRHQ7Q.d.ts} +255 -2
  7. package/dist/access-service.d.ts +2 -2
  8. package/dist/access-service.js +2 -2
  9. package/dist/bootstrap.d.ts +1 -1
  10. package/dist/{chunk-XUGVP7ZU.js → chunk-23RYLGYA.js} +184 -54
  11. package/dist/chunk-23RYLGYA.js.map +1 -0
  12. package/dist/{chunk-CNRZ6WJU.js → chunk-3IJEQWQX.js} +4 -4
  13. package/dist/{chunk-6GIKAUTN.js → chunk-MMJANTJX.js} +33 -2
  14. package/dist/{chunk-6GIKAUTN.js.map → chunk-MMJANTJX.js.map} +1 -1
  15. package/dist/{chunk-FQYFMIKG.js → chunk-TUMH6EDV.js} +4 -4
  16. package/dist/{chunk-FUXV6HSO.js → chunk-TVOPSKOK.js} +3 -3
  17. package/dist/{chunk-5ETA6OAS.js → chunk-YAFSTKTH.js} +608 -80
  18. package/dist/chunk-YAFSTKTH.js.map +1 -0
  19. package/dist/{cli-DrL2Nv4j.d.ts → cli-BG4ybtJr.d.ts} +2 -2
  20. package/dist/cli.d.ts +3 -3
  21. package/dist/cli.js +5 -5
  22. package/dist/explicit-capture.d.ts +1 -1
  23. package/dist/index.d.ts +4 -4
  24. package/dist/index.js +6 -6
  25. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  26. package/dist/{orchestrator-DEQW9j0Z.d.ts → orchestrator-CX-oqwJq.d.ts} +58 -0
  27. package/dist/orchestrator.d.ts +1 -1
  28. package/dist/orchestrator.js +2 -2
  29. package/package.json +1 -1
  30. package/src/access-service-lcm-forgery.test.ts +410 -0
  31. package/src/access-service-observe-lcm-parity.test.ts +1397 -0
  32. package/src/access-service-observe-scope.test.ts +599 -0
  33. package/src/access-service-raw-excerpt-read-gate.test.ts +443 -0
  34. package/src/access-service.ts +1270 -113
  35. package/src/coding/coding-namespace.test.ts +44 -0
  36. package/src/coding/coding-namespace.ts +163 -0
  37. package/src/orchestrator.ts +335 -77
  38. package/dist/chunk-5ETA6OAS.js.map +0 -1
  39. package/dist/chunk-XUGVP7ZU.js.map +0 -1
  40. /package/dist/{chunk-CNRZ6WJU.js.map → chunk-3IJEQWQX.js.map} +0 -0
  41. /package/dist/{chunk-FQYFMIKG.js.map → chunk-TUMH6EDV.js.map} +0 -0
  42. /package/dist/{chunk-FUXV6HSO.js.map → chunk-TVOPSKOK.js.map} +0 -0
@@ -307,6 +307,7 @@ import {
307
307
  } from "./namespaces/principal.js";
308
308
  import {
309
309
  combineNamespaces,
310
+ lcmReadSessionIdsForNamespaces,
310
311
  resolveCodingNamespaceOverlay,
311
312
  } from "./coding/coding-namespace.js";
312
313
  import type { CodingContext } from "./types.js";
@@ -2011,6 +2012,66 @@ export class Orchestrator {
2011
2012
  return this.applyCodingNamespaceOverlay(sessionKey, base);
2012
2013
  }
2013
2014
 
2015
+ /**
2016
+ * Effective namespace a same-session LCM/structured-history READER must use
2017
+ * to find what the access `observe` surface WROTE (#1495 thread 2).
2018
+ *
2019
+ * This MUST mirror the `observe` scope plan's write-namespace resolution, NOT
2020
+ * `resolveSelfNamespace`: when no coding overlay applies, `observe` archives
2021
+ * under `config.defaultNamespace` (an unqualified observed turn is NOT moved
2022
+ * to the principal self namespace — identical to
2023
+ * `resolveCodingScopedWriteNamespace`/`memory_store`, rule 39). Only when a
2024
+ * coding overlay actually changes the namespace does the writer (and so the
2025
+ * reader) use the overlaid `project-*` namespace. Returning the self base for
2026
+ * the no-overlay case would prefix the read key with a namespace the writer
2027
+ * never used, so the reader would miss its own evidence.
2028
+ *
2029
+ * Honours the access-surface `principalOverride` (#1505 thread 2, codex): when
2030
+ * a recall supplies an authenticated principal NOT encoded in the raw
2031
+ * `sessionKey`, `observe` archived LCM under THAT principal's base namespace.
2032
+ * Deriving the base from `resolvePrincipal(sessionKey)` alone could fall back
2033
+ * to `default`, so principal `alice` observing `sess-1` would write under
2034
+ * `alice` but READ under `default`. Threading the override here keeps the read
2035
+ * base identical to the write base.
2036
+ *
2037
+ * READ-AUTHORIZATION gate (#1505 round 3, codex P2 "Gate LCM recall keys by
2038
+ * readable namespaces"): the overlay LCM read key is a `<principal>-project-*`
2039
+ * sub-namespace of the principal SELF base. The normal recall namespace set
2040
+ * below only substitutes the coding overlay when the principal SELF base is
2041
+ * actually in the readable recall set (`recallNamespacesForPrincipal` — gated
2042
+ * by `defaultRecallNamespaces.includes("self")` AND `canReadNamespace`). If a
2043
+ * principal can WRITE but not READ its self namespace (or `defaultRecall-
2044
+ * Namespaces` omits `self`), QMD/file recall never touches those overlay rows,
2045
+ * so neither may the LCM read key. When the self base is NOT readable, fall
2046
+ * back to the default store — exactly what an unqualified, unauthorized recall
2047
+ * resolves to — rather than injecting overlay rows the rest of recall excludes
2048
+ * (rule 42 read/write parity; rule 48 least-privilege).
2049
+ */
2050
+ private lcmReadNamespaceForSession(
2051
+ sessionKey?: string,
2052
+ principalOverride?: string,
2053
+ ): string {
2054
+ const principal =
2055
+ typeof principalOverride === "string" && principalOverride.length > 0
2056
+ ? principalOverride
2057
+ : this.resolvePrincipal(sessionKey);
2058
+ const base = defaultNamespaceForPrincipal(principal, this.config);
2059
+ const overlaid = this.applyCodingNamespaceOverlay(sessionKey, base);
2060
+ // No overlay → collapse to the default store so the LCM key is the raw
2061
+ // sessionKey, exactly what an unqualified observe archived under.
2062
+ if (overlaid === base) return this.config.defaultNamespace;
2063
+ // Overlay applied. Only honour it when the principal SELF base is in the
2064
+ // readable recall set (same gate the recall namespace set uses to
2065
+ // substitute the overlay). Otherwise the overlay rows are unauthorized for
2066
+ // this reader — fall back to the default store so the LCM read matches
2067
+ // what QMD/file recall would surface.
2068
+ const selfReadableInRecall = recallNamespacesForPrincipal(
2069
+ principal,
2070
+ this.config,
2071
+ ).includes(base);
2072
+ return selfReadableInRecall ? overlaid : this.config.defaultNamespace;
2073
+ }
2074
+
2014
2075
  /**
2015
2076
  * Attach a coding-agent context to a session (issue #569). Called by the
2016
2077
  * Claude Code / Codex / Cursor connectors at session start after
@@ -6890,6 +6951,10 @@ export class Orchestrator {
6890
6951
  .digest("hex")
6891
6952
  .slice(0, 16);
6892
6953
  const sectionBuckets = new Map<string, string[]>();
6954
+ // The effective LCM read session_id SET is computed below from
6955
+ // `recallNamespaces` (the SAME read-authorized namespace set normal QMD/file
6956
+ // recall searches, incl. coding `readFallbacks`). See the
6957
+ // `lcmReadSessionIds` derivation after `recallNamespaces` is built.
6893
6958
  const queryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
6894
6959
  cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
6895
6960
  cronRecallNormalizedQueryMaxChars:
@@ -7119,6 +7184,86 @@ export class Orchestrator {
7119
7184
  } else {
7120
7185
  recallNamespaces = readableRecallNamespaces;
7121
7186
  }
7187
+ // Effective LCM read NAMESPACE SET (#1505 thread "Include coding fallback
7188
+ // namespaces in LCM reads"). `observe` archives LCM / structured history
7189
+ // under `${effectiveNamespace}:${sessionKey}` for whichever namespace was
7190
+ // effective at write time. A branch-scoped session whose evidence was
7191
+ // archived at project / root scope must still surface it, exactly as normal
7192
+ // QMD/file recall does — QMD/file recall searches the primary overlay key AND
7193
+ // `codingOverlay.readFallbacks` (project / root), NOT just the primary
7194
+ // overlay key. The prior single `lcmReadSessionId` only targeted the primary
7195
+ // overlay, so branch-scoped sessions missed fallback LCM evidence.
7196
+ //
7197
+ // READ-AUTHORIZATION (preserved from the prior round's
7198
+ // `lcmReadNamespaceForSession` gate; rule 39 / 42 / 48): the coding-overlay
7199
+ // namespace AND its fallbacks are `<principal>-project-*` sub-namespaces of
7200
+ // the principal SELF base, authorized transitively by that base. They are
7201
+ // included ONLY when the principal self base is in the readable recall set
7202
+ // (`readableRecallNamespaces` — gated by `defaultRecallNamespaces.includes
7203
+ // ("self")` AND `canReadNamespace`). When the self base is NOT readable (e.g.
7204
+ // a write-only / self-omitted principal), the overlay rows are unauthorized
7205
+ // for this reader, so the LCM read collapses to the default store — exactly
7206
+ // what an unqualified, unauthorized recall resolves to — and NEVER searches a
7207
+ // `<principal>-project-*` key (no cross-tenant read leak). This mirrors what
7208
+ // the rest of recall surfaces for such a principal (its readable
7209
+ // shared/policy namespaces have no per-session LCM key, so they contribute
7210
+ // nothing here). `recallNamespaces` itself appends fallbacks unconditionally
7211
+ // for QMD/file recall; the LCM read keys apply the stricter, self-base gate
7212
+ // so the prior round's authorization invariant is preserved.
7213
+ const codingOverlaySelfReadable =
7214
+ codingOverlay !== null &&
7215
+ readableRecallNamespaces.includes(principalSelfNamespace);
7216
+ let lcmReadNamespaces: string[];
7217
+ if (namespaceOverride) {
7218
+ // Explicit namespace already read-authorized above (canReadNamespace gate).
7219
+ lcmReadNamespaces = [namespaceOverride];
7220
+ } else if (codingOverlay && codingSelfNamespace && codingOverlaySelfReadable) {
7221
+ // Self base readable → overlay rows authorized. Read the primary overlay
7222
+ // key first, then each coding read fallback (project → root), combined with
7223
+ // the principal base for isolation — the SAME ordered set QMD/file recall
7224
+ // searches for this authorized coding session.
7225
+ const fallbackNs = codingOverlay.readFallbacks.map((fallback) =>
7226
+ combineNamespaces(principalSelfNamespace, fallback),
7227
+ );
7228
+ lcmReadNamespaces = [codingSelfNamespace, ...fallbackNs];
7229
+ } else {
7230
+ // No overlay, OR overlay present but self base unreadable → collapse to the
7231
+ // default store (raw sessionKey), exactly as the prior round did. No
7232
+ // `<principal>-project-*` overlay key is searched.
7233
+ lcmReadNamespaces = [this.config.defaultNamespace];
7234
+ }
7235
+ // Map the ordered, read-authorized namespace set → ordered, deduped LCM read
7236
+ // session_id set. Single-user / no-overlay recall passes a single-namespace
7237
+ // set that collapses to the raw `sessionKey`, so this is `[sessionKey]` —
7238
+ // byte-for-byte the pre-#1495 single-key behavior.
7239
+ const lcmReadSessionIds = lcmReadSessionIdsForNamespaces(
7240
+ lcmReadNamespaces,
7241
+ sessionKey,
7242
+ this.config.defaultNamespace,
7243
+ );
7244
+ // Query an LCM-backed read across the ordered read key set and return the
7245
+ // FIRST non-empty result (#1505 fallback-namespace unification). The primary
7246
+ // overlay key is tried first; if a branch-scoped session has no rows under
7247
+ // its branch key, the project / root fallback keys are tried in order. Each
7248
+ // builder applies its own per-session budget/limit, so taking the first hit
7249
+ // (rather than concatenating across keys) preserves existing budgets while
7250
+ // recovering fallback evidence. When the set is a single key (single-user /
7251
+ // no-overlay / explicit-namespace), this is exactly one call — unchanged.
7252
+ // `lcmSessionId` is `string | undefined`: a SESSIONLESS recall yields the
7253
+ // single `undefined` key so the LCM builders run ONE archive-wide read with
7254
+ // no `session_id` filter (pre-#1505 behavior; the builders accept an optional
7255
+ // `sessionId`). NEVER the literal "default" session id (codex P2).
7256
+ const firstNonEmptyLcmRead = async <T>(
7257
+ read: (lcmSessionId: string | undefined) => Promise<T>,
7258
+ isEmpty: (value: T) => boolean,
7259
+ empty: T,
7260
+ ): Promise<T> => {
7261
+ for (const lcmSessionId of lcmReadSessionIds) {
7262
+ const value = await read(lcmSessionId);
7263
+ if (!isEmpty(value)) return value;
7264
+ }
7265
+ return empty;
7266
+ };
7122
7267
  const qmdAvailable = this.qmd.isAvailable();
7123
7268
  let graphDecisionStatus: IntentDebugSnapshot["graphDecision"]["status"] =
7124
7269
  recallDecision.plannedMode === "graph_mode" ? "skipped" : "not_requested";
@@ -9368,15 +9513,24 @@ export class Orchestrator {
9368
9513
  (recallMode as RecallPlanMode) !== "no_recall"
9369
9514
  ) {
9370
9515
  try {
9371
- const explicitCueSection = await buildExplicitCueRecallSection({
9372
- engine: this.lcmEngine,
9373
- sessionId: sessionKey,
9374
- query: retrievalQuery,
9375
- maxChars: explicitCueMaxChars,
9376
- maxReferences:
9377
- this.getRecallSectionNumber("explicit-cue", "maxResults") ??
9378
- this.config.explicitCueRecallMaxReferences,
9379
- });
9516
+ const explicitCueSection = await firstNonEmptyLcmRead(
9517
+ (lcmSessionId) =>
9518
+ buildExplicitCueRecallSection({
9519
+ engine: this.lcmEngine,
9520
+ // #1495 thread 3 + #1505 fallback unification: read across the
9521
+ // ordered LCM read key set (primary overlay → coding fallbacks)
9522
+ // so a branch-scoped session finds its own explicit-cue evidence
9523
+ // even when archived at project/root scope (rule 39).
9524
+ sessionId: lcmSessionId,
9525
+ query: retrievalQuery,
9526
+ maxChars: explicitCueMaxChars,
9527
+ maxReferences:
9528
+ this.getRecallSectionNumber("explicit-cue", "maxResults") ??
9529
+ this.config.explicitCueRecallMaxReferences,
9530
+ }),
9531
+ (s) => !s,
9532
+ "",
9533
+ );
9380
9534
  if (explicitCueSection) {
9381
9535
  this.appendRecallSection(
9382
9536
  sectionBuckets,
@@ -9406,21 +9560,29 @@ export class Orchestrator {
9406
9560
  shouldRecallTargetedFactEvidence(retrievalQuery)
9407
9561
  ) {
9408
9562
  try {
9409
- const targetedFactSection = await buildTargetedFactRecallSection({
9410
- engine: this.lcmEngine,
9411
- sessionId: sessionKey,
9412
- query: retrievalQuery,
9413
- maxChars: targetedFactMaxChars,
9414
- maxSearchResults:
9415
- this.getRecallSectionNumber("targeted-facts", "maxResults") ??
9416
- this.config.targetedFactRecallMaxResults,
9417
- maxScanWindowTurns:
9418
- this.getRecallSectionNumber("targeted-facts", "maxTurns") ??
9419
- this.config.targetedFactRecallScanWindowTurns,
9420
- maxScanWindowTokens:
9421
- this.getRecallSectionNumber("targeted-facts", "maxTokens") ??
9422
- this.config.targetedFactRecallScanWindowTokens,
9423
- });
9563
+ const targetedFactSection = await firstNonEmptyLcmRead(
9564
+ (lcmSessionId) =>
9565
+ buildTargetedFactRecallSection({
9566
+ engine: this.lcmEngine,
9567
+ // #1495 + #1505 fallback unification: read across the ordered LCM
9568
+ // read key set so a branch-scoped session finds its own
9569
+ // targeted-fact evidence even when archived at project/root scope.
9570
+ sessionId: lcmSessionId,
9571
+ query: retrievalQuery,
9572
+ maxChars: targetedFactMaxChars,
9573
+ maxSearchResults:
9574
+ this.getRecallSectionNumber("targeted-facts", "maxResults") ??
9575
+ this.config.targetedFactRecallMaxResults,
9576
+ maxScanWindowTurns:
9577
+ this.getRecallSectionNumber("targeted-facts", "maxTurns") ??
9578
+ this.config.targetedFactRecallScanWindowTurns,
9579
+ maxScanWindowTokens:
9580
+ this.getRecallSectionNumber("targeted-facts", "maxTokens") ??
9581
+ this.config.targetedFactRecallScanWindowTokens,
9582
+ }),
9583
+ (s) => !s,
9584
+ "",
9585
+ );
9424
9586
  if (targetedFactSection) {
9425
9587
  this.appendRecallSection(
9426
9588
  sectionBuckets,
@@ -9451,21 +9613,29 @@ export class Orchestrator {
9451
9613
  shouldRecallFocusedListEvidence(retrievalQuery)
9452
9614
  ) {
9453
9615
  try {
9454
- const focusedListSection = await buildFocusedListRecallSection({
9455
- engine: this.lcmEngine,
9456
- sessionId: sessionKey,
9457
- query: retrievalQuery,
9458
- maxChars: focusedListMaxChars,
9459
- maxSearchResults:
9460
- this.getRecallSectionNumber("focused-list", "maxResults") ??
9461
- this.config.focusedListRecallMaxResults,
9462
- maxScanWindowTurns:
9463
- this.getRecallSectionNumber("focused-list", "maxTurns") ??
9464
- this.config.focusedListRecallScanWindowTurns,
9465
- maxScanWindowTokens:
9466
- this.getRecallSectionNumber("focused-list", "maxTokens") ??
9467
- this.config.focusedListRecallScanWindowTokens,
9468
- });
9616
+ const focusedListSection = await firstNonEmptyLcmRead(
9617
+ (lcmSessionId) =>
9618
+ buildFocusedListRecallSection({
9619
+ engine: this.lcmEngine,
9620
+ // #1495 thread 3 + #1505 fallback unification: read across the
9621
+ // ordered LCM read key set so a branch-scoped session reads its own
9622
+ // focused-list/count evidence even at project/root scope (rule 39).
9623
+ sessionId: lcmSessionId,
9624
+ query: retrievalQuery,
9625
+ maxChars: focusedListMaxChars,
9626
+ maxSearchResults:
9627
+ this.getRecallSectionNumber("focused-list", "maxResults") ??
9628
+ this.config.focusedListRecallMaxResults,
9629
+ maxScanWindowTurns:
9630
+ this.getRecallSectionNumber("focused-list", "maxTurns") ??
9631
+ this.config.focusedListRecallScanWindowTurns,
9632
+ maxScanWindowTokens:
9633
+ this.getRecallSectionNumber("focused-list", "maxTokens") ??
9634
+ this.config.focusedListRecallScanWindowTokens,
9635
+ }),
9636
+ (s) => !s,
9637
+ "",
9638
+ );
9469
9639
  if (focusedListSection) {
9470
9640
  this.appendRecallSection(
9471
9641
  sectionBuckets,
@@ -9500,22 +9670,30 @@ export class Orchestrator {
9500
9670
  (responseGuidanceMatchesQuery || responseGuidanceForcedByPipeline)
9501
9671
  ) {
9502
9672
  try {
9503
- const responseGuidanceSection = await buildResponseGuidanceRecallSection({
9504
- engine: this.lcmEngine,
9505
- sessionId: sessionKey,
9506
- query: retrievalQuery,
9507
- maxChars: responseGuidanceMaxChars,
9508
- maxSearchResults:
9509
- this.getRecallSectionNumber("response-guidance", "maxResults") ??
9510
- this.config.responseGuidanceRecallMaxResults,
9511
- maxScanWindowTurns:
9512
- this.getRecallSectionNumber("response-guidance", "maxTurns") ??
9513
- this.config.responseGuidanceRecallScanWindowTurns,
9514
- maxScanWindowTokens:
9515
- this.getRecallSectionNumber("response-guidance", "maxTokens") ??
9516
- this.config.responseGuidanceRecallScanWindowTokens,
9517
- forceGeneric: responseGuidanceForcedByPipeline,
9518
- });
9673
+ const responseGuidanceSection = await firstNonEmptyLcmRead(
9674
+ (lcmSessionId) =>
9675
+ buildResponseGuidanceRecallSection({
9676
+ engine: this.lcmEngine,
9677
+ // #1495 thread 3 + #1505 fallback unification: read across the
9678
+ // ordered LCM read key set so a branch-scoped session reads its own
9679
+ // response-guidance evidence even at project/root scope (rule 39).
9680
+ sessionId: lcmSessionId,
9681
+ query: retrievalQuery,
9682
+ maxChars: responseGuidanceMaxChars,
9683
+ maxSearchResults:
9684
+ this.getRecallSectionNumber("response-guidance", "maxResults") ??
9685
+ this.config.responseGuidanceRecallMaxResults,
9686
+ maxScanWindowTurns:
9687
+ this.getRecallSectionNumber("response-guidance", "maxTurns") ??
9688
+ this.config.responseGuidanceRecallScanWindowTurns,
9689
+ maxScanWindowTokens:
9690
+ this.getRecallSectionNumber("response-guidance", "maxTokens") ??
9691
+ this.config.responseGuidanceRecallScanWindowTokens,
9692
+ forceGeneric: responseGuidanceForcedByPipeline,
9693
+ }),
9694
+ (s) => !s,
9695
+ "",
9696
+ );
9519
9697
  if (responseGuidanceSection) {
9520
9698
  this.appendRecallSection(
9521
9699
  sectionBuckets,
@@ -9544,21 +9722,29 @@ export class Orchestrator {
9544
9722
  shouldRecallEventOrderEvidence(retrievalQuery)
9545
9723
  ) {
9546
9724
  try {
9547
- const eventOrderSection = await buildEventOrderRecallSection({
9548
- engine: this.lcmEngine,
9549
- sessionId: sessionKey,
9550
- query: retrievalQuery,
9551
- maxChars: eventOrderMaxChars,
9552
- maxItems:
9553
- this.getRecallSectionNumber("event-order", "maxResults") ??
9554
- this.config.eventOrderRecallMaxResults,
9555
- maxScanWindowTurns:
9556
- this.getRecallSectionNumber("event-order", "maxTurns") ??
9557
- this.config.eventOrderRecallScanWindowTurns,
9558
- maxScanWindowTokens:
9559
- this.getRecallSectionNumber("event-order", "maxTokens") ??
9560
- this.config.eventOrderRecallScanWindowTokens,
9561
- });
9725
+ const eventOrderSection = await firstNonEmptyLcmRead(
9726
+ (lcmSessionId) =>
9727
+ buildEventOrderRecallSection({
9728
+ engine: this.lcmEngine,
9729
+ // #1495 thread 3 + #1505 fallback unification: read across the
9730
+ // ordered LCM read key set so a branch-scoped session reads its own
9731
+ // chronological event-order evidence even at project/root scope.
9732
+ sessionId: lcmSessionId,
9733
+ query: retrievalQuery,
9734
+ maxChars: eventOrderMaxChars,
9735
+ maxItems:
9736
+ this.getRecallSectionNumber("event-order", "maxResults") ??
9737
+ this.config.eventOrderRecallMaxResults,
9738
+ maxScanWindowTurns:
9739
+ this.getRecallSectionNumber("event-order", "maxTurns") ??
9740
+ this.config.eventOrderRecallScanWindowTurns,
9741
+ maxScanWindowTokens:
9742
+ this.getRecallSectionNumber("event-order", "maxTokens") ??
9743
+ this.config.eventOrderRecallScanWindowTokens,
9744
+ }),
9745
+ (s) => !s,
9746
+ "",
9747
+ );
9562
9748
  if (eventOrderSection) {
9563
9749
  this.appendRecallSection(
9564
9750
  sectionBuckets,
@@ -9732,9 +9918,20 @@ export class Orchestrator {
9732
9918
  (recallMode as RecallPlanMode) !== "no_recall"
9733
9919
  ) {
9734
9920
  try {
9735
- const structuredMatches = await this.lcmEngine.searchStructuredParts(
9736
- sessionKey ?? "default",
9737
- retrievalQuery,
9921
+ const structuredMatches = await firstNonEmptyLcmRead(
9922
+ (lcmSessionId) =>
9923
+ this.lcmEngine!.searchStructuredParts(
9924
+ // #1495 + #1505 fallback unification: read across the ordered LCM
9925
+ // read key set so a branch-scoped session reads its own structured
9926
+ // message-part evidence even when archived at project/root scope.
9927
+ // Structured parts are inherently per-session (the DAG is keyed by
9928
+ // session_id), so a SESSIONLESS read (`undefined`) normalizes to
9929
+ // empty → no section, the correct pre-#1505 behavior (codex P2).
9930
+ lcmSessionId ?? "",
9931
+ retrievalQuery,
9932
+ ),
9933
+ (matches) => matches.length === 0,
9934
+ [],
9738
9935
  );
9739
9936
  const structuredSection = this.lcmEngine.formatStructuredRecall(
9740
9937
  structuredMatches,
@@ -9758,9 +9955,20 @@ export class Orchestrator {
9758
9955
  }
9759
9956
  }
9760
9957
  }
9761
- const lcmSection = await this.lcmEngine.assembleRecall(
9762
- sessionKey ?? "default",
9763
- this.config.recallBudgetChars,
9958
+ const lcmSection = await firstNonEmptyLcmRead(
9959
+ (lcmSessionId) =>
9960
+ this.lcmEngine!.assembleRecall(
9961
+ // #1495 + #1505 fallback unification: read across the ordered LCM
9962
+ // read key set so a branch-scoped session reads its own
9963
+ // compressed-history evidence even at project/root scope.
9964
+ // Compressed history is inherently per-session (a per-session DAG),
9965
+ // so a SESSIONLESS read (`undefined`) normalizes to empty → no
9966
+ // section, the correct pre-#1505 behavior (codex P2).
9967
+ lcmSessionId ?? "",
9968
+ this.config.recallBudgetChars,
9969
+ ),
9970
+ (s) => !s,
9971
+ "",
9764
9972
  );
9765
9973
  if (lcmSection) {
9766
9974
  this.appendRecallSection(
@@ -11235,6 +11443,28 @@ export class Orchestrator {
11235
11443
  deadlineMs?: number;
11236
11444
  archiveLcm?: boolean;
11237
11445
  abortSignal?: AbortSignal;
11446
+ /**
11447
+ * Pin extraction writes to this namespace instead of deriving one from
11448
+ * `defaultNamespaceForPrincipal(resolvePrincipal(sessionKey))` + the
11449
+ * coding overlay (#1495). The access `observe` surface resolves a single
11450
+ * effective scope plan and passes its `writeNamespace` here so the
11451
+ * extracted memories land in the SAME namespace as LCM archival,
11452
+ * objective-state snapshots, and project-scoped recall — without relying
11453
+ * on re-deriving the namespace from a namespace-prefixed session key.
11454
+ * Same hook bulk-import uses (#460).
11455
+ */
11456
+ writeNamespaceOverride?: string;
11457
+ /**
11458
+ * Pin the provenance PRINCIPAL instead of deriving it from
11459
+ * `resolvePrincipal(turn.sessionKey)` (#1495 thread 1). The access
11460
+ * `observe` surface authenticates the caller at the transport layer and
11461
+ * passes its resolved principal here so extracted-memory provenance uses
11462
+ * the SAME identity the surface authorized — independent of storage
11463
+ * routing (`writeNamespaceOverride`) and of whatever `resolvePrincipal`
11464
+ * would parse from the raw session key. Mirrors the recall path's
11465
+ * `principalOverride` (issue #570 PR 4).
11466
+ */
11467
+ principalOverride?: string;
11238
11468
  } = {},
11239
11469
  ): Promise<void> {
11240
11470
  if (!Array.isArray(turns) || turns.length === 0) return;
@@ -11321,6 +11551,8 @@ export class Orchestrator {
11321
11551
  bufferKey,
11322
11552
  extractionDeadlineMs: options.deadlineMs,
11323
11553
  abortSignal: options.abortSignal,
11554
+ writeNamespaceOverride: options.writeNamespaceOverride,
11555
+ principalOverride: options.principalOverride,
11324
11556
  onTaskSettled: (err) => (err ? reject(err) : resolve()),
11325
11557
  }).catch(reject);
11326
11558
  }),
@@ -11717,6 +11949,12 @@ export class Orchestrator {
11717
11949
  * regardless of user-configured principal routing rules.
11718
11950
  */
11719
11951
  writeNamespaceOverride?: string;
11952
+ /**
11953
+ * Pin the provenance principal (#1495 thread 1). Forwarded to
11954
+ * `runExtraction` so access `observe` can record provenance under the
11955
+ * authenticated principal instead of `resolvePrincipal(sessionKey)`.
11956
+ */
11957
+ principalOverride?: string;
11720
11958
  } = {},
11721
11959
  ): Promise<void> {
11722
11960
  const bufferKey = options.bufferKey ?? turnsToExtract[0]?.sessionKey ?? "default";
@@ -11790,6 +12028,7 @@ export class Orchestrator {
11790
12028
  abortSignal: options.abortSignal,
11791
12029
  failOnExtractionFailure: options.failOnExtractionFailure === true,
11792
12030
  writeNamespaceOverride: options.writeNamespaceOverride,
12031
+ principalOverride: options.principalOverride,
11793
12032
  });
11794
12033
  settleTask(undefined, result);
11795
12034
  } catch (err) {
@@ -11946,6 +12185,14 @@ export class Orchestrator {
11946
12185
  * for provenance; only the storage target is overridden.
11947
12186
  */
11948
12187
  writeNamespaceOverride?: string;
12188
+ /**
12189
+ * Pin the provenance principal instead of deriving it from
12190
+ * `resolvePrincipal(sessionKey)` (#1495 thread 1). When set, this is the
12191
+ * identity an access surface already authenticated; used so observed-turn
12192
+ * provenance is correct even though `turn.sessionKey` is the ORIGINAL
12193
+ * (un-prefixed) key and storage is pinned via `writeNamespaceOverride`.
12194
+ */
12195
+ principalOverride?: string;
11949
12196
  } = {},
11950
12197
  ): Promise<ExtractionRunResult> {
11951
12198
  log.debug(`running extraction on ${turns.length} turns`);
@@ -12039,7 +12286,18 @@ export class Orchestrator {
12039
12286
  };
12040
12287
  }
12041
12288
 
12042
- const principal = resolvePrincipal(sessionKey, this.config);
12289
+ // Provenance principal honours the access-surface override (#1495 thread 1,
12290
+ // mirroring the recall path's `principalOverride`, issue #570 PR 4). Access
12291
+ // surfaces that authenticated the caller at the transport layer pass their
12292
+ // resolved principal so provenance uses the SAME identity the surface
12293
+ // authorized, instead of `resolvePrincipal(sessionKey)` — which on a
12294
+ // namespace-prefixed key would collapse to `default`. The ORIGINAL,
12295
+ // un-prefixed session key still drives threading.
12296
+ const principal =
12297
+ typeof options.principalOverride === "string" &&
12298
+ options.principalOverride.length > 0
12299
+ ? options.principalOverride
12300
+ : resolvePrincipal(sessionKey, this.config);
12043
12301
  // Write path — overlay the coding-agent namespace (issue #569) when the
12044
12302
  // session has a codingContext and `codingMode.projectScope` is true.
12045
12303
  // Explicit `writeNamespaceOverride` from callers still wins, matching