@remnic/core 9.40.0 → 9.41.0

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 (275) hide show
  1. package/dist/access-admin-ops-surface.d.ts +10 -10
  2. package/dist/access-admin-ops-surface.js +5 -4
  3. package/dist/access-authorization-probe.d.ts +10 -10
  4. package/dist/access-authorization-probe.js +6 -5
  5. package/dist/access-boundary.d.ts +10 -10
  6. package/dist/access-boundary.js +5 -4
  7. package/dist/access-cli.js +16 -15
  8. package/dist/access-cli.js.map +1 -1
  9. package/dist/access-extraction-force-flush.d.ts +10 -10
  10. package/dist/access-extraction-force-flush.js +5 -4
  11. package/dist/access-health-types.d.ts +1 -1
  12. package/dist/access-http-lcm-compaction.d.ts +10 -10
  13. package/dist/access-http-lifecycle-flush.d.ts +10 -10
  14. package/dist/access-http.d.ts +10 -10
  15. package/dist/access-http.js +10 -9
  16. package/dist/access-identity-continuity-surface.d.ts +8 -8
  17. package/dist/access-identity-continuity-surface.js +5 -4
  18. package/dist/access-lcm-surface.d.ts +10 -10
  19. package/dist/access-lcm-surface.js +5 -4
  20. package/dist/access-mcp.d.ts +10 -10
  21. package/dist/access-mcp.js +8 -7
  22. package/dist/access-memory-search-fanout.d.ts +2 -2
  23. package/dist/{access-namespace-preflight-BbDJBq0e.d.ts → access-namespace-preflight-47dOyty9.d.ts} +1 -1
  24. package/dist/access-namespace-preflight.d.ts +3 -3
  25. package/dist/access-namespace-preflight.js +5 -4
  26. package/dist/access-observe-write-surface.d.ts +10 -10
  27. package/dist/access-observe-write-surface.js +5 -4
  28. package/dist/access-operations-batch.js +6 -5
  29. package/dist/access-operations.d.ts +10 -10
  30. package/dist/access-operations.js +7 -6
  31. package/dist/access-recall-concurrency.d.ts +10 -10
  32. package/dist/access-recall-concurrency.js +5 -4
  33. package/dist/access-recall-response.d.ts +10 -10
  34. package/dist/access-recall-response.js +5 -4
  35. package/dist/access-recall-surface.d.ts +10 -10
  36. package/dist/access-recall-surface.js +5 -4
  37. package/dist/{access-service-B2h6rITp.d.ts → access-service-D1BkxwSf.d.ts} +7 -7
  38. package/dist/access-service-helpers.d.ts +10 -10
  39. package/dist/access-service.d.ts +10 -10
  40. package/dist/access-service.js +5 -4
  41. package/dist/access-surface-catalog.d.ts +10 -10
  42. package/dist/access-wearables-meetings-surface.d.ts +3 -3
  43. package/dist/action-confidence.d.ts +1 -1
  44. package/dist/active-memory-bridge.d.ts +1 -1
  45. package/dist/active-recall.d.ts +1 -1
  46. package/dist/active-recall.js +2 -2
  47. package/dist/behavior-learner.d.ts +1 -1
  48. package/dist/behavior-signals.d.ts +1 -1
  49. package/dist/bootstrap.d.ts +8 -8
  50. package/dist/briefing.d.ts +2 -2
  51. package/dist/buffer-surprise-report.d.ts +1 -1
  52. package/dist/buffer-turn-helpers.d.ts +1 -1
  53. package/dist/buffer.d.ts +2 -2
  54. package/dist/bulk-import/index.d.ts +3 -3
  55. package/dist/calibration.d.ts +1 -1
  56. package/dist/capabilities.d.ts +1 -1
  57. package/dist/{catalog-Bz2LxGNb.d.ts → catalog-DPQynQZG.d.ts} +1 -1
  58. package/dist/causal-behavior.d.ts +1 -1
  59. package/dist/causal-consolidation.d.ts +1 -1
  60. package/dist/causal-trajectory-graph.d.ts +1 -1
  61. package/dist/{chunk-443XCR34.js → chunk-3SG4FGDV.js} +53 -41
  62. package/dist/chunk-3SG4FGDV.js.map +1 -0
  63. package/dist/{chunk-XPQ5ZTL6.js → chunk-6I2C3EOH.js} +6 -6
  64. package/dist/chunk-7VIB7PAE.js +142 -0
  65. package/dist/chunk-7VIB7PAE.js.map +1 -0
  66. package/dist/{chunk-AYICY7IE.js → chunk-AXUU7SG3.js} +2 -2
  67. package/dist/{chunk-RK6WMGTX.js → chunk-CQ5MR2HQ.js} +13 -9
  68. package/dist/chunk-CQ5MR2HQ.js.map +1 -0
  69. package/dist/{chunk-KW4ZFTUS.js → chunk-CWPDM66T.js} +5 -5
  70. package/dist/{chunk-NEO4QCID.js → chunk-FHEJ22B7.js} +10 -4
  71. package/dist/chunk-FHEJ22B7.js.map +1 -0
  72. package/dist/{chunk-EBWOOJEP.js → chunk-GDC4INBF.js} +2 -2
  73. package/dist/{chunk-UIVRPXRR.js → chunk-KXGN6G2Y.js} +3 -3
  74. package/dist/chunk-KXGN6G2Y.js.map +1 -0
  75. package/dist/{chunk-AXHMCRS3.js → chunk-OYFDEJZB.js} +23 -6
  76. package/dist/chunk-OYFDEJZB.js.map +1 -0
  77. package/dist/{chunk-SD4A6IZY.js → chunk-PWKZV4A3.js} +2 -2
  78. package/dist/{chunk-XYXYEDPL.js → chunk-Q4XL7LJW.js} +26 -5
  79. package/dist/chunk-Q4XL7LJW.js.map +1 -0
  80. package/dist/{chunk-MOHZIKZM.js → chunk-TM72UOX2.js} +2 -2
  81. package/dist/{chunk-GLBK35BT.js → chunk-UGCUVLOL.js} +14 -10
  82. package/dist/chunk-UGCUVLOL.js.map +1 -0
  83. package/dist/{chunk-Q6QYN56R.js → chunk-UQVGDEAJ.js} +2 -2
  84. package/dist/{chunk-KYYRJVE6.js → chunk-XRWVXPEM.js} +2 -2
  85. package/dist/{cli-CO8qKPM5.d.ts → cli-IUhV-lmk.d.ts} +5 -5
  86. package/dist/cli.d.ts +12 -12
  87. package/dist/cli.js +15 -14
  88. package/dist/compounding/engine.d.ts +2 -2
  89. package/dist/compounding/preference-consolidator.d.ts +1 -1
  90. package/dist/compression-optimizer.d.ts +1 -1
  91. package/dist/config.d.ts +1 -1
  92. package/dist/config.js +2 -2
  93. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  94. package/dist/connectors/codex-materialize.d.ts +1 -1
  95. package/dist/connectors/index.d.ts +1 -1
  96. package/dist/consolidation-provenance-check.d.ts +2 -2
  97. package/dist/consolidation-undo.d.ts +2 -2
  98. package/dist/contradiction/index.d.ts +2 -2
  99. package/dist/conversation-index/backend.d.ts +1 -1
  100. package/dist/conversation-index/chunker.d.ts +1 -1
  101. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  102. package/dist/conversation-index/indexer.d.ts +1 -1
  103. package/dist/conversation-index/search.d.ts +1 -1
  104. package/dist/corpus-watermark.d.ts +7 -1
  105. package/dist/corpus-watermark.js +2 -2
  106. package/dist/day-summary.d.ts +1 -1
  107. package/dist/delinearize.d.ts +1 -1
  108. package/dist/direct-answer-wiring.d.ts +1 -1
  109. package/dist/direct-answer.d.ts +1 -1
  110. package/dist/embedding-fallback.d.ts +1 -1
  111. package/dist/enrichment/index.d.ts +1 -1
  112. package/dist/entity-retrieval.d.ts +2 -2
  113. package/dist/entity-schema.d.ts +1 -1
  114. package/dist/explicit-capture.d.ts +8 -8
  115. package/dist/extraction-error-classification.d.ts +1 -1
  116. package/dist/extraction-faithfulness.d.ts +1 -1
  117. package/dist/extraction-judge-telemetry.d.ts +1 -1
  118. package/dist/extraction-judge-training.d.ts +1 -1
  119. package/dist/extraction-judge.d.ts +1 -1
  120. package/dist/extraction-liveness.d.ts +1 -1
  121. package/dist/extraction-liveness.js +3 -1
  122. package/dist/extraction-source-grounding-rules.d.ts +1 -1
  123. package/dist/extraction-source-grounding.d.ts +1 -1
  124. package/dist/extraction.d.ts +1 -1
  125. package/dist/fallback-llm.d.ts +1 -1
  126. package/dist/graph-dashboard-diff.d.ts +1 -1
  127. package/dist/graph-dashboard-key.d.ts +1 -1
  128. package/dist/graph-dashboard-parser.d.ts +1 -1
  129. package/dist/graph-edge-reinforcement.d.ts +1 -1
  130. package/dist/graph-snapshot.d.ts +1 -1
  131. package/dist/graph.d.ts +1 -1
  132. package/dist/importance.d.ts +1 -1
  133. package/dist/importers/index.d.ts +1 -1
  134. package/dist/in-flight-reads.d.ts +1 -1
  135. package/dist/index.d.ts +19 -19
  136. package/dist/index.js +16 -15
  137. package/dist/intent.d.ts +1 -1
  138. package/dist/lcm/engine.d.ts +1 -1
  139. package/dist/lcm/index.d.ts +1 -1
  140. package/dist/lcm/tools.d.ts +1 -1
  141. package/dist/lifecycle.d.ts +1 -1
  142. package/dist/live-connectors-runner.d.ts +1 -1
  143. package/dist/local-llm.d.ts +1 -1
  144. package/dist/local-model-endpoint.d.ts +1 -1
  145. package/dist/maintenance/memory-governance.d.ts +1 -1
  146. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  147. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
  148. package/dist/{maintenance-D2jkiT9r.d.ts → maintenance-DVBLXkFw.d.ts} +3 -3
  149. package/dist/mcp-memory-inspector-app.d.ts +10 -10
  150. package/dist/memory-action-policy.d.ts +1 -1
  151. package/dist/memory-cache.d.ts +1 -1
  152. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  153. package/dist/memory-projection-store.d.ts +1 -1
  154. package/dist/memory-provenance.d.ts +1 -1
  155. package/dist/memory-worth-outcomes.d.ts +2 -2
  156. package/dist/models-json.d.ts +1 -1
  157. package/dist/namespaces/migrate.d.ts +4 -3
  158. package/dist/namespaces/migrate.js +1 -1
  159. package/dist/namespaces/principal.d.ts +1 -1
  160. package/dist/namespaces/search.d.ts +1 -1
  161. package/dist/namespaces/storage.d.ts +3 -3
  162. package/dist/native-knowledge.d.ts +1 -1
  163. package/dist/offline-sync-impression-drain.d.ts +1 -1
  164. package/dist/operator-doctor-corpus.d.ts +1 -1
  165. package/dist/operator-doctor-corpus.js +3 -3
  166. package/dist/operator-doctor-replica.d.ts +1 -1
  167. package/dist/operator-toolkit.d.ts +5 -4
  168. package/dist/operator-toolkit.js +7 -6
  169. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  170. package/dist/orchestration/maintenance.d.ts +4 -4
  171. package/dist/{orchestrator-SHaRXCUF.d.ts → orchestrator-fYTncnLy.d.ts} +7 -7
  172. package/dist/orchestrator.d.ts +8 -8
  173. package/dist/orchestrator.js +16 -15
  174. package/dist/patterns-cli.d.ts +1 -1
  175. package/dist/{pipeline-B5_tS1Yc.d.ts → pipeline-Cs76XC-p.d.ts} +1 -1
  176. package/dist/policy-runtime.d.ts +1 -1
  177. package/dist/proactive-contention.d.ts +1 -1
  178. package/dist/provenance.d.ts +1 -1
  179. package/dist/{qmd-CkEV_SPG.d.ts → qmd-WJwvBgiF.d.ts} +1 -1
  180. package/dist/qmd-preflight.d.ts +2 -2
  181. package/dist/qmd-recall-cache.d.ts +1 -1
  182. package/dist/qmd.d.ts +2 -2
  183. package/dist/recall-concurrency-config.d.ts +1 -1
  184. package/dist/recall-disclosure-escalation.d.ts +1 -1
  185. package/dist/recall-explain-renderer.d.ts +1 -1
  186. package/dist/recall-memory-map.d.ts +1 -1
  187. package/dist/recall-planner-llm.d.ts +1 -1
  188. package/dist/recall-state.d.ts +1 -1
  189. package/dist/recall-tag-filter.d.ts +1 -1
  190. package/dist/recall-timings.d.ts +1 -1
  191. package/dist/recall-xray-cli.d.ts +1 -1
  192. package/dist/recall-xray-renderer.d.ts +1 -1
  193. package/dist/recall-xray.d.ts +1 -1
  194. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  195. package/dist/replay/normalizers/claude.d.ts +1 -1
  196. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  197. package/dist/replay/normalizers/shared.d.ts +1 -1
  198. package/dist/replay/runner.d.ts +1 -1
  199. package/dist/replay/types.d.ts +1 -1
  200. package/dist/replica-divergence.d.ts +1 -1
  201. package/dist/replica-peers-config.d.ts +1 -1
  202. package/dist/resolve-auth-token.d.ts +1 -1
  203. package/dist/resume-bundles.js +3 -3
  204. package/dist/retrieval-agents.d.ts +2 -2
  205. package/dist/retrieval-tiers.d.ts +1 -1
  206. package/dist/routing/engine.d.ts +1 -1
  207. package/dist/routing/store.d.ts +1 -1
  208. package/dist/salvage-envelope.d.ts +1 -1
  209. package/dist/{scope-profiles-DeAdnocS.d.ts → scope-profiles-CQ0NAleS.d.ts} +1 -1
  210. package/dist/search/embed-helper.d.ts +1 -1
  211. package/dist/search/factory.d.ts +1 -1
  212. package/dist/search/index.d.ts +1 -1
  213. package/dist/search/lancedb-backend.d.ts +1 -1
  214. package/dist/search/meilisearch-backend.d.ts +1 -1
  215. package/dist/search/noop-backend.d.ts +1 -1
  216. package/dist/search/orama-backend.d.ts +1 -1
  217. package/dist/search/port.d.ts +1 -1
  218. package/dist/search/remote-backend.d.ts +1 -1
  219. package/dist/{semantic-consolidation-DR_h-JUD.d.ts → semantic-consolidation-BWxOBMtz.d.ts} +1 -1
  220. package/dist/semantic-consolidation.d.ts +2 -2
  221. package/dist/semantic-rule-verifier.d.ts +1 -1
  222. package/dist/{service-BTb94UPJ.d.ts → service-CLuMO-pa.d.ts} +2 -2
  223. package/dist/session-observer-bands.d.ts +1 -1
  224. package/dist/session-observer-state.d.ts +1 -1
  225. package/dist/shared-context/manager.d.ts +1 -1
  226. package/dist/signal.d.ts +1 -1
  227. package/dist/source-agent-qualifier.d.ts +1 -1
  228. package/dist/{storage-ax2EIsVS.d.ts → storage-Blr71GpS.d.ts} +1 -1
  229. package/dist/storage.d.ts +2 -2
  230. package/dist/summarizer.d.ts +1 -1
  231. package/dist/summary-snapshot.d.ts +1 -1
  232. package/dist/temporal-supersession.d.ts +2 -2
  233. package/dist/temporal-timeline-recall.d.ts +1 -1
  234. package/dist/temporal-validity.d.ts +1 -1
  235. package/dist/threading.d.ts +1 -1
  236. package/dist/tier-migration.d.ts +2 -2
  237. package/dist/tier-routing.d.ts +1 -1
  238. package/dist/topics.d.ts +1 -1
  239. package/dist/transcript.d.ts +1 -1
  240. package/dist/trust-score-stage.d.ts +1 -1
  241. package/dist/trust-score.d.ts +1 -1
  242. package/dist/{types-CCGL-9NX.d.ts → types-CqWwMPr_.d.ts} +41 -59
  243. package/dist/types.d.ts +1 -1
  244. package/dist/utility-runtime.d.ts +1 -1
  245. package/dist/write-envelope.d.ts +1 -1
  246. package/package.json +2 -2
  247. package/src/access-http.test.ts +2 -2
  248. package/src/access-service-health.test.ts +39 -17
  249. package/src/access-service.ts +25 -3
  250. package/src/cli.ts +3 -3
  251. package/src/corpus-watermark.test.ts +16 -0
  252. package/src/corpus-watermark.ts +28 -5
  253. package/src/extraction-liveness.test.ts +81 -19
  254. package/src/extraction-liveness.ts +96 -98
  255. package/src/extraction-watermark.test.ts +290 -0
  256. package/src/namespaces/migrate.ts +10 -2
  257. package/src/operator-doctor-corpus.test.ts +7 -0
  258. package/src/operator-doctor-corpus.ts +1 -1
  259. package/src/operator-toolkit.ts +8 -8
  260. package/src/orchestration/extraction-watermark.ts +188 -0
  261. package/dist/chunk-443XCR34.js.map +0 -1
  262. package/dist/chunk-AXHMCRS3.js.map +0 -1
  263. package/dist/chunk-GLBK35BT.js.map +0 -1
  264. package/dist/chunk-NEO4QCID.js.map +0 -1
  265. package/dist/chunk-RK6WMGTX.js.map +0 -1
  266. package/dist/chunk-UIVRPXRR.js.map +0 -1
  267. package/dist/chunk-XYXYEDPL.js.map +0 -1
  268. /package/dist/{chunk-XPQ5ZTL6.js.map → chunk-6I2C3EOH.js.map} +0 -0
  269. /package/dist/{chunk-AYICY7IE.js.map → chunk-AXUU7SG3.js.map} +0 -0
  270. /package/dist/{chunk-KW4ZFTUS.js.map → chunk-CWPDM66T.js.map} +0 -0
  271. /package/dist/{chunk-EBWOOJEP.js.map → chunk-GDC4INBF.js.map} +0 -0
  272. /package/dist/{chunk-SD4A6IZY.js.map → chunk-PWKZV4A3.js.map} +0 -0
  273. /package/dist/{chunk-MOHZIKZM.js.map → chunk-TM72UOX2.js.map} +0 -0
  274. /package/dist/{chunk-Q6QYN56R.js.map → chunk-UQVGDEAJ.js.map} +0 -0
  275. /package/dist/{chunk-KYYRJVE6.js.map → chunk-XRWVXPEM.js.map} +0 -0
@@ -21,7 +21,7 @@
21
21
  import { coerceBool, coerceNumber } from "./connectors/coerce.js";
22
22
  import { log } from "./logger.js";
23
23
  import type { OperatorDoctorCheck } from "./operator-doctor-types.js";
24
- import type { MetaState, PluginConfig } from "./types.js";
24
+ import type { PluginConfig } from "./types.js";
25
25
 
26
26
  /** Default staleness window: 24h. A watermark older than this with a non-empty buffer is degraded. */
27
27
  const DEFAULT_STALE_WINDOW_MS = 86_400_000;
@@ -60,23 +60,57 @@ export interface ExtractionBufferSource {
60
60
  getBufferSnapshot(): Promise<ExtractionBufferSnapshot>;
61
61
  }
62
62
 
63
+ export interface ExtractionRootStats {
64
+ extractionCount?: number;
65
+ lastConsolidationAt?: string | null;
66
+ }
67
+
68
+ export interface ExtractionWatermarkRead {
69
+ lastExtractionAt: string | null;
70
+ readFailed: boolean;
71
+ readError?: string;
72
+ pending?: boolean;
73
+ rootStats?: ExtractionRootStats;
74
+ }
75
+
76
+ export type ExtractionWatermarkOrStorage =
77
+ | ExtractionWatermarkRead
78
+ | { readMetadata(): Promise<{ lastExtractionAt?: string | null } | null> }
79
+ | { loadMeta(): Promise<{ lastExtractionAt?: string | null }> };
80
+
81
+ export async function coerceExtractionWatermark(
82
+ watermark: ExtractionWatermarkOrStorage,
83
+ ): Promise<ExtractionWatermarkRead> {
84
+ if (watermark && "readFailed" in watermark && typeof watermark.readFailed === "boolean") {
85
+ return watermark as ExtractionWatermarkRead;
86
+ }
87
+ try {
88
+ let lastExtractionAt: string | null = null;
89
+ if ("loadMeta" in watermark && typeof watermark.loadMeta === "function") {
90
+ const meta = await watermark.loadMeta();
91
+ lastExtractionAt = meta?.lastExtractionAt ?? null;
92
+ } else if ("readMetadata" in watermark && typeof watermark.readMetadata === "function") {
93
+ const meta = await watermark.readMetadata();
94
+ lastExtractionAt = meta?.lastExtractionAt ?? null;
95
+ }
96
+ return {
97
+ lastExtractionAt,
98
+ readFailed: true,
99
+ readError: "aggregate watermark unavailable from legacy root-store-only storage argument",
100
+ };
101
+ } catch (error) {
102
+ return {
103
+ lastExtractionAt: null,
104
+ readFailed: true,
105
+ readError: error instanceof Error ? error.message : String(error),
106
+ };
107
+ }
108
+ }
109
+
63
110
  /**
64
- * The liveness payload surfaced on `/health.extraction` and in the doctor/stats
65
- * details.
66
- *
67
- * DAEMON-SCOPED SIGNAL: a daemon exposes a single extraction verdict; the
68
- * last-extraction watermark is read from the daemon's default/root store, so
69
- * `/health` returns the same `extraction` block for every namespace argument.
70
- *
71
- * KNOWN LIMITATION (issue #2159): extraction stamps `lastExtractionAt` on the
72
- * PER-NAMESPACE store (`storageFor(selfNamespace)`), so this watermark reflects
73
- * only extraction that stamped the root store (default-namespace work). On a
74
- * single-namespace deployment (the common case) the watermark and buffer share
75
- * one scope and the verdict is exact. On a multi-namespace deployment the verdict
76
- * is NOT guaranteed correct for work isolated to a non-default namespace: it can
77
- * over-report `degraded` (the default namespace idle while another is extracting)
78
- * or miss a stall confined to a non-default namespace. Aggregating the watermark
79
- * across namespace stores is tracked in #2159.
111
+ * The daemon-scoped liveness payload surfaced on `/health.extraction` and in
112
+ * doctor/stats details. Its watermark is the maximum successful extraction
113
+ * across every distinct namespace store, matching the daemon-wide buffer scope.
80
114
  */
81
115
  export interface ExtractionLivenessStatus {
82
116
  lastExtractionAt: string | null;
@@ -85,18 +119,8 @@ export interface ExtractionLivenessStatus {
85
119
  oldestBufferedTurnAgeMs: number | null;
86
120
  degraded: boolean;
87
121
  degradedReason: string | null;
88
- /**
89
- * Scope of the last-extraction watermark: `root-store` today (read from the
90
- * daemon's default/root store only), `aggregate` once it is resolved across
91
- * every namespace store (#2159). A monitor can alert on
92
- * `watermarkScope !== "aggregate"` while namespaces are enabled instead of
93
- * trusting a number that may be wrong in either direction.
94
- */
95
- watermarkScope: "root-store" | "aggregate";
96
- }
97
-
98
- interface ExtractionLivenessStorageLike {
99
- loadMeta(): Promise<Pick<MetaState, "lastExtractionAt">>;
122
+ watermarkPending?: boolean;
123
+ watermarkScope: "aggregate";
100
124
  }
101
125
 
102
126
  interface ExtractionLivenessOrchestratorLike {
@@ -190,7 +214,7 @@ export function evaluateExtractionLiveness(input: {
190
214
  nowMs: number;
191
215
  metaReadFailed?: boolean;
192
216
  metaReadError?: string;
193
- watermarkScope?: "root-store" | "aggregate";
217
+ watermarkPending?: boolean;
194
218
  }): ExtractionLivenessStatus {
195
219
  const { config, lastExtractionAt, snapshot, nowMs } = input;
196
220
  const lastMs = lastExtractionAt !== null ? Date.parse(lastExtractionAt) : Number.NaN;
@@ -204,8 +228,10 @@ export function evaluateExtractionLiveness(input: {
204
228
  // (§22): a storage/pipeline outage must not read as healthy, and must stay
205
229
  // distinct from a pipeline that has genuinely never extracted.
206
230
  const metaReadFailed = input.metaReadFailed === true;
231
+ const watermarkPending = input.watermarkPending === true;
207
232
  const readFailed = snapshot.readFailed === true;
208
- const degraded = config.enabled && (metaReadFailed || readFailed || (hasBacklog && stale));
233
+ const degraded =
234
+ config.enabled && (metaReadFailed || readFailed || (!watermarkPending && hasBacklog && stale));
209
235
 
210
236
  let degradedReason: string | null = null;
211
237
  if (degraded) {
@@ -230,7 +256,8 @@ export function evaluateExtractionLiveness(input: {
230
256
  oldestBufferedTurnAgeMs: ageMsFrom(snapshot.oldestTurnTimestamp, nowMs),
231
257
  degraded,
232
258
  degradedReason,
233
- watermarkScope: input.watermarkScope ?? "root-store",
259
+ ...(watermarkPending ? { watermarkPending: true } : {}),
260
+ watermarkScope: "aggregate",
234
261
  };
235
262
  }
236
263
 
@@ -262,40 +289,27 @@ async function readBufferSnapshot(
262
289
  }
263
290
 
264
291
  /**
265
- * Gather the liveness status from a meta store (last-extraction watermark) and
266
- * an optional buffer (backlog). A read failure on EITHER side is captured and
267
- * surfaced as a DEGRADED status with a distinct reason (§22) rather than being
268
- * swallowed into a healthy-looking verdict; no failure path throws.
292
+ * Gather the liveness status from an aggregate watermark read and an optional
293
+ * buffer. A read failure on either side is surfaced as a degraded status with a
294
+ * distinct reason rather than swallowed into a healthy-looking verdict.
269
295
  */
270
296
  export async function gatherExtractionLivenessStatus(input: {
271
297
  config: ExtractionLivenessConfig;
272
- storage: ExtractionLivenessStorageLike;
298
+ watermark: ExtractionWatermarkOrStorage;
273
299
  buffer: ExtractionBufferSource | undefined;
274
300
  nowMs: number;
275
301
  }): Promise<ExtractionLivenessStatus> {
276
- const { config, storage, buffer, nowMs } = input;
277
- let lastExtractionAt: string | null = null;
278
- let metaReadFailed = false;
279
- let metaReadError: string | undefined;
280
- try {
281
- lastExtractionAt = (await storage.loadMeta()).lastExtractionAt ?? null;
282
- } catch (err) {
283
- // A swallowed loadMeta() failure that reads as "never extracted" is the
284
- // exact §22 conflation this feature exists to kill — surface it explicitly.
285
- metaReadFailed = true;
286
- metaReadError = err instanceof Error ? err.message : String(err);
287
- }
302
+ const { config, buffer, nowMs } = input;
303
+ const watermarkRead = await coerceExtractionWatermark(input.watermark);
288
304
  const snapshot = await readBufferSnapshot(buffer);
289
305
  return evaluateExtractionLiveness({
290
306
  config,
291
- lastExtractionAt,
307
+ lastExtractionAt: watermarkRead.lastExtractionAt,
292
308
  snapshot,
293
309
  nowMs,
294
- metaReadFailed,
295
- metaReadError,
296
- // Today the watermark comes from the root store only; #2159 flips this to
297
- // "aggregate" when the resolver reads across every namespace store.
298
- watermarkScope: "root-store",
310
+ metaReadFailed: watermarkRead.readFailed,
311
+ metaReadError: watermarkRead.readError,
312
+ watermarkPending: watermarkRead.pending,
299
313
  });
300
314
  }
301
315
 
@@ -349,25 +363,19 @@ export function resolveExtractionLivenessConfig(
349
363
  }
350
364
 
351
365
  /**
352
- * Compute the liveness status for the authenticated `/health` payload and emit
353
- * the throttled aggregated WARN.
354
- *
355
- * The watermark is read from the daemon's default/root store (`orchestrator.storage`),
356
- * which is why `/health` returns the same `extraction` block for any namespace
357
- * argument. KNOWN LIMITATION (issue #2159): extraction writes `lastExtractionAt`
358
- * per-namespace, so this watermark reflects only default-namespace extraction and
359
- * the verdict is exact only for a single-namespace daemon (see
360
- * `ExtractionLivenessStatus`).
366
+ * Compute the daemon-wide liveness status for the authenticated `/health`
367
+ * payload and emit the throttled aggregate warning.
361
368
  */
362
369
  export async function computeExtractionLivenessStatus(
363
- orchestrator: ExtractionLivenessOrchestratorLike & { storage: ExtractionLivenessStorageLike },
370
+ orchestrator: ExtractionLivenessOrchestratorLike,
371
+ watermark: ExtractionWatermarkOrStorage,
364
372
  throttle?: ExtractionLivenessWarnThrottle,
365
373
  nowMs: number = Date.now(),
366
374
  ): Promise<ExtractionLivenessStatus> {
367
375
  const config = resolveExtractionLivenessConfig(orchestrator.config.extractionLiveness);
368
376
  const status = await gatherExtractionLivenessStatus({
369
377
  config,
370
- storage: orchestrator.storage,
378
+ watermark,
371
379
  buffer: orchestrator.buffer,
372
380
  nowMs,
373
381
  });
@@ -381,22 +389,18 @@ export async function computeExtractionLivenessStatus(
381
389
  */
382
390
  export async function summarizeExtractionLiveness(
383
391
  config: Pick<PluginConfig, "extractionLiveness">,
384
- storage: ExtractionLivenessStorageLike,
392
+ watermark: ExtractionWatermarkOrStorage,
385
393
  buffer?: ExtractionBufferSource,
386
394
  nowMs: number = Date.now(),
387
395
  ): Promise<OperatorDoctorCheck> {
388
396
  const livenessConfig = resolveExtractionLivenessConfig(config.extractionLiveness);
389
397
  const status = await gatherExtractionLivenessStatus({
390
398
  config: livenessConfig,
391
- storage,
399
+ watermark,
392
400
  buffer,
393
401
  nowMs,
394
402
  });
395
- const scopeNote =
396
- ` Watermark scope: ${status.watermarkScope}` +
397
- (status.watermarkScope === "root-store"
398
- ? " (default-namespace only; multi-namespace aggregation tracked in #2159)."
399
- : ".");
403
+ const scopeNote = ` Watermark scope: ${status.watermarkScope}.`;
400
404
  const summary =
401
405
  (status.degraded
402
406
  ? `Extraction pipeline degraded: ${status.degradedReason}.`
@@ -416,44 +420,38 @@ export async function summarizeExtractionLiveness(
416
420
  };
417
421
  }
418
422
 
419
- /** Meta fields the `remnic stats` view renders directly, beyond the liveness watermark. */
420
- type ExtractionStatsMeta = Pick<MetaState, "extractionCount" | "lastExtractionAt" | "lastConsolidationAt">;
421
-
422
423
  /**
423
- * Lines for `remnic stats`: extraction watermark, buffer backlog, and the
424
- * liveness verdict so dashboards can alert on a stalled pipeline. The meta
425
- * watermark is loaded HERE (not by the caller) so a meta-read failure surfaces
426
- * as an explicit DEGRADED verdict plus "unavailable" counts (§22) instead of
427
- * crashing the whole `stats` command.
424
+ * Lines for `remnic stats`: the aggregate extraction watermark, root metadata
425
+ * counters, buffer backlog, and liveness verdict. Root counter failures render
426
+ * those counters unavailable; the injected aggregate read alone determines
427
+ * watermark liveness so stats cannot diverge from health and doctor.
428
428
  */
429
429
  export async function renderExtractionLivenessStats(
430
- orchestrator: ExtractionLivenessOrchestratorLike & { storage: { loadMeta(): Promise<ExtractionStatsMeta> } },
430
+ orchestrator: ExtractionLivenessOrchestratorLike,
431
+ watermark: ExtractionWatermarkRead,
431
432
  nowMs: number = Date.now(),
432
433
  ): Promise<string[]> {
433
- let meta: ExtractionStatsMeta | null = null;
434
- let metaReadFailed = false;
435
- let metaReadError: string | undefined;
436
- try {
437
- meta = await orchestrator.storage.loadMeta();
438
- } catch (err) {
439
- metaReadFailed = true;
440
- metaReadError = err instanceof Error ? err.message : String(err);
441
- }
442
434
  const status = evaluateExtractionLiveness({
443
435
  config: resolveExtractionLivenessConfig(orchestrator.config.extractionLiveness),
444
- lastExtractionAt: meta?.lastExtractionAt ?? null,
436
+ lastExtractionAt: watermark.lastExtractionAt,
445
437
  snapshot: await readBufferSnapshot(orchestrator.buffer),
446
438
  nowMs,
447
- metaReadFailed,
448
- metaReadError,
439
+ metaReadFailed: watermark.readFailed,
440
+ metaReadError: watermark.readError,
441
+ watermarkPending: watermark.pending,
449
442
  });
450
443
  const oldestAge =
451
444
  status.oldestBufferedTurnAgeMs !== null ? formatAgeMs(status.oldestBufferedTurnAgeMs) : "n/a";
452
- const unavailable = metaReadFailed ? "unavailable" : "never";
445
+ const rootStats = watermark.rootStats;
446
+ const watermarkDisplay = watermark.readFailed
447
+ ? "unavailable"
448
+ : watermark.pending
449
+ ? "pending"
450
+ : status.lastExtractionAt ?? "never";
453
451
  return [
454
- `Extractions: ${meta?.extractionCount ?? unavailable}`,
455
- `Last extraction: ${meta?.lastExtractionAt ?? unavailable}`,
456
- `Last consolidation: ${meta?.lastConsolidationAt ?? unavailable}`,
452
+ `Extractions: ${rootStats?.extractionCount ?? "unavailable"}`,
453
+ `Last extraction: ${watermarkDisplay}`,
454
+ `Last consolidation: ${rootStats ? rootStats.lastConsolidationAt ?? "never" : "unavailable"}`,
457
455
  `Buffered sessions: ${status.bufferedSessionCount} (${status.pendingTurnCount} turns pending)`,
458
456
  `Oldest buffered turn age: ${oldestAge}`,
459
457
  `Extraction watermark scope: ${status.watermarkScope}`,
@@ -0,0 +1,290 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ import { TOKEN_CAPABILITIES_VERSION } from "./access-token-capabilities.js";
8
+ import { parseConfig } from "./config.js";
9
+ import { CorpusWatermarkCache } from "./corpus-watermark.js";
10
+ import { readAggregateExtractionWatermark } from "./orchestration/extraction-watermark.js";
11
+ import type { PluginConfig } from "./types.js";
12
+
13
+ interface FakeWatermarkStorage {
14
+ readonly dir: string;
15
+ loadMeta(): Promise<{ lastExtractionAt: string | null }>;
16
+ }
17
+
18
+ function storage(dir: string, read: () => Promise<string | null>): FakeWatermarkStorage {
19
+ return {
20
+ dir,
21
+ loadMeta: async () => ({ lastExtractionAt: await read() }),
22
+ };
23
+ }
24
+
25
+ async function namespacedFixture(): Promise<{
26
+ memoryDir: string;
27
+ config: PluginConfig;
28
+ namespaceDirs: Record<string, string>;
29
+ }> {
30
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-extraction-watermark-"));
31
+ const namespaceDirs = {
32
+ "team-a": path.join(memoryDir, "namespaces", "team-a"),
33
+ "team-b": path.join(memoryDir, "namespaces", "team-b"),
34
+ };
35
+ await Promise.all(Object.values(namespaceDirs).map((dir) => mkdir(dir, { recursive: true })));
36
+ return {
37
+ memoryDir,
38
+ config: parseConfig({ memoryDir, namespacesEnabled: true, defaultNamespace: "default" }),
39
+ namespaceDirs,
40
+ };
41
+ }
42
+
43
+ test("aggregate watermark selects the most recent extraction across distinct namespace stores", async () => {
44
+ const fixture = await namespacedFixture();
45
+ try {
46
+ const timestamps = {
47
+ default: "2026-07-20T12:00:00.000Z",
48
+ "team-a": "2026-07-21T12:00:00.000Z",
49
+ "team-b": "2026-07-22T12:00:00.000Z",
50
+ };
51
+ const result = await readAggregateExtractionWatermark({
52
+ config: fixture.config,
53
+ rootStorage: storage(fixture.memoryDir, async () => timestamps.default),
54
+ storageForNamespace: async (namespace) =>
55
+ storage(fixture.namespaceDirs[namespace], async () => timestamps[namespace as keyof typeof timestamps]),
56
+ });
57
+
58
+ assert.deepEqual(result, {
59
+ lastExtractionAt: timestamps["team-b"],
60
+ readFailed: false,
61
+ });
62
+ } finally {
63
+ await rm(fixture.memoryDir, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ test("a namespace-isolated extraction advances the daemon aggregate watermark", async () => {
68
+ const fixture = await namespacedFixture();
69
+ try {
70
+ const rootTimestamp = "2026-07-20T12:00:00.000Z";
71
+ let teamTimestamp = "2026-07-19T12:00:00.000Z";
72
+ const options = {
73
+ config: fixture.config,
74
+ rootStorage: storage(fixture.memoryDir, async () => rootTimestamp),
75
+ storageForNamespace: async (namespace: string) =>
76
+ storage(fixture.namespaceDirs[namespace], async () => (namespace === "team-a" ? teamTimestamp : null)),
77
+ };
78
+
79
+ assert.equal((await readAggregateExtractionWatermark(options)).lastExtractionAt, rootTimestamp);
80
+ teamTimestamp = "2026-07-23T12:00:00.000Z";
81
+ assert.equal((await readAggregateExtractionWatermark(options)).lastExtractionAt, teamTimestamp);
82
+ } finally {
83
+ await rm(fixture.memoryDir, { recursive: true, force: true });
84
+ }
85
+ });
86
+
87
+ test("a partial namespace read returns readFailed instead of a survivor's fresh watermark", async () => {
88
+ const fixture = await namespacedFixture();
89
+ try {
90
+ const result = await readAggregateExtractionWatermark({
91
+ config: fixture.config,
92
+ rootStorage: storage(fixture.memoryDir, async () => "2026-07-23T12:00:00.000Z"),
93
+ storageForNamespace: async (namespace) =>
94
+ storage(fixture.namespaceDirs[namespace], async () => {
95
+ if (namespace === "team-b") throw new Error("meta store unavailable");
96
+ return "2026-07-22T12:00:00.000Z";
97
+ }),
98
+ });
99
+
100
+ assert.equal(result.lastExtractionAt, null, "a surviving timestamp must not be reported as complete");
101
+ assert.equal(result.readFailed, true);
102
+ assert.match(result.readError ?? "", /namespace watermark unreadable/);
103
+ assert.match(result.readError ?? "", /meta store unavailable/);
104
+ } finally {
105
+ await rm(fixture.memoryDir, { recursive: true, force: true });
106
+ }
107
+ });
108
+ test("scoped capabilities filter candidate namespaces during aggregate read", async () => {
109
+ const fixture = await namespacedFixture();
110
+ try {
111
+ let scannedTeamB = false;
112
+ const result = await readAggregateExtractionWatermark({
113
+ config: fixture.config,
114
+ rootStorage: storage(fixture.memoryDir, async () => "2026-07-20T12:00:00.000Z"),
115
+ storageForNamespace: async (namespace) => {
116
+ if (namespace === "team-b") scannedTeamB = true;
117
+ return storage(fixture.namespaceDirs[namespace], async () => "2026-07-25T12:00:00.000Z");
118
+ },
119
+ caps: { version: TOKEN_CAPABILITIES_VERSION, namespaces: ["team-a"] },
120
+ });
121
+
122
+ assert.equal(scannedTeamB, false, "team-b should be skipped when token is restricted to team-a");
123
+ assert.equal(result.readFailed, false);
124
+ } finally {
125
+ await rm(fixture.memoryDir, { recursive: true, force: true });
126
+ }
127
+ });
128
+
129
+ test("default root capability check uses normalized aliases for migrated storage", async () => {
130
+ const fixture = await namespacedFixture();
131
+ try {
132
+ const migratedDefaultDir = path.join(fixture.memoryDir, "namespaces", "default");
133
+ await mkdir(migratedDefaultDir, { recursive: true });
134
+ const result = await readAggregateExtractionWatermark({
135
+ config: { ...fixture.config, defaultNamespace: " default " },
136
+ rootStorage: storage(fixture.memoryDir, async () => {
137
+ throw new Error("inactive legacy root is unreadable");
138
+ }),
139
+ storageForNamespace: async (_namespace, rootDir) =>
140
+ storage(rootDir, async () =>
141
+ path.resolve(rootDir) === path.resolve(migratedDefaultDir)
142
+ ? "2026-07-26T12:00:00.000Z"
143
+ : "2026-07-24T12:00:00.000Z",
144
+ ),
145
+ caps: { version: TOKEN_CAPABILITIES_VERSION, namespaces: ["default"] },
146
+ });
147
+
148
+ assert.equal(result.lastExtractionAt, "2026-07-26T12:00:00.000Z");
149
+ assert.equal(result.readFailed, false);
150
+ } finally {
151
+ await rm(fixture.memoryDir, { recursive: true, force: true });
152
+ }
153
+ });
154
+ test("unparsable non-null timestamp in namespace store fails the aggregate read", async () => {
155
+ const fixture = await namespacedFixture();
156
+ try {
157
+ const result = await readAggregateExtractionWatermark({
158
+ config: fixture.config,
159
+ rootStorage: storage(fixture.memoryDir, async () => "2026-07-20T12:00:00.000Z"),
160
+ storageForNamespace: async (namespace) =>
161
+ storage(fixture.namespaceDirs[namespace], async () => (namespace === "team-a" ? "invalid-date-string" : "2026-07-25T12:00:00.000Z")),
162
+ });
163
+
164
+ assert.equal(result.lastExtractionAt, null);
165
+ assert.equal(result.readFailed, true);
166
+ assert.match(result.readError ?? "", /watermark timestamp invalid/);
167
+ } finally {
168
+ await rm(fixture.memoryDir, { recursive: true, force: true });
169
+ }
170
+ });
171
+
172
+ test("namespace discovery I/O failure is an explicit incomplete read", async () => {
173
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-extraction-watermark-enumeration-"));
174
+ try {
175
+ await writeFile(path.join(memoryDir, "namespaces"), "not a directory", "utf8");
176
+ const config = parseConfig({ memoryDir, namespacesEnabled: true });
177
+ const result = await readAggregateExtractionWatermark({
178
+ config,
179
+ rootStorage: storage(memoryDir, async () => "2026-07-23T12:00:00.000Z"),
180
+ storageForNamespace: async (_namespace, rootDir) => storage(rootDir, async () => null),
181
+ });
182
+
183
+ assert.equal(result.lastExtractionAt, null);
184
+ assert.equal(result.readFailed, true);
185
+ assert.match(result.readError ?? "", /namespace watermark enumeration failed/);
186
+ assert.match(result.readError ?? "", /ENOTDIR/);
187
+ } finally {
188
+ await rm(memoryDir, { recursive: true, force: true });
189
+ }
190
+ });
191
+
192
+ test("a failed stale-cache refresh makes later reads incomplete", async () => {
193
+ const fixture = await namespacedFixture();
194
+ try {
195
+ let now = 1;
196
+ const cache = new CorpusWatermarkCache({ ttlMs: 1, clock: () => now });
197
+ const options = {
198
+ config: fixture.config,
199
+ rootStorage: storage(fixture.memoryDir, async () => "2026-07-20T12:00:00.000Z"),
200
+ storageForNamespace: async (namespace: string) =>
201
+ storage(fixture.namespaceDirs[namespace], async () => "2026-07-21T12:00:00.000Z"),
202
+ rootsCache: cache,
203
+ };
204
+
205
+ const coldRead = await readAggregateExtractionWatermark(options);
206
+ assert.equal(coldRead.readFailed, false);
207
+ assert.equal(coldRead.pending, true);
208
+ await cache.whenIdle();
209
+ assert.equal((await readAggregateExtractionWatermark(options)).readFailed, false);
210
+
211
+ await rm(path.join(fixture.memoryDir, "namespaces"), { recursive: true, force: true });
212
+ await writeFile(path.join(fixture.memoryDir, "namespaces"), "not a directory", "utf8");
213
+ now += 2;
214
+ assert.equal(
215
+ (await readAggregateExtractionWatermark(options)).readFailed,
216
+ false,
217
+ "the stale value may be served while refresh is still in flight",
218
+ );
219
+ await cache.whenIdle();
220
+ const failedRefresh = await readAggregateExtractionWatermark(options);
221
+ assert.equal(failedRefresh.lastExtractionAt, null);
222
+ assert.equal(failedRefresh.readFailed, true);
223
+ assert.match(failedRefresh.readError ?? "", /namespace watermark enumeration failed/);
224
+ } finally {
225
+ await rm(fixture.memoryDir, { recursive: true, force: true });
226
+ }
227
+ });
228
+
229
+ test("a cold namespace-root cache is pending, not a failed read", async () => {
230
+ const fixture = await namespacedFixture();
231
+ try {
232
+ let rootReads = 0;
233
+ const result = await readAggregateExtractionWatermark({
234
+ config: fixture.config,
235
+ rootStorage: storage(fixture.memoryDir, async () => {
236
+ rootReads += 1;
237
+ return "2026-07-23T12:00:00.000Z";
238
+ }),
239
+ storageForNamespace: async (namespace) => storage(fixture.namespaceDirs[namespace], async () => null),
240
+ rootsCache: {
241
+ getResolvedRootsStatus: () => ({ roots: undefined, refreshError: undefined }),
242
+ },
243
+ });
244
+
245
+ assert.equal(result.lastExtractionAt, null);
246
+ assert.equal(result.readFailed, false);
247
+ assert.equal(result.pending, true);
248
+ assert.equal(rootReads, 0, "a root survivor is not read or published before enumeration completes");
249
+ } finally {
250
+ await rm(fixture.memoryDir, { recursive: true, force: true });
251
+ }
252
+ });
253
+
254
+ test("namespaces disabled performs exactly one root metadata read", async () => {
255
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-extraction-watermark-single-"));
256
+ try {
257
+ const config = parseConfig({ memoryDir, namespacesEnabled: false });
258
+ let rootReads = 0;
259
+ let namespaceResolutions = 0;
260
+ const timestamp = "2026-07-23T12:00:00.000Z";
261
+ const result = await readAggregateExtractionWatermark({
262
+ config,
263
+ rootStorage: {
264
+ dir: memoryDir,
265
+ loadMeta: async () => {
266
+ rootReads += 1;
267
+ return {
268
+ lastExtractionAt: timestamp,
269
+ extractionCount: 9,
270
+ lastConsolidationAt: null,
271
+ };
272
+ },
273
+ },
274
+ storageForNamespace: async () => {
275
+ namespaceResolutions += 1;
276
+ throw new Error("disabled path must not resolve namespace storage");
277
+ },
278
+ });
279
+
280
+ assert.deepEqual(result, {
281
+ lastExtractionAt: timestamp,
282
+ readFailed: false,
283
+ rootStats: { extractionCount: 9, lastConsolidationAt: null },
284
+ });
285
+ assert.equal(rootReads, 1);
286
+ assert.equal(namespaceResolutions, 0);
287
+ } finally {
288
+ await rm(memoryDir, { recursive: true, force: true });
289
+ }
290
+ });
@@ -65,6 +65,7 @@ async function hasAnyLegacyData(rootDir: string): Promise<boolean> {
65
65
 
66
66
  async function discoverConfiguredNamespaces(
67
67
  config: PluginConfig,
68
+ propagateDiscoveryErrors: boolean,
68
69
  ): Promise<string[]> {
69
70
  const discovered = new Set<string>([
70
71
  config.defaultNamespace,
@@ -85,7 +86,10 @@ async function discoverConfiguredNamespaces(
85
86
  discovered.add(namespace);
86
87
  }
87
88
  }
88
- } catch {
89
+ } catch (error) {
90
+ if (propagateDiscoveryErrors && (error as NodeJS.ErrnoException).code !== "ENOENT") {
91
+ throw error;
92
+ }
89
93
  // No namespace directory yet.
90
94
  }
91
95
 
@@ -95,9 +99,13 @@ async function discoverConfiguredNamespaces(
95
99
  export async function listNamespaces(options: {
96
100
  config: PluginConfig;
97
101
  storageRouter?: NamespaceStorageRouter;
102
+ propagateDiscoveryErrors?: boolean;
98
103
  }): Promise<NamespaceInventoryEntry[]> {
99
104
  const storageRouter = options.storageRouter ?? new NamespaceStorageRouter(options.config);
100
- const namespaces = await discoverConfiguredNamespaces(options.config);
105
+ const namespaces = await discoverConfiguredNamespaces(
106
+ options.config,
107
+ options.propagateDiscoveryErrors ?? false,
108
+ );
101
109
  const items = await Promise.all(
102
110
  namespaces.map(async (namespace) => {
103
111
  const storage = await storageRouter.storageFor(namespace);
@@ -143,11 +143,18 @@ test("runOperatorDoctor: includes the corpus_watermark check", async () => {
143
143
  const { root, memoryDir, configPath, orchestrator } = await makeFixture();
144
144
  try {
145
145
  await writeMemory(memoryDir, "facts/2026-03-08/a.md");
146
+ let metadataReads = 0;
147
+ const loadMeta = orchestrator.storage.loadMeta.bind(orchestrator.storage);
148
+ orchestrator.storage.loadMeta = async () => {
149
+ metadataReads += 1;
150
+ return loadMeta();
151
+ };
146
152
  const report = await runOperatorDoctor({ configPath, orchestrator });
147
153
  const check = report.checks.find((c) => c.key === "corpus_watermark");
148
154
  assert.ok(check, "expected a corpus_watermark check");
149
155
  assert.equal(check?.status, "ok");
150
156
  assert.equal(corpusOf(check)[0]?.memoryFileCount, 1);
157
+ assert.equal(metadataReads, 1, "doctor reuses the root metadata read for extraction liveness");
151
158
  } finally {
152
159
  await rm(root, { recursive: true, force: true });
153
160
  }
@@ -42,7 +42,7 @@ export async function summarizeCorpusWatermark(
42
42
  // The doctor orchestrator has no live namespace catalog, so config-driven
43
43
  // enumeration (deduped by root inside the shared helper) is the whole set.
44
44
  const rootByNamespace = new Map<string, string>();
45
- for (const root of await resolveCorpusNamespaceRoots({ config })) {
45
+ for (const root of await resolveCorpusNamespaceRoots({ config, propagateDiscoveryErrors: true })) {
46
46
  rootByNamespace.set(root.namespace, root.rootDir);
47
47
  }
48
48
  resolved = [...rootByNamespace.keys()];