@remnic/core 9.3.676 → 9.3.678

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 (92) hide show
  1. package/dist/access-cli.js +21 -21
  2. package/dist/access-http.js +16 -16
  3. package/dist/access-mcp.js +15 -15
  4. package/dist/access-schema.js +3 -3
  5. package/dist/access-service.js +13 -13
  6. package/dist/capabilities.d.ts +29 -1
  7. package/dist/capabilities.js +5 -3
  8. package/dist/causal-behavior.js +2 -2
  9. package/dist/causal-chain.js +2 -2
  10. package/dist/causal-consolidation.js +2 -2
  11. package/dist/causal-retrieval.js +2 -2
  12. package/dist/causal-trajectory-graph.js +1 -1
  13. package/dist/causal-trajectory.js +1 -1
  14. package/dist/{chunk-SDLJ2W7S.js → chunk-2AP4QJX5.js} +4 -4
  15. package/dist/{chunk-Y56J7CXW.js → chunk-2LDBXPLB.js} +10 -5
  16. package/dist/chunk-2LDBXPLB.js.map +1 -0
  17. package/dist/{chunk-ZLINDOBG.js → chunk-4PPMUNV5.js} +5 -5
  18. package/dist/{chunk-Q5ZU3RNY.js → chunk-57ME5VSI.js} +4 -4
  19. package/dist/{chunk-R37A3BEW.js → chunk-7AAKSHDG.js} +147 -122
  20. package/dist/chunk-7AAKSHDG.js.map +1 -0
  21. package/dist/{chunk-T2AOOHDA.js → chunk-ACYX37IM.js} +2 -2
  22. package/dist/{chunk-DOCTITOP.js → chunk-DGEZKYVI.js} +4 -4
  23. package/dist/{chunk-Q6MIDQEL.js → chunk-EQYP3HA6.js} +2 -2
  24. package/dist/{chunk-52LZ42LI.js → chunk-ERA5RSMZ.js} +1 -1
  25. package/dist/{chunk-SF45RQDX.js → chunk-K2JYO6QV.js} +4 -4
  26. package/dist/{chunk-IPLYGWQF.js → chunk-KQAFEZQX.js} +5 -5
  27. package/dist/{chunk-RI5XBIZ6.js → chunk-PBH4JUAB.js} +14 -2
  28. package/dist/{chunk-RI5XBIZ6.js.map → chunk-PBH4JUAB.js.map} +1 -1
  29. package/dist/{chunk-XVVEKF5I.js → chunk-PCGCQTU6.js} +22 -22
  30. package/dist/{chunk-B6IUW76R.js → chunk-Q2H5U37U.js} +1 -1
  31. package/dist/{chunk-WXGTC424.js → chunk-RC3AFF6Z.js} +1 -1
  32. package/dist/{chunk-HQCGRSRU.js → chunk-SECQS4G4.js} +2 -2
  33. package/dist/{chunk-QLRYXOAD.js → chunk-UDJLF3BO.js} +2 -2
  34. package/dist/{chunk-OG7A6AZX.js → chunk-UNZLU2MX.js} +4 -4
  35. package/dist/{chunk-B55KFEGS.js → chunk-YJ4J2JJ2.js} +10 -10
  36. package/dist/{chunk-NE566K4E.js → chunk-YXWAILM4.js} +2 -2
  37. package/dist/{chunk-OUWAQVDJ.js → chunk-Z6SEG36L.js} +4 -4
  38. package/dist/cli.js +25 -25
  39. package/dist/{coding-graph-types-Dd2tGrnm.d.ts → coding/coding-graph-types.d.ts} +1 -1
  40. package/dist/coding/coding-graph-types.js +10 -0
  41. package/dist/coding/coding-graph-types.js.map +1 -0
  42. package/dist/coding/optional-coding-graph.d.ts +2 -2
  43. package/dist/coding/optional-coding-graph.js +1 -1
  44. package/dist/compounding/engine.js +1 -1
  45. package/dist/contradiction/index.js +4 -4
  46. package/dist/{graph-edge-decay-PUFNHOBS.js → graph-edge-decay-KSVJGCZW.js} +2 -2
  47. package/dist/graph-snapshot.js +2 -2
  48. package/dist/graph.d.ts +11 -0
  49. package/dist/graph.js +1 -1
  50. package/dist/index.d.ts +1 -1
  51. package/dist/index.js +38 -38
  52. package/dist/lcm/index.js +3 -3
  53. package/dist/namespaces/migrate.js +8 -8
  54. package/dist/namespaces/search.js +7 -7
  55. package/dist/operator-toolkit.js +10 -10
  56. package/dist/orchestrator.js +17 -17
  57. package/dist/search/factory.js +6 -6
  58. package/dist/search/index.js +11 -11
  59. package/dist/search/lancedb-backend.js +2 -2
  60. package/dist/search/meilisearch-backend.js +2 -2
  61. package/dist/search/orama-backend.js +2 -2
  62. package/dist/transfer/autodetect.js +1 -1
  63. package/dist/transfer/backup.js +1 -1
  64. package/dist/transfer/capsule-export.js +2 -2
  65. package/package.json +7 -2
  66. package/src/capabilities.test.ts +173 -0
  67. package/src/capabilities.ts +61 -0
  68. package/src/graph.ts +20 -4
  69. package/src/orchestrator.ts +85 -208
  70. package/src/scopes/scope-plan.test.ts +360 -0
  71. package/src/scopes/scope-plan.ts +320 -0
  72. package/dist/chunk-R37A3BEW.js.map +0 -1
  73. package/dist/chunk-Y56J7CXW.js.map +0 -1
  74. /package/dist/{chunk-SDLJ2W7S.js.map → chunk-2AP4QJX5.js.map} +0 -0
  75. /package/dist/{chunk-ZLINDOBG.js.map → chunk-4PPMUNV5.js.map} +0 -0
  76. /package/dist/{chunk-Q5ZU3RNY.js.map → chunk-57ME5VSI.js.map} +0 -0
  77. /package/dist/{chunk-T2AOOHDA.js.map → chunk-ACYX37IM.js.map} +0 -0
  78. /package/dist/{chunk-DOCTITOP.js.map → chunk-DGEZKYVI.js.map} +0 -0
  79. /package/dist/{chunk-Q6MIDQEL.js.map → chunk-EQYP3HA6.js.map} +0 -0
  80. /package/dist/{chunk-52LZ42LI.js.map → chunk-ERA5RSMZ.js.map} +0 -0
  81. /package/dist/{chunk-SF45RQDX.js.map → chunk-K2JYO6QV.js.map} +0 -0
  82. /package/dist/{chunk-IPLYGWQF.js.map → chunk-KQAFEZQX.js.map} +0 -0
  83. /package/dist/{chunk-XVVEKF5I.js.map → chunk-PCGCQTU6.js.map} +0 -0
  84. /package/dist/{chunk-B6IUW76R.js.map → chunk-Q2H5U37U.js.map} +0 -0
  85. /package/dist/{chunk-WXGTC424.js.map → chunk-RC3AFF6Z.js.map} +0 -0
  86. /package/dist/{chunk-HQCGRSRU.js.map → chunk-SECQS4G4.js.map} +0 -0
  87. /package/dist/{chunk-QLRYXOAD.js.map → chunk-UDJLF3BO.js.map} +0 -0
  88. /package/dist/{chunk-OG7A6AZX.js.map → chunk-UNZLU2MX.js.map} +0 -0
  89. /package/dist/{chunk-B55KFEGS.js.map → chunk-YJ4J2JJ2.js.map} +0 -0
  90. /package/dist/{chunk-NE566K4E.js.map → chunk-YXWAILM4.js.map} +0 -0
  91. /package/dist/{chunk-OUWAQVDJ.js.map → chunk-Z6SEG36L.js.map} +0 -0
  92. /package/dist/{graph-edge-decay-PUFNHOBS.js.map → graph-edge-decay-KSVJGCZW.js.map} +0 -0
@@ -245,7 +245,12 @@ import {
245
245
  type TrustZoneSearchResult,
246
246
  } from "./trust-zones.js";
247
247
  import { tryDirectAnswer, type DirectAnswerSources } from "./direct-answer-wiring.js";
248
- import { resolveCapabilities, type CapabilitySet } from "./capabilities.js";
248
+ import {
249
+ resolveCapabilities,
250
+ resolveGraphConstructionCapabilities,
251
+ type CapabilitySet,
252
+ type GraphConstructionCapabilitySet,
253
+ } from "./capabilities.js";
249
254
  import { DEFAULT_TAXONOMY } from "./taxonomy/index.js";
250
255
  import {
251
256
  searchHarmonicRetrieval,
@@ -325,6 +330,7 @@ import {
325
330
  recallNamespacesForPrincipal,
326
331
  resolvePrincipal,
327
332
  } from "./namespaces/principal.js";
333
+ import { resolveScopePlan } from "./scopes/scope-plan.js";
328
334
  import {
329
335
  expandScopeProfileReadNamespaces,
330
336
  resolveScopeProfilePlan,
@@ -5755,6 +5761,7 @@ export class Orchestrator {
5755
5761
  // entry, and thread the frozen set down (issue #1523). Never re-read the
5756
5762
  // migrated flags off `this.config` mid-operation.
5757
5763
  const caps = resolveCapabilities(this.config);
5764
+ const graphCaps = resolveGraphConstructionCapabilities(this.config); // #1566 Cluster A
5758
5765
  const abortController = new AbortController();
5759
5766
  const onAbort = () => {
5760
5767
  abortController.abort();
@@ -5770,7 +5777,8 @@ export class Orchestrator {
5770
5777
  options.principalOverride.length > 0
5771
5778
  ? options.principalOverride
5772
5779
  : resolvePrincipal(sessionKey, this.config);
5773
- if (this.config.namespacesEnabled && !principal) {
5780
+ const namespacesEnabled = this.config.namespacesEnabled;
5781
+ if (namespacesEnabled && !principal) {
5774
5782
  throw new Error("authentication required: namespaces are enabled and no principal was supplied");
5775
5783
  }
5776
5784
 
@@ -5830,7 +5838,7 @@ export class Orchestrator {
5830
5838
  const recallPromise = this.recallInternal(prompt, sessionKey, {
5831
5839
  ...options,
5832
5840
  abortSignal: abortController.signal,
5833
- }, caps);
5841
+ }, caps, graphCaps);
5834
5842
  const RECALL_TIMEOUT_MS = this.config.recallOuterTimeoutMs ?? 75_000;
5835
5843
  if (RECALL_TIMEOUT_MS <= 0) {
5836
5844
  return await recallPromise;
@@ -5862,6 +5870,7 @@ export class Orchestrator {
5862
5870
  options.namespace?.trim() || undefined,
5863
5871
  options.principalOverride,
5864
5872
  caps,
5873
+ namespacesEnabled,
5865
5874
  );
5866
5875
  } catch (err) {
5867
5876
  log.debug(`direct-answer observation setup failed: ${err}`);
@@ -5931,62 +5940,27 @@ export class Orchestrator {
5931
5940
  namespaceOverride: string | undefined,
5932
5941
  principalOverride: string | undefined,
5933
5942
  caps: CapabilitySet,
5943
+ namespacesEnabled: boolean,
5934
5944
  ): void {
5935
5945
  const expectedSnapshot = this.lastRecall.get(sessionKey);
5936
5946
  if (expectedSnapshot === null) return;
5937
5947
  if (expectedSnapshot.plannerMode === "no_recall") return;
5938
5948
 
5939
- const principal = principalOverride ?? resolvePrincipal(sessionKey, this.config);
5940
- // Coding-agent overlay (issue #569) is applied when the session has a
5941
- // coding context and there is no explicit namespaceOverride mirrors
5942
- // the main recall path above.
5943
- const observationCodingOverlay =
5944
- namespaceOverride && canReadNamespace(principal, namespaceOverride, this.config)
5945
- ? null
5946
- : this.applyCodingRecallOverlay(sessionKey);
5947
- const observationPrincipalSelf = defaultNamespaceForPrincipal(principal, this.config);
5948
- const observationCodingSelf = observationCodingOverlay
5949
- ? combineNamespaces(observationPrincipalSelf, observationCodingOverlay.namespace)
5950
- : null;
5951
- const observationScopeProfilePlan =
5952
- namespaceOverride && canReadNamespace(principal, namespaceOverride, this.config)
5953
- ? null
5954
- : resolveScopeProfilePlan({
5955
- config: this.config,
5956
- principal,
5957
- codingContext: sessionKey
5958
- ? this.getCodingContextForSession(sessionKey)
5959
- : null,
5960
- codingOverlay: observationCodingOverlay,
5961
- });
5962
- let observationNamespaces: string[];
5963
- if (namespaceOverride && canReadNamespace(principal, namespaceOverride, this.config)) {
5964
- observationNamespaces = [namespaceOverride];
5965
- } else if (observationScopeProfilePlan) {
5966
- observationNamespaces = expandScopeProfileReadNamespaces({
5967
- profilePlan: observationScopeProfilePlan,
5968
- principalSelfNamespace: observationScopeProfilePlan.baseNamespace,
5969
- config: this.config,
5970
- principal,
5971
- codingOverlay: observationCodingOverlay,
5972
- legacyRecallNamespaces: recallNamespacesForPrincipal(principal, this.config),
5973
- });
5974
- } else if (observationCodingOverlay && observationCodingSelf) {
5975
- // Rule 42 / parity with the main recall path: substitute the self
5976
- // namespace within the principal's recall list rather than
5977
- // replacing the full list. Preserves shared and policy-include
5978
- // namespaces for direct-answer observation queries.
5979
- const base = recallNamespacesForPrincipal(principal, this.config);
5980
- const mapped = base.map((ns) =>
5981
- ns === observationPrincipalSelf ? observationCodingSelf : ns,
5982
- );
5983
- const fallbackNs = observationCodingOverlay.readFallbacks.map((fallback) =>
5984
- combineNamespaces(observationPrincipalSelf, fallback),
5985
- );
5986
- observationNamespaces = Array.from(new Set<string>([...mapped, ...fallbackNs]));
5987
- } else {
5988
- observationNamespaces = recallNamespacesForPrincipal(principal, this.config);
5989
- }
5949
+ // Resolve the observation namespace set through the SAME ScopePlan resolver
5950
+ // the main recall path uses (#1521). The observe path does NOT throw on an
5951
+ // unreadable override (it falls through to the coding/legacy branches), so
5952
+ // we skip the readability gate the recall path enforces.
5953
+ const observationScopePlan = resolveScopePlan({
5954
+ config: this.config,
5955
+ sessionKey,
5956
+ namespace: namespaceOverride,
5957
+ principalOverride,
5958
+ codingContext: sessionKey
5959
+ ? this.getCodingContextForSession(sessionKey)
5960
+ : null,
5961
+ namespacesEnabled,
5962
+ });
5963
+ const observationNamespaces = observationScopePlan.readNamespaces;
5990
5964
  const observationQueryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
5991
5965
  cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
5992
5966
  cronRecallNormalizedQueryMaxChars:
@@ -7311,6 +7285,7 @@ export class Orchestrator {
7311
7285
  sessionKey?: string,
7312
7286
  options: RecallInvocationOptions = {},
7313
7287
  caps: CapabilitySet = resolveCapabilities(this.config),
7288
+ graphCaps: GraphConstructionCapabilitySet = resolveGraphConstructionCapabilities(this.config),
7314
7289
  ): Promise<string> {
7315
7290
  const recallStart = Date.now();
7316
7291
  // Backend degradations observed by this recall's QMD searches (#1536):
@@ -7472,7 +7447,7 @@ export class Orchestrator {
7472
7447
  const recallModeDecisionOptions = {
7473
7448
  plannerEnabled: caps.recallPlanner,
7474
7449
  graphRecallEnabled: caps.graphRecall,
7475
- multiGraphMemoryEnabled: this.config.multiGraphMemoryEnabled,
7450
+ multiGraphMemoryEnabled: graphCaps.multiGraphMemory,
7476
7451
  graphExpandedIntentEnabled: caps.graphExpandedIntent,
7477
7452
  prompt,
7478
7453
  };
@@ -7587,14 +7562,11 @@ export class Orchestrator {
7587
7562
  && options.principalOverride.length > 0
7588
7563
  ? options.principalOverride
7589
7564
  : resolvePrincipal(sessionKey, this.config);
7590
- if (this.config.namespacesEnabled && !principal) {
7565
+ const namespacesEnabled = this.config.namespacesEnabled;
7566
+ if (namespacesEnabled && !principal) {
7591
7567
  throw new Error("authentication required: namespaces are enabled and no principal was supplied");
7592
7568
  }
7593
7569
  const namespaceOverride = options.namespace?.trim() || undefined;
7594
- const readableRecallNamespaces = recallNamespacesForPrincipal(
7595
- principal,
7596
- this.config,
7597
- );
7598
7570
  if (
7599
7571
  namespaceOverride &&
7600
7572
  !canReadNamespace(principal, namespaceOverride, this.config)
@@ -7603,147 +7575,35 @@ export class Orchestrator {
7603
7575
  `namespace override is not readable: ${namespaceOverride}`,
7604
7576
  );
7605
7577
  }
7606
- // Recall path overlay the coding-agent namespace (issue #569) when
7607
- // the session has a codingContext and `codingMode.projectScope` is true.
7608
- // Explicit `namespace` option still wins, preserving pre-#569 semantics.
7609
- //
7610
- // Rule 42: the overlay substitutes the SELF namespace within the
7611
- // principal's recall list — it does NOT replace the full list. Shared
7612
- // and `includeInRecallByDefault` policy namespaces stay in the recall
7613
- // set so coding sessions continue to see team/shared memories. The
7614
- // overlay is combined with the principal base through `combineNamespaces`
7615
- // to preserve principal isolation (cross-tenant leakage guard).
7616
- const codingOverlay = namespaceOverride ? null : this.applyCodingRecallOverlay(sessionKey);
7617
- const principalSelfNamespace = defaultNamespaceForPrincipal(principal, this.config);
7618
- const codingSelfNamespace = codingOverlay
7619
- ? combineNamespaces(principalSelfNamespace, codingOverlay.namespace)
7620
- : null;
7621
- const scopeProfilePlan = namespaceOverride
7622
- ? null
7623
- : resolveScopeProfilePlan({
7624
- config: this.config,
7625
- principal,
7626
- codingContext: sessionKey
7627
- ? this.getCodingContextForSession(sessionKey)
7628
- : null,
7629
- codingOverlay,
7630
- });
7631
- const profileEffectiveNamespace = scopeProfilePlan?.writeNamespace || scopeProfilePlan?.readNamespaces[0];
7632
- const selfNamespace =
7633
- namespaceOverride ??
7634
- profileEffectiveNamespace ??
7635
- codingSelfNamespace ??
7636
- principalSelfNamespace;
7637
- let recallNamespaces: string[];
7638
- if (namespaceOverride) {
7639
- recallNamespaces = [namespaceOverride];
7640
- } else if (scopeProfilePlan) {
7641
- recallNamespaces = expandScopeProfileReadNamespaces({
7642
- profilePlan: scopeProfilePlan,
7643
- principalSelfNamespace: scopeProfilePlan.baseNamespace,
7644
- config: this.config,
7645
- principal,
7646
- codingOverlay,
7647
- legacyRecallNamespaces: readableRecallNamespaces,
7648
- });
7649
- } else if (codingOverlay && codingSelfNamespace) {
7650
- // Substitute the principal's self namespace with the coding-scoped
7651
- // one, and append any read fallbacks (branch→project, PR 3) combined
7652
- // with the principal base so principal isolation is preserved on
7653
- // fallback entries as well.
7654
- const mapped = readableRecallNamespaces.map((ns) =>
7655
- ns === principalSelfNamespace ? codingSelfNamespace : ns,
7656
- );
7657
- const fallbackNs = codingOverlay.readFallbacks.map((fallback) =>
7658
- combineNamespaces(principalSelfNamespace, fallback),
7659
- );
7660
- recallNamespaces = Array.from(new Set<string>([...mapped, ...fallbackNs]));
7661
- } else {
7662
- recallNamespaces = readableRecallNamespaces;
7663
- }
7664
- // Catalog touch (issue #1499): record reads against the recalled namespaces
7665
- // so the catalog reflects active read scopes. Best-effort, failure-tolerant.
7666
- // Round 3 (codex P2): gate behind the no_recall guard — when the planner
7667
- // selects `no_recall` retrieval is skipped entirely (see the early return at
7668
- // `recallMode === "no_recall"` below), so marking every readable namespace as
7669
- // read would falsely inflate `lastReadAt` / catalog recency.
7670
- // Round 4 (codex P2): also skip when the effective memory result limit is
7671
- // zero (`topK: 0`, a disabled/zero `memories` recall section, etc.). The QMD
7672
- // path explicitly returns before searching when `recallResultLimit <= 0`, so
7673
- // no namespace is actually read and the touch would be spurious.
7674
- // NOTE: the catalog read touch is recorded LATER, immediately after the
7675
- // Phase 1 `throwIfRecallAborted` gate (round 6, codex P2 / cursor Medium —
7676
- // NDXHa/NDmle), so it fires only once retrieval is actually about to run.
7677
- // Recording it here (recall entry) would set `lastReadAt` for recalls that
7678
- // are aborted, error out, or short-circuit before any QMD/filesystem read.
7679
-
7680
- // Effective LCM read NAMESPACE SET (#1505 thread "Include coding fallback
7681
- // namespaces in LCM reads"). `observe` archives LCM / structured history
7682
- // under `${effectiveNamespace}:${sessionKey}` for whichever namespace was
7683
- // effective at write time. A branch-scoped session whose evidence was
7684
- // archived at project / root scope must still surface it, exactly as normal
7685
- // QMD/file recall does — QMD/file recall searches the primary overlay key AND
7686
- // `codingOverlay.readFallbacks` (project / root), NOT just the primary
7687
- // overlay key. The prior single `lcmReadSessionId` only targeted the primary
7688
- // overlay, so branch-scoped sessions missed fallback LCM evidence.
7578
+ // Resolve every namespace-bearing field through ONE ScopePlan (#1521): the
7579
+ // read set, the LCM read keys, the coding overlay, and the scope-profile plan
7580
+ // all come from a single pure resolver that delegates to the same helpers
7581
+ // the inline code used. Parity snapshots in scope-plan.test.ts pin the
7582
+ // outputs so this migration cannot change behavior.
7689
7583
  //
7690
- // READ-AUTHORIZATION (preserved from the prior round's
7691
- // `lcmReadNamespaceForSession` gate; rule 39 / 42 / 48): the coding-overlay
7692
- // namespace AND its fallbacks are `<principal>-project-*` sub-namespaces of
7693
- // the principal SELF base, authorized transitively by that base. They are
7694
- // included ONLY when the principal self base is in the readable recall set
7695
- // (`readableRecallNamespaces` — gated by `defaultRecallNamespaces.includes
7696
- // ("self")` AND `canReadNamespace`). When the self base is NOT readable (e.g.
7697
- // a write-only / self-omitted principal), the overlay rows are unauthorized
7698
- // for this reader, so the LCM read collapses to the default store — exactly
7699
- // what an unqualified, unauthorized recall resolves to — and NEVER searches a
7700
- // `<principal>-project-*` key (no cross-tenant read leak). This mirrors what
7701
- // the rest of recall surfaces for such a principal (its readable
7702
- // shared/policy namespaces have no per-session LCM key, so they contribute
7703
- // nothing here). `recallNamespaces` itself appends fallbacks unconditionally
7704
- // for QMD/file recall; the LCM read keys apply the stricter, self-base gate
7705
- // so the prior round's authorization invariant is preserved.
7706
- const codingOverlaySelfReadable =
7707
- codingOverlay !== null &&
7708
- (scopeProfilePlan
7709
- ? scopeProfilePlan.layers.some((layer) => layer.id === "userProject" && layer.readable)
7710
- : readableRecallNamespaces.includes(principalSelfNamespace));
7711
- let lcmReadNamespaces: string[];
7712
- if (namespaceOverride) {
7713
- // Explicit namespace already read-authorized above (canReadNamespace gate).
7714
- lcmReadNamespaces = [namespaceOverride];
7715
- } else if (scopeProfilePlan) {
7716
- // Scope profiles define a layered read stack; LCM-backed evidence uses the
7717
- // same namespace set as QMD/file recall so team/global/shared observations
7718
- // are not silently skipped.
7719
- lcmReadNamespaces = recallNamespaces;
7720
- } else if (codingOverlay && codingSelfNamespace && codingOverlaySelfReadable) {
7721
- // Self base readable → overlay rows authorized. Read the primary overlay
7722
- // key first, then each coding read fallback (project → root), combined with
7723
- // the principal base for isolation — the SAME ordered set QMD/file recall
7724
- // searches for this authorized coding session.
7725
- const fallbackNs = codingOverlay.readFallbacks.map((fallback) =>
7726
- combineNamespaces(principalSelfNamespace, fallback),
7727
- );
7728
- lcmReadNamespaces = [codingSelfNamespace, ...fallbackNs];
7729
- } else {
7730
- // No overlay, OR overlay present but self base unreadable → collapse to the
7731
- // default store (raw sessionKey), exactly as the prior round did. No
7732
- // `<principal>-project-*` overlay key is searched.
7733
- lcmReadNamespaces = [this.config.defaultNamespace];
7734
- }
7735
- // Map the ordered, read-authorized namespace set → ordered, deduped LCM read
7736
- // session_id set. Single-user / no-overlay recall passes a single-namespace
7737
- // set that collapses to the raw `sessionKey`, so this is `[sessionKey]` —
7738
- // byte-for-byte the pre-#1495 single-key behavior.
7739
- const lcmReadSessionIds =
7740
- scopeProfilePlan && !sessionKey
7741
- ? []
7742
- : lcmReadSessionIdsForNamespaces(
7743
- lcmReadNamespaces,
7744
- sessionKey,
7745
- this.config.defaultNamespace,
7746
- );
7584
+ // Catalog read touch (issue #1499) is recorded LATER — after the Phase 1
7585
+ // abort gate so it fires only when retrieval actually runs, not for
7586
+ // aborted / short-circuited recalls.
7587
+ const scopePlan = resolveScopePlan({
7588
+ config: this.config,
7589
+ sessionKey,
7590
+ namespace: options.namespace,
7591
+ principalOverride:
7592
+ typeof options.principalOverride === "string"
7593
+ && options.principalOverride.length > 0
7594
+ ? options.principalOverride
7595
+ : undefined,
7596
+ codingContext: sessionKey
7597
+ ? this.getCodingContextForSession(sessionKey)
7598
+ : null,
7599
+ namespacesEnabled,
7600
+ });
7601
+ const {
7602
+ readNamespaces: recallNamespaces,
7603
+ baseNamespace: selfNamespace,
7604
+ scopeProfilePlan,
7605
+ lcmReadSessionIds,
7606
+ } = scopePlan;
7747
7607
  // Query an LCM-backed read across the ordered read key set and return the
7748
7608
  // FIRST non-empty result (#1505 fallback-namespace unification). The primary
7749
7609
  // overlay key is tried first; if a branch-scoped session has no rows under its
@@ -7825,7 +7685,7 @@ export class Orchestrator {
7825
7685
  shadowMode: graphDecisionShadowMode,
7826
7686
  qmdAvailable,
7827
7687
  graphRecallEnabled: caps.graphRecall,
7828
- multiGraphMemoryEnabled: this.config.multiGraphMemoryEnabled,
7688
+ multiGraphMemoryEnabled: graphCaps.multiGraphMemory,
7829
7689
  },
7830
7690
  });
7831
7691
 
@@ -11045,7 +10905,7 @@ export class Orchestrator {
11045
10905
  );
11046
10906
 
11047
10907
  const isFullModeGraphAssist =
11048
- this.config.multiGraphMemoryEnabled &&
10908
+ graphCaps.multiGraphMemory &&
11049
10909
  caps.graphAssistInFullMode &&
11050
10910
  recallMode === "full" &&
11051
10911
  memoryResults.length >=
@@ -11468,6 +11328,7 @@ export class Orchestrator {
11468
11328
  recallResultLimit,
11469
11329
  recallMode,
11470
11330
  caps,
11331
+ graphCaps,
11471
11332
  queryAwarePrefilter,
11472
11333
  abortSignal: options.abortSignal,
11473
11334
  onDegradation: (degradation) => {
@@ -11695,6 +11556,7 @@ export class Orchestrator {
11695
11556
  recallResultLimit,
11696
11557
  recallMode,
11697
11558
  caps,
11559
+ graphCaps,
11698
11560
  queryAwarePrefilter,
11699
11561
  abortSignal: options.abortSignal,
11700
11562
  onDegradation: (degradation) => {
@@ -11809,6 +11671,7 @@ export class Orchestrator {
11809
11671
  recallResultLimit,
11810
11672
  recallMode,
11811
11673
  caps,
11674
+ graphCaps,
11812
11675
  queryAwarePrefilter,
11813
11676
  abortSignal: options.abortSignal,
11814
11677
  onDegradation: (degradation) => {
@@ -11857,6 +11720,7 @@ export class Orchestrator {
11857
11720
  recallResultLimit,
11858
11721
  recallMode,
11859
11722
  caps,
11723
+ graphCaps,
11860
11724
  queryAwarePrefilter,
11861
11725
  abortSignal: options.abortSignal,
11862
11726
  onDegradation: (degradation) => {
@@ -14073,6 +13937,7 @@ export class Orchestrator {
14073
13937
  sourceContext?: { sessionKey?: string; principal?: string; validAt?: string },
14074
13938
  baseNamespace?: string,
14075
13939
  scopeProfileWritePlan?: ResolvedScopeProfilePlan | null,
13940
+ graphCaps: GraphConstructionCapabilitySet = resolveGraphConstructionCapabilities(this.config),
14076
13941
  ): Promise<string[]> {
14077
13942
  // Inline source attribution (issue #369). When enabled, every extracted
14078
13943
  // fact is rewritten to carry a compact provenance tag inside its body so
@@ -14734,7 +14599,7 @@ export class Orchestrator {
14734
14599
  allMemsForGraph: null,
14735
14600
  memoryPathById: new Map<string, string>(),
14736
14601
  };
14737
- if (this.config.multiGraphMemoryEnabled) {
14602
+ if (graphCaps.multiGraphMemory) {
14738
14603
  try {
14739
14604
  created.allMemsForGraph = await targetStorage.readAllMemories();
14740
14605
  for (const [id, relPath] of buildMemoryPathById(
@@ -14751,7 +14616,7 @@ export class Orchestrator {
14751
14616
  return created;
14752
14617
  };
14753
14618
  let threadEpisodeIdsForGraph: string[] | undefined;
14754
- if (this.config.multiGraphMemoryEnabled && threadIdForExtraction) {
14619
+ if (graphCaps.multiGraphMemory && threadIdForExtraction) {
14755
14620
  try {
14756
14621
  const thread = await this.threading.loadThread(threadIdForExtraction);
14757
14622
  threadEpisodeIdsForGraph = thread?.episodeIds
@@ -15591,7 +15456,7 @@ export class Orchestrator {
15591
15456
  });
15592
15457
  }
15593
15458
  // v8.2: graph edge building for chunked memories
15594
- if (this.config.multiGraphMemoryEnabled) {
15459
+ if (graphCaps.multiGraphMemory) {
15595
15460
  try {
15596
15461
  const graphContext = await ensureGraphContext(targetStorage);
15597
15462
  const entityRef =
@@ -15624,6 +15489,7 @@ export class Orchestrator {
15624
15489
  threadIdForExtraction ?? undefined,
15625
15490
  threadEpisodeIdsForGraph,
15626
15491
  graphContext.previousPersistedRelPath,
15492
+ graphCaps,
15627
15493
  );
15628
15494
  graphContext.previousPersistedRelPath = parentRelPath;
15629
15495
  } catch {
@@ -15783,7 +15649,7 @@ export class Orchestrator {
15783
15649
  source: extractionWriteSource,
15784
15650
  });
15785
15651
  // v8.2: graph edge building (fail-open — errors caught inside GraphIndex)
15786
- if (this.config.multiGraphMemoryEnabled) {
15652
+ if (graphCaps.multiGraphMemory) {
15787
15653
  try {
15788
15654
  const graphContext = await ensureGraphContext(targetStorage);
15789
15655
  const entityRef =
@@ -15816,6 +15682,7 @@ export class Orchestrator {
15816
15682
  threadIdForExtraction ?? undefined,
15817
15683
  threadEpisodeIdsForGraph,
15818
15684
  graphContext.previousPersistedRelPath,
15685
+ graphCaps,
15819
15686
  );
15820
15687
  graphContext.previousPersistedRelPath = memoryRelPath;
15821
15688
  } catch {
@@ -16087,6 +15954,7 @@ export class Orchestrator {
16087
15954
  threadIdForEdge: string | undefined,
16088
15955
  threadEpisodeIdsForGraph: string[] | undefined,
16089
15956
  fallbackCausalPredecessor: string | undefined,
15957
+ graphCaps: GraphConstructionCapabilitySet = resolveGraphConstructionCapabilities(this.config),
16090
15958
  ): Promise<void> {
16091
15959
  // Entity siblings: other memories sharing the same entityRef
16092
15960
  const entitySiblings: string[] = [];
@@ -16123,7 +15991,7 @@ export class Orchestrator {
16123
15991
  }
16124
15992
  if (
16125
15993
  recentInThread.length === 0 &&
16126
- this.config.graphWriteSessionAdjacencyEnabled !== false &&
15994
+ graphCaps.graphWriteSessionAdjacency &&
16127
15995
  fallbackCausalPredecessor &&
16128
15996
  fallbackCausalPredecessor !== memoryRelPath
16129
15997
  ) {
@@ -16140,6 +16008,12 @@ export class Orchestrator {
16140
16008
  recentInThread,
16141
16009
  entitySiblings,
16142
16010
  causalPredecessor,
16011
+ graphCapsOverride: {
16012
+ entityGraph: graphCaps.entityGraph,
16013
+ timeGraph: graphCaps.timeGraph,
16014
+ causalGraph: graphCaps.causalGraph,
16015
+ multiGraphMemory: graphCaps.multiGraphMemory,
16016
+ },
16143
16017
  });
16144
16018
  }
16145
16019
 
@@ -18755,6 +18629,8 @@ export class Orchestrator {
18755
18629
  * equivalent config-derived set — behavior-preserving.
18756
18630
  */
18757
18631
  caps?: CapabilitySet;
18632
+ /** Graph-construction gates resolved at recall entry (#1566 Cluster A). */
18633
+ graphCaps?: GraphConstructionCapabilitySet;
18758
18634
  queryAwarePrefilter?: QueryAwarePrefilter;
18759
18635
  abortSignal?: AbortSignal;
18760
18636
  /** Backend degradation observer — cold-tier QMD must report like hot (#1536). */
@@ -18777,6 +18653,7 @@ export class Orchestrator {
18777
18653
  // Prefer the threaded set; fall back to a config-derived set so direct
18778
18654
  // callers (unit tests) behave identically to the recall pipeline (#1523).
18779
18655
  const caps = options.caps ?? resolveCapabilities(this.config);
18656
+ const graphCaps = options.graphCaps ?? resolveGraphConstructionCapabilities(this.config);
18780
18657
  if (options.queryAwarePrefilter?.candidatePaths?.size === 0) {
18781
18658
  if (options.xrayPoolSizeSink) options.xrayPoolSizeSink.size = 0;
18782
18659
  return [];
@@ -18991,7 +18868,7 @@ export class Orchestrator {
18991
18868
 
18992
18869
  const isFullModeGraphAssist =
18993
18870
  this.config.qmdTierParityGraphEnabled &&
18994
- this.config.multiGraphMemoryEnabled &&
18871
+ graphCaps.multiGraphMemory &&
18995
18872
  caps.graphAssistInFullMode &&
18996
18873
  options.recallMode === "full" &&
18997
18874
  results.length >= Math.max(1, this.config.graphAssistMinSeedResults ?? 3);