@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
@@ -4,6 +4,7 @@ import { access, mkdir, readFile, readdir, stat, unlink, writeFile } from "node:
4
4
  import { lintWorkspaceFiles } from "./hygiene.js";
5
5
  import { parseConfig } from "./config.js";
6
6
  import { summarizeExtractionLiveness, type ExtractionBufferSource } from "./extraction-liveness.js";
7
+ import { readAggregateExtractionWatermark } from "./orchestration/extraction-watermark.js";
7
8
  import type { OperatorDoctorCheck } from "./operator-doctor-types.js";
8
9
  import { readEnvVar, resolveHomeDir } from "./runtime/env.js";
9
10
  import { resolvePluginEntry } from "./plugin-entry-resolver.js";
@@ -53,6 +54,7 @@ import type {
53
54
  DreamsPhasesConfig,
54
55
  FileHygieneConfig,
55
56
  MemoryFile,
57
+ MetaState,
56
58
  PluginConfig,
57
59
  } from "./types.js";
58
60
  import { reportBufferSurpriseDistribution } from "./buffer-surprise-report.js";
@@ -155,12 +157,12 @@ interface ConversationIndexLike {
155
157
  }>;
156
158
  };
157
159
  }
158
-
159
160
  export interface OperatorToolkitOrchestrator extends ConversationIndexLike {
160
161
  config: PluginConfig;
161
162
  storage: StorageManager;
162
163
  qmd: QmdRuntimeLike;
163
164
  buffer?: ExtractionBufferSource;
165
+ getStorage?(namespace: string): Promise<StorageManager>;
164
166
  }
165
167
 
166
168
  export interface OperatorConfigLoadResult {
@@ -1496,11 +1498,10 @@ export async function runOperatorDoctor(options: OperatorDoctorOptions): Promise
1496
1498
  checks.push(await summarizeGraphEdgeDecayStatus(config));
1497
1499
 
1498
1500
  // Tier distribution (issue #686 retention-completion).
1499
- // Shows hot/cold counts, per-status breakdown, and forgotten-memory count.
1500
- // Informational only — never errors, never blocks doctor from returning ok.
1501
1501
  checks.push(await summarizeTierDistribution(options.orchestrator.storage));
1502
- checks.push(await summarizeDreamsPhases(config, storage));
1503
- checks.push(await summarizeExtractionLiveness(config, storage, options.orchestrator.buffer));
1502
+ checks.push(await summarizeDreamsPhases(config, storage, meta));
1503
+ const extractionWatermark = await readAggregateExtractionWatermark({ config, rootStorage: storage, rootMeta: meta, storageForNamespace: (ns, rootDir) => options.orchestrator.getStorage?.(ns) ?? new StorageManager(rootDir) });
1504
+ checks.push(await summarizeExtractionLiveness(config, extractionWatermark, options.orchestrator.buffer));
1504
1505
  checks.push(...(await summarizeCorpusAndReplica(config, (d) => new StorageManager(d), options.resolveSecretRef)));
1505
1506
  // Security mitigation status (issue #565).
1506
1507
  // Reports whether the cross-namespace budget and anomaly detection
@@ -1898,11 +1899,10 @@ export async function summarizeTierDistribution(
1898
1899
  export async function summarizeDreamsPhases(
1899
1900
  config: Pick<PluginConfig, "memoryDir" | "dreamsPhases">,
1900
1901
  storage: StorageManager = new StorageManager(config.memoryDir),
1902
+ loadedMeta?: MetaState,
1901
1903
  ): Promise<OperatorDoctorCheck> {
1902
1904
  const phases: DreamsPhasesConfig = config.dreamsPhases;
1903
-
1904
- // Load meta.json for best-available last-run timestamps.
1905
- const meta = await storage.loadMeta();
1905
+ const meta = loadedMeta ?? (await storage.loadMeta());
1906
1906
 
1907
1907
  let deepSleepLastRun: string | null = null;
1908
1908
  let deepSleepLastRunWarning: string | null = null;
@@ -0,0 +1,188 @@
1
+ import path from "node:path";
2
+
3
+ import { capabilityAllowsNamespace, type TokenCapabilities } from "../access-token-capabilities.js";
4
+ import { resolveNamespaceCapabilities } from "../capabilities.js";
5
+ import { resolveCorpusNamespaceRoots, type CorpusNamespaceRoot } from "../corpus-watermark.js";
6
+ import { normalizeNamespaceIdentity } from "../namespaces/identity.js";
7
+ import type { ExtractionRootStats, ExtractionWatermarkRead } from "../extraction-liveness.js";
8
+ import type { MetaState, PluginConfig } from "../types.js";
9
+
10
+ export interface ExtractionWatermarkMeta extends Pick<MetaState, "lastExtractionAt"> {
11
+ extractionCount?: number;
12
+ lastConsolidationAt?: string | null;
13
+ }
14
+
15
+ export interface ExtractionWatermarkStorage {
16
+ readonly dir: string;
17
+ loadMeta(): Promise<ExtractionWatermarkMeta>;
18
+ }
19
+
20
+ export interface ExtractionNamespaceRootCache {
21
+ getResolvedRootsStatus(compute: () => Promise<CorpusNamespaceRoot[]>): {
22
+ roots: CorpusNamespaceRoot[] | undefined;
23
+ refreshError: unknown;
24
+ };
25
+ }
26
+
27
+ export interface AggregateExtractionWatermarkOptions {
28
+ config: PluginConfig;
29
+ rootStorage: ExtractionWatermarkStorage;
30
+ rootMeta?: ExtractionWatermarkMeta;
31
+ storageForNamespace(
32
+ namespace: string,
33
+ rootDir: string
34
+ ): ExtractionWatermarkStorage | Promise<ExtractionWatermarkStorage>;
35
+ rootsCache?: ExtractionNamespaceRootCache;
36
+ caps?: TokenCapabilities;
37
+ }
38
+
39
+ function readFailure(reason: string, rootStats?: ExtractionRootStats): ExtractionWatermarkRead {
40
+ return {
41
+ lastExtractionAt: null,
42
+ readFailed: true,
43
+ readError: reason,
44
+ ...(rootStats ? { rootStats } : {}),
45
+ };
46
+ }
47
+
48
+ function errorMessage(error: unknown): string {
49
+ return error instanceof Error ? error.message : String(error);
50
+ }
51
+
52
+ function watermarkFromMeta(meta: ExtractionWatermarkMeta, name: string): ExtractionWatermarkRead {
53
+ if (meta.lastExtractionAt !== null && meta.lastExtractionAt !== undefined) {
54
+ const parsed = Date.parse(meta.lastExtractionAt);
55
+ if (!Number.isFinite(parsed)) {
56
+ return readFailure(`${name} watermark timestamp invalid`);
57
+ }
58
+ }
59
+ const hasRootStats = meta.extractionCount !== undefined || meta.lastConsolidationAt !== undefined;
60
+ return {
61
+ lastExtractionAt: meta.lastExtractionAt ?? null,
62
+ readFailed: false,
63
+ ...(hasRootStats
64
+ ? {
65
+ rootStats: {
66
+ extractionCount: meta.extractionCount,
67
+ lastConsolidationAt: meta.lastConsolidationAt,
68
+ },
69
+ }
70
+ : {}),
71
+ };
72
+ }
73
+
74
+ async function readWatermark(storage: ExtractionWatermarkStorage, name: string): Promise<ExtractionWatermarkRead> {
75
+ try {
76
+ return watermarkFromMeta(await storage.loadMeta(), name);
77
+ } catch (error) {
78
+ return readFailure(`${name} watermark unreadable: ${errorMessage(error)}`);
79
+ }
80
+ }
81
+
82
+ async function resolveRoots(
83
+ options: AggregateExtractionWatermarkOptions
84
+ ): Promise<CorpusNamespaceRoot[] | ExtractionWatermarkRead> {
85
+ const compute = () =>
86
+ resolveCorpusNamespaceRoots({
87
+ config: options.config,
88
+ propagateDiscoveryErrors: true,
89
+ });
90
+ if (options.rootsCache) {
91
+ const { roots, refreshError } = options.rootsCache.getResolvedRootsStatus(compute);
92
+ if (refreshError !== undefined) {
93
+ return readFailure(`namespace watermark enumeration failed: ${errorMessage(refreshError)}`);
94
+ }
95
+ return roots ?? { lastExtractionAt: null, readFailed: false, pending: true };
96
+ }
97
+ try {
98
+ return await compute();
99
+ } catch (error) {
100
+ return readFailure(`namespace watermark enumeration failed: ${errorMessage(error)}`);
101
+ }
102
+ }
103
+
104
+ function isReadFailure(value: CorpusNamespaceRoot[] | ExtractionWatermarkRead): value is ExtractionWatermarkRead {
105
+ return !Array.isArray(value);
106
+ }
107
+
108
+ function newerWatermark(current: string | null, candidate: string | null): string | null {
109
+ if (candidate === null) return current;
110
+ const candidateMs = Date.parse(candidate);
111
+ if (!Number.isFinite(candidateMs)) return current;
112
+ if (current === null) return candidate;
113
+ const currentMs = Date.parse(current);
114
+ return !Number.isFinite(currentMs) || candidateMs > currentMs ? candidate : current;
115
+ }
116
+
117
+ export async function readAggregateExtractionWatermark(
118
+ options: AggregateExtractionWatermarkOptions
119
+ ): Promise<ExtractionWatermarkRead> {
120
+ if (!resolveNamespaceCapabilities(options.config).namespaces) {
121
+ return options.rootMeta
122
+ ? watermarkFromMeta(options.rootMeta, "root store")
123
+ : readWatermark(options.rootStorage, "root store");
124
+ }
125
+
126
+ const resolved = await resolveRoots(options);
127
+ if (isReadFailure(resolved)) return resolved;
128
+
129
+ const rootDir = path.resolve(options.rootStorage.dir);
130
+ const seenDirs = new Set<string>([rootDir]);
131
+ const targets: CorpusNamespaceRoot[] = [];
132
+ for (const root of resolved) {
133
+ if (
134
+ options.caps &&
135
+ !root.namespaces.some((ns) =>
136
+ capabilityAllowsNamespace(options.caps, normalizeNamespaceIdentity(ns)),
137
+ )
138
+ ) {
139
+ continue;
140
+ }
141
+ const resolvedDir = path.resolve(root.rootDir);
142
+ if (seenDirs.has(resolvedDir)) continue;
143
+ seenDirs.add(resolvedDir);
144
+ targets.push(root);
145
+ }
146
+
147
+ const defaultNamespace = normalizeNamespaceIdentity(options.config.defaultNamespace);
148
+ const hasMigratedDefault = resolved.some(
149
+ (root) =>
150
+ path.resolve(root.rootDir) !== rootDir &&
151
+ root.namespaces.some((namespace) => normalizeNamespaceIdentity(namespace) === defaultNamespace),
152
+ );
153
+ const canAccessRoot =
154
+ !hasMigratedDefault &&
155
+ (!options.caps || capabilityAllowsNamespace(options.caps, defaultNamespace));
156
+ let rootRead: ExtractionWatermarkRead = { lastExtractionAt: null, readFailed: false };
157
+ if (canAccessRoot) {
158
+ rootRead = await readWatermark(options.rootStorage, "root store");
159
+ if (rootRead.readFailed) return rootRead;
160
+ }
161
+ let lastExtractionAt = rootRead.lastExtractionAt;
162
+
163
+ const reads = await Promise.all(
164
+ targets.map(async (target) => {
165
+ try {
166
+ const storage = await options.storageForNamespace(target.namespace, target.rootDir);
167
+ return await readWatermark(storage, "namespace");
168
+ } catch (error) {
169
+ return readFailure(`namespace watermark storage unavailable: ${errorMessage(error)}`);
170
+ }
171
+ })
172
+ );
173
+
174
+ for (const read of reads) {
175
+ if (read.readFailed) {
176
+ return readFailure(
177
+ read.readError ?? "namespace watermark unreadable",
178
+ rootRead.rootStats
179
+ );
180
+ }
181
+ lastExtractionAt = newerWatermark(lastExtractionAt, read.lastExtractionAt);
182
+ }
183
+ return {
184
+ lastExtractionAt,
185
+ readFailed: false,
186
+ ...(rootRead.rootStats ? { rootStats: rootRead.rootStats } : {}),
187
+ };
188
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/extraction-liveness.ts"],"sourcesContent":["/**\n * Extraction-pipeline liveness (issue #2151).\n *\n * A daemon's implicit extraction can fail continuously for months while\n * `/health` stays green and recall keeps serving: every individual failure is\n * logged (timeouts, fallback-no-parsed-output), but nothing aggregates them\n * into a checkable status. This module surfaces a liveness *watermark* — the\n * last successful extraction plus the current buffer backlog — and decides when\n * that watermark is stale enough to call the pipeline degraded.\n *\n * The §22 error-vs-empty principle applies at the pipeline level: \"no\n * extraction succeeded for 90 days\" (a non-empty buffer with a stale/absent\n * watermark) must be distinguishable from \"nothing to extract\" (an empty\n * buffer). Degradation therefore requires a NON-EMPTY buffer — an idle daemon\n * with nothing buffered is healthy, not degraded.\n *\n * The core decision (`evaluateExtractionLiveness`) is a pure function shared by\n * every surface (authenticated `/health`, `remnic doctor`, `remnic stats`) so\n * the verdict never diverges across code paths.\n */\nimport { coerceBool, coerceNumber } from \"./connectors/coerce.js\";\nimport { log } from \"./logger.js\";\nimport type { OperatorDoctorCheck } from \"./operator-doctor-types.js\";\nimport type { MetaState, PluginConfig } from \"./types.js\";\n\n/** Default staleness window: 24h. A watermark older than this with a non-empty buffer is degraded. */\nconst DEFAULT_STALE_WINDOW_MS = 86_400_000;\n\nexport interface ExtractionLivenessConfig {\n /** Master gate. When false, liveness is never reported degraded. Default true. */\n enabled: boolean;\n /**\n * How stale the last-successful-extraction watermark may get (ms) before a\n * non-empty buffer flags the pipeline degraded. Default 24h.\n */\n staleWindowMs: number;\n}\n\n/**\n * A read-only snapshot of buffered, not-yet-extracted work. Structurally\n * satisfied by `SmartBuffer.getBufferSnapshot()` so callers can pass the live\n * buffer without this module depending on the buffer implementation.\n */\nexport interface ExtractionBufferSnapshot {\n bufferedSessionCount: number;\n pendingTurnCount: number;\n oldestTurnTimestamp: string | null;\n /**\n * Set by the liveness reader (never by `SmartBuffer`) when the buffer exists\n * but its snapshot read threw. An unreadable buffer is a pipeline fault, not\n * an empty queue (§22): it degrades the pipeline with a distinct reason\n * instead of masquerading as a zero-backlog (healthy) snapshot.\n */\n readFailed?: boolean;\n /** Read-failure detail, surfaced in the degraded reason when `readFailed`. */\n readError?: string;\n}\n\nexport interface ExtractionBufferSource {\n getBufferSnapshot(): Promise<ExtractionBufferSnapshot>;\n}\n\n/**\n * The liveness payload surfaced on `/health.extraction` and in the doctor/stats\n * details.\n *\n * DAEMON-SCOPED SIGNAL: a daemon exposes a single extraction verdict; the\n * last-extraction watermark is read from the daemon's default/root store, so\n * `/health` returns the same `extraction` block for every namespace argument.\n *\n * KNOWN LIMITATION (issue #2159): extraction stamps `lastExtractionAt` on the\n * PER-NAMESPACE store (`storageFor(selfNamespace)`), so this watermark reflects\n * only extraction that stamped the root store (default-namespace work). On a\n * single-namespace deployment (the common case) the watermark and buffer share\n * one scope and the verdict is exact. On a multi-namespace deployment the verdict\n * is NOT guaranteed correct for work isolated to a non-default namespace: it can\n * over-report `degraded` (the default namespace idle while another is extracting)\n * or miss a stall confined to a non-default namespace. Aggregating the watermark\n * across namespace stores is tracked in #2159.\n */\nexport interface ExtractionLivenessStatus {\n lastExtractionAt: string | null;\n bufferedSessionCount: number;\n pendingTurnCount: number;\n oldestBufferedTurnAgeMs: number | null;\n degraded: boolean;\n degradedReason: string | null;\n /**\n * Scope of the last-extraction watermark: `root-store` today (read from the\n * daemon's default/root store only), `aggregate` once it is resolved across\n * every namespace store (#2159). A monitor can alert on\n * `watermarkScope !== \"aggregate\"` while namespaces are enabled instead of\n * trusting a number that may be wrong in either direction.\n */\n watermarkScope: \"root-store\" | \"aggregate\";\n}\n\ninterface ExtractionLivenessStorageLike {\n loadMeta(): Promise<Pick<MetaState, \"lastExtractionAt\">>;\n}\n\ninterface ExtractionLivenessOrchestratorLike {\n config: Pick<PluginConfig, \"extractionLiveness\">;\n buffer?: ExtractionBufferSource;\n}\n\n/**\n * Parse the `extractionLiveness` config block. Follows the nested-block shape\n * validation used elsewhere in `config.ts` and coerces string booleans (§24:\n * CLI/JSON deliver `\"false\"`/`\"0\"` as strings, which must read as falsy).\n */\nexport function parseExtractionLivenessConfig(\n cfg: Record<string, unknown>,\n): ExtractionLivenessConfig {\n const raw = cfg.extractionLiveness;\n if (raw !== undefined && (raw === null || typeof raw !== \"object\" || Array.isArray(raw))) {\n throw new Error(`extractionLiveness must be a plain object (got ${JSON.stringify(raw)})`);\n }\n const block = (raw ?? {}) as Record<string, unknown>;\n return {\n // `?? true` keeps the default on when the key is absent while honoring\n // explicit \"false\"/\"0\"/false opt-outs (coerceBool handles the string forms).\n enabled: coerceBool(block.enabled) ?? true,\n staleWindowMs: parseStaleWindowMs(block.staleWindowMs),\n };\n}\n\n/**\n * Parse `extractionLiveness.staleWindowMs`. Absent (`undefined`) → the 24h\n * default; any PRESENT value (including an explicit `null`) must coerce to a\n * positive INTEGER of milliseconds, else THROW. A fractional or non-positive\n * value (\"0.5\" would floor to 0, making every backlog instantly stale; 1.9 would\n * floor to 1) is rejected, never floored/reinterpreted (§1/§17/§39). Mirrors\n * config.ts's `parseIntegerInClosedRange` (round 9 finding 1): only an absent key\n * falls back; an explicit `null` is invalid input, not \"use the default\".\n * Inlined rather than imported because config.ts already imports this module\n * (importing back would be circular), the same reason `recall-concurrency-config.ts`\n * inlines its numeric validation.\n */\nfunction parseStaleWindowMs(value: unknown): number {\n // Only an absent key (undefined) falls back to the default; an explicit `null`\n // is invalid input and proceeds to validation → throw (mirrors\n // parseIntegerInClosedRange, round 9 finding 1).\n if (value === undefined) return DEFAULT_STALE_WINDOW_MS;\n const coerced = coerceNumber(value);\n if (coerced === undefined || !Number.isFinite(coerced) || !Number.isInteger(coerced) || coerced < 1) {\n throw new Error(\n `extractionLiveness.staleWindowMs must be an integer greater than or equal to 1; got ${JSON.stringify(value)}`,\n );\n }\n return coerced;\n}\n\n/** Milliseconds between `timestamp` and `nowMs`, clamped at 0; null when unparsable/absent. */\nfunction ageMsFrom(timestamp: string | null, nowMs: number): number | null {\n if (typeof timestamp !== \"string\") return null;\n const parsed = Date.parse(timestamp);\n if (!Number.isFinite(parsed)) return null;\n return Math.max(0, nowMs - parsed);\n}\n\n/** Compact human-readable duration (\"3d\", \"5h\", \"12m\", \"45s\") for logs and CLI output. */\nexport function formatAgeMs(ms: number): string {\n const s = Math.max(0, Math.floor(ms / 1000));\n if (s >= 86_400) return `${Math.floor(s / 86_400)}d`;\n if (s >= 3_600) return `${Math.floor(s / 3_600)}h`;\n if (s >= 60) return `${Math.floor(s / 60)}m`;\n return `${s}s`;\n}\n\n/**\n * Pure liveness verdict. Degraded when the feature is enabled AND any of: the\n * last-extraction watermark could not be READ (a storage fault, §22), the buffer\n * read failed (a pipeline fault, §22), or the buffer is non-empty and the\n * watermark is absent or stale. Each cause yields a DISTINCT reason so an\n * operator can tell a meta-read failure from a buffer-read failure from a\n * pipeline that has genuinely never extracted.\n *\n * Staleness uses a half-open freshness window (§23): an extraction is fresh\n * while its age is in `[0, staleWindowMs)`, so an age of EXACTLY `staleWindowMs`\n * is stale (the upper bound on \"fresh\" is exclusive). A null/unparsable watermark\n * means \"never succeeded\" and is treated as stale — as is a FUTURE watermark (a\n * clock-skewed or corrupt timestamp ahead of now must not read as a fresh\n * extraction and hide a real backlog stall).\n */\nexport function evaluateExtractionLiveness(input: {\n config: ExtractionLivenessConfig;\n lastExtractionAt: string | null;\n snapshot: ExtractionBufferSnapshot;\n nowMs: number;\n metaReadFailed?: boolean;\n metaReadError?: string;\n watermarkScope?: \"root-store\" | \"aggregate\";\n}): ExtractionLivenessStatus {\n const { config, lastExtractionAt, snapshot, nowMs } = input;\n const lastMs = lastExtractionAt !== null ? Date.parse(lastExtractionAt) : Number.NaN;\n const hasWatermark = Number.isFinite(lastMs);\n const ageMs = nowMs - lastMs;\n // A future watermark (negative age) is clock-skewed or corrupt data, not a\n // fresh extraction: treat it as stale so a real backlog stall is not hidden.\n const stale = !hasWatermark || ageMs < 0 || ageMs >= config.staleWindowMs;\n const hasBacklog = snapshot.bufferedSessionCount > 0;\n // A watermark or buffer that cannot be READ is a fault, not an empty queue\n // (§22): a storage/pipeline outage must not read as healthy, and must stay\n // distinct from a pipeline that has genuinely never extracted.\n const metaReadFailed = input.metaReadFailed === true;\n const readFailed = snapshot.readFailed === true;\n const degraded = config.enabled && (metaReadFailed || readFailed || (hasBacklog && stale));\n\n let degradedReason: string | null = null;\n if (degraded) {\n if (metaReadFailed) {\n degradedReason = `extraction watermark unreadable: ${input.metaReadError ?? \"meta read failed\"}`;\n } else if (readFailed) {\n degradedReason = `extraction buffer unreadable: ${snapshot.readError ?? \"buffer snapshot read failed\"}`;\n } else {\n const watermark = hasWatermark\n ? `last succeeded ${formatAgeMs(nowMs - lastMs)} ago`\n : \"no successful extraction on record\";\n degradedReason =\n `${watermark}; ${snapshot.bufferedSessionCount} buffered session(s), ` +\n `${snapshot.pendingTurnCount} turn(s) pending extraction`;\n }\n }\n\n return {\n lastExtractionAt,\n bufferedSessionCount: snapshot.bufferedSessionCount,\n pendingTurnCount: snapshot.pendingTurnCount,\n oldestBufferedTurnAgeMs: ageMsFrom(snapshot.oldestTurnTimestamp, nowMs),\n degraded,\n degradedReason,\n watermarkScope: input.watermarkScope ?? \"root-store\",\n };\n}\n\nconst EMPTY_SNAPSHOT: ExtractionBufferSnapshot = {\n bufferedSessionCount: 0,\n pendingTurnCount: 0,\n oldestTurnTimestamp: null,\n};\n\n/**\n * Read a buffer snapshot without throwing. An ABSENT buffer degrades to an empty\n * (healthy) snapshot — some service constructions have no orchestrator buffer. A\n * buffer that EXISTS but whose read throws yields a `readFailed` snapshot so the\n * verdict can distinguish a pipeline fault from an empty queue (§22).\n */\nasync function readBufferSnapshot(\n buffer: ExtractionBufferSource | undefined,\n): Promise<ExtractionBufferSnapshot> {\n if (!buffer || typeof buffer.getBufferSnapshot !== \"function\") return EMPTY_SNAPSHOT;\n try {\n return await buffer.getBufferSnapshot();\n } catch (err) {\n return {\n ...EMPTY_SNAPSHOT,\n readFailed: true,\n readError: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n/**\n * Gather the liveness status from a meta store (last-extraction watermark) and\n * an optional buffer (backlog). A read failure on EITHER side is captured and\n * surfaced as a DEGRADED status with a distinct reason (§22) rather than being\n * swallowed into a healthy-looking verdict; no failure path throws.\n */\nexport async function gatherExtractionLivenessStatus(input: {\n config: ExtractionLivenessConfig;\n storage: ExtractionLivenessStorageLike;\n buffer: ExtractionBufferSource | undefined;\n nowMs: number;\n}): Promise<ExtractionLivenessStatus> {\n const { config, storage, buffer, nowMs } = input;\n let lastExtractionAt: string | null = null;\n let metaReadFailed = false;\n let metaReadError: string | undefined;\n try {\n lastExtractionAt = (await storage.loadMeta()).lastExtractionAt ?? null;\n } catch (err) {\n // A swallowed loadMeta() failure that reads as \"never extracted\" is the\n // exact §22 conflation this feature exists to kill — surface it explicitly.\n metaReadFailed = true;\n metaReadError = err instanceof Error ? err.message : String(err);\n }\n const snapshot = await readBufferSnapshot(buffer);\n return evaluateExtractionLiveness({\n config,\n lastExtractionAt,\n snapshot,\n nowMs,\n metaReadFailed,\n metaReadError,\n // Today the watermark comes from the root store only; #2159 flips this to\n // \"aggregate\" when the resolver reads across every namespace store.\n watermarkScope: \"root-store\",\n });\n}\n\n/**\n * Aggregated-WARN throttle (issue #2151): emit at most ONE warning per\n * staleness window while degraded — not one line per failed extraction attempt.\n * State is instance-scoped (held by the owning service), never a module global\n * (§5), so multiple service instances throttle independently.\n */\nexport class ExtractionLivenessWarnThrottle {\n private lastWarnAtMs: number | null = null;\n\n /** Warn if degraded and outside the throttle window; returns whether a WARN was emitted. */\n maybeWarn(status: ExtractionLivenessStatus, staleWindowMs: number, nowMs: number): boolean {\n if (!status.degraded) {\n // Recovered (or never degraded): reset so a fresh episode warns at once.\n this.lastWarnAtMs = null;\n return false;\n }\n if (this.lastWarnAtMs !== null && nowMs - this.lastWarnAtMs < staleWindowMs) return false;\n this.lastWarnAtMs = nowMs;\n log.warn(\n `extraction pipeline liveness degraded: ${status.degradedReason ?? \"buffered turns are not being extracted\"}`,\n );\n return true;\n }\n}\n\n/**\n * Normalize the `extractionLiveness` block at the read boundary. `parseConfig`\n * always populates it, but a host adapter, an older persisted config, or a\n * hand-built `PluginConfig` can hand us an absent, partial, or loosely-typed\n * block - and every liveness surface (/health, doctor, stats) must degrade to\n * the documented default (enabled, 24h window) rather than throw. Both fields\n * are coerced with the same rules as `parseExtractionLivenessConfig` (§24:\n * `\"false\"`/`\"0\"` read as falsy; a numeric string like `\"5000\"` parses), except a\n * present but non-integer or non-positive `staleWindowMs` falls back to the\n * default here rather than throwing.\n */\nexport function resolveExtractionLivenessConfig(\n block: { enabled?: unknown; staleWindowMs?: unknown } | undefined,\n): ExtractionLivenessConfig {\n const staleWindowMs = coerceNumber(block?.staleWindowMs);\n return {\n enabled: coerceBool(block?.enabled) ?? true,\n staleWindowMs:\n staleWindowMs !== undefined && Number.isInteger(staleWindowMs) && staleWindowMs >= 1\n ? staleWindowMs\n : DEFAULT_STALE_WINDOW_MS,\n };\n}\n\n/**\n * Compute the liveness status for the authenticated `/health` payload and emit\n * the throttled aggregated WARN.\n *\n * The watermark is read from the daemon's default/root store (`orchestrator.storage`),\n * which is why `/health` returns the same `extraction` block for any namespace\n * argument. KNOWN LIMITATION (issue #2159): extraction writes `lastExtractionAt`\n * per-namespace, so this watermark reflects only default-namespace extraction and\n * the verdict is exact only for a single-namespace daemon (see\n * `ExtractionLivenessStatus`).\n */\nexport async function computeExtractionLivenessStatus(\n orchestrator: ExtractionLivenessOrchestratorLike & { storage: ExtractionLivenessStorageLike },\n throttle?: ExtractionLivenessWarnThrottle,\n nowMs: number = Date.now(),\n): Promise<ExtractionLivenessStatus> {\n const config = resolveExtractionLivenessConfig(orchestrator.config.extractionLiveness);\n const status = await gatherExtractionLivenessStatus({\n config,\n storage: orchestrator.storage,\n buffer: orchestrator.buffer,\n nowMs,\n });\n throttle?.maybeWarn(status, config.staleWindowMs, nowMs);\n return status;\n}\n\n/**\n * `remnic doctor` check for extraction-pipeline liveness. Status `warn` when\n * degraded (with a remediation hint), `ok` otherwise.\n */\nexport async function summarizeExtractionLiveness(\n config: Pick<PluginConfig, \"extractionLiveness\">,\n storage: ExtractionLivenessStorageLike,\n buffer?: ExtractionBufferSource,\n nowMs: number = Date.now(),\n): Promise<OperatorDoctorCheck> {\n const livenessConfig = resolveExtractionLivenessConfig(config.extractionLiveness);\n const status = await gatherExtractionLivenessStatus({\n config: livenessConfig,\n storage,\n buffer,\n nowMs,\n });\n const scopeNote =\n ` Watermark scope: ${status.watermarkScope}` +\n (status.watermarkScope === \"root-store\"\n ? \" (default-namespace only; multi-namespace aggregation tracked in #2159).\"\n : \".\");\n const summary =\n (status.degraded\n ? `Extraction pipeline degraded: ${status.degradedReason}.`\n : `Extraction pipeline healthy: last extraction ${status.lastExtractionAt ?? \"never\"}, ` +\n `${status.bufferedSessionCount} buffered session(s), ${status.pendingTurnCount} turn(s) pending` +\n `${livenessConfig.enabled ? \"\" : \" (liveness check disabled)\"}.`) + scopeNote;\n return {\n key: \"extraction_liveness\",\n status: status.degraded ? \"warn\" : \"ok\",\n summary,\n remediation: status.degraded\n ? \"Extraction has stalled while turns are buffered. Inspect daemon logs for extraction \" +\n \"timeouts or fallback-no-parsed-output errors, verify the extraction model/provider is \" +\n \"reachable, then force a flush (e.g. `remnic flush`).\"\n : undefined,\n details: { ...status, staleWindowMs: livenessConfig.staleWindowMs, enabled: livenessConfig.enabled },\n };\n}\n\n/** Meta fields the `remnic stats` view renders directly, beyond the liveness watermark. */\ntype ExtractionStatsMeta = Pick<MetaState, \"extractionCount\" | \"lastExtractionAt\" | \"lastConsolidationAt\">;\n\n/**\n * Lines for `remnic stats`: extraction watermark, buffer backlog, and the\n * liveness verdict so dashboards can alert on a stalled pipeline. The meta\n * watermark is loaded HERE (not by the caller) so a meta-read failure surfaces\n * as an explicit DEGRADED verdict plus \"unavailable\" counts (§22) instead of\n * crashing the whole `stats` command.\n */\nexport async function renderExtractionLivenessStats(\n orchestrator: ExtractionLivenessOrchestratorLike & { storage: { loadMeta(): Promise<ExtractionStatsMeta> } },\n nowMs: number = Date.now(),\n): Promise<string[]> {\n let meta: ExtractionStatsMeta | null = null;\n let metaReadFailed = false;\n let metaReadError: string | undefined;\n try {\n meta = await orchestrator.storage.loadMeta();\n } catch (err) {\n metaReadFailed = true;\n metaReadError = err instanceof Error ? err.message : String(err);\n }\n const status = evaluateExtractionLiveness({\n config: resolveExtractionLivenessConfig(orchestrator.config.extractionLiveness),\n lastExtractionAt: meta?.lastExtractionAt ?? null,\n snapshot: await readBufferSnapshot(orchestrator.buffer),\n nowMs,\n metaReadFailed,\n metaReadError,\n });\n const oldestAge =\n status.oldestBufferedTurnAgeMs !== null ? formatAgeMs(status.oldestBufferedTurnAgeMs) : \"n/a\";\n const unavailable = metaReadFailed ? \"unavailable\" : \"never\";\n return [\n `Extractions: ${meta?.extractionCount ?? unavailable}`,\n `Last extraction: ${meta?.lastExtractionAt ?? unavailable}`,\n `Last consolidation: ${meta?.lastConsolidationAt ?? unavailable}`,\n `Buffered sessions: ${status.bufferedSessionCount} (${status.pendingTurnCount} turns pending)`,\n `Oldest buffered turn age: ${oldestAge}`,\n `Extraction watermark scope: ${status.watermarkScope}`,\n `Extraction liveness: ${status.degraded ? `DEGRADED — ${status.degradedReason}` : \"ok\"}`,\n ];\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/corpus-watermark.ts"],"sourcesContent":["/**\n * Corpus watermark (issue #2149) — a cheap, comparable fingerprint of a\n * daemon's active-memory corpus.\n *\n * Deployment shape this addresses: two daemons behind an active/backup VIP,\n * each owning its own memory directory. Nothing compared the two corpora, so a\n * pair could diverge for months (e.g. one holding ~190k files, its backup\n * ~340k with different histories) with no signal until a manual audit. This\n * module ships the WATERMARK primitive and the read surfaces that expose it;\n * peer polling and divergence alerting are a follow-up.\n *\n * The digest is a CENSUS fingerprint (per day-partition file counts), NOT a\n * content hash — two daemons that agree on how many memory FILES live in\n * each `<tier>:<category>/<day>` partition produce the same digest, so a\n * differing digest is a cheap divergence signal without reading or hashing file\n * bodies. Buckets are tier-aware (`hot:` / `cold:`): demoted memories stay\n * reachable via cold recall, so the census counts BOTH tiers and a hot/cold\n * split shows in the digest rather than being silently folded together (issue\n * #2156 finding D). `newestWriteAt` stays scoped to the newest HOT partition\n * only, so the freshness probe stays O(one day's active files) even on a 100k+\n * corpus.\n *\n * Robustness (issue #2156 review rounds):\n * - `/health` and `remnic doctor` resolve their namespace set through the ONE\n * {@link resolveCorpusNamespaceRoots} helper — config-driven enumeration\n * unioned with the PERSISTED namespace catalog read from config — so both\n * surfaces enumerate the same tenants and cannot drift.\n * - The `/health` builder serves through a stale-while-revalidate\n * {@link CorpusWatermarkCache}, so a probe NEVER awaits the recursive corpus\n * scan (it returns the cached/stale value and refreshes in the background).\n * - Each namespace's census brackets its hot+cold walk with a corpus-mutation\n * sentinel and retries when a tier migration races it, so a transient\n * double-count/miss is never cached as a false divergence.\n * - An unreadable corpus root (EACCES) is OMITTED, not published as a false\n * empty; a missing root (ENOENT — a not-yet-created namespace) is a genuine\n * empty corpus.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { capabilityAllowsNamespace, type TokenCapabilities } from \"./access-token-capabilities.js\";\nimport { listNamespaces } from \"./namespaces/migrate.js\";\nimport type { PluginConfig } from \"./types.js\";\n\nexport interface CorpusWatermark {\n namespace: string;\n /**\n * Count of memory files under the census scan roots (hot + cold). This is a\n * FILE census, not a status-filtered active count: reading each file's\n * frontmatter status would defeat the cheap-probe design, so an in-place\n * status change (e.g. archived-in-place) is not reflected here.\n */\n memoryFileCount: number;\n /** Newest `YYYY-MM-DD` day-partition seen in the HOT tier, or null when none is dated. */\n newestPartition: string | null;\n /** ISO max mtime within the newest HOT partition, or null when it has no files. */\n newestWriteAt: string | null;\n /** sha256 hex over the deterministic per-partition census. */\n digest: string;\n computedAt: string;\n}\n\n/** Explicit bucket for paths that do not match the `<category>/<day>` shape. */\nexport const UNPARTITIONED_BUCKET = \"unpartitioned\";\n\n/** Storage tier a memory file lives in. */\nexport type CorpusTier = \"hot\" | \"cold\";\n\nconst DAY_PARTITION_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n/** Cold-tier root segment (relative to a namespace base dir): `<baseDir>/cold/...`. */\nconst COLD_TIER_DIR = \"cold\";\n\n/**\n * True when `token` is not just `YYYY-MM-DD`-shaped but a REAL calendar day.\n * The regex alone accepts impossible dates (e.g. `2026-99-99`) that would then\n * sort lexically above every valid partition and corrupt `newestPartition`\n * (issue #2156 round-6); round-tripping through a UTC Date rejects month/day\n * overflow. A malformed dir falls back to the unpartitioned bucket.\n */\nfunction isRealCalendarDay(token: string): boolean {\n if (!DAY_PARTITION_RE.test(token)) return false;\n const [year, month, day] = token.split(\"-\").map(Number);\n const date = new Date(Date.UTC(year, month - 1, day));\n return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;\n}\n\ninterface ParsedPath {\n readonly bucket: string;\n readonly date: string | null;\n readonly tier: CorpusTier;\n}\n\nfunction parsePartition(baseDir: string, filePath: string): ParsedPath {\n const rel = path.relative(baseDir, filePath).split(path.sep).join(\"/\");\n const parts = rel.split(\"/\");\n // Cold-tier files live under `<baseDir>/cold/<category>/<day>/...`; hot files\n // sit directly under `<baseDir>/<category>/<day>/...`. Strip the leading\n // `cold/` segment so the category/day parse is tier-agnostic, then prefix the\n // bucket with the tier so a hot and cold partition of the same day never\n // collide (they must diverge the digest, not fold together).\n const tier: CorpusTier = parts[0] === COLD_TIER_DIR ? \"cold\" : \"hot\";\n const categoryParts = tier === \"cold\" ? parts.slice(1) : parts;\n if (categoryParts.length >= 2 && isRealCalendarDay(categoryParts[1])) {\n return { bucket: `${tier}:${categoryParts[0]}/${categoryParts[1]}`, date: categoryParts[1], tier };\n }\n return { bucket: `${tier}:${UNPARTITIONED_BUCKET}`, date: null, tier };\n}\n\n/**\n * Build the per-partition census: bucket key -> file count. Bucket keys are\n * tier-aware (`hot:`/`cold:`). Paths not matching a day-partition land in the\n * explicit tier {@link UNPARTITIONED_BUCKET}; they are never silently dropped.\n * Pure — no filesystem access.\n */\nexport function buildPartitionCensus(paths: readonly string[], baseDir: string): Map<string, number> {\n const census = new Map<string, number>();\n for (const filePath of paths) {\n const { bucket } = parsePartition(baseDir, filePath);\n census.set(bucket, (census.get(bucket) ?? 0) + 1);\n }\n return census;\n}\n\n/**\n * Deterministic census -> digest. Bucket keys are SORTED before serialization\n * (AGENTS.md pattern 26: insertion order must not affect a hash), then emitted\n * as `\"<bucket>:<count>\"` lines joined by `\\n` and hashed with sha256. Pure —\n * separately exported so it can be unit-tested without a filesystem.\n */\nexport function digestPartitionCensus(census: ReadonlyMap<string, number> | Record<string, number>): string {\n const entries = census instanceof Map ? [...census.entries()] : Object.entries(census);\n const serialized = entries\n .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))\n .map(([bucket, count]) => `${bucket}:${count}`)\n .join(\"\\n\");\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n}\n\n/**\n * Compute a watermark for a single namespace from an already-collected path\n * list (both tiers, under `baseDir`) plus its memory base directory. Never\n * throws on an empty list — an empty corpus yields the digest of the empty\n * census and null timestamps. Only the newest HOT partition's files are\n * `stat`-ed, so freshness stays cheap even on a 100k+ corpus.\n */\nexport async function computeCorpusWatermark(input: {\n namespace: string;\n paths: readonly string[];\n baseDir: string;\n now?: Date;\n}): Promise<CorpusWatermark> {\n const { namespace, paths, baseDir } = input;\n\n const census = new Map<string, number>();\n let newestPartition: string | null = null;\n const parsedByPath: ParsedPath[] = [];\n for (const filePath of paths) {\n const parsed = parsePartition(baseDir, filePath);\n parsedByPath.push(parsed);\n census.set(parsed.bucket, (census.get(parsed.bucket) ?? 0) + 1);\n // Freshness is the newest ACTIVE (hot) write. Cold memories are demoted and\n // keep their original (older) partition date, so they never advance the\n // freshness probe and their files are never stat-ed below.\n if (parsed.tier === \"hot\" && parsed.date !== null && (newestPartition === null || parsed.date > newestPartition)) {\n newestPartition = parsed.date;\n }\n }\n\n const digest = digestPartitionCensus(census);\n\n let newestWriteAt: string | null = null;\n if (newestPartition !== null) {\n let maxMtimeMs = Number.NEGATIVE_INFINITY;\n for (let i = 0; i < paths.length; i += 1) {\n if (parsedByPath[i].tier !== \"hot\" || parsedByPath[i].date !== newestPartition) continue;\n try {\n const st = await stat(paths[i]);\n if (st.mtimeMs > maxMtimeMs) maxMtimeMs = st.mtimeMs;\n } catch (err) {\n // A file vanishing between scan and stat (ENOENT) is an expected race —\n // skip it. A backend read failure (EACCES/EIO/…) means the corpus is\n // unreadable: propagate so the namespace is omitted, not published with\n // a stale/null freshness (issue #2156 round-6).\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n }\n if (maxMtimeMs > Number.NEGATIVE_INFINITY) {\n newestWriteAt = new Date(maxMtimeMs).toISOString();\n }\n }\n\n // Stamp AFTER the (possibly slow) stat loop so an age-based HA consumer never\n // sees a timestamp earlier than when the scan actually finished (finding\n // round-4). An explicit `now` (tests / determinism) still wins.\n const computedAt = (input.now ?? new Date()).toISOString();\n\n return {\n namespace,\n memoryFileCount: paths.length,\n newestPartition,\n newestWriteAt,\n digest,\n computedAt,\n };\n}\n\n/** Minimal storage surface a watermark needs — satisfied by StorageManager. */\nexport interface CorpusStorage {\n readonly dir: string;\n /** Hot-tier active memory paths (the recall corpus). */\n collectActiveMemoryPaths(options?: { propagateReadErrors?: boolean }): Promise<string[]>;\n /** Cold-tier (demoted-but-reachable) memory paths (issue #2156 finding D). */\n collectColdMemoryPaths(options?: { propagateReadErrors?: boolean }): Promise<string[]>;\n /**\n * Optional combined hot+cold corpus-mutation sentinel. When present, the\n * census brackets its scan with it and retries on change so a tier migration\n * racing the walkers is not cached as a false divergence. Absent (test fakes)\n * ⇒ single-pass.\n */\n getCorpusScanVersion?(): string | Promise<string>;\n}\n\n/** Max census re-scans when a corpus mutation keeps racing the walkers. */\nconst CENSUS_RACE_MAX_ATTEMPTS = 3;\n\n/**\n * Compute one namespace's watermark from its storage, spanning BOTH tiers so a\n * cold-tier divergence is caught. Reads are STRICT: the collectors propagate a\n * backend read failure (EACCES on the root OR any nested category dir, ENOTDIR,\n * …) so an unreadable corpus is OMITTED by the caller rather than published as a\n * false empty census; a missing root (ENOENT — a not-yet-created namespace)\n * stays a genuine empty corpus. The hot+cold walk is bracketed by the\n * corpus-mutation sentinel (when the storage exposes one) and retried on change;\n * if it never stabilizes (sustained tier churn) this THROWS rather than caching\n * a transient double-count/miss as a false divergence.\n */\nasync function computeNamespaceWatermark(\n namespace: string,\n storage: CorpusStorage,\n now?: Date,\n): Promise<CorpusWatermark> {\n const readVersion = storage.getCorpusScanVersion?.bind(storage);\n const maxAttempts = readVersion ? CENSUS_RACE_MAX_ATTEMPTS : 1;\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n const before = readVersion ? await readVersion() : null;\n const [hotPaths, coldPaths] = await Promise.all([\n storage.collectActiveMemoryPaths({ propagateReadErrors: true }),\n storage.collectColdMemoryPaths({ propagateReadErrors: true }),\n ]);\n const result = await computeCorpusWatermark({ namespace, paths: [...hotPaths, ...coldPaths], baseDir: storage.dir, now });\n const after = readVersion ? await readVersion() : null;\n if (before === after) return result;\n // A tier write (e.g. hot→cold migration) raced the two walkers, so the\n // count/digest may double-count or miss the in-flight memory. Retry for a\n // consistent snapshot.\n }\n throw new Error(`corpus census for namespace \"${namespace}\" did not stabilize after ${maxAttempts} attempts`);\n}\n\n/**\n * Compute watermarks for a set of namespaces. A namespace whose storage is\n * unavailable (or unreadable) is skipped rather than failing the whole read, so\n * a single bad namespace never blanks the health/doctor payload.\n */\nexport async function computeCorpusWatermarks(\n namespaces: readonly string[],\n storageFor: (namespace: string) => CorpusStorage | Promise<CorpusStorage>,\n now?: Date,\n): Promise<CorpusWatermark[]> {\n const watermarks: CorpusWatermark[] = [];\n for (const namespace of namespaces) {\n try {\n watermarks.push(await computeNamespaceWatermark(namespace, await storageFor(namespace), now));\n } catch {\n // Storage unavailable/unreadable for this namespace — degrade gracefully.\n }\n }\n return watermarks;\n}\n\n/** A namespace and the memory root its corpus lives under. */\nexport interface CorpusNamespaceRoot {\n /** Representative namespace label for this root (default wins on collapse). */\n readonly namespace: string;\n readonly rootDir: string;\n /**\n * Every configured namespace name that resolves to `rootDir` (aliases that\n * collapsed onto one physical root, e.g. `global` + `shared` when namespaces\n * are disabled). `/health` filters visibility against ALL of these so a token\n * scoped to a non-representative alias still sees the shared corpus (issue\n * #2156 round-10). Always includes `namespace`.\n */\n readonly namespaces: readonly string[];\n}\n\n/**\n * Resolve the namespace set the corpus census covers, shared by the `/health`\n * builder AND the `remnic doctor` corpus check (issue #2156 finding C) so the\n * two surfaces enumerate the SAME tenants — and, critically, resolve each\n * namespace's ROOT the SAME way — and cannot drift. Enumeration is purely\n * config-driven (`listNamespaces({ config })`): it discovers the default,\n * shared, policy, and on-disk `namespaces/<ns>` tenants and resolves every root\n * through the namespace storage router, exactly as the live daemon's\n * `getStorage(namespace)` does. It still works when the namespace catalog is\n * opted out. The catalog is deliberately NOT unioned in: a catalog-registered\n * tenant with no directory has an empty corpus (nothing to fingerprint), and\n * trusting `catalog.storageDir` here could make the doctor scan a different root\n * than `/health` for the same namespace (issue #2156 round-8). Deduped by\n * resolved root so a namespaces-disabled / flat-root deployment reports its\n * single shared corpus once, under the default-namespace label.\n */\nexport async function resolveCorpusNamespaceRoots(options: {\n config: PluginConfig;\n}): Promise<CorpusNamespaceRoot[]> {\n const { config } = options;\n const configDriven = await listNamespaces({ config });\n // Default namespace first so it wins the representative label when several\n // configured names collapse onto one root (namespaces disabled → memoryDir).\n const ordered = [\n ...configDriven.filter((entry) => entry.namespace === config.defaultNamespace),\n ...configDriven.filter((entry) => entry.namespace !== config.defaultNamespace),\n ];\n const seenNamespaces = new Set<string>();\n const byRoot = new Map<string, { namespace: string; rootDir: string; namespaces: string[] }>();\n for (const entry of ordered) {\n if (seenNamespaces.has(entry.namespace)) continue;\n seenNamespaces.add(entry.namespace);\n const existing = byRoot.get(entry.rootDir);\n if (existing) {\n existing.namespaces.push(entry.namespace); // an alias collapsing onto a seen root\n } else {\n byRoot.set(entry.rootDir, { namespace: entry.namespace, rootDir: entry.rootDir, namespaces: [entry.namespace] });\n }\n }\n const roots: CorpusNamespaceRoot[] = [...byRoot.values()];\n if (roots.length === 0) {\n roots.push({ namespace: config.defaultNamespace, rootDir: config.memoryDir, namespaces: [config.defaultNamespace] });\n }\n return roots;\n}\n\n/**\n * Divergence detection tolerates a watermark that is stale by up to a minute\n * (peers are compared on a slow cadence); a health/readiness probe does NOT\n * tolerate a full corpus scan (a recursive realpath-per-Markdown-file walk) on\n * every request. This TTL is that trade-off: recompute a namespace's watermark\n * at most once per window; probes in between serve the cached census (whose\n * `computedAt` shows its staleness).\n */\nexport const WATERMARK_CACHE_TTL_MS = 60_000;\n\n/**\n * Cap on concurrent background corpus scans. A probe that finds many namespaces\n * cold/expired would otherwise fan out one recursive hot/cold walk PER namespace\n * at once, a periodic unbounded filesystem-work burst that can starve recall and\n * storage (issue #2156 round-9). Excess refreshes queue and start as slots free;\n * single-flight still dedupes duplicate requests for the same namespace.\n */\nconst MAX_CONCURRENT_WATERMARK_REFRESHES = 4;\n\n/**\n * Instance-scoped (AGENTS.md pattern 5 — never a bare module global)\n * stale-while-revalidate cache in front of the per-namespace corpus scan.\n * {@link get} NEVER awaits the scan: it returns the freshest already-computed\n * watermark (stale allowed, or undefined for a cold namespace) and triggers a\n * single-flight background refresh when the entry is missing or expired. So a\n * `/health` probe is always O(1) — it never blocks on a recursive corpus walk,\n * even on the first request after startup or after a TTL expiry. Owned by the\n * long-lived access service instance.\n */\nexport class CorpusWatermarkCache {\n private readonly entries = new Map<string, { value: CorpusWatermark; expiresAt: number }>();\n private readonly inFlight = new Map<string, Promise<void>>();\n private readonly ttlMs: number;\n private readonly clock: () => number;\n private rootsEntry: { value: CorpusNamespaceRoot[]; expiresAt: number } | undefined;\n private rootsInFlight: Promise<void> | undefined;\n private readonly queued = new Map<string, () => void>();\n private activeRefreshes = 0;\n private readonly maxConcurrentRefreshes: number;\n\n constructor(options: { ttlMs?: number; clock?: () => number; maxConcurrentRefreshes?: number } = {}) {\n this.ttlMs = options.ttlMs ?? WATERMARK_CACHE_TTL_MS;\n this.clock = options.clock ?? Date.now;\n this.maxConcurrentRefreshes = Math.max(1, options.maxConcurrentRefreshes ?? MAX_CONCURRENT_WATERMARK_REFRESHES);\n }\n\n get(namespace: string, compute: () => Promise<CorpusWatermark>): CorpusWatermark | undefined {\n const cached = this.entries.get(namespace);\n const fresh = cached !== undefined && this.clock() < cached.expiresAt;\n if (!fresh) this.refresh(namespace, compute);\n return cached?.value;\n }\n\n private refresh(namespace: string, compute: () => Promise<CorpusWatermark>): void {\n // Single-flight + not-already-queued: one background scan per namespace.\n if (this.inFlight.has(namespace) || this.queued.has(namespace)) return;\n const run = (): void => {\n this.activeRefreshes += 1;\n const pending = compute()\n .then((value) => {\n this.entries.set(namespace, { value, expiresAt: this.clock() + this.ttlMs });\n })\n .catch(() => {\n // Failed/unreadable scan: never cache it; keep serving any stale value.\n })\n .finally(() => {\n this.inFlight.delete(namespace);\n this.activeRefreshes -= 1;\n this.drainQueue();\n });\n this.inFlight.set(namespace, pending);\n };\n // Bound the concurrent fan-out: run now if a slot is free, else queue.\n if (this.activeRefreshes < this.maxConcurrentRefreshes) {\n run();\n } else {\n this.queued.set(namespace, run);\n }\n }\n\n private drainQueue(): void {\n while (this.activeRefreshes < this.maxConcurrentRefreshes && this.queued.size > 0) {\n const next = this.queued.entries().next().value;\n if (!next) break;\n const [namespace, run] = next;\n this.queued.delete(namespace);\n run();\n }\n }\n\n\n /**\n * Stale-while-revalidate the resolved namespace roots (issue #2156 round-7).\n * Namespace enumeration (config scan + persisted-catalog parse) is O(tenants)\n * filesystem work; without this it ran on every probe ahead of the watermark\n * cache. NEVER awaits: returns the cached/stale roots (or undefined when cold)\n * and single-flights a background refresh.\n */\n getResolvedRoots(compute: () => Promise<CorpusNamespaceRoot[]>): CorpusNamespaceRoot[] | undefined {\n const fresh = this.rootsEntry !== undefined && this.clock() < this.rootsEntry.expiresAt;\n if (!fresh && this.rootsInFlight === undefined) {\n this.rootsInFlight = compute()\n .then((value) => {\n this.rootsEntry = { value, expiresAt: this.clock() + this.ttlMs };\n })\n .catch(() => {\n // Enumeration failed: keep serving any stale roots; retry next probe.\n })\n .finally(() => {\n this.rootsInFlight = undefined;\n });\n }\n return this.rootsEntry?.value;\n }\n /** Await every background refresh — in-flight AND queued (issue #2156 round-9) —\n * plus any roots refresh (shutdown / deterministic tests). */\n async whenIdle(): Promise<void> {\n while (this.inFlight.size > 0 || this.rootsInFlight !== undefined || this.queued.size > 0) {\n const pending: Promise<unknown>[] = [...this.inFlight.values()];\n if (this.rootsInFlight) pending.push(this.rootsInFlight);\n if (pending.length === 0) {\n this.drainQueue(); // queued work with no running peer — start it\n await Promise.resolve();\n continue;\n }\n await Promise.all(pending);\n }\n }\n}\n\n/** Orchestrator surface the service watermark builder reads — satisfied by Orchestrator. */\nexport interface CorpusWatermarkHost {\n config: PluginConfig;\n getStorage(namespace: string): CorpusStorage | Promise<CorpusStorage>;\n}\n\nexport interface ServiceCorpusWatermarkOptions {\n /** Stale-while-revalidate cache so probes never await the corpus scan. */\n cache?: CorpusWatermarkCache;\n /**\n * Presenting token capabilities. When the token is namespace-restricted the\n * fleet view is filtered to the namespaces it may access (issue #2156 finding\n * B); an unrestricted/legacy token keeps the full fleet view.\n */\n caps?: TokenCapabilities | null;\n now?: Date;\n}\n\n/**\n * Build the per-namespace watermark array served on `/health`. Namespaces are\n * resolved through {@link resolveCorpusNamespaceRoots} (shared with the doctor),\n * filtered to the caller's capabilities, then served through the (optional)\n * stale-while-revalidate cache so a probe never blocks on a scan. Degrades to an\n * empty array — never throws — if enumeration is unavailable; an individual\n * namespace whose scan fails is omitted, not allowed to blank the payload.\n */\nexport async function computeServiceCorpusWatermarks(\n host: CorpusWatermarkHost,\n options: ServiceCorpusWatermarkOptions = {},\n): Promise<CorpusWatermark[]> {\n return (await computeServiceCorpusCensus(host, options)).watermarks;\n}\n\n/**\n * Watermarks PLUS whether every configured namespace made it into the set.\n *\n * Completeness is returned ATOMICALLY with the data rather than stashed in a\n * side channel: every early exit here (enumeration threw, roots still warming,\n * a per-namespace scan failed, a cache entry not yet populated) yields an\n * incomplete set, and a caller that certifies convergence against a partial\n * census would hide the divergence of whatever was dropped (issue #2149).\n */\nexport async function computeServiceCorpusCensus(\n host: CorpusWatermarkHost,\n options: ServiceCorpusWatermarkOptions = {},\n): Promise<{ watermarks: CorpusWatermark[]; complete: boolean }> {\n let roots: CorpusNamespaceRoot[] | undefined;\n if (options.cache) {\n roots = options.cache.getResolvedRoots(() => resolveCorpusNamespaceRoots({ config: host.config }));\n } else {\n try {\n roots = await resolveCorpusNamespaceRoots({ config: host.config });\n } catch {\n return { watermarks: [], complete: false }; // never enumerated — not an empty corpus\n }\n }\n // Enumeration still warming (cold): served on a later probe, and NOT complete.\n if (!roots) return { watermarks: [], complete: false };\n const visible = roots.filter((root) => root.namespaces.some((ns) => capabilityAllowsNamespace(options.caps, ns)));\n const watermarks: CorpusWatermark[] = [];\n let complete = true;\n for (const { namespace } of visible) {\n const compute = async (): Promise<CorpusWatermark> =>\n computeNamespaceWatermark(namespace, await host.getStorage(namespace), options.now);\n if (options.cache) {\n const cached = options.cache.get(namespace, compute);\n if (cached) watermarks.push(cached);\n else complete = false; // still warming — the set is not the full census yet\n } else {\n try {\n watermarks.push(await compute());\n } catch {\n // One tenant's storage/scan failed — omit just that namespace.\n complete = false;\n }\n }\n }\n return { watermarks, complete };\n}\n\n\n"],"mappings":";;;;;;;;AAsCA,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AACrB,OAAO,UAAU;AAwBV,IAAM,uBAAuB;AAKpC,IAAM,mBAAmB;AAEzB,IAAM,gBAAgB;AAStB,SAAS,kBAAkB,OAAwB;AACjD,MAAI,CAAC,iBAAiB,KAAK,KAAK,EAAG,QAAO;AAC1C,QAAM,CAAC,MAAM,OAAO,GAAG,IAAI,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM;AACtD,QAAM,OAAO,IAAI,KAAK,KAAK,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;AACpD,SAAO,KAAK,eAAe,MAAM,QAAQ,KAAK,YAAY,MAAM,QAAQ,KAAK,KAAK,WAAW,MAAM;AACrG;AAQA,SAAS,eAAe,SAAiB,UAA8B;AACrE,QAAM,MAAM,KAAK,SAAS,SAAS,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,KAAK,GAAG;AACrE,QAAM,QAAQ,IAAI,MAAM,GAAG;AAM3B,QAAM,OAAmB,MAAM,CAAC,MAAM,gBAAgB,SAAS;AAC/D,QAAM,gBAAgB,SAAS,SAAS,MAAM,MAAM,CAAC,IAAI;AACzD,MAAI,cAAc,UAAU,KAAK,kBAAkB,cAAc,CAAC,CAAC,GAAG;AACpE,WAAO,EAAE,QAAQ,GAAG,IAAI,IAAI,cAAc,CAAC,CAAC,IAAI,cAAc,CAAC,CAAC,IAAI,MAAM,cAAc,CAAC,GAAG,KAAK;AAAA,EACnG;AACA,SAAO,EAAE,QAAQ,GAAG,IAAI,IAAI,oBAAoB,IAAI,MAAM,MAAM,KAAK;AACvE;AAQO,SAAS,qBAAqB,OAA0B,SAAsC;AACnG,QAAM,SAAS,oBAAI,IAAoB;AACvC,aAAW,YAAY,OAAO;AAC5B,UAAM,EAAE,OAAO,IAAI,eAAe,SAAS,QAAQ;AACnD,WAAO,IAAI,SAAS,OAAO,IAAI,MAAM,KAAK,KAAK,CAAC;AAAA,EAClD;AACA,SAAO;AACT;AAQO,SAAS,sBAAsB,QAAsE;AAC1G,QAAM,UAAU,kBAAkB,MAAM,CAAC,GAAG,OAAO,QAAQ,CAAC,IAAI,OAAO,QAAQ,MAAM;AACrF,QAAM,aAAa,QAChB,KAAK,CAAC,GAAG,MAAO,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI,CAAE,EACvD,IAAI,CAAC,CAAC,QAAQ,KAAK,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,EAC7C,KAAK,IAAI;AACZ,SAAO,WAAW,QAAQ,EAAE,OAAO,UAAU,EAAE,OAAO,KAAK;AAC7D;AASA,eAAsB,uBAAuB,OAKhB;AAC3B,QAAM,EAAE,WAAW,OAAO,QAAQ,IAAI;AAEtC,QAAM,SAAS,oBAAI,IAAoB;AACvC,MAAI,kBAAiC;AACrC,QAAM,eAA6B,CAAC;AACpC,aAAW,YAAY,OAAO;AAC5B,UAAM,SAAS,eAAe,SAAS,QAAQ;AAC/C,iBAAa,KAAK,MAAM;AACxB,WAAO,IAAI,OAAO,SAAS,OAAO,IAAI,OAAO,MAAM,KAAK,KAAK,CAAC;AAI9D,QAAI,OAAO,SAAS,SAAS,OAAO,SAAS,SAAS,oBAAoB,QAAQ,OAAO,OAAO,kBAAkB;AAChH,wBAAkB,OAAO;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,SAAS,sBAAsB,MAAM;AAE3C,MAAI,gBAA+B;AACnC,MAAI,oBAAoB,MAAM;AAC5B,QAAI,aAAa,OAAO;AACxB,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;AACxC,UAAI,aAAa,CAAC,EAAE,SAAS,SAAS,aAAa,CAAC,EAAE,SAAS,gBAAiB;AAChF,UAAI;AACF,cAAM,KAAK,MAAM,KAAK,MAAM,CAAC,CAAC;AAC9B,YAAI,GAAG,UAAU,WAAY,cAAa,GAAG;AAAA,MAC/C,SAAS,KAAK;AAKZ,YAAK,IAA8B,SAAS,SAAU,OAAM;AAAA,MAC9D;AAAA,IACF;AACA,QAAI,aAAa,OAAO,mBAAmB;AACzC,sBAAgB,IAAI,KAAK,UAAU,EAAE,YAAY;AAAA,IACnD;AAAA,EACF;AAKA,QAAM,cAAc,MAAM,OAAO,oBAAI,KAAK,GAAG,YAAY;AAEzD,SAAO;AAAA,IACL;AAAA,IACA,iBAAiB,MAAM;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAmBA,IAAM,2BAA2B;AAajC,eAAe,0BACb,WACA,SACA,KAC0B;AAC1B,QAAM,cAAc,QAAQ,sBAAsB,KAAK,OAAO;AAC9D,QAAM,cAAc,cAAc,2BAA2B;AAC7D,WAAS,UAAU,GAAG,UAAU,aAAa,WAAW,GAAG;AACzD,UAAM,SAAS,cAAc,MAAM,YAAY,IAAI;AACnD,UAAM,CAAC,UAAU,SAAS,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC9C,QAAQ,yBAAyB,EAAE,qBAAqB,KAAK,CAAC;AAAA,MAC9D,QAAQ,uBAAuB,EAAE,qBAAqB,KAAK,CAAC;AAAA,IAC9D,CAAC;AACD,UAAM,SAAS,MAAM,uBAAuB,EAAE,WAAW,OAAO,CAAC,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,QAAQ,KAAK,IAAI,CAAC;AACxH,UAAM,QAAQ,cAAc,MAAM,YAAY,IAAI;AAClD,QAAI,WAAW,MAAO,QAAO;AAAA,EAI/B;AACA,QAAM,IAAI,MAAM,gCAAgC,SAAS,6BAA6B,WAAW,WAAW;AAC9G;AAOA,eAAsB,wBACpB,YACA,YACA,KAC4B;AAC5B,QAAM,aAAgC,CAAC;AACvC,aAAW,aAAa,YAAY;AAClC,QAAI;AACF,iBAAW,KAAK,MAAM,0BAA0B,WAAW,MAAM,WAAW,SAAS,GAAG,GAAG,CAAC;AAAA,IAC9F,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAiCA,eAAsB,4BAA4B,SAEf;AACjC,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,eAAe,MAAM,eAAe,EAAE,OAAO,CAAC;AAGpD,QAAM,UAAU;AAAA,IACd,GAAG,aAAa,OAAO,CAAC,UAAU,MAAM,cAAc,OAAO,gBAAgB;AAAA,IAC7E,GAAG,aAAa,OAAO,CAAC,UAAU,MAAM,cAAc,OAAO,gBAAgB;AAAA,EAC/E;AACA,QAAM,iBAAiB,oBAAI,IAAY;AACvC,QAAM,SAAS,oBAAI,IAA0E;AAC7F,aAAW,SAAS,SAAS;AAC3B,QAAI,eAAe,IAAI,MAAM,SAAS,EAAG;AACzC,mBAAe,IAAI,MAAM,SAAS;AAClC,UAAM,WAAW,OAAO,IAAI,MAAM,OAAO;AACzC,QAAI,UAAU;AACZ,eAAS,WAAW,KAAK,MAAM,SAAS;AAAA,IAC1C,OAAO;AACL,aAAO,IAAI,MAAM,SAAS,EAAE,WAAW,MAAM,WAAW,SAAS,MAAM,SAAS,YAAY,CAAC,MAAM,SAAS,EAAE,CAAC;AAAA,IACjH;AAAA,EACF;AACA,QAAM,QAA+B,CAAC,GAAG,OAAO,OAAO,CAAC;AACxD,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,KAAK,EAAE,WAAW,OAAO,kBAAkB,SAAS,OAAO,WAAW,YAAY,CAAC,OAAO,gBAAgB,EAAE,CAAC;AAAA,EACrH;AACA,SAAO;AACT;AAUO,IAAM,yBAAyB;AAStC,IAAM,qCAAqC;AAYpC,IAAM,uBAAN,MAA2B;AAAA,EACf,UAAU,oBAAI,IAA2D;AAAA,EACzE,WAAW,oBAAI,IAA2B;AAAA,EAC1C;AAAA,EACA;AAAA,EACT;AAAA,EACA;AAAA,EACS,SAAS,oBAAI,IAAwB;AAAA,EAC9C,kBAAkB;AAAA,EACT;AAAA,EAEjB,YAAY,UAAqF,CAAC,GAAG;AACnG,SAAK,QAAQ,QAAQ,SAAS;AAC9B,SAAK,QAAQ,QAAQ,SAAS,KAAK;AACnC,SAAK,yBAAyB,KAAK,IAAI,GAAG,QAAQ,0BAA0B,kCAAkC;AAAA,EAChH;AAAA,EAEA,IAAI,WAAmB,SAAsE;AAC3F,UAAM,SAAS,KAAK,QAAQ,IAAI,SAAS;AACzC,UAAM,QAAQ,WAAW,UAAa,KAAK,MAAM,IAAI,OAAO;AAC5D,QAAI,CAAC,MAAO,MAAK,QAAQ,WAAW,OAAO;AAC3C,WAAO,QAAQ;AAAA,EACjB;AAAA,EAEQ,QAAQ,WAAmB,SAA+C;AAEhF,QAAI,KAAK,SAAS,IAAI,SAAS,KAAK,KAAK,OAAO,IAAI,SAAS,EAAG;AAChE,UAAM,MAAM,MAAY;AACtB,WAAK,mBAAmB;AACxB,YAAM,UAAU,QAAQ,EACrB,KAAK,CAAC,UAAU;AACf,aAAK,QAAQ,IAAI,WAAW,EAAE,OAAO,WAAW,KAAK,MAAM,IAAI,KAAK,MAAM,CAAC;AAAA,MAC7E,CAAC,EACA,MAAM,MAAM;AAAA,MAEb,CAAC,EACA,QAAQ,MAAM;AACb,aAAK,SAAS,OAAO,SAAS;AAC9B,aAAK,mBAAmB;AACxB,aAAK,WAAW;AAAA,MAClB,CAAC;AACH,WAAK,SAAS,IAAI,WAAW,OAAO;AAAA,IACtC;AAEA,QAAI,KAAK,kBAAkB,KAAK,wBAAwB;AACtD,UAAI;AAAA,IACN,OAAO;AACL,WAAK,OAAO,IAAI,WAAW,GAAG;AAAA,IAChC;AAAA,EACF;AAAA,EAEQ,aAAmB;AACzB,WAAO,KAAK,kBAAkB,KAAK,0BAA0B,KAAK,OAAO,OAAO,GAAG;AACjF,YAAM,OAAO,KAAK,OAAO,QAAQ,EAAE,KAAK,EAAE;AAC1C,UAAI,CAAC,KAAM;AACX,YAAM,CAAC,WAAW,GAAG,IAAI;AACzB,WAAK,OAAO,OAAO,SAAS;AAC5B,UAAI;AAAA,IACN;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,iBAAiB,SAAkF;AACjG,UAAM,QAAQ,KAAK,eAAe,UAAa,KAAK,MAAM,IAAI,KAAK,WAAW;AAC9E,QAAI,CAAC,SAAS,KAAK,kBAAkB,QAAW;AAC9C,WAAK,gBAAgB,QAAQ,EAC1B,KAAK,CAAC,UAAU;AACf,aAAK,aAAa,EAAE,OAAO,WAAW,KAAK,MAAM,IAAI,KAAK,MAAM;AAAA,MAClE,CAAC,EACA,MAAM,MAAM;AAAA,MAEb,CAAC,EACA,QAAQ,MAAM;AACb,aAAK,gBAAgB;AAAA,MACvB,CAAC;AAAA,IACL;AACA,WAAO,KAAK,YAAY;AAAA,EAC1B;AAAA;AAAA;AAAA,EAGA,MAAM,WAA0B;AAC9B,WAAO,KAAK,SAAS,OAAO,KAAK,KAAK,kBAAkB,UAAa,KAAK,OAAO,OAAO,GAAG;AACzF,YAAM,UAA8B,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC;AAC9D,UAAI,KAAK,cAAe,SAAQ,KAAK,KAAK,aAAa;AACvD,UAAI,QAAQ,WAAW,GAAG;AACxB,aAAK,WAAW;AAChB,cAAM,QAAQ,QAAQ;AACtB;AAAA,MACF;AACA,YAAM,QAAQ,IAAI,OAAO;AAAA,IAC3B;AAAA,EACF;AACF;AA4BA,eAAsB,+BACpB,MACA,UAAyC,CAAC,GACd;AAC5B,UAAQ,MAAM,2BAA2B,MAAM,OAAO,GAAG;AAC3D;AAWA,eAAsB,2BACpB,MACA,UAAyC,CAAC,GACqB;AAC/D,MAAI;AACJ,MAAI,QAAQ,OAAO;AACjB,YAAQ,QAAQ,MAAM,iBAAiB,MAAM,4BAA4B,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC;AAAA,EACnG,OAAO;AACL,QAAI;AACF,cAAQ,MAAM,4BAA4B,EAAE,QAAQ,KAAK,OAAO,CAAC;AAAA,IACnE,QAAQ;AACN,aAAO,EAAE,YAAY,CAAC,GAAG,UAAU,MAAM;AAAA,IAC3C;AAAA,EACF;AAEA,MAAI,CAAC,MAAO,QAAO,EAAE,YAAY,CAAC,GAAG,UAAU,MAAM;AACrD,QAAM,UAAU,MAAM,OAAO,CAAC,SAAS,KAAK,WAAW,KAAK,CAAC,OAAO,0BAA0B,QAAQ,MAAM,EAAE,CAAC,CAAC;AAChH,QAAM,aAAgC,CAAC;AACvC,MAAI,WAAW;AACf,aAAW,EAAE,UAAU,KAAK,SAAS;AACnC,UAAM,UAAU,YACd,0BAA0B,WAAW,MAAM,KAAK,WAAW,SAAS,GAAG,QAAQ,GAAG;AACpF,QAAI,QAAQ,OAAO;AACjB,YAAM,SAAS,QAAQ,MAAM,IAAI,WAAW,OAAO;AACnD,UAAI,OAAQ,YAAW,KAAK,MAAM;AAAA,UAC7B,YAAW;AAAA,IAClB,OAAO;AACL,UAAI;AACF,mBAAW,KAAK,MAAM,QAAQ,CAAC;AAAA,MACjC,QAAQ;AAEN,mBAAW;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,YAAY,SAAS;AAChC;","names":[]}