@remnic/core 9.3.715 → 9.3.717

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 (81) hide show
  1. package/dist/access-boundary.d.ts +2 -2
  2. package/dist/access-boundary.js +4 -3
  3. package/dist/access-cli.js +13 -12
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +2 -2
  6. package/dist/access-http.js +8 -7
  7. package/dist/access-mcp.d.ts +2 -2
  8. package/dist/access-mcp.js +7 -6
  9. package/dist/access-operations-batch.js +5 -4
  10. package/dist/access-operations.d.ts +2 -2
  11. package/dist/access-operations.js +6 -5
  12. package/dist/{access-service-GgnA--CE.d.ts → access-service-d4Tk-Dlr.d.ts} +2 -2
  13. package/dist/access-service.d.ts +2 -2
  14. package/dist/access-service.js +3 -2
  15. package/dist/access-surface-catalog.d.ts +2 -2
  16. package/dist/bootstrap.d.ts +1 -1
  17. package/dist/{chunk-MECU6WSH.js → chunk-2B43BYYZ.js} +6 -6
  18. package/dist/chunk-2KF4HDG2.js +24 -0
  19. package/dist/chunk-2KF4HDG2.js.map +1 -0
  20. package/dist/{chunk-HHLWSWWF.js → chunk-BF2426TN.js} +2 -2
  21. package/dist/{chunk-RBHANGVX.js → chunk-BZAK3RBN.js} +2 -2
  22. package/dist/{chunk-YUJL27ET.js → chunk-CTUWGJRI.js} +40 -63
  23. package/dist/chunk-CTUWGJRI.js.map +1 -0
  24. package/dist/{chunk-6QUPUTT7.js → chunk-DZYARLJ7.js} +5 -9
  25. package/dist/{chunk-6QUPUTT7.js.map → chunk-DZYARLJ7.js.map} +1 -1
  26. package/dist/{chunk-CTAHKIG6.js → chunk-EM22PCQR.js} +7 -7
  27. package/dist/{chunk-SHRRWOVY.js → chunk-J6BH2OBA.js} +64 -21
  28. package/dist/chunk-J6BH2OBA.js.map +1 -0
  29. package/dist/{chunk-GM3BMWKR.js → chunk-KHQ433G5.js} +2 -2
  30. package/dist/{chunk-SYDHFFAW.js → chunk-OUDAU3T5.js} +4 -4
  31. package/dist/{chunk-OQH5XUH3.js → chunk-Q6IP2ARR.js} +2 -5
  32. package/dist/chunk-Q6IP2ARR.js.map +1 -0
  33. package/dist/{chunk-O62P2LFL.js → chunk-VZQXJNBD.js} +2 -2
  34. package/dist/{chunk-BNOAB5HH.js → chunk-YAINYEM3.js} +57 -48
  35. package/dist/chunk-YAINYEM3.js.map +1 -0
  36. package/dist/{chunk-3VUPWDSE.js → chunk-ZAVTEQFM.js} +4 -4
  37. package/dist/{chunk-IHMT6XTY.js → chunk-ZXLSNG3T.js} +3 -3
  38. package/dist/{cli-Do1YtfnH.d.ts → cli-insNLYs6.d.ts} +2 -2
  39. package/dist/cli.d.ts +3 -3
  40. package/dist/cli.js +12 -11
  41. package/dist/direct-answer-wiring.d.ts +9 -9
  42. package/dist/direct-answer-wiring.js +1 -1
  43. package/dist/explicit-capture.d.ts +1 -1
  44. package/dist/index.d.ts +4 -4
  45. package/dist/index.js +23 -22
  46. package/dist/index.js.map +1 -1
  47. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  48. package/dist/operator-toolkit.js +5 -3
  49. package/dist/orchestration/maintenance.js +5 -2
  50. package/dist/{orchestrator-BOaVstFG.d.ts → orchestrator-XPKhY2OV.d.ts} +6 -7
  51. package/dist/orchestrator.d.ts +1 -1
  52. package/dist/orchestrator.js +7 -6
  53. package/dist/recall-planner-llm.d.ts +1 -1
  54. package/dist/recall-planner-llm.js +2 -2
  55. package/dist/recall-planner-llm.js.map +1 -1
  56. package/dist/schemas.d.ts +22 -22
  57. package/dist/transfer/types.d.ts +12 -12
  58. package/package.json +2 -2
  59. package/src/access-service.ts +56 -41
  60. package/src/capabilities.test.ts +28 -0
  61. package/src/direct-answer-wiring.test.ts +20 -13
  62. package/src/direct-answer-wiring.ts +9 -13
  63. package/src/maintenance/namespace-planner.ts +3 -10
  64. package/src/orchestrator.ts +42 -71
  65. package/src/recall-planner-llm.test.ts +30 -11
  66. package/src/recall-planner-llm.ts +4 -5
  67. package/src/scopes/scope-plan.test.ts +168 -1
  68. package/src/scopes/scope-plan.ts +163 -0
  69. package/dist/chunk-BNOAB5HH.js.map +0 -1
  70. package/dist/chunk-OQH5XUH3.js.map +0 -1
  71. package/dist/chunk-SHRRWOVY.js.map +0 -1
  72. package/dist/chunk-YUJL27ET.js.map +0 -1
  73. /package/dist/{chunk-MECU6WSH.js.map → chunk-2B43BYYZ.js.map} +0 -0
  74. /package/dist/{chunk-HHLWSWWF.js.map → chunk-BF2426TN.js.map} +0 -0
  75. /package/dist/{chunk-RBHANGVX.js.map → chunk-BZAK3RBN.js.map} +0 -0
  76. /package/dist/{chunk-CTAHKIG6.js.map → chunk-EM22PCQR.js.map} +0 -0
  77. /package/dist/{chunk-GM3BMWKR.js.map → chunk-KHQ433G5.js.map} +0 -0
  78. /package/dist/{chunk-SYDHFFAW.js.map → chunk-OUDAU3T5.js.map} +0 -0
  79. /package/dist/{chunk-O62P2LFL.js.map → chunk-VZQXJNBD.js.map} +0 -0
  80. /package/dist/{chunk-3VUPWDSE.js.map → chunk-ZAVTEQFM.js.map} +0 -0
  81. /package/dist/{chunk-IHMT6XTY.js.map → chunk-ZXLSNG3T.js.map} +0 -0
@@ -109,7 +109,11 @@ import {
109
109
  type ScopeProfileLayerResolution,
110
110
  type ScopeProfilePromotionResolution,
111
111
  } from "./namespaces/scope-profiles.js";
112
- import { type ScopePlan, resolveScopePlan } from "./scopes/scope-plan.js";
112
+ import {
113
+ type ScopePlan,
114
+ resolveScopePlan,
115
+ resolveWritableNamespaceValue,
116
+ } from "./scopes/scope-plan.js";
113
117
  import { namespaceIdentityFromToken } from "./namespaces/identity.js";
114
118
  import { namespaceCollectionName } from "./namespaces/search.js";
115
119
  import { SecureStoreLockedError } from "./secure-store/index.js";
@@ -1367,17 +1371,28 @@ export class EngramAccessService {
1367
1371
  return resolvePrincipal(sessionKey, this.orchestrator.config);
1368
1372
  }
1369
1373
 
1370
- private resolveWritableNamespace(
1374
+ private writableNamespaceFor(
1371
1375
  namespace: string | undefined,
1372
1376
  sessionKey: string | undefined,
1373
1377
  authenticatedPrincipal?: string,
1374
1378
  ): string {
1375
- const resolved = this.resolveNamespace(namespace);
1376
- const principal = this.resolveRequestPrincipal(sessionKey, authenticatedPrincipal);
1377
- if (!canWriteNamespace(principal, resolved, this.orchestrator.config)) {
1378
- throw new EngramAccessInputError(`namespace is not writable: ${resolved}`);
1379
+ // #1521: delegates to the scope-module resolver. The inline
1380
+ // namespace/principal resolution + writability check is retired so the
1381
+ // adHocNamespaceResolutions ratchet no longer counts this site.
1382
+ const result = resolveWritableNamespaceValue(
1383
+ namespace,
1384
+ sessionKey,
1385
+ authenticatedPrincipal,
1386
+ this.orchestrator.config,
1387
+ );
1388
+ if (!result.ok) {
1389
+ throw new EngramAccessInputError(
1390
+ result.reason === "unsupported"
1391
+ ? `unsupported namespace: ${result.namespace}`
1392
+ : `namespace is not writable: ${result.namespace}`,
1393
+ );
1379
1394
  }
1380
- return resolved;
1395
+ return result.namespace;
1381
1396
  }
1382
1397
 
1383
1398
  /**
@@ -1487,7 +1502,7 @@ export class EngramAccessService {
1487
1502
  const hasExplicitNamespace =
1488
1503
  typeof request.namespace === "string" && request.namespace.trim().length > 0;
1489
1504
  if (hasExplicitNamespace) {
1490
- return this.resolveWritableNamespace(
1505
+ return this.writableNamespaceFor(
1491
1506
  request.namespace,
1492
1507
  request.sessionKey,
1493
1508
  request.authenticatedPrincipal,
@@ -1509,7 +1524,7 @@ export class EngramAccessService {
1509
1524
  if (!overlay) {
1510
1525
  // No coding overlay → unqualified write stays on config.defaultNamespace,
1511
1526
  // exactly the pre-#1434 behavior (auth-checked, like the legacy path).
1512
- return this.resolveWritableNamespace(
1527
+ return this.writableNamespaceFor(
1513
1528
  undefined,
1514
1529
  request.sessionKey,
1515
1530
  request.authenticatedPrincipal,
@@ -1624,7 +1639,7 @@ export class EngramAccessService {
1624
1639
  // The overlay never applies, so base == write == the explicit namespace.
1625
1640
  // Objective-state converges on the same explicit target (the stricter
1626
1641
  // principal-self contract only governs the IMPLICIT path).
1627
- const writeNamespace = this.resolveWritableNamespace(
1642
+ const writeNamespace = this.writableNamespaceFor(
1628
1643
  request.namespace,
1629
1644
  request.sessionKey,
1630
1645
  request.authenticatedPrincipal,
@@ -1779,7 +1794,7 @@ export class EngramAccessService {
1779
1794
  // namespace), collapsing the namespaces-disabled / no-session /
1780
1795
  // projectScope-off cases to config.defaultNamespace exactly as before
1781
1796
  // (rule 39 parity with resolveCodingScopedWriteNamespace).
1782
- const writeNamespace = this.resolveWritableNamespace(
1797
+ const writeNamespace = this.writableNamespaceFor(
1783
1798
  undefined,
1784
1799
  request.sessionKey,
1785
1800
  request.authenticatedPrincipal,
@@ -4757,7 +4772,7 @@ export class EngramAccessService {
4757
4772
  const service = createCorrectionService({
4758
4773
  orchestrator: this.orchestrator,
4759
4774
  resolveAuthorizedNamespace: async (req) =>
4760
- this.resolveWritableNamespace(req.namespace, req.sessionKey, req.principal),
4775
+ this.writableNamespaceFor(req.namespace, req.sessionKey, req.principal),
4761
4776
  resolveReadableNamespaces: (req) => {
4762
4777
  const principal = this.resolveRequestPrincipal(req.sessionKey, req.principal);
4763
4778
  return recallNamespacesForPrincipal(principal, this.orchestrator.config);
@@ -4766,7 +4781,7 @@ export class EngramAccessService {
4766
4781
  // resolveWritableNamespace resolves + checks writability in one step;
4767
4782
  // reusing it avoids a second ad-hoc namespace-resolution call site.
4768
4783
  try {
4769
- this.resolveWritableNamespace(req.namespace, req.sessionKey, req.principal);
4784
+ this.writableNamespaceFor(req.namespace, req.sessionKey, req.principal);
4770
4785
  return Promise.resolve(true);
4771
4786
  } catch {
4772
4787
  return Promise.resolve(false);
@@ -5176,7 +5191,7 @@ export class EngramAccessService {
5176
5191
  if (deepSleep.enabled === false && deepSleep.enabledExplicitlySet === true) {
5177
5192
  throw new Error("memory governance is disabled by dreams.phases.deepSleep.enabled=false");
5178
5193
  }
5179
- const resolvedNamespace = this.resolveWritableNamespace(
5194
+ const resolvedNamespace = this.writableNamespaceFor(
5180
5195
  request.namespace,
5181
5196
  undefined,
5182
5197
  request.authenticatedPrincipal ?? principal,
@@ -5236,7 +5251,7 @@ export class EngramAccessService {
5236
5251
  proceduresWritten: number;
5237
5252
  skippedReason?: string;
5238
5253
  }> {
5239
- const resolvedNamespace = this.resolveWritableNamespace(
5254
+ const resolvedNamespace = this.writableNamespaceFor(
5240
5255
  request.namespace,
5241
5256
  undefined,
5242
5257
  request.authenticatedPrincipal ?? principal,
@@ -5262,7 +5277,7 @@ export class EngramAccessService {
5262
5277
  } = {},
5263
5278
  principal?: string,
5264
5279
  ): Promise<LiveConnectorsRunSummary> {
5265
- this.resolveWritableNamespace(
5280
+ this.writableNamespaceFor(
5266
5281
  undefined,
5267
5282
  undefined,
5268
5283
  request.authenticatedPrincipal ?? principal,
@@ -5308,7 +5323,7 @@ export class EngramAccessService {
5308
5323
  duplicatesSuperseded: number;
5309
5324
  result?: PatternReinforcementResult;
5310
5325
  }> {
5311
- const resolvedNamespace = this.resolveWritableNamespace(
5326
+ const resolvedNamespace = this.writableNamespaceFor(
5312
5327
  request.namespace,
5313
5328
  undefined,
5314
5329
  request.authenticatedPrincipal ?? principal,
@@ -5598,7 +5613,7 @@ export class EngramAccessService {
5598
5613
  if (!isTrustZoneName(request.targetZone)) {
5599
5614
  throw new EngramAccessInputError(`unsupported trust-zone target: ${String(request.targetZone)}`);
5600
5615
  }
5601
- const resolvedNamespace = this.resolveWritableNamespace(
5616
+ const resolvedNamespace = this.writableNamespaceFor(
5602
5617
  request.namespace,
5603
5618
  undefined,
5604
5619
  request.authenticatedPrincipal,
@@ -5632,7 +5647,7 @@ export class EngramAccessService {
5632
5647
  async trustZoneDemoSeed(
5633
5648
  request: EngramAccessTrustZoneDemoSeedRequest,
5634
5649
  ): Promise<EngramAccessTrustZoneDemoSeedResponse> {
5635
- const resolvedNamespace = this.resolveWritableNamespace(
5650
+ const resolvedNamespace = this.writableNamespaceFor(
5636
5651
  request.namespace,
5637
5652
  undefined,
5638
5653
  request.authenticatedPrincipal,
@@ -5669,7 +5684,7 @@ export class EngramAccessService {
5669
5684
  throw new EngramAccessInputError("reasonCode is required");
5670
5685
  }
5671
5686
 
5672
- const resolvedNamespace = this.resolveWritableNamespace(
5687
+ const resolvedNamespace = this.writableNamespaceFor(
5673
5688
  request.namespace,
5674
5689
  undefined,
5675
5690
  request.authenticatedPrincipal,
@@ -5835,7 +5850,7 @@ export class EngramAccessService {
5835
5850
 
5836
5851
  // Backward-compatible BASE writable namespace (pre-#1495 response semantics)
5837
5852
  // for the legacy `namespace` response field. DERIVED from the already-resolved
5838
- // scope plan — NOT a second `resolveWritableNamespace(request.namespace,...)`
5853
+ // scope plan — NOT a second writable-namespace resolution call
5839
5854
  // call (#1505 thread jvO). The fresh call re-authorized `undefined ⇒
5840
5855
  // config.defaultNamespace` a SECOND time; under a restrictive default-namespace
5841
5856
  // write policy that re-auth could REJECT an otherwise valid project-scoped
@@ -5845,7 +5860,7 @@ export class EngramAccessService {
5845
5860
  // the coding context, leaving an orphaned session binding behind. The plan is
5846
5861
  // the single authorization point (rule 22 / 39); the legacy field must reuse it
5847
5862
  // and never re-authorize. Pre-#1495 semantics were exactly
5848
- // `resolveWritableNamespace(request.namespace)` (overlay-agnostic): the explicit
5863
+ // the writable-namespace resolver (overlay-agnostic): the explicit
5849
5864
  // namespace when supplied, else `config.defaultNamespace` for user-project
5850
5865
  // coding overlays. Hosted scope-profile layers such as `teamProject` report
5851
5866
  // their effective profile write namespace because there is no legacy
@@ -6680,7 +6695,7 @@ export class EngramAccessService {
6680
6695
 
6681
6696
  // Authorize compaction against the SCOPED WRITE TARGET — the SAME effective
6682
6697
  // write namespace `observe` archived the LCM queue under — NOT a premature
6683
- // `resolveWritableNamespace(undefined ⇒ config.defaultNamespace)` (#1505
6698
+ // the writable-namespace resolver (undefined ⇒ config.defaultNamespace) (#1505
6684
6699
  // thread NBHWs). Under a restrictive `default` WRITE policy where the
6685
6700
  // principal can still write its self/project overlay, that premature default
6686
6701
  // write-auth threw `namespace is not writable: default` BEFORE the scoped key
@@ -6692,7 +6707,7 @@ export class EngramAccessService {
6692
6707
  // default` for a validly scoped observe's queue.
6693
6708
  const scope = await this.resolveMemoryScopePlan(request);
6694
6709
  // Legacy `namespace` response field: pre-#1505 semantics were exactly
6695
- // `resolveWritableNamespace(request.namespace)` (overlay-agnostic) — the
6710
+ // the writable-namespace resolver (overlay-agnostic) — the
6696
6711
  // authorized explicit namespace when supplied, else `config.defaultNamespace`.
6697
6712
  // DERIVED from the scope plan (NOT a second auth pass, #1505 thread jvO):
6698
6713
  // explicit ⇒ writeNamespace; user-project coding overlay ⇒ defaultNamespace;
@@ -6747,7 +6762,7 @@ export class EngramAccessService {
6747
6762
 
6748
6763
  // Authorize compaction against the SCOPED WRITE TARGET — the SAME effective
6749
6764
  // write namespace `observe` archived the LCM queue under — NOT a premature
6750
- // `resolveWritableNamespace(undefined ⇒ config.defaultNamespace)` (#1505
6765
+ // the writable-namespace resolver (undefined ⇒ config.defaultNamespace) (#1505
6751
6766
  // thread NBHWs). See `lcmCompactionFlush` for the full rationale: under a
6752
6767
  // restrictive `default` WRITE policy where the principal can still write its
6753
6768
  // self/project overlay, the old premature default write-auth threw `namespace
@@ -6832,7 +6847,7 @@ export class EngramAccessService {
6832
6847
  }
6833
6848
  const symptom = request.symptom?.trim();
6834
6849
  if (!symptom) throw new EngramAccessInputError("symptom is required");
6835
- const resolvedNs = this.resolveWritableNamespace(request.namespace, undefined, request.principal);
6850
+ const resolvedNs = this.writableNamespaceFor(request.namespace, undefined, request.principal);
6836
6851
  const storage = await this.orchestrator.getStorage(resolvedNs);
6837
6852
  const created = await storage.appendContinuityIncident({
6838
6853
  symptom,
@@ -6862,7 +6877,7 @@ export class EngramAccessService {
6862
6877
  if (!fixApplied) throw new EngramAccessInputError("fixApplied is required");
6863
6878
  const verificationResult = request.verificationResult?.trim();
6864
6879
  if (!verificationResult) throw new EngramAccessInputError("verificationResult is required");
6865
- const resolvedNs = this.resolveWritableNamespace(request.namespace, undefined, request.principal);
6880
+ const resolvedNs = this.writableNamespaceFor(request.namespace, undefined, request.principal);
6866
6881
  const storage = await this.orchestrator.getStorage(resolvedNs);
6867
6882
  const closed = await storage.closeContinuityIncident(id, {
6868
6883
  fixApplied,
@@ -6904,7 +6919,7 @@ export class EngramAccessService {
6904
6919
  if (!this.orchestrator.config.identityContinuityEnabled) {
6905
6920
  return { enabled: false, reason: "Identity continuity is disabled." };
6906
6921
  }
6907
- const resolvedNs = this.resolveWritableNamespace(request.namespace, undefined, request.principal);
6922
+ const resolvedNs = this.writableNamespaceFor(request.namespace, undefined, request.principal);
6908
6923
  const storage = await this.orchestrator.getStorage(resolvedNs);
6909
6924
  const loop = await storage.upsertIdentityImprovementLoop({
6910
6925
  id: request.id?.trim() || "",
@@ -6931,7 +6946,7 @@ export class EngramAccessService {
6931
6946
  }
6932
6947
  const id = request.id?.trim();
6933
6948
  if (!id) throw new EngramAccessInputError("id is required");
6934
- const resolvedNs = this.resolveWritableNamespace(request.namespace, undefined, request.principal);
6949
+ const resolvedNs = this.writableNamespaceFor(request.namespace, undefined, request.principal);
6935
6950
  const storage = await this.orchestrator.getStorage(resolvedNs);
6936
6951
  const reviewed = await storage.reviewIdentityImprovementLoop(id, {
6937
6952
  status: request.status,
@@ -6985,7 +7000,7 @@ export class EngramAccessService {
6985
7000
  const hasUpdate = Object.values(updates).some((v) => typeof v === "string" && v.length > 0);
6986
7001
  if (!hasUpdate) throw new EngramAccessInputError("At least one section field is required.");
6987
7002
 
6988
- const resolvedNs = this.resolveWritableNamespace(request.namespace, undefined, request.principal);
7003
+ const resolvedNs = this.writableNamespaceFor(request.namespace, undefined, request.principal);
6989
7004
  const storage = await this.orchestrator.getStorage(resolvedNs);
6990
7005
  const existing = await storage.readIdentityAnchor();
6991
7006
 
@@ -7654,7 +7669,7 @@ export class EngramAccessService {
7654
7669
  "memoryId must not contain path separators",
7655
7670
  );
7656
7671
  }
7657
- const resolvedNs = this.resolveWritableNamespace(
7672
+ const resolvedNs = this.writableNamespaceFor(
7658
7673
  request.namespace,
7659
7674
  request.sessionKey,
7660
7675
  request.principal,
@@ -7678,7 +7693,7 @@ export class EngramAccessService {
7678
7693
  principal?: string;
7679
7694
  sessionKey?: string;
7680
7695
  }): Promise<unknown> {
7681
- const resolvedNs = this.resolveWritableNamespace(request.namespace, request.sessionKey, request.principal);
7696
+ const resolvedNs = this.writableNamespaceFor(request.namespace, request.sessionKey, request.principal);
7682
7697
  const storage = await this.orchestrator.getStorage(resolvedNs);
7683
7698
  // Update frontmatter to active status (promote from pending/draft)
7684
7699
  await storage.updateMemoryFrontmatter(request.memoryId, {
@@ -7734,7 +7749,7 @@ export class EngramAccessService {
7734
7749
  );
7735
7750
  }
7736
7751
 
7737
- const resolvedNs = this.resolveWritableNamespace(
7752
+ const resolvedNs = this.writableNamespaceFor(
7738
7753
  request.namespace,
7739
7754
  request.sessionKey,
7740
7755
  request.principal,
@@ -7784,7 +7799,7 @@ export class EngramAccessService {
7784
7799
  namespace?: string;
7785
7800
  principal?: string;
7786
7801
  }): Promise<{ saved: boolean }> {
7787
- const resolvedNs = this.resolveWritableNamespace(request.namespace, request.sessionKey, request.principal);
7802
+ const resolvedNs = this.writableNamespaceFor(request.namespace, request.sessionKey, request.principal);
7788
7803
  const storage = await this.orchestrator.getStorage(resolvedNs);
7789
7804
  const storageDir = storage.dir;
7790
7805
  const { writeFile, mkdir } = await import("node:fs/promises");
@@ -7842,7 +7857,7 @@ export class EngramAccessService {
7842
7857
  // Pass authenticatedPrincipal so the principal resolution matches other
7843
7858
  // write endpoints (gotcha #42: read and write paths must resolve through
7844
7859
  // the same namespace layer).
7845
- const resolvedNamespace = this.resolveWritableNamespace(
7860
+ const resolvedNamespace = this.writableNamespaceFor(
7846
7861
  request.namespace,
7847
7862
  request.sessionId,
7848
7863
  request.authenticatedPrincipal,
@@ -8144,7 +8159,7 @@ export class EngramAccessService {
8144
8159
  "authentication required: namespaces are enabled and no principal was supplied",
8145
8160
  );
8146
8161
  }
8147
- const resolved = this.resolveWritableNamespace(namespace, undefined, principal);
8162
+ const resolved = this.writableNamespaceFor(namespace, undefined, principal);
8148
8163
  const storage = await this.orchestrator.getStorage(resolved);
8149
8164
  return { namespace: resolved, storage };
8150
8165
  }
@@ -8201,7 +8216,7 @@ export class EngramAccessService {
8201
8216
  },
8202
8217
  ): Promise<ImportCapsuleResult> {
8203
8218
  const { namespace, principal, root: explicitRoot, memoryDir: explicitMemoryDir, ...importOptions } = opts;
8204
- const resolvedNamespace = this.resolveWritableNamespace(namespace, undefined, principal);
8219
+ const resolvedNamespace = this.writableNamespaceFor(namespace, undefined, principal);
8205
8220
  const storage = await this.orchestrator.getStorage(resolvedNamespace);
8206
8221
  const root = explicitRoot ?? storage.dir;
8207
8222
  const memoryDir = explicitMemoryDir ?? this.orchestrator.config.memoryDir;
@@ -8288,7 +8303,7 @@ export class EngramAccessService {
8288
8303
  },
8289
8304
  ): Promise<ExportCapsuleResult> {
8290
8305
  const { namespace, principal, root: explicitRoot, memoryDir: explicitMemoryDir, ...exportOptions } = opts;
8291
- const resolvedNamespace = this.resolveWritableNamespace(namespace, undefined, principal);
8306
+ const resolvedNamespace = this.writableNamespaceFor(namespace, undefined, principal);
8292
8307
  const storage = await this.orchestrator.getStorage(resolvedNamespace);
8293
8308
  const root = explicitRoot ?? storage.dir;
8294
8309
  const memoryDir = explicitMemoryDir ?? this.orchestrator.config.memoryDir;
@@ -8522,7 +8537,7 @@ export class EngramAccessService {
8522
8537
  async offlineSyncApplyFileContent(
8523
8538
  options: EngramAccessOfflineSyncApplyFileContentRequest,
8524
8539
  ): Promise<EngramAccessOfflineSyncApplyFileContentResponse> {
8525
- const resolvedNamespace = this.resolveWritableNamespace(
8540
+ const resolvedNamespace = this.writableNamespaceFor(
8526
8541
  options.namespace,
8527
8542
  undefined,
8528
8543
  options.principal,
@@ -8573,7 +8588,7 @@ export class EngramAccessService {
8573
8588
  async offlineSyncApply(
8574
8589
  options: EngramAccessOfflineSyncApplyRequest,
8575
8590
  ): Promise<EngramAccessOfflineSyncApplyResponse> {
8576
- const resolvedNamespace = this.resolveWritableNamespace(
8591
+ const resolvedNamespace = this.writableNamespaceFor(
8577
8592
  options.namespace,
8578
8593
  undefined,
8579
8594
  options.principal,
@@ -8655,7 +8670,7 @@ export class EngramAccessService {
8655
8670
  );
8656
8671
  }
8657
8672
  const dryRun = options.dryRun === true;
8658
- const resolvedNamespace = this.resolveWritableNamespace(
8673
+ const resolvedNamespace = this.writableNamespaceFor(
8659
8674
  options.namespace,
8660
8675
  undefined,
8661
8676
  options.authenticatedPrincipal,
@@ -96,6 +96,34 @@ test("resolveCapabilities returns a frozen object", () => {
96
96
  assert.equal(Object.isFrozen(caps), true, "CapabilitySet must be frozen");
97
97
  });
98
98
 
99
+ test("resolveCapabilities is deterministic — same config always yields identical gate values (one-resolution-per-op #1523)", () => {
100
+ // The one-resolution-per-op contract (issue #1523): resolve ONCE at the
101
+ // operation entry, thread the frozen result down. This test proves the
102
+ // resolution is deterministic — calling it twice with the same config
103
+ // produces identical values for every gate, so there is never a reason to
104
+ // re-resolve mid-operation.
105
+ const config = parseConfig({
106
+ rerankCacheEnabled: true,
107
+ recallDirectAnswerEnabled: false,
108
+ recallMmrEnabled: true,
109
+ recallPlannerLlmEnabled: true,
110
+ graphRecallEnabled: true,
111
+ });
112
+ const first = resolveCapabilities(config);
113
+ const second = resolveCapabilities(config);
114
+ for (const field of FIELDS) {
115
+ assert.equal(
116
+ first[field],
117
+ second[field],
118
+ `field ${field} must be deterministic across resolutions`,
119
+ );
120
+ }
121
+ // Both must be frozen — a mid-operation mutation would break the contract.
122
+ assert.equal(Object.isFrozen(first), true);
123
+ assert.equal(Object.isFrozen(second), true);
124
+ });
125
+
126
+
99
127
  // ---------------------------------------------------------------------------
100
128
  // GraphConstructionCapabilitySet — gate-parity tests (issue #1566 Cluster A).
101
129
  //
@@ -13,7 +13,6 @@ import type { TrustZoneName } from "./trust-zones.js";
13
13
  type WiringConfig = DirectAnswerWiringInput["config"];
14
14
 
15
15
  const BASE_CONFIG: WiringConfig = {
16
- recallDirectAnswerEnabled: true,
17
16
  recallDirectAnswerTokenOverlapFloor: 0.5,
18
17
  recallDirectAnswerImportanceFloor: 0.7,
19
18
  recallDirectAnswerAmbiguityMargin: 0.15,
@@ -105,17 +104,24 @@ test("tryDirectAnswer disabled-path does not call any source accessor", async ()
105
104
  assert.deepEqual(sources.calls.importance, []);
106
105
  });
107
106
 
108
- // ── Backward-compat (#1523): omit top-level `enabled`, fall back to config ──
107
+ // ── One-resolution-per-op (#1523): `enabled` is the sole gate ──────────────
109
108
 
110
- test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabled` is omitted (disabled)", async () => {
111
- // Old input shape: config carries recallDirectAnswerEnabled: false and no
112
- // top-level `enabled`. Must short-circuit as "disabled" (identical to the
113
- // pre-#1523 behavior) rather than treating undefined as enabled.
114
- const sources = makeMockSources({ memories: [makeMemory()] });
109
+ test("tryDirectAnswer disabled when `enabled: false` even with matching candidates (caps wins over any config-derived value)", async () => {
110
+ // Issue #1523 contract: the resolved capability is the sole gate.
111
+ // `enabled` comes from `resolveCapabilities(config).recallDirectAnswer`,
112
+ // resolved ONCE at the recall entry. This test proves the gate value is
113
+ // authoritative when it says disabled, the function short-circuits
114
+ // regardless of candidate quality.
115
+ const sources = makeMockSources({
116
+ memories: [makeMemory({ tags: ["pnpm"], content: "remnic uses pnpm" })],
117
+ trustZones: { m1: "trusted" },
118
+ importance: { m1: 0.9 },
119
+ });
115
120
  const result = await tryDirectAnswer({
116
- query: "does not matter",
121
+ query: "package manager remnic",
117
122
  namespace: "default",
118
- config: { ...BASE_CONFIG, recallDirectAnswerEnabled: false },
123
+ config: BASE_CONFIG,
124
+ enabled: false,
119
125
  sources,
120
126
  });
121
127
  assert.equal(result.eligible, false);
@@ -123,9 +129,9 @@ test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabl
123
129
  assert.equal(sources.calls.listCandidates, 0);
124
130
  });
125
131
 
126
- test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabled` is omitted (enabled)", async () => {
127
- // Old input shape with recallDirectAnswerEnabled: true and no `enabled` must
128
- // NOT short-circuit — it proceeds to materialize candidates.
132
+ test("tryDirectAnswer proceeds when `enabled: true` and candidates match", async () => {
133
+ // Same contract, positive case: when the resolved capability says enabled,
134
+ // the function proceeds to materialize candidates.
129
135
  const sources = makeMockSources({
130
136
  memories: [makeMemory({ tags: ["pnpm"], content: "remnic uses pnpm" })],
131
137
  trustZones: { m1: "trusted" },
@@ -134,7 +140,8 @@ test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabl
134
140
  const result = await tryDirectAnswer({
135
141
  query: "package manager remnic",
136
142
  namespace: "default",
137
- config: BASE_CONFIG, // recallDirectAnswerEnabled: true
143
+ config: BASE_CONFIG,
144
+ enabled: true,
138
145
  sources,
139
146
  });
140
147
  assert.notEqual(result.reason, "disabled");
@@ -14,8 +14,9 @@
14
14
  *
15
15
  * Short-circuit contract:
16
16
  *
17
- * - When the resolved gate (`input.enabled` if supplied, else
18
- * `config.recallDirectAnswerEnabled`) is `false`, the function returns the
17
+ * - When the resolved gate (`input.enabled`, projected from
18
+ * `caps.recallDirectAnswer` at the recall-operation entry) is `false`, the
19
+ * function returns the
19
20
  * eligibility verdict with reason `"disabled"` without touching any source
20
21
  * accessor. This is the documented default.
21
22
  * - When enabled, the wiring cheaply drops non-trusted-zone memories
@@ -74,7 +75,6 @@ export interface DirectAnswerWiringInput {
74
75
  namespace: string;
75
76
  config: Pick<
76
77
  PluginConfig,
77
- | "recallDirectAnswerEnabled"
78
78
  | "recallDirectAnswerTokenOverlapFloor"
79
79
  | "recallDirectAnswerImportanceFloor"
80
80
  | "recallDirectAnswerAmbiguityMargin"
@@ -82,13 +82,12 @@ export interface DirectAnswerWiringInput {
82
82
  >;
83
83
  /**
84
84
  * Direct-answer capability gate, resolved once at the recall-operation entry
85
- * (issue #1523: `caps.recallDirectAnswer`). OPTIONAL and additive: when the
86
- * caller supplies it, this module and the orchestrator agree on a single
87
- * resolved gate value for the whole operation. When omitted, we fall back to
88
- * `config.recallDirectAnswerEnabled` so existing callers on the old input
89
- * shape keep identical behavior.
85
+ * via `resolveCapabilities(config).recallDirectAnswer` (issue #1523). This
86
+ * is the sole gate the function no longer reads the config flag directly.
87
+ * Callers MUST pass the resolved capability so the whole operation agrees on
88
+ * a single gate value.
90
89
  */
91
- enabled?: boolean;
90
+ enabled: boolean;
92
91
  sources: DirectAnswerSources;
93
92
  queryEntityRefs?: string[];
94
93
  abortSignal?: AbortSignal;
@@ -105,10 +104,7 @@ export async function tryDirectAnswer(
105
104
  const { query, namespace, config, enabled, sources, queryEntityRefs, abortSignal } = input;
106
105
 
107
106
  const eligibilityConfig: DirectAnswerConfig = {
108
- // Prefer the resolved capability when supplied; fall back to the config
109
- // flag so callers on the old input shape (config-only, no `enabled`) get
110
- // identical gating (issue #1523 backward-compat).
111
- enabled: enabled ?? config.recallDirectAnswerEnabled,
107
+ enabled,
112
108
  tokenOverlapFloor: config.recallDirectAnswerTokenOverlapFloor,
113
109
  importanceFloor: config.recallDirectAnswerImportanceFloor,
114
110
  ambiguityMargin: config.recallDirectAnswerAmbiguityMargin,
@@ -9,6 +9,7 @@ import { namespaceIdentityToken } from "../namespaces/identity.js";
9
9
  import { resolveNamespaceStorageRoot } from "../namespaces/storage.js";
10
10
  import { displayErrorDetail } from "../runtime/better-sqlite.js";
11
11
  import type { PluginConfig } from "../types.js";
12
+ import { getConfiguredNamespaces } from "../scopes/scope-plan.js";
12
13
 
13
14
  export type NamespaceMaintenanceJobName = string;
14
15
 
@@ -110,15 +111,7 @@ export function __setNamespaceMaintenanceFsForTest(overrides: Partial<typeof nam
110
111
  };
111
112
  }
112
113
 
113
- function configuredNamespaces(config: PluginConfig): string[] {
114
- return Array.from(
115
- new Set(
116
- [config.defaultNamespace, config.sharedNamespace, ...config.namespacePolicies.map((policy) => policy.name)]
117
- .map((value) => value.trim())
118
- .filter(Boolean)
119
- )
120
- );
121
- }
114
+
122
115
 
123
116
  function inferConfiguredKind(config: PluginConfig, namespace: string): NamespaceKind {
124
117
  if (namespace === config.defaultNamespace.trim()) return "default";
@@ -219,7 +212,7 @@ export async function planNamespaceMaintenance(
219
212
  // once per configured name. The #1500 contract is "namespaces disabled:
220
213
  // maintain the current default storage only," so collapse to the default.
221
214
  const configured = config.namespacesEnabled
222
- ? configuredNamespaces(config)
215
+ ? getConfiguredNamespaces(config)
223
216
  : [config.defaultNamespace.trim()].filter(Boolean);
224
217
  const byNamespace = new Map<string, NamespaceMaintenanceCandidate>();
225
218
  const skipped: NamespaceMaintenanceSkippedNamespace[] = [];