@remnic/core 9.3.716 → 9.3.718

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 (257) hide show
  1. package/dist/access-boundary.d.ts +5 -5
  2. package/dist/access-boundary.js +10 -9
  3. package/dist/access-cli.js +26 -25
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +5 -5
  6. package/dist/access-http.js +14 -13
  7. package/dist/access-mcp.d.ts +5 -5
  8. package/dist/access-mcp.js +13 -12
  9. package/dist/access-operations-batch.js +11 -10
  10. package/dist/access-operations.d.ts +5 -5
  11. package/dist/access-operations.js +12 -11
  12. package/dist/{access-service-BW8cm_uU.d.ts → access-service-CZbnt0t2.d.ts} +4 -4
  13. package/dist/access-service.d.ts +5 -5
  14. package/dist/access-service.js +9 -8
  15. package/dist/access-surface-catalog.d.ts +5 -5
  16. package/dist/action-confidence.d.ts +1 -1
  17. package/dist/active-memory-bridge.d.ts +1 -1
  18. package/dist/active-recall.d.ts +1 -1
  19. package/dist/active-recall.js +1 -1
  20. package/dist/behavior-learner.d.ts +1 -1
  21. package/dist/behavior-signals.d.ts +1 -1
  22. package/dist/bootstrap.d.ts +4 -4
  23. package/dist/briefing.d.ts +3 -1
  24. package/dist/briefing.js +3 -3
  25. package/dist/buffer-surprise-report.d.ts +1 -1
  26. package/dist/buffer.d.ts +1 -1
  27. package/dist/calibration.d.ts +1 -1
  28. package/dist/capabilities.d.ts +1 -1
  29. package/dist/{catalog-CbR0CsUw.d.ts → catalog-CEYlMs9X.d.ts} +1 -1
  30. package/dist/causal-behavior.d.ts +1 -1
  31. package/dist/causal-consolidation.d.ts +1 -1
  32. package/dist/causal-consolidation.js +4 -4
  33. package/dist/{chunk-JEIROMLX.js → chunk-2COL35PK.js} +95 -9
  34. package/dist/chunk-2COL35PK.js.map +1 -0
  35. package/dist/{chunk-XZTUMWKQ.js → chunk-2PTBLS2S.js} +2 -2
  36. package/dist/{chunk-2Y6LMOQQ.js → chunk-2VHMVWPU.js} +2 -2
  37. package/dist/{chunk-6UU43ZGD.js → chunk-4KZGLDJJ.js} +2 -2
  38. package/dist/{chunk-K44MIN6D.js → chunk-5RHM43BJ.js} +3 -3
  39. package/dist/{chunk-MECU6WSH.js → chunk-72VDBALG.js} +18 -18
  40. package/dist/{chunk-UY4JMEAK.js → chunk-7EEVXW7X.js} +2 -2
  41. package/dist/{chunk-XCTDHF7U.js → chunk-A5TUPZ3N.js} +33 -1
  42. package/dist/chunk-A5TUPZ3N.js.map +1 -0
  43. package/dist/{chunk-WI4QX7GH.js → chunk-A66YRSOS.js} +2 -2
  44. package/dist/{chunk-CTAHKIG6.js → chunk-ANI5DUAE.js} +10 -8
  45. package/dist/{chunk-CTAHKIG6.js.map → chunk-ANI5DUAE.js.map} +1 -1
  46. package/dist/{chunk-O62P2LFL.js → chunk-APUJIMDM.js} +2 -2
  47. package/dist/{chunk-MPHDVHIB.js → chunk-DIFU33GU.js} +2 -2
  48. package/dist/{chunk-DXGS5YOQ.js → chunk-EH27LHML.js} +2 -2
  49. package/dist/{chunk-BNOAB5HH.js → chunk-F3PVXKHP.js} +78 -1442
  50. package/dist/chunk-F3PVXKHP.js.map +1 -0
  51. package/dist/{chunk-6QUPUTT7.js → chunk-HLIR3RJ7.js} +6 -10
  52. package/dist/{chunk-6QUPUTT7.js.map → chunk-HLIR3RJ7.js.map} +1 -1
  53. package/dist/{chunk-SHRRWOVY.js → chunk-J6BH2OBA.js} +64 -21
  54. package/dist/chunk-J6BH2OBA.js.map +1 -0
  55. package/dist/{chunk-6FUJ2VYY.js → chunk-JAKYZGV4.js} +3 -3
  56. package/dist/{chunk-3VUPWDSE.js → chunk-JIZGYQYH.js} +4 -4
  57. package/dist/{chunk-QOIONSOI.js → chunk-KRGC4EDX.js} +2 -2
  58. package/dist/{chunk-AJOIM6UR.js → chunk-L5ASZA2F.js} +2 -2
  59. package/dist/{chunk-I56LKMF7.js → chunk-LXSG4L55.js} +2 -2
  60. package/dist/{chunk-MSVSP5VO.js → chunk-N3DUH6OO.js} +1 -1
  61. package/dist/chunk-N3DUH6OO.js.map +1 -0
  62. package/dist/{chunk-SQ4LU27I.js → chunk-NFRBADNR.js} +195 -80
  63. package/dist/chunk-NFRBADNR.js.map +1 -0
  64. package/dist/chunk-OJU73BUX.js +1688 -0
  65. package/dist/chunk-OJU73BUX.js.map +1 -0
  66. package/dist/{chunk-JVEMUX6L.js → chunk-OZ2KQ7HF.js} +2 -2
  67. package/dist/{chunk-SYDHFFAW.js → chunk-P5IPFZVH.js} +5 -5
  68. package/dist/{chunk-GM3BMWKR.js → chunk-REWQAFSA.js} +2 -2
  69. package/dist/{chunk-V7GDTQ6I.js → chunk-S35FX6MU.js} +2 -2
  70. package/dist/{chunk-MTHNJCOJ.js → chunk-S3OPGGZB.js} +2 -2
  71. package/dist/{chunk-IHMT6XTY.js → chunk-U5H7WHTS.js} +6 -4
  72. package/dist/{chunk-IHMT6XTY.js.map → chunk-U5H7WHTS.js.map} +1 -1
  73. package/dist/{chunk-RBHANGVX.js → chunk-UCJ5CDTK.js} +6 -6
  74. package/dist/{chunk-HHLWSWWF.js → chunk-URM3HW2R.js} +2 -2
  75. package/dist/{chunk-VO4UICII.js → chunk-YTZLBBEB.js} +2 -2
  76. package/dist/{chunk-OC56UWV5.js → chunk-YYOK4GVF.js} +2 -2
  77. package/dist/{cli-Dpivy32W.d.ts → cli-1orcdXOY.d.ts} +3 -3
  78. package/dist/cli.d.ts +6 -6
  79. package/dist/cli.js +28 -27
  80. package/dist/compounding/engine.d.ts +1 -1
  81. package/dist/compounding/engine.js +3 -3
  82. package/dist/compounding/preference-consolidator.d.ts +1 -1
  83. package/dist/compression-optimizer.d.ts +1 -1
  84. package/dist/config.d.ts +1 -1
  85. package/dist/config.js +1 -1
  86. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  87. package/dist/connectors/codex-materialize-runner.js +3 -3
  88. package/dist/connectors/codex-materialize.d.ts +1 -1
  89. package/dist/connectors/index.d.ts +1 -1
  90. package/dist/connectors/index.js +3 -3
  91. package/dist/consolidation-provenance-check.d.ts +1 -1
  92. package/dist/consolidation-undo.d.ts +1 -1
  93. package/dist/contradiction/index.d.ts +1 -1
  94. package/dist/conversation-index/backend.d.ts +1 -1
  95. package/dist/conversation-index/chunker.d.ts +1 -1
  96. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  97. package/dist/conversation-index/indexer.d.ts +1 -1
  98. package/dist/conversation-index/search.d.ts +1 -1
  99. package/dist/day-summary.d.ts +1 -1
  100. package/dist/delinearize.d.ts +1 -1
  101. package/dist/direct-answer-wiring.d.ts +1 -1
  102. package/dist/direct-answer.d.ts +1 -1
  103. package/dist/embedding-fallback.d.ts +1 -1
  104. package/dist/enrichment/index.d.ts +1 -1
  105. package/dist/entity-retrieval.d.ts +1 -1
  106. package/dist/entity-retrieval.js +3 -3
  107. package/dist/entity-schema.d.ts +1 -1
  108. package/dist/explicit-capture.d.ts +4 -4
  109. package/dist/extraction-faithfulness.d.ts +1 -1
  110. package/dist/extraction-judge-telemetry.d.ts +1 -1
  111. package/dist/extraction-judge-training.d.ts +1 -1
  112. package/dist/extraction-judge.d.ts +1 -1
  113. package/dist/extraction.d.ts +1 -1
  114. package/dist/fallback-llm.d.ts +1 -1
  115. package/dist/identity-continuity.d.ts +1 -1
  116. package/dist/importance.d.ts +1 -1
  117. package/dist/index.d.ts +9 -9
  118. package/dist/index.js +42 -41
  119. package/dist/index.js.map +1 -1
  120. package/dist/intent.d.ts +1 -1
  121. package/dist/lcm/engine.d.ts +1 -1
  122. package/dist/lcm/index.d.ts +1 -1
  123. package/dist/lcm/tools.d.ts +1 -1
  124. package/dist/lifecycle.d.ts +1 -1
  125. package/dist/live-connectors-runner.d.ts +1 -1
  126. package/dist/local-llm.d.ts +1 -1
  127. package/dist/maintenance/memory-governance.d.ts +1 -1
  128. package/dist/maintenance/memory-governance.js +3 -3
  129. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  130. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  131. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  132. package/dist/memory-action-policy.d.ts +1 -1
  133. package/dist/memory-cache.d.ts +1 -1
  134. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  135. package/dist/memory-projection-store.d.ts +1 -1
  136. package/dist/memory-provenance.d.ts +1 -1
  137. package/dist/memory-worth-outcomes.d.ts +1 -1
  138. package/dist/models-json.d.ts +1 -1
  139. package/dist/namespaces/migrate.d.ts +2 -2
  140. package/dist/namespaces/migrate.js +4 -4
  141. package/dist/namespaces/principal.d.ts +1 -1
  142. package/dist/namespaces/search.d.ts +1 -1
  143. package/dist/namespaces/storage.d.ts +2 -2
  144. package/dist/namespaces/storage.js +3 -3
  145. package/dist/native-knowledge.d.ts +1 -1
  146. package/dist/operator-toolkit.d.ts +1 -1
  147. package/dist/operator-toolkit.js +11 -9
  148. package/dist/orchestration/maintenance.d.ts +2 -2
  149. package/dist/orchestration/maintenance.js +8 -5
  150. package/dist/{orchestrator-vygo2YIM.d.ts → orchestrator-kOxredSr.d.ts} +22 -5
  151. package/dist/orchestrator.d.ts +4 -4
  152. package/dist/orchestrator.js +17 -16
  153. package/dist/patterns-cli.d.ts +1 -1
  154. package/dist/policy-runtime.d.ts +1 -1
  155. package/dist/provenance.d.ts +1 -1
  156. package/dist/qmd-recall-cache.d.ts +1 -1
  157. package/dist/qmd.d.ts +1 -1
  158. package/dist/recall-disclosure-escalation.d.ts +1 -1
  159. package/dist/recall-explain-renderer.d.ts +1 -1
  160. package/dist/recall-explain-renderer.js +3 -3
  161. package/dist/recall-planner-llm.d.ts +1 -1
  162. package/dist/recall-state.d.ts +1 -1
  163. package/dist/recall-tag-filter.d.ts +1 -1
  164. package/dist/recall-xray-cli.d.ts +1 -1
  165. package/dist/recall-xray-cli.js +4 -4
  166. package/dist/recall-xray-renderer.d.ts +1 -1
  167. package/dist/recall-xray-renderer.js +3 -3
  168. package/dist/recall-xray.d.ts +1 -1
  169. package/dist/recall-xray.js +2 -2
  170. package/dist/resolve-auth-token.d.ts +1 -1
  171. package/dist/resume-bundles.js +2 -2
  172. package/dist/retrieval-agents.d.ts +1 -1
  173. package/dist/retrieval-tiers.d.ts +1 -1
  174. package/dist/routing/engine.d.ts +1 -1
  175. package/dist/routing/store.d.ts +1 -1
  176. package/dist/schemas.d.ts +22 -22
  177. package/dist/search/embed-helper.d.ts +1 -1
  178. package/dist/search/factory.d.ts +1 -1
  179. package/dist/search/index.d.ts +1 -1
  180. package/dist/search/lancedb-backend.d.ts +1 -1
  181. package/dist/search/meilisearch-backend.d.ts +1 -1
  182. package/dist/search/noop-backend.d.ts +1 -1
  183. package/dist/search/orama-backend.d.ts +1 -1
  184. package/dist/search/port.d.ts +1 -1
  185. package/dist/search/remote-backend.d.ts +1 -1
  186. package/dist/{semantic-consolidation-DSjMDZ1u.d.ts → semantic-consolidation-B58v3S-a.d.ts} +1 -1
  187. package/dist/semantic-consolidation.d.ts +2 -2
  188. package/dist/semantic-consolidation.js +4 -4
  189. package/dist/semantic-rule-promotion.js +3 -3
  190. package/dist/semantic-rule-verifier.d.ts +1 -1
  191. package/dist/semantic-rule-verifier.js +3 -3
  192. package/dist/session-observer-bands.d.ts +1 -1
  193. package/dist/session-observer-state.d.ts +1 -1
  194. package/dist/shared-context/manager.d.ts +1 -1
  195. package/dist/signal.d.ts +1 -1
  196. package/dist/storage.d.ts +1 -1
  197. package/dist/storage.js +2 -2
  198. package/dist/summarizer.d.ts +1 -1
  199. package/dist/summary-snapshot.d.ts +1 -1
  200. package/dist/temporal-supersession.d.ts +1 -1
  201. package/dist/temporal-validity.d.ts +1 -1
  202. package/dist/threading.d.ts +1 -1
  203. package/dist/tier-migration.d.ts +1 -1
  204. package/dist/tier-routing.d.ts +1 -1
  205. package/dist/topics.d.ts +1 -1
  206. package/dist/transcript.d.ts +1 -1
  207. package/dist/transfer/types.d.ts +12 -12
  208. package/dist/{types-D9dwzU_x.d.ts → types-ACmDOScJ.d.ts} +36 -0
  209. package/dist/types.d.ts +1 -1
  210. package/dist/types.js +1 -1
  211. package/dist/utility-runtime.d.ts +1 -1
  212. package/dist/verified-recall.js +3 -3
  213. package/package.json +2 -2
  214. package/src/access-service.ts +56 -41
  215. package/src/briefing.ts +32 -0
  216. package/src/config.ts +32 -0
  217. package/src/correction/index.ts +24 -0
  218. package/src/correction/passive-capture.test.ts +500 -0
  219. package/src/correction/passive-capture.ts +335 -0
  220. package/src/correction/passive-correction-detector.test.ts +263 -0
  221. package/src/correction/passive-correction-detector.ts +325 -0
  222. package/src/correction/passive-correction-notifications.ts +123 -0
  223. package/src/maintenance/namespace-planner.ts +3 -10
  224. package/src/orchestrator.ts +231 -73
  225. package/src/scopes/scope-plan.test.ts +168 -1
  226. package/src/scopes/scope-plan.ts +163 -0
  227. package/src/types.ts +36 -0
  228. package/dist/chunk-BNOAB5HH.js.map +0 -1
  229. package/dist/chunk-JEIROMLX.js.map +0 -1
  230. package/dist/chunk-MSVSP5VO.js.map +0 -1
  231. package/dist/chunk-SHRRWOVY.js.map +0 -1
  232. package/dist/chunk-SQ4LU27I.js.map +0 -1
  233. package/dist/chunk-XCTDHF7U.js.map +0 -1
  234. /package/dist/{chunk-XZTUMWKQ.js.map → chunk-2PTBLS2S.js.map} +0 -0
  235. /package/dist/{chunk-2Y6LMOQQ.js.map → chunk-2VHMVWPU.js.map} +0 -0
  236. /package/dist/{chunk-6UU43ZGD.js.map → chunk-4KZGLDJJ.js.map} +0 -0
  237. /package/dist/{chunk-K44MIN6D.js.map → chunk-5RHM43BJ.js.map} +0 -0
  238. /package/dist/{chunk-MECU6WSH.js.map → chunk-72VDBALG.js.map} +0 -0
  239. /package/dist/{chunk-UY4JMEAK.js.map → chunk-7EEVXW7X.js.map} +0 -0
  240. /package/dist/{chunk-WI4QX7GH.js.map → chunk-A66YRSOS.js.map} +0 -0
  241. /package/dist/{chunk-O62P2LFL.js.map → chunk-APUJIMDM.js.map} +0 -0
  242. /package/dist/{chunk-MPHDVHIB.js.map → chunk-DIFU33GU.js.map} +0 -0
  243. /package/dist/{chunk-DXGS5YOQ.js.map → chunk-EH27LHML.js.map} +0 -0
  244. /package/dist/{chunk-6FUJ2VYY.js.map → chunk-JAKYZGV4.js.map} +0 -0
  245. /package/dist/{chunk-3VUPWDSE.js.map → chunk-JIZGYQYH.js.map} +0 -0
  246. /package/dist/{chunk-QOIONSOI.js.map → chunk-KRGC4EDX.js.map} +0 -0
  247. /package/dist/{chunk-AJOIM6UR.js.map → chunk-L5ASZA2F.js.map} +0 -0
  248. /package/dist/{chunk-I56LKMF7.js.map → chunk-LXSG4L55.js.map} +0 -0
  249. /package/dist/{chunk-JVEMUX6L.js.map → chunk-OZ2KQ7HF.js.map} +0 -0
  250. /package/dist/{chunk-SYDHFFAW.js.map → chunk-P5IPFZVH.js.map} +0 -0
  251. /package/dist/{chunk-GM3BMWKR.js.map → chunk-REWQAFSA.js.map} +0 -0
  252. /package/dist/{chunk-V7GDTQ6I.js.map → chunk-S35FX6MU.js.map} +0 -0
  253. /package/dist/{chunk-MTHNJCOJ.js.map → chunk-S3OPGGZB.js.map} +0 -0
  254. /package/dist/{chunk-RBHANGVX.js.map → chunk-UCJ5CDTK.js.map} +0 -0
  255. /package/dist/{chunk-HHLWSWWF.js.map → chunk-URM3HW2R.js.map} +0 -0
  256. /package/dist/{chunk-VO4UICII.js.map → chunk-YTZLBBEB.js.map} +0 -0
  257. /package/dist/{chunk-OC56UWV5.js.map → chunk-YYOK4GVF.js.map} +0 -0
@@ -24,6 +24,10 @@ import { SmartBuffer } from "./buffer.js";
24
24
  import { chunkContent, type ChunkingConfig } from "./chunking.js";
25
25
  import { semanticChunkContent, type SemanticChunkResult } from "./semantic-chunking.js";
26
26
  import { ExtractionEngine } from "./extraction.js";
27
+ import { detectPassiveCorrections } from "./correction/passive-correction-detector.js";
28
+ import { capturePassiveCorrections, type PassiveCaptureConfig } from "./correction/passive-capture.js";
29
+ import { createCorrectionService } from "./correction/correction-access-wiring.js";
30
+ import type { CorrectionService } from "./correction/correction-service.js";
27
31
  import { isAboveImportanceThreshold, scoreImportance } from "./importance.js";
28
32
  import {
29
33
  judgeFactDurability,
@@ -338,11 +342,16 @@ import {
338
342
  } from "./namespaces/identity.js";
339
343
  import {
340
344
  canReadNamespace,
345
+ canWriteNamespace,
341
346
  defaultNamespaceForPrincipal,
342
347
  recallNamespacesForPrincipal,
343
348
  resolvePrincipal,
344
349
  } from "./namespaces/principal.js";
345
- import { resolveScopePlan } from "./scopes/scope-plan.js";
350
+ import {
351
+ getConfiguredNamespaces,
352
+ resolveNamespaceFromStorageDir,
353
+ resolveScopePlan,
354
+ } from "./scopes/scope-plan.js";
346
355
  import {
347
356
  expandScopeProfileReadNamespaces,
348
357
  resolveScopeProfilePlan,
@@ -1976,6 +1985,16 @@ export class Orchestrator {
1976
1985
  { count: number; lastAccessed: string }
1977
1986
  > = new Map();
1978
1987
 
1988
+ // Passive correction capture (issue #1581) — dedup state + lazy service.
1989
+ private passiveCorrectionDedup: Set<string> = new Set();
1990
+ private _passiveCorrectionService: CorrectionService | null = null;
1991
+ private passiveCorrectionTelemetry: {
1992
+ detected: number;
1993
+ queued: number;
1994
+ autoApplied: number;
1995
+ suppressedReasonCounts: Record<string, number>;
1996
+ } = { detected: 0, queued: 0, autoApplied: 0, suppressedReasonCounts: {} };
1997
+
1979
1998
  // Background serial queue for extractions (agent_end optimization)
1980
1999
  // Queue stores promises that resolve when extraction should run
1981
2000
  private extractionQueue: Array<() => Promise<void>> = [];
@@ -2317,18 +2336,11 @@ export class Orchestrator {
2317
2336
  return this.storageRouter.storageFor(ns);
2318
2337
  }
2319
2338
 
2320
- private configuredNamespaces(): string[] {
2321
- return Array.from(
2322
- new Set(
2323
- [
2324
- this.config.defaultNamespace,
2325
- this.config.sharedNamespace,
2326
- ...this.config.namespacePolicies.map((policy) => policy.name),
2327
- ]
2328
- .map((value) => value.trim())
2329
- .filter(Boolean),
2330
- ),
2331
- );
2339
+ private configuredNamespaceList(): string[] {
2340
+ // #1521: delegates to the scope-module resolver. The inline derivation is
2341
+ // retired so the adHocNamespaceResolutions ratchet no longer counts this
2342
+ // site.
2343
+ return getConfiguredNamespaces(this.config);
2332
2344
  }
2333
2345
 
2334
2346
  private rememberNamespaceStorageDirHint(namespace: string, storageDir?: string): void {
@@ -2456,7 +2468,7 @@ export class Orchestrator {
2456
2468
  }
2457
2469
 
2458
2470
  const configured = new Set(
2459
- this.configuredNamespaces().map((namespace) => normalizeNamespaceIdentity(namespace)),
2471
+ this.configuredNamespaceList().map((namespace) => normalizeNamespaceIdentity(namespace)),
2460
2472
  );
2461
2473
  const preferredByStorageDir = new Map<
2462
2474
  string,
@@ -2570,7 +2582,7 @@ export class Orchestrator {
2570
2582
  new Set(
2571
2583
  (options.namespaces?.length
2572
2584
  ? options.namespaces
2573
- : this.configuredNamespaces()
2585
+ : this.configuredNamespaceList()
2574
2586
  )
2575
2587
  .map((value) => value.trim())
2576
2588
  .filter(Boolean),
@@ -4138,7 +4150,7 @@ export class Orchestrator {
4138
4150
  if (options?.dryRun !== true) {
4139
4151
  try {
4140
4152
  await this.processEntitySynthesisQueue(
4141
- this.namespaceFromStorageDir(targetStorage.dir),
4153
+ this.storageDirNamespace(targetStorage.dir),
4142
4154
  5,
4143
4155
  );
4144
4156
  } catch (error) {
@@ -13015,6 +13027,139 @@ export class Orchestrator {
13015
13027
  }
13016
13028
  }
13017
13029
 
13030
+ /**
13031
+ * Passive correction capture (issue #1581) — detects corrections expressed
13032
+ * passively in conversation turns and routes them to the Correction Contract
13033
+ * (#1580). Called from `runExtraction` after persistence completes.
13034
+ *
13035
+ * Thin wiring: delegates ALL correction logic to the detector + capture
13036
+ * modules + the CorrectionService. This method only checks gates, calls the
13037
+ * detector, and routes results. Fail-open: capture errors never block the
13038
+ * extraction return path.
13039
+ */
13040
+ private async maybeCapturePassiveCorrections(
13041
+ turns: readonly BufferTurn[],
13042
+ opts: {
13043
+ sessionKey: string;
13044
+ principal?: string;
13045
+ namespace: string;
13046
+ bufferKey: string;
13047
+ isLiveSession: boolean;
13048
+ },
13049
+ ): Promise<void> {
13050
+ const mode = this.config.correctionCaptureMode;
13051
+ if (mode === "off") return;
13052
+ if (!this.config.correctionEnabled) return;
13053
+
13054
+ try {
13055
+ const corrections = detectPassiveCorrections(
13056
+ turns.map((t) => ({ role: t.role, content: t.content })),
13057
+ );
13058
+ if (corrections.length === 0) return;
13059
+
13060
+ // Replay/import: force queue-only mode even if config says auto.
13061
+ const effectiveMode = opts.isLiveSession ? mode : "queue";
13062
+ const captureConfig: PassiveCaptureConfig = {
13063
+ mode: effectiveMode,
13064
+ confidenceFloor: this.config.correctionCaptureConfidenceFloor,
13065
+ autoApplyMaxAffected: this.config.correctionCaptureAutoApplyMaxAffected,
13066
+ };
13067
+
13068
+ const service = this.passiveCorrectionService();
13069
+ const result = await capturePassiveCorrections(
13070
+ corrections,
13071
+ {
13072
+ correctionEnabled: this.config.correctionEnabled,
13073
+ isLiveSession: opts.isLiveSession,
13074
+ bufferKey: opts.bufferKey,
13075
+ sessionKey: opts.sessionKey,
13076
+ principal: opts.principal,
13077
+ namespace: opts.namespace,
13078
+ },
13079
+ captureConfig,
13080
+ {
13081
+ planCorrection: (req) => service.plan(req),
13082
+ applyCorrection: (planId, applyOpts) => service.apply(planId, applyOpts),
13083
+ storageDir: async (ns) => (await this.getStorage(ns)).dir,
13084
+ // Resolve `[m:xxxx]` handles to concrete memory ids via the single
13085
+ // shared helper (#1582). Returns null on miss/ambiguity so the
13086
+ // capture loop drops the handle and the planner falls back to text
13087
+ // search (review: "memory handles not resolved").
13088
+ resolveHandle: (ref, sessionKey) => {
13089
+ try {
13090
+ return this.resolveMemoryIdOrHandle(ref, sessionKey);
13091
+ } catch {
13092
+ return null;
13093
+ }
13094
+ },
13095
+ },
13096
+ this.passiveCorrectionDedup,
13097
+ );
13098
+
13099
+ // Accumulate telemetry
13100
+ this.passiveCorrectionTelemetry.detected += result.telemetry.detected;
13101
+ this.passiveCorrectionTelemetry.queued += result.telemetry.queued;
13102
+ this.passiveCorrectionTelemetry.autoApplied += result.telemetry.autoApplied;
13103
+ for (const [reason, count] of Object.entries(result.telemetry.suppressedReasons)) {
13104
+ this.passiveCorrectionTelemetry.suppressedReasonCounts[reason] =
13105
+ (this.passiveCorrectionTelemetry.suppressedReasonCounts[reason] ?? 0) + count;
13106
+ }
13107
+ } catch (err) {
13108
+ // Fail-open: passive capture never blocks extraction.
13109
+ log.debug(
13110
+ `passive-correction: capture failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`,
13111
+ );
13112
+ }
13113
+ }
13114
+
13115
+ /** Lazily construct the CorrectionService for passive capture. Stateless
13116
+ * across requests (per #1580 design); cached for the orchestrator's life. */
13117
+ private passiveCorrectionService(): CorrectionService {
13118
+ if (this._passiveCorrectionService) return this._passiveCorrectionService;
13119
+ this._passiveCorrectionService = createCorrectionService({
13120
+ orchestrator: this,
13121
+ // Session-scoped write ACL (review: "passive capture bypasses write
13122
+ // ACL"). A correction detected in session S plans only against S readable
13123
+ // namespaces and applies only to writable ones (rule 42) — passive
13124
+ // capture never becomes a cross-tenant mutation vector. Mirrors the
13125
+ // access-service createCorrectionService wiring rather than bypassing it.
13126
+ resolveAuthorizedNamespace: async (req) => {
13127
+ const principal = req.principal || resolvePrincipal(req.sessionKey, this.config);
13128
+ const ns = req.namespace ?? defaultNamespaceForPrincipal(principal, this.config);
13129
+ if (!canWriteNamespace(principal, ns, this.config)) {
13130
+ throw new Error(
13131
+ `passive correction: namespace "${ns}" is not writable for principal ${principal ?? "(none)"}`,
13132
+ );
13133
+ }
13134
+ return ns;
13135
+ },
13136
+ resolveReadableNamespaces: (req) => {
13137
+ const principal = req.principal || resolvePrincipal(req.sessionKey, this.config);
13138
+ return recallNamespacesForPrincipal(principal, this.config);
13139
+ },
13140
+ canWriteNamespace: async (req) => {
13141
+ const principal = req.principal || resolvePrincipal(req.sessionKey, this.config);
13142
+ return canWriteNamespace(principal, req.namespace, this.config);
13143
+ },
13144
+ llmComplete: async ({ system, user }) => {
13145
+ const llmResult = await this.localLlm.chatCompletion(
13146
+ [
13147
+ { role: "system", content: system },
13148
+ { role: "user", content: user },
13149
+ ],
13150
+ { operation: "correction-classify", priority: "background" },
13151
+ );
13152
+ if (!llmResult) {
13153
+ throw new Error(
13154
+ "passive correction classify+draft: local LLM unavailable (disabled or in cooldown)",
13155
+ );
13156
+ }
13157
+ return llmResult.content;
13158
+ },
13159
+ });
13160
+ return this._passiveCorrectionService;
13161
+ }
13162
+
13018
13163
  private async runExtraction(
13019
13164
  turns: BufferTurn[],
13020
13165
  options: {
@@ -13100,13 +13245,13 @@ export class Orchestrator {
13100
13245
  );
13101
13246
  if (targetTurns.length === 0) {
13102
13247
  log.debug("skipping extraction: no non-context turns after normalization");
13248
+ // Context-only turns may still contain corrections (review: "context-only
13249
+ // turns skip capture"). Scan normalizedTurns before clearing the buffer.
13250
+ if (normalizedTurns.length > 0) {
13251
+ await this.maybeCapturePassiveCorrections(normalizedTurns as BufferTurn[], { sessionKey, principal: resolvePrincipal(sessionKey, this.config), namespace: this.resolveSelfNamespace(sessionKey), bufferKey, isLiveSession: clearBufferAfterExtraction });
13252
+ }
13103
13253
  await clearBuffer();
13104
- return {
13105
- status: "skipped",
13106
- reason: "empty_normalized_turns",
13107
- persistedCount: 0,
13108
- durableOutputCount: 0,
13109
- };
13254
+ return { status: "skipped", reason: "empty_normalized_turns", persistedCount: 0, durableOutputCount: 0 };
13110
13255
  }
13111
13256
  const sourceValidAt = latestSourceValidAtFromTurns(targetTurns);
13112
13257
  throwIfDeadlineExceeded("before_extract");
@@ -13126,6 +13271,30 @@ export class Orchestrator {
13126
13271
  `skipping extraction: below threshold (totalChars=${totalChars}, userTurns=${userTurns.length})`,
13127
13272
  );
13128
13273
  await clearBuffer();
13274
+ // Passive correction capture runs even when extraction is skipped for
13275
+ // being below the char/user-turn threshold (review: "skipped extraction
13276
+ // skips capture"). A short correction like "stop using Vim" is under
13277
+ // extractionMinChars but is exactly the high-value case this feature
13278
+ // exists for. The write namespace resolves through the SAME scope-profile
13279
+ // plan as the full extraction path so corrections target the active
13280
+ // profile's write layer, not just resolveSelfNamespace (review:
13281
+ // "below-threshold wrong namespace"). The ACL in passiveCorrectionService
13282
+ // authorizes the actual plan/apply.
13283
+ {
13284
+ const capturePrincipal =
13285
+ typeof options.principalOverride === "string" && options.principalOverride.length > 0
13286
+ ? options.principalOverride
13287
+ : resolvePrincipal(sessionKey, this.config);
13288
+ const captureWO = typeof options.writeNamespaceOverride === "string" && options.writeNamespaceOverride.length > 0
13289
+ ? options.writeNamespaceOverride : undefined;
13290
+ const captureCodingCtx = sessionKey ? this.getCodingContextForSession(sessionKey) : null;
13291
+ const captureCodingOv = resolveCodingNamespaceOverlay(captureCodingCtx, this.config.codingMode, this.config.defaultNamespace);
13292
+ const captureScopePlan = resolveScopeProfilePlan({ config: this.config, principal: capturePrincipal, codingContext: captureCodingCtx, codingOverlay: captureCodingOv });
13293
+ const captureNamespace = captureWO ?? captureScopePlan?.writeNamespace ?? this.resolveSelfNamespace(sessionKey);
13294
+ await this.maybeCapturePassiveCorrections(normalizedTurns as BufferTurn[], {
13295
+ sessionKey, principal: capturePrincipal, namespace: captureNamespace, bufferKey, isLiveSession: clearBufferAfterExtraction,
13296
+ });
13297
+ }
13129
13298
  return {
13130
13299
  status: "skipped",
13131
13300
  reason: "below_threshold",
@@ -13325,13 +13494,12 @@ export class Orchestrator {
13325
13494
  meta.lastExtractionAt = new Date().toISOString();
13326
13495
  await storage.saveMeta(meta);
13327
13496
  }
13497
+ // Correction-only turns that meet char/user-turn thresholds but yield
13498
+ // zero facts still need passive capture (review: "empty extraction skips
13499
+ // capture"). selfNamespace/principal already resolved above.
13500
+ await this.maybeCapturePassiveCorrections(normalizedTurns as BufferTurn[], { sessionKey, principal, namespace: selfNamespace, bufferKey, isLiveSession: clearBufferAfterExtraction });
13328
13501
  await clearBuffer();
13329
- return {
13330
- status: "skipped",
13331
- reason: "empty_extraction_result",
13332
- persistedCount: 0,
13333
- durableOutputCount: 0,
13334
- };
13502
+ return { status: "skipped", reason: "empty_extraction_result", persistedCount: 0, durableOutputCount: 0 };
13335
13503
  }
13336
13504
 
13337
13505
  let threadIdForExtraction: string | null = null;
@@ -13448,6 +13616,19 @@ export class Orchestrator {
13448
13616
 
13449
13617
  await clearBuffer({ ignoreAbort: true });
13450
13618
 
13619
+ // Passive correction capture (issue #1581) — detect corrections expressed
13620
+ // passively in the extracted turns and route to the Correction Contract.
13621
+ // Runs AFTER persistence + buffer clear so a capture failure never blocks
13622
+ // the extraction return. `clearBufferAfterExtraction` gates live-session
13623
+ // auto-apply (replay/import → queue-only).
13624
+ await this.maybeCapturePassiveCorrections(normalizedTurns as BufferTurn[], {
13625
+ sessionKey,
13626
+ principal,
13627
+ namespace: selfNamespace,
13628
+ bufferKey,
13629
+ isLiveSession: clearBufferAfterExtraction,
13630
+ });
13631
+
13451
13632
  // Build memory box from this extraction (v8.0 Phase 2A)
13452
13633
  // Topics are derived from the current extraction's facts and entities only —
13453
13634
  // not from readAllMemories() — so box topics accurately reflect the current
@@ -13971,7 +14152,7 @@ export class Orchestrator {
13971
14152
  )
13972
14153
  return false;
13973
14154
  if (
13974
- this.namespaceFromStorageDir(targetStorage.dir) ===
14155
+ this.storageDirNamespace(targetStorage.dir) ===
13975
14156
  this.config.sharedNamespace
13976
14157
  )
13977
14158
  return false;
@@ -14797,7 +14978,7 @@ export class Orchestrator {
14797
14978
  let targetNamespaceName =
14798
14979
  baseNamespace && baseNamespace.length > 0
14799
14980
  ? baseNamespace
14800
- : this.namespaceFromStorageDir(targetStorage.dir);
14981
+ : this.storageDirNamespace(targetStorage.dir);
14801
14982
  let routedRuleId: string | undefined;
14802
14983
  let routedNamespaceExplicit = false;
14803
14984
  if (routeRules.length > 0) {
@@ -14838,7 +15019,7 @@ export class Orchestrator {
14838
15019
  fact.scope === "global" &&
14839
15020
  !routedNamespaceExplicit
14840
15021
  ) {
14841
- const currentNs = this.namespaceFromStorageDir(targetStorage.dir);
15022
+ const currentNs = this.storageDirNamespace(targetStorage.dir);
14842
15023
  if (currentNs !== this.config.sharedNamespace && profileAllowsSharedWrites) {
14843
15024
  try {
14844
15025
  targetStorage = await this.storageRouter.storageFor(
@@ -15106,7 +15287,7 @@ export class Orchestrator {
15106
15287
  this.qmd.isAvailable() &&
15107
15288
  faithfulnessEnforceStatus !== "pending_review"
15108
15289
  ) {
15109
- const targetNamespace = this.namespaceFromStorageDir(targetStorage.dir);
15290
+ const targetNamespace = this.storageDirNamespace(targetStorage.dir);
15110
15291
  const contradiction = await this.checkForContradiction(
15111
15292
  fact.content,
15112
15293
  writeCategory,
@@ -15525,7 +15706,7 @@ export class Orchestrator {
15525
15706
  memoryId: parentId,
15526
15707
  category: writeCategory,
15527
15708
  content: fact.content,
15528
- namespace: this.namespaceFromStorageDir(targetStorage.dir),
15709
+ namespace: this.storageDirNamespace(targetStorage.dir),
15529
15710
  confidence: fact.confidence,
15530
15711
  source: "extraction",
15531
15712
  }),
@@ -15536,7 +15717,7 @@ export class Orchestrator {
15536
15717
 
15537
15718
  // Suggest links for this memory (Phase 3A)
15538
15719
  if (this.config.memoryLinkingEnabled && this.qmd.isAvailable()) {
15539
- const targetNamespace = this.namespaceFromStorageDir(targetStorage.dir);
15720
+ const targetNamespace = this.storageDirNamespace(targetStorage.dir);
15540
15721
  const suggestedLinks = await this.suggestLinksForMemory(
15541
15722
  fact.content,
15542
15723
  writeCategory,
@@ -15642,7 +15823,7 @@ export class Orchestrator {
15642
15823
  memoryId,
15643
15824
  category: writeCategory,
15644
15825
  content: fact.content,
15645
- namespace: this.namespaceFromStorageDir(targetStorage.dir),
15826
+ namespace: this.storageDirNamespace(targetStorage.dir),
15646
15827
  confidence: fact.confidence,
15647
15828
  source: "extraction",
15648
15829
  }),
@@ -15788,7 +15969,7 @@ export class Orchestrator {
15788
15969
  const baseTouchNamespace =
15789
15970
  baseNamespace && baseNamespace.length > 0
15790
15971
  ? baseNamespace
15791
- : this.namespaceFromStorageDir(storage.dir);
15972
+ : this.storageDirNamespace(storage.dir);
15792
15973
  };
15793
15974
  const recordDurableNonFactWrite = () => {
15794
15975
  durableNonFactWritten = true;
@@ -17979,7 +18160,7 @@ export class Orchestrator {
17979
18160
 
17980
18161
  const fallbackNamespace =
17981
18162
  fallbackStorageDir !== null
17982
- ? this.namespaceFromStorageDir(fallbackStorageDir)
18163
+ ? this.storageDirNamespace(fallbackStorageDir)
17983
18164
  : this.config.defaultNamespace;
17984
18165
  if (
17985
18166
  recallNamespaces.length === 0 ||
@@ -18169,7 +18350,7 @@ export class Orchestrator {
18169
18350
 
18170
18351
  const fallbackNamespace =
18171
18352
  fallbackStorageDir !== null
18172
- ? this.namespaceFromStorageDir(fallbackStorageDir)
18353
+ ? this.storageDirNamespace(fallbackStorageDir)
18173
18354
  : this.config.defaultNamespace;
18174
18355
  maybeAddStorage(fallbackStorage, fallbackNamespace);
18175
18356
 
@@ -20034,41 +20215,18 @@ export class Orchestrator {
20034
20215
  return namespaceIdentityFromToken(m[1]) ?? m[1];
20035
20216
  }
20036
20217
 
20037
- private namespaceFromStorageDir(storageDir: string): string {
20038
- if (!this.config.namespacesEnabled) return this.config.defaultNamespace;
20039
- const resolvedStorageDir = path.resolve(storageDir);
20040
- const resolvedMemoryDir = path.resolve(this.config.memoryDir);
20041
- if (resolvedStorageDir === resolvedMemoryDir)
20042
- return this.config.defaultNamespace;
20043
- const m = resolvedStorageDir.match(/[\\/]namespaces[\\/]([^\\/]+)$/);
20044
- if (!m?.[1]) return this.config.defaultNamespace;
20045
- const dirName = m[1];
20046
- // Token-shaped raw names (round 6, codex P2 — NBsFz): a dir name might be a
20047
- // tokenized identity OR a literal raw namespace name that merely LOOKS like a
20048
- // token (e.g. a configured or dynamic name `ns-616c706861`). The round-trip check below
20049
- // (`namespaceIdentityToken(decoded) === dirName`) is TAUTOLOGICAL for a
20050
- // canonical token string, so it cannot tell a tokenized dir for `alpha` apart
20051
- // from the legacy raw root of a namespace literally named `ns-616c706861`
20052
- // (codex NRCve). A dir name that is itself a KNOWN namespace (configured or
20053
- // catalog-owned at this exact storage root) is therefore preserved as the
20054
- // literal namespace BEFORE attempting to decode it.
20055
- if (this.configuredNamespaces().includes(dirName)) {
20056
- return dirName;
20057
- }
20058
- this.loadNamespaceStorageDirHintsFromCatalog();
20059
- const hintedNamespaces = this.namespaceStorageDirHints.get(resolvedStorageDir);
20060
- if (hintedNamespaces?.has(dirName)) {
20061
- return dirName;
20062
- }
20063
- if (hintedNamespaces?.size === 1) {
20064
- const [hintedNamespace] = hintedNamespaces;
20065
- if (hintedNamespace) return hintedNamespace;
20066
- }
20067
- const decoded = namespaceIdentityFromToken(dirName);
20068
- if (decoded && namespaceIdentityToken(decoded) === dirName) {
20069
- return decoded;
20070
- }
20071
- return dirName;
20218
+ private storageDirNamespace(storageDir: string): string {
20219
+ // #1521: delegates to the scope-module resolver. The inline dir→namespace
20220
+ // derivation (token round-trip guard, catalog hints) is retired so the
20221
+ // adHocNamespaceResolutions ratchet no longer counts this site. Hints are
20222
+ // loaded lazily via the callback (only after early returns, matching the
20223
+ // original behavior — codex P2).
20224
+ return resolveNamespaceFromStorageDir(storageDir, {
20225
+ config: this.config,
20226
+ configuredNamespaces: this.configuredNamespaceList(),
20227
+ hints: this.namespaceStorageDirHints,
20228
+ loadHints: () => this.loadNamespaceStorageDirHintsFromCatalog(),
20229
+ });
20072
20230
  }
20073
20231
 
20074
20232
  // #1522: catalog touch methods removed — touches now happen at the storage chokepoint.
@@ -21,7 +21,12 @@
21
21
  import assert from "node:assert/strict";
22
22
  import test from "node:test";
23
23
 
24
- import { resolveScopePlan } from "./scope-plan.js";
24
+ import {
25
+ getConfiguredNamespaces,
26
+ resolveNamespaceFromStorageDir,
27
+ resolveScopePlan,
28
+ resolveWritableNamespaceValue,
29
+ } from "./scope-plan.js";
25
30
  import {
26
31
  combineNamespaces,
27
32
  lcmSessionKeyForNamespace,
@@ -358,3 +363,165 @@ test("scope-plan: LCM session ids match lcmSessionKeyForNamespace encoding", ()
358
363
  );
359
364
  assert.deepEqual([...plan.lcmReadSessionIds], expected);
360
365
  });
366
+
367
+ // ──────────────────────────────────────────────────────────────────────────
368
+ // Fitness tests: centralized namespace-resolution helpers (#1521 step 3–4)
369
+ //
370
+ // These pin the three pure helpers that replace the ad-hoc resolution scattered
371
+ // across orchestrator.ts and access-service.ts. They MUST match the pre-migration
372
+ // behavior byte-for-byte (the snapshots were derived from the original inline
373
+ // implementations).
374
+ // ──────────────────────────────────────────────────────────────────────────
375
+
376
+ test("getConfiguredNamespaces: default + shared + policies, trimmed and deduped", () => {
377
+ const config = baseConfig({
378
+ defaultNamespace: "default",
379
+ sharedNamespace: "shared",
380
+ namespacePolicies: [
381
+ { name: "team-a", readPrincipals: ["*"], writePrincipals: ["*"] },
382
+ { name: "team-b", readPrincipals: ["*"], writePrincipals: ["*"] },
383
+ ],
384
+ });
385
+ const ns = getConfiguredNamespaces(config);
386
+ assert.deepEqual(ns, ["default", "shared", "team-a", "team-b"]);
387
+ });
388
+
389
+ test("getConfiguredNamespaces: trims whitespace and drops empty", () => {
390
+ const config = baseConfig({
391
+ defaultNamespace: " default ",
392
+ sharedNamespace: "",
393
+ namespacePolicies: [
394
+ { name: "team-a ", readPrincipals: ["*"], writePrincipals: ["*"] },
395
+ ],
396
+ });
397
+ const ns = getConfiguredNamespaces(config);
398
+ assert.deepEqual(ns, ["default", "team-a"]);
399
+ });
400
+
401
+ test("getConfiguredNamespaces: deduplicates identical names", () => {
402
+ const config = baseConfig({
403
+ defaultNamespace: "default",
404
+ sharedNamespace: "default",
405
+ namespacePolicies: [
406
+ { name: "default", readPrincipals: ["*"], writePrincipals: ["*"] },
407
+ ],
408
+ });
409
+ const ns = getConfiguredNamespaces(config);
410
+ assert.deepEqual(ns, ["default"]);
411
+ });
412
+
413
+ // ── resolveNamespaceFromStorageDir ──────────────────────────────────────────
414
+
415
+ test("resolveNamespaceFromStorageDir: namespaces disabled → always default", () => {
416
+ const config = baseConfig({ namespacesEnabled: false });
417
+ const ns = resolveNamespaceFromStorageDir("/some/path/namespaces/team-a", {
418
+ config,
419
+ configuredNamespaces: getConfiguredNamespaces(config),
420
+ });
421
+ assert.equal(ns, "default");
422
+ });
423
+
424
+ test("resolveNamespaceFromStorageDir: memory root → default", () => {
425
+ const config = baseConfig({ memoryDir: "/data/memory" });
426
+ const ns = resolveNamespaceFromStorageDir("/data/memory", {
427
+ config,
428
+ configuredNamespaces: getConfiguredNamespaces(config),
429
+ });
430
+ assert.equal(ns, "default");
431
+ });
432
+
433
+ test("resolveNamespaceFromStorageDir: configured namespace dir → that namespace", () => {
434
+ const config = baseConfig({
435
+ memoryDir: "/data/memory",
436
+ namespacePolicies: [
437
+ { name: "team-a", readPrincipals: ["*"], writePrincipals: ["*"] },
438
+ ],
439
+ });
440
+ const ns = resolveNamespaceFromStorageDir("/data/memory/namespaces/team-a", {
441
+ config,
442
+ configuredNamespaces: getConfiguredNamespaces(config),
443
+ });
444
+ assert.equal(ns, "team-a");
445
+ });
446
+
447
+ test("resolveNamespaceFromStorageDir: tokenized dir → decoded namespace", () => {
448
+ const config = baseConfig({ memoryDir: "/data/memory" });
449
+ // "alpha" tokenizes to ns-616c706861
450
+ const ns = resolveNamespaceFromStorageDir("/data/memory/namespaces/ns-616c706861", {
451
+ config,
452
+ configuredNamespaces: getConfiguredNamespaces(config),
453
+ });
454
+ assert.equal(ns, "alpha");
455
+ });
456
+
457
+ test("resolveNamespaceFromStorageDir: unknown dir → literal dir name", () => {
458
+ const config = baseConfig({ memoryDir: "/data/memory" });
459
+ const ns = resolveNamespaceFromStorageDir("/data/memory/namespaces/custom-ns", {
460
+ config,
461
+ configuredNamespaces: getConfiguredNamespaces(config),
462
+ });
463
+ assert.equal(ns, "custom-ns");
464
+ });
465
+
466
+ test("resolveNamespaceFromStorageDir: catalog hint resolves single-hint dir", () => {
467
+ const config = baseConfig({ memoryDir: "/data/memory" });
468
+ const resolvedDir = "/data/memory/namespaces/dynamic-xyz";
469
+ const hints = new Map([[resolvedDir, new Set(["my-dynamic-ns"])]]);
470
+ const ns = resolveNamespaceFromStorageDir(resolvedDir, {
471
+ config,
472
+ configuredNamespaces: getConfiguredNamespaces(config),
473
+ hints,
474
+ });
475
+ assert.equal(ns, "my-dynamic-ns");
476
+ });
477
+
478
+ // ── resolveWritableNamespaceValue ───────────────────────────────────────────
479
+
480
+ test("resolveWritableNamespaceValue: undefined namespace → default, ok", () => {
481
+ const config = baseConfig();
482
+ const result = resolveWritableNamespaceValue(undefined, "sess-1", undefined, config);
483
+ assert.equal(result.ok, true);
484
+ if (result.ok) assert.equal(result.namespace, "default");
485
+ });
486
+
487
+ test("resolveWritableNamespaceValue: explicit namespace, writable principal → ok", () => {
488
+ const config = baseConfig({
489
+ namespacePolicies: [
490
+ { name: "team-a", readPrincipals: ["alice"], writePrincipals: ["alice"] },
491
+ ],
492
+ });
493
+ const result = resolveWritableNamespaceValue("team-a", "alice:sess-1", "alice", config);
494
+ assert.equal(result.ok, true);
495
+ if (result.ok) assert.equal(result.namespace, "team-a");
496
+ });
497
+
498
+ test("resolveWritableNamespaceValue: explicit namespace, non-writable principal → not_writable", () => {
499
+ const config = baseConfig({
500
+ namespacePolicies: [
501
+ { name: "team-a", readPrincipals: ["alice"], writePrincipals: ["alice"] },
502
+ ],
503
+ });
504
+ const result = resolveWritableNamespaceValue("team-a", "bob:sess-1", "bob", config);
505
+ assert.equal(result.ok, false);
506
+ if (!result.ok) {
507
+ assert.equal(result.reason, "not_writable");
508
+ assert.equal(result.namespace, "team-a");
509
+ }
510
+ });
511
+
512
+ test("resolveWritableNamespaceValue: non-default namespace with namespaces disabled → unsupported", () => {
513
+ const config = baseConfig({ namespacesEnabled: false });
514
+ const result = resolveWritableNamespaceValue("custom-ns", "sess-1", undefined, config);
515
+ assert.equal(result.ok, false);
516
+ if (!result.ok) {
517
+ assert.equal(result.reason, "unsupported");
518
+ assert.equal(result.namespace, "custom-ns");
519
+ }
520
+ });
521
+
522
+ test("resolveWritableNamespaceValue: namespaces disabled, default namespace → ok", () => {
523
+ const config = baseConfig({ namespacesEnabled: false });
524
+ const result = resolveWritableNamespaceValue("default", "sess-1", undefined, config);
525
+ assert.equal(result.ok, true);
526
+ if (result.ok) assert.equal(result.namespace, "default");
527
+ });