@remnic/core 9.6.39 → 9.7.1

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 (373) hide show
  1. package/dist/access-admin-ops-surface.d.ts +6 -6
  2. package/dist/access-admin-ops-surface.js +16 -16
  3. package/dist/access-boundary.d.ts +6 -6
  4. package/dist/access-boundary.js +17 -17
  5. package/dist/access-cli.js +56 -55
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-http.d.ts +6 -6
  8. package/dist/access-http.js +22 -22
  9. package/dist/access-identity-continuity-surface.d.ts +5 -5
  10. package/dist/access-identity-continuity-surface.js +16 -16
  11. package/dist/access-lcm-surface.d.ts +6 -6
  12. package/dist/access-lcm-surface.js +16 -16
  13. package/dist/access-mcp.d.ts +6 -6
  14. package/dist/access-mcp.js +21 -21
  15. package/dist/access-observe-write-surface.d.ts +6 -6
  16. package/dist/access-observe-write-surface.js +16 -16
  17. package/dist/access-operations-batch.js +19 -19
  18. package/dist/access-operations.d.ts +6 -6
  19. package/dist/access-operations.js +20 -20
  20. package/dist/access-recall-surface.d.ts +6 -6
  21. package/dist/access-recall-surface.js +16 -16
  22. package/dist/{access-service-Fw9AYEqG.d.ts → access-service-CJ5hrLqR.d.ts} +4 -4
  23. package/dist/access-service.d.ts +6 -6
  24. package/dist/access-service.js +16 -16
  25. package/dist/access-surface-catalog.d.ts +6 -6
  26. package/dist/action-confidence.d.ts +1 -1
  27. package/dist/active-memory-bridge.d.ts +1 -1
  28. package/dist/active-memory-bridge.js +3 -3
  29. package/dist/active-recall.d.ts +1 -1
  30. package/dist/active-recall.js +1 -1
  31. package/dist/{auto-sync-4A5YBWYO.js → auto-sync-676N6N6Y.js} +4 -4
  32. package/dist/behavior-learner.d.ts +1 -1
  33. package/dist/behavior-signals.d.ts +1 -1
  34. package/dist/bootstrap.d.ts +5 -5
  35. package/dist/briefing.d.ts +2 -2
  36. package/dist/briefing.js +5 -5
  37. package/dist/buffer-surprise-report.d.ts +1 -1
  38. package/dist/buffer.d.ts +2 -2
  39. package/dist/buffer.js +2 -2
  40. package/dist/bulk-import/index.d.ts +3 -3
  41. package/dist/calibration.d.ts +1 -1
  42. package/dist/calibration.js +1 -1
  43. package/dist/capabilities.d.ts +4 -2
  44. package/dist/capabilities.js +1 -1
  45. package/dist/{catalog-CnS_MP-8.d.ts → catalog-XCOyxXKw.d.ts} +1 -1
  46. package/dist/causal-behavior.d.ts +1 -1
  47. package/dist/causal-consolidation.d.ts +1 -1
  48. package/dist/causal-consolidation.js +7 -7
  49. package/dist/causal-trajectory-graph.d.ts +1 -1
  50. package/dist/{chunk-SFHI7JBE.js → chunk-2FSHROYK.js} +3 -3
  51. package/dist/{chunk-WHZ7MK72.js → chunk-4YUPLX7U.js} +3 -3
  52. package/dist/{chunk-ZIKJD6NL.js → chunk-53Y3TETS.js} +2 -2
  53. package/dist/{chunk-UI4FVU6T.js → chunk-5CJVLG3H.js} +2 -2
  54. package/dist/{chunk-BAVVI5GP.js → chunk-7PND4PUC.js} +3 -3
  55. package/dist/{chunk-NREKOGZ2.js → chunk-ARD5NE4M.js} +3 -3
  56. package/dist/{chunk-GCTJG5MP.js → chunk-AWHOZTF6.js} +4 -4
  57. package/dist/{chunk-VSF7AAGE.js → chunk-BFPETA3S.js} +3 -2
  58. package/dist/chunk-BFPETA3S.js.map +1 -0
  59. package/dist/{chunk-ZGPDSEKG.js → chunk-BFVEEB7W.js} +2 -2
  60. package/dist/{chunk-AZK3NEHL.js → chunk-BLXNPBJI.js} +2 -2
  61. package/dist/chunk-C7SRUWBW.js +54 -0
  62. package/dist/chunk-C7SRUWBW.js.map +1 -0
  63. package/dist/{chunk-VTRF5QBX.js → chunk-CBOHQNTJ.js} +2 -2
  64. package/dist/{chunk-ZSDRGPMV.js → chunk-D6KNJRGX.js} +2 -2
  65. package/dist/{chunk-2V6L3L56.js → chunk-DDHT5J5M.js} +33 -8
  66. package/dist/chunk-DDHT5J5M.js.map +1 -0
  67. package/dist/{chunk-3WULMX34.js → chunk-DSWYYEIK.js} +10 -1
  68. package/dist/chunk-DSWYYEIK.js.map +1 -0
  69. package/dist/{chunk-U5JP7FCM.js → chunk-E47TABDR.js} +2 -2
  70. package/dist/{chunk-7TEWOG3R.js → chunk-FMECZFUT.js} +2 -2
  71. package/dist/{chunk-QWGNDWKB.js → chunk-FNMFJWBY.js} +2 -2
  72. package/dist/{chunk-CZGIIADK.js → chunk-FXGC5UUH.js} +56 -11
  73. package/dist/chunk-FXGC5UUH.js.map +1 -0
  74. package/dist/{chunk-JIKJPDQJ.js → chunk-GKCCHHZK.js} +15 -5
  75. package/dist/chunk-GKCCHHZK.js.map +1 -0
  76. package/dist/{chunk-EDD2G35Z.js → chunk-GWMTFDHF.js} +4 -4
  77. package/dist/{chunk-VCW74PNH.js → chunk-H67QFYUK.js} +2 -2
  78. package/dist/{chunk-5ELELET5.js → chunk-HT2DQ27U.js} +3 -3
  79. package/dist/{chunk-VU3SVYMA.js → chunk-HTIIXNJQ.js} +2 -2
  80. package/dist/{chunk-I73G4UH7.js → chunk-IALLX7EE.js} +2 -2
  81. package/dist/{chunk-PE6XR4GG.js → chunk-IKQTFQMI.js} +2 -2
  82. package/dist/{chunk-XZ5I7OZR.js → chunk-KIPLG7S7.js} +4 -4
  83. package/dist/{chunk-QLVLW7UD.js → chunk-KSBEEMQF.js} +17 -5
  84. package/dist/chunk-KSBEEMQF.js.map +1 -0
  85. package/dist/{chunk-RU73ZROD.js → chunk-KWJP22WP.js} +12 -12
  86. package/dist/{chunk-MNUHL34L.js → chunk-LKT755LP.js} +2 -2
  87. package/dist/{chunk-PI23H7FV.js → chunk-M7VGG3ID.js} +3 -3
  88. package/dist/{chunk-PWSZNML3.js → chunk-NGJT7IW5.js} +284 -38
  89. package/dist/chunk-NGJT7IW5.js.map +1 -0
  90. package/dist/{chunk-HVE3UVAB.js → chunk-NS4GEMLR.js} +3 -3
  91. package/dist/{chunk-ZMXXHUEZ.js → chunk-O24E5CFS.js} +3 -3
  92. package/dist/{chunk-3BZDBG7U.js → chunk-PML3GU4Q.js} +3 -3
  93. package/dist/{chunk-DQ2CJYUM.js → chunk-PSQ32V5P.js} +3 -3
  94. package/dist/{chunk-7XY6TJLM.js → chunk-PUPQ2PMC.js} +3 -3
  95. package/dist/{chunk-EBVUFGJ4.js → chunk-QFOLNA2C.js} +2 -2
  96. package/dist/{chunk-METJNEIB.js → chunk-QMUERLZK.js} +1 -1
  97. package/dist/chunk-QMUERLZK.js.map +1 -0
  98. package/dist/{chunk-PQG2AOL4.js → chunk-QRYNEVHP.js} +28 -4
  99. package/dist/{chunk-PQG2AOL4.js.map → chunk-QRYNEVHP.js.map} +1 -1
  100. package/dist/{chunk-CVEEJKRD.js → chunk-RQTCDICG.js} +2 -2
  101. package/dist/{chunk-F7AE7L6G.js → chunk-RZEJDF3S.js} +2 -2
  102. package/dist/{chunk-YFBTVOMB.js → chunk-SYNYI4ZM.js} +2 -2
  103. package/dist/{chunk-BRDJ2HFP.js → chunk-TRWPCR6P.js} +2 -2
  104. package/dist/{chunk-YAN6NTJN.js → chunk-UKA7GNI2.js} +3 -3
  105. package/dist/{chunk-TQNRI55H.js → chunk-UR6PBX2J.js} +4 -51
  106. package/dist/chunk-UR6PBX2J.js.map +1 -0
  107. package/dist/{chunk-XH2Y6UIW.js → chunk-VD56CJRR.js} +4 -4
  108. package/dist/{chunk-J3XHPFEU.js → chunk-VXMWDXIJ.js} +2 -2
  109. package/dist/{chunk-QT2RZNRT.js → chunk-VY6LKKVN.js} +2 -2
  110. package/dist/{chunk-PRTA6444.js → chunk-W4PGTAS2.js} +2 -2
  111. package/dist/{chunk-WX4XTAOI.js → chunk-WGE3OYC3.js} +7 -7
  112. package/dist/{chunk-RVLE2SNP.js → chunk-X6FSCBJZ.js} +22 -22
  113. package/dist/{chunk-6MWGGPJJ.js → chunk-Y4EMHIQ5.js} +7 -7
  114. package/dist/{chunk-IGCQHJSQ.js → chunk-Z3HJ5L3Z.js} +2 -2
  115. package/dist/{chunk-O2EL7EGG.js → chunk-ZQ7UBUZ6.js} +2 -2
  116. package/dist/{chunk-5SORASJK.js → chunk-ZXUG3AFP.js} +2 -2
  117. package/dist/{cli-PGF41MN6.d.ts → cli-CtECZr1q.d.ts} +5 -5
  118. package/dist/cli.d.ts +8 -8
  119. package/dist/cli.js +37 -36
  120. package/dist/compounding/engine.d.ts +2 -2
  121. package/dist/compounding/engine.js +7 -7
  122. package/dist/compounding/preference-consolidator.d.ts +1 -1
  123. package/dist/compression-optimizer.d.ts +1 -1
  124. package/dist/config.d.ts +1 -1
  125. package/dist/config.js +1 -1
  126. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  127. package/dist/connectors/codex-materialize-runner.js +5 -5
  128. package/dist/connectors/codex-materialize.d.ts +1 -1
  129. package/dist/connectors/index.d.ts +1 -1
  130. package/dist/connectors/index.js +7 -7
  131. package/dist/consolidation-provenance-check.d.ts +2 -2
  132. package/dist/consolidation-undo.d.ts +2 -2
  133. package/dist/contradiction/index.d.ts +2 -2
  134. package/dist/contradiction/index.js +2 -2
  135. package/dist/{contradiction-scan-SSKCAMLX.js → contradiction-scan-J57T5QV7.js} +3 -3
  136. package/dist/conversation-index/backend.d.ts +1 -1
  137. package/dist/conversation-index/chunker.d.ts +1 -1
  138. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  139. package/dist/conversation-index/indexer.d.ts +1 -1
  140. package/dist/conversation-index/search.d.ts +1 -1
  141. package/dist/day-summary.d.ts +1 -1
  142. package/dist/day-summary.js +2 -2
  143. package/dist/delinearize.d.ts +1 -1
  144. package/dist/direct-answer-wiring.d.ts +1 -1
  145. package/dist/direct-answer.d.ts +1 -1
  146. package/dist/embedding-fallback.d.ts +1 -1
  147. package/dist/embedding-fallback.js +2 -2
  148. package/dist/enrichment/index.d.ts +1 -1
  149. package/dist/entity-retrieval.d.ts +2 -2
  150. package/dist/entity-retrieval.js +6 -6
  151. package/dist/entity-schema.d.ts +1 -1
  152. package/dist/explicit-capture.d.ts +5 -5
  153. package/dist/explicit-capture.js +2 -2
  154. package/dist/extraction-faithfulness.d.ts +1 -1
  155. package/dist/extraction-faithfulness.js +2 -2
  156. package/dist/extraction-judge-telemetry.d.ts +1 -1
  157. package/dist/extraction-judge-training.d.ts +1 -1
  158. package/dist/extraction-judge.d.ts +1 -1
  159. package/dist/extraction-judge.js +2 -2
  160. package/dist/extraction.d.ts +17 -2
  161. package/dist/extraction.js +12 -7
  162. package/dist/fallback-llm.d.ts +5 -2
  163. package/dist/fallback-llm.js +1 -1
  164. package/dist/{first-start-migration-4I3VDSYS.js → first-start-migration-TI67YMLG.js} +2 -2
  165. package/dist/graph-dashboard-diff.d.ts +1 -1
  166. package/dist/graph-dashboard-key.d.ts +1 -1
  167. package/dist/graph-dashboard-parser.d.ts +1 -1
  168. package/dist/graph-edge-reinforcement.d.ts +1 -1
  169. package/dist/graph-snapshot.d.ts +1 -1
  170. package/dist/graph.d.ts +1 -1
  171. package/dist/identity-continuity.d.ts +1 -1
  172. package/dist/importance.d.ts +1 -1
  173. package/dist/importers/index.d.ts +1 -1
  174. package/dist/index.d.ts +13 -13
  175. package/dist/index.js +56 -55
  176. package/dist/intent.d.ts +1 -1
  177. package/dist/lcm/engine.d.ts +1 -1
  178. package/dist/lcm/index.d.ts +1 -1
  179. package/dist/lcm/tools.d.ts +1 -1
  180. package/dist/lifecycle.d.ts +1 -1
  181. package/dist/live-connectors-runner.d.ts +1 -1
  182. package/dist/live-connectors-runner.js +3 -2
  183. package/dist/local-llm.d.ts +1 -1
  184. package/dist/local-llm.js +2 -2
  185. package/dist/local-model-endpoint.d.ts +1 -1
  186. package/dist/maintenance/memory-governance.d.ts +1 -1
  187. package/dist/maintenance/memory-governance.js +5 -5
  188. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
  189. package/dist/maintenance/rebuild-memory-projection.js +6 -6
  190. package/dist/mcp-memory-inspector-app.d.ts +6 -6
  191. package/dist/memory-action-policy.d.ts +1 -1
  192. package/dist/memory-cache.d.ts +1 -1
  193. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  194. package/dist/memory-projection-store.d.ts +1 -1
  195. package/dist/memory-provenance.d.ts +1 -1
  196. package/dist/memory-worth-outcomes.d.ts +2 -2
  197. package/dist/models-json.d.ts +1 -1
  198. package/dist/namespaces/migrate.d.ts +3 -3
  199. package/dist/namespaces/migrate.js +10 -10
  200. package/dist/namespaces/principal.d.ts +1 -1
  201. package/dist/namespaces/principal.js +2 -2
  202. package/dist/namespaces/search.d.ts +1 -1
  203. package/dist/namespaces/search.js +4 -4
  204. package/dist/namespaces/storage.d.ts +3 -3
  205. package/dist/namespaces/storage.js +6 -6
  206. package/dist/native-knowledge.d.ts +1 -1
  207. package/dist/operator-toolkit.d.ts +2 -2
  208. package/dist/operator-toolkit.js +16 -16
  209. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  210. package/dist/orchestration/compression-guideline-coordinator.js +2 -2
  211. package/dist/orchestration/maintenance.d.ts +2 -2
  212. package/dist/orchestration/maintenance.js +10 -10
  213. package/dist/{orchestrator-xePY-WPe.d.ts → orchestrator-CKctWsw9.d.ts} +51 -4
  214. package/dist/orchestrator.d.ts +5 -5
  215. package/dist/orchestrator.js +56 -55
  216. package/dist/patterns-cli.d.ts +1 -1
  217. package/dist/{pipeline-Zgwz-Kne.d.ts → pipeline-BWwGIys5.d.ts} +1 -1
  218. package/dist/policy-runtime.d.ts +1 -1
  219. package/dist/provenance.d.ts +1 -1
  220. package/dist/{qmd-B6Xwfn5_.d.ts → qmd-DFuUrBWW.d.ts} +1 -1
  221. package/dist/qmd-preflight.d.ts +2 -2
  222. package/dist/qmd-recall-cache.d.ts +1 -1
  223. package/dist/qmd.d.ts +2 -2
  224. package/dist/recall-disclosure-escalation.d.ts +1 -1
  225. package/dist/recall-explain-renderer.d.ts +1 -1
  226. package/dist/recall-explain-renderer.js +3 -3
  227. package/dist/recall-planner-llm.d.ts +1 -1
  228. package/dist/recall-planner-llm.js +1 -1
  229. package/dist/recall-state.d.ts +1 -1
  230. package/dist/recall-tag-filter.d.ts +1 -1
  231. package/dist/recall-timings.d.ts +1 -1
  232. package/dist/recall-xray-cli.d.ts +1 -1
  233. package/dist/recall-xray-cli.js +4 -4
  234. package/dist/recall-xray-renderer.d.ts +1 -1
  235. package/dist/recall-xray-renderer.js +3 -3
  236. package/dist/recall-xray.d.ts +1 -1
  237. package/dist/recall-xray.js +2 -2
  238. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  239. package/dist/replay/normalizers/claude.d.ts +1 -1
  240. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  241. package/dist/replay/normalizers/shared.d.ts +1 -1
  242. package/dist/replay/runner.d.ts +1 -1
  243. package/dist/replay/types.d.ts +1 -1
  244. package/dist/resolve-auth-token.d.ts +1 -1
  245. package/dist/resume-bundles.js +2 -2
  246. package/dist/retrieval-agents.d.ts +2 -2
  247. package/dist/retrieval-tiers.d.ts +1 -1
  248. package/dist/routing/engine.d.ts +1 -1
  249. package/dist/routing/store.d.ts +1 -1
  250. package/dist/schemas.d.ts +22 -22
  251. package/dist/search/embed-helper.d.ts +1 -1
  252. package/dist/search/embed-helper.js +2 -2
  253. package/dist/search/factory.d.ts +1 -1
  254. package/dist/search/factory.js +3 -3
  255. package/dist/search/index.d.ts +1 -1
  256. package/dist/search/index.js +3 -3
  257. package/dist/search/lancedb-backend.d.ts +1 -1
  258. package/dist/search/meilisearch-backend.d.ts +1 -1
  259. package/dist/search/noop-backend.d.ts +1 -1
  260. package/dist/search/orama-backend.d.ts +1 -1
  261. package/dist/search/port.d.ts +1 -1
  262. package/dist/search/remote-backend.d.ts +1 -1
  263. package/dist/{semantic-consolidation-ClfD_CWg.d.ts → semantic-consolidation-1Wlr7NWL.d.ts} +1 -1
  264. package/dist/semantic-consolidation.d.ts +2 -2
  265. package/dist/semantic-consolidation.js +7 -7
  266. package/dist/semantic-rule-promotion.js +5 -5
  267. package/dist/semantic-rule-verifier.d.ts +1 -1
  268. package/dist/semantic-rule-verifier.js +5 -5
  269. package/dist/session-observer-bands.d.ts +1 -1
  270. package/dist/session-observer-state.d.ts +1 -1
  271. package/dist/shared-context/manager.d.ts +1 -1
  272. package/dist/shared-context/manager.js +2 -2
  273. package/dist/signal.d.ts +1 -1
  274. package/dist/{storage-ZrHZ0dSM.d.ts → storage-BQwP90sb.d.ts} +1 -1
  275. package/dist/storage.d.ts +2 -2
  276. package/dist/storage.js +4 -4
  277. package/dist/summarizer.d.ts +1 -1
  278. package/dist/summarizer.js +4 -4
  279. package/dist/summary-snapshot.d.ts +1 -1
  280. package/dist/temporal-supersession.d.ts +2 -2
  281. package/dist/temporal-timeline-recall.d.ts +1 -1
  282. package/dist/temporal-validity.d.ts +1 -1
  283. package/dist/threading.d.ts +1 -1
  284. package/dist/tier-migration.d.ts +2 -2
  285. package/dist/tier-routing.d.ts +1 -1
  286. package/dist/{tier-stats-5XF4UBRQ.js → tier-stats-KV7IWCJR.js} +2 -2
  287. package/dist/topics.d.ts +1 -1
  288. package/dist/transcript.d.ts +1 -1
  289. package/dist/transfer/types.d.ts +12 -12
  290. package/dist/trust-score-stage.d.ts +1 -1
  291. package/dist/trust-score.d.ts +1 -1
  292. package/dist/{types-BbtQZBq9.d.ts → types-DgIkyAsK.d.ts} +39 -1
  293. package/dist/types.d.ts +1 -1
  294. package/dist/types.js +1 -1
  295. package/dist/utility-runtime.d.ts +1 -1
  296. package/dist/verified-recall.js +5 -5
  297. package/package.json +2 -2
  298. package/src/buffer-session.test.ts +40 -8
  299. package/src/buffer.ts +41 -17
  300. package/src/capabilities.test.ts +1 -0
  301. package/src/capabilities.ts +4 -0
  302. package/src/config.test.ts +19 -0
  303. package/src/config.ts +29 -0
  304. package/src/extraction.test.ts +39 -0
  305. package/src/extraction.ts +72 -2
  306. package/src/fallback-llm.ts +26 -5
  307. package/src/operator-toolkit.ts +53 -0
  308. package/src/orchestration/extraction-run.test.ts +639 -0
  309. package/src/orchestration/extraction-run.ts +366 -2
  310. package/src/orchestration/session-context.ts +2 -0
  311. package/src/orchestration/turn-ingestion.ts +5 -0
  312. package/src/orchestrator-flush.test.ts +36 -2
  313. package/src/orchestrator.ts +12 -1
  314. package/src/storage.ts +34 -0
  315. package/src/types.ts +40 -0
  316. package/dist/chunk-2V6L3L56.js.map +0 -1
  317. package/dist/chunk-3WULMX34.js.map +0 -1
  318. package/dist/chunk-CZGIIADK.js.map +0 -1
  319. package/dist/chunk-JIKJPDQJ.js.map +0 -1
  320. package/dist/chunk-METJNEIB.js.map +0 -1
  321. package/dist/chunk-PWSZNML3.js.map +0 -1
  322. package/dist/chunk-QLVLW7UD.js.map +0 -1
  323. package/dist/chunk-TQNRI55H.js.map +0 -1
  324. package/dist/chunk-VSF7AAGE.js.map +0 -1
  325. /package/dist/{auto-sync-4A5YBWYO.js.map → auto-sync-676N6N6Y.js.map} +0 -0
  326. /package/dist/{chunk-SFHI7JBE.js.map → chunk-2FSHROYK.js.map} +0 -0
  327. /package/dist/{chunk-WHZ7MK72.js.map → chunk-4YUPLX7U.js.map} +0 -0
  328. /package/dist/{chunk-ZIKJD6NL.js.map → chunk-53Y3TETS.js.map} +0 -0
  329. /package/dist/{chunk-UI4FVU6T.js.map → chunk-5CJVLG3H.js.map} +0 -0
  330. /package/dist/{chunk-BAVVI5GP.js.map → chunk-7PND4PUC.js.map} +0 -0
  331. /package/dist/{chunk-NREKOGZ2.js.map → chunk-ARD5NE4M.js.map} +0 -0
  332. /package/dist/{chunk-GCTJG5MP.js.map → chunk-AWHOZTF6.js.map} +0 -0
  333. /package/dist/{chunk-ZGPDSEKG.js.map → chunk-BFVEEB7W.js.map} +0 -0
  334. /package/dist/{chunk-AZK3NEHL.js.map → chunk-BLXNPBJI.js.map} +0 -0
  335. /package/dist/{chunk-VTRF5QBX.js.map → chunk-CBOHQNTJ.js.map} +0 -0
  336. /package/dist/{chunk-ZSDRGPMV.js.map → chunk-D6KNJRGX.js.map} +0 -0
  337. /package/dist/{chunk-U5JP7FCM.js.map → chunk-E47TABDR.js.map} +0 -0
  338. /package/dist/{chunk-7TEWOG3R.js.map → chunk-FMECZFUT.js.map} +0 -0
  339. /package/dist/{chunk-QWGNDWKB.js.map → chunk-FNMFJWBY.js.map} +0 -0
  340. /package/dist/{chunk-EDD2G35Z.js.map → chunk-GWMTFDHF.js.map} +0 -0
  341. /package/dist/{chunk-VCW74PNH.js.map → chunk-H67QFYUK.js.map} +0 -0
  342. /package/dist/{chunk-5ELELET5.js.map → chunk-HT2DQ27U.js.map} +0 -0
  343. /package/dist/{chunk-VU3SVYMA.js.map → chunk-HTIIXNJQ.js.map} +0 -0
  344. /package/dist/{chunk-I73G4UH7.js.map → chunk-IALLX7EE.js.map} +0 -0
  345. /package/dist/{chunk-PE6XR4GG.js.map → chunk-IKQTFQMI.js.map} +0 -0
  346. /package/dist/{chunk-XZ5I7OZR.js.map → chunk-KIPLG7S7.js.map} +0 -0
  347. /package/dist/{chunk-RU73ZROD.js.map → chunk-KWJP22WP.js.map} +0 -0
  348. /package/dist/{chunk-MNUHL34L.js.map → chunk-LKT755LP.js.map} +0 -0
  349. /package/dist/{chunk-PI23H7FV.js.map → chunk-M7VGG3ID.js.map} +0 -0
  350. /package/dist/{chunk-HVE3UVAB.js.map → chunk-NS4GEMLR.js.map} +0 -0
  351. /package/dist/{chunk-ZMXXHUEZ.js.map → chunk-O24E5CFS.js.map} +0 -0
  352. /package/dist/{chunk-3BZDBG7U.js.map → chunk-PML3GU4Q.js.map} +0 -0
  353. /package/dist/{chunk-DQ2CJYUM.js.map → chunk-PSQ32V5P.js.map} +0 -0
  354. /package/dist/{chunk-7XY6TJLM.js.map → chunk-PUPQ2PMC.js.map} +0 -0
  355. /package/dist/{chunk-EBVUFGJ4.js.map → chunk-QFOLNA2C.js.map} +0 -0
  356. /package/dist/{chunk-CVEEJKRD.js.map → chunk-RQTCDICG.js.map} +0 -0
  357. /package/dist/{chunk-F7AE7L6G.js.map → chunk-RZEJDF3S.js.map} +0 -0
  358. /package/dist/{chunk-YFBTVOMB.js.map → chunk-SYNYI4ZM.js.map} +0 -0
  359. /package/dist/{chunk-BRDJ2HFP.js.map → chunk-TRWPCR6P.js.map} +0 -0
  360. /package/dist/{chunk-YAN6NTJN.js.map → chunk-UKA7GNI2.js.map} +0 -0
  361. /package/dist/{chunk-XH2Y6UIW.js.map → chunk-VD56CJRR.js.map} +0 -0
  362. /package/dist/{chunk-J3XHPFEU.js.map → chunk-VXMWDXIJ.js.map} +0 -0
  363. /package/dist/{chunk-QT2RZNRT.js.map → chunk-VY6LKKVN.js.map} +0 -0
  364. /package/dist/{chunk-PRTA6444.js.map → chunk-W4PGTAS2.js.map} +0 -0
  365. /package/dist/{chunk-WX4XTAOI.js.map → chunk-WGE3OYC3.js.map} +0 -0
  366. /package/dist/{chunk-RVLE2SNP.js.map → chunk-X6FSCBJZ.js.map} +0 -0
  367. /package/dist/{chunk-6MWGGPJJ.js.map → chunk-Y4EMHIQ5.js.map} +0 -0
  368. /package/dist/{chunk-IGCQHJSQ.js.map → chunk-Z3HJ5L3Z.js.map} +0 -0
  369. /package/dist/{chunk-O2EL7EGG.js.map → chunk-ZQ7UBUZ6.js.map} +0 -0
  370. /package/dist/{chunk-5SORASJK.js.map → chunk-ZXUG3AFP.js.map} +0 -0
  371. /package/dist/{contradiction-scan-SSKCAMLX.js.map → contradiction-scan-J57T5QV7.js.map} +0 -0
  372. /package/dist/{first-start-migration-4I3VDSYS.js.map → first-start-migration-TI67YMLG.js.map} +0 -0
  373. /package/dist/{tier-stats-5XF4UBRQ.js.map → tier-stats-KV7IWCJR.js.map} +0 -0
@@ -0,0 +1,639 @@
1
+ /**
2
+ * Extraction retry/backoff + circuit-breaker tests (extraction hot-loop
3
+ * hardening). Drives a real ExtractionRunCoordinator over tmpdir StorageManager
4
+ * instances with a fake extraction engine so the retry gate, per-fingerprint
5
+ * backoff, provider circuit breaker, recovery, class-specific caps, the
6
+ * never-mark-processed invariant, force-flush bypass, config parity, backoff
7
+ * math, cache coherence, and namespace isolation are all exercised end to end.
8
+ */
9
+
10
+ import assert from "node:assert/strict";
11
+ import { mkdtemp, rm } from "node:fs/promises";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ import test from "node:test";
15
+
16
+ import { SmartBuffer } from "../buffer.js";
17
+ import {
18
+ ExtractionRunCoordinator,
19
+ type ExtractionRunCoordinatorDeps,
20
+ computeExtractionRetryNextEligibleMs,
21
+ capExtractionRetryStateEntries,
22
+ } from "./extraction-run.js";
23
+ import { StorageManager } from "../storage.js";
24
+ import { parseConfig } from "../config.js";
25
+ import type { ExtractionEngine } from "../extraction.js";
26
+ import type { ThreadingManager } from "../threading.js";
27
+ import type { BufferTurn, ExtractionResult, ExtractionFailureClass, PluginConfig } from "../types.js";
28
+ import type { TierMigrationCycleSummary } from "../recall-state.js";
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Result factories
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function failureResult(cls: ExtractionFailureClass): ExtractionResult {
35
+ return {
36
+ facts: [],
37
+ profileUpdates: [],
38
+ entities: [],
39
+ questions: [],
40
+ extractionFailure: `synthetic_${cls}`,
41
+ extractionFailureClass: cls,
42
+ };
43
+ }
44
+
45
+ function successResult(): ExtractionResult {
46
+ return {
47
+ facts: [{ content: "a durable fact", category: "fact", confidence: 0.9, tags: [] }],
48
+ profileUpdates: [],
49
+ entities: [],
50
+ questions: [],
51
+ };
52
+ }
53
+
54
+ function makeTurns(content: string): BufferTurn[] {
55
+ return [
56
+ {
57
+ role: "user",
58
+ content: `u:${content}`,
59
+ timestamp: "2026-07-15T00:00:00Z",
60
+ persistProcessedFingerprint: true,
61
+ },
62
+ {
63
+ role: "assistant",
64
+ content: `a:${content}`,
65
+ timestamp: "2026-07-15T00:00:01Z",
66
+ persistProcessedFingerprint: true,
67
+ },
68
+ ] as BufferTurn[];
69
+ }
70
+
71
+ const migrationSummary: TierMigrationCycleSummary = {
72
+ trigger: "extraction",
73
+ scanned: 0,
74
+ migrated: 0,
75
+ promoted: 0,
76
+ demoted: 0,
77
+ limit: 0,
78
+ dryRun: false,
79
+ };
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Harness
83
+ // ---------------------------------------------------------------------------
84
+
85
+ interface Harness {
86
+ config: PluginConfig;
87
+ storageForNs: (ns: string) => Promise<StorageManager>;
88
+ newCoordinator: () => ExtractionRunCoordinator;
89
+ engineCalls: () => number;
90
+ setRespond: (fn: (turns: BufferTurn[]) => ExtractionResult) => void;
91
+ recordedProcessedCount: () => number;
92
+ run: (
93
+ coord: ExtractionRunCoordinator,
94
+ content: string,
95
+ opts?: { force?: boolean; failClosed?: boolean; writeNamespaceOverride?: string },
96
+ ) => Promise<{ status: string; reason?: string }>;
97
+ cleanup: () => Promise<void>;
98
+ }
99
+
100
+ async function makeHarness(overrides: Record<string, unknown> = {}): Promise<Harness> {
101
+ StorageManager.clearAllStaticCaches();
102
+ const baseDir = await mkdtemp(path.join(os.tmpdir(), "extraction-retry-"));
103
+ const config = parseConfig({
104
+ memoryDir: baseDir,
105
+ qmdEnabled: false,
106
+ ...overrides,
107
+ });
108
+
109
+ const storages = new Map<string, StorageManager>();
110
+ const storageForNs = async (ns: string): Promise<StorageManager> => {
111
+ let s = storages.get(ns);
112
+ if (!s) {
113
+ s = new StorageManager(path.join(baseDir, `ns-${ns.replace(/[^a-z0-9]+/gi, "_")}`));
114
+ await s.ensureDirectories();
115
+ storages.set(ns, s);
116
+ }
117
+ return s;
118
+ };
119
+
120
+ const defaultStorage = await storageForNs("default");
121
+ const buffer = new SmartBuffer(config, defaultStorage);
122
+ await buffer.load();
123
+
124
+ let engineCalls = 0;
125
+ let respond: (turns: BufferTurn[]) => ExtractionResult = () => successResult();
126
+ let recordedProcessedCount = 0;
127
+
128
+ const makeDeps = (): ExtractionRunCoordinatorDeps => ({
129
+ config,
130
+ getBuffer: () => buffer,
131
+ getExtraction: () => ({
132
+ extract: async (turns: Parameters<ExtractionEngine["extract"]>[0]): Promise<ExtractionResult> => {
133
+ engineCalls += 1;
134
+ return respond(turns as BufferTurn[]);
135
+ },
136
+ }),
137
+ getStorageRouter: () => ({
138
+ storageFor: async (ns: string) => storageForNs(ns),
139
+ }),
140
+ getThreading: () => ({
141
+ processTurn: async (..._args: Parameters<ThreadingManager["processTurn"]>) => "thread-1",
142
+ updateThreadTitle: async (..._args: Parameters<ThreadingManager["updateThreadTitle"]>) => {},
143
+ }),
144
+ persistExtraction: async () => ["memory-1"],
145
+ maybeCapturePassiveCorrections: async () => {},
146
+ resolveSelfNamespace: () => "default",
147
+ getCodingContextForSession: () => null,
148
+ applyCodingNamespaceOverlay: () => "default",
149
+ boxBuilderFor: () => ({ onExtraction: async () => {} }),
150
+ appendPersistedThreadEpisodes: async () => {},
151
+ maybeScheduleConsolidation: () => {},
152
+ requestQmdMaintenance: () => {},
153
+ runTierMigrationCycle: async () => migrationSummary,
154
+ getLastPersistExtractionDeferredCount: () => 0,
155
+ recordProcessedExtractionFingerprint: async () => {
156
+ recordedProcessedCount += 1;
157
+ },
158
+ });
159
+
160
+ return {
161
+ config,
162
+ storageForNs,
163
+ newCoordinator: () => new ExtractionRunCoordinator(makeDeps()),
164
+ engineCalls: () => engineCalls,
165
+ setRespond: (fn) => {
166
+ respond = fn;
167
+ },
168
+ recordedProcessedCount: () => recordedProcessedCount,
169
+ run: async (coord, content, opts = {}) => {
170
+ const result = await coord.runExtraction(makeTurns(content), {
171
+ skipCharThreshold: true,
172
+ skipUserTurnThreshold: true,
173
+ clearBufferAfterExtraction: false,
174
+ bufferKey: content,
175
+ failOnExtractionFailure: opts.failClosed === true,
176
+ forceExtractionAttempt: opts.force === true,
177
+ writeNamespaceOverride: opts.writeNamespaceOverride,
178
+ });
179
+ return { status: result.status, reason: result.reason };
180
+ },
181
+ cleanup: async () => {
182
+ StorageManager.clearAllStaticCaches();
183
+ await rm(baseDir, { recursive: true, force: true });
184
+ },
185
+ };
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Backoff math (pure)
190
+ // ---------------------------------------------------------------------------
191
+
192
+ test("computeExtractionRetryNextEligibleMs: progresses through schedule and caps", () => {
193
+ const sched = [1000, 5000, 30000];
194
+ const cap = 60000;
195
+ const noJitter = () => 0.5; // jitter factor = 1 exactly
196
+ assert.equal(computeExtractionRetryNextEligibleMs(1, sched, cap, 0.2, 0, noJitter), 1000);
197
+ assert.equal(computeExtractionRetryNextEligibleMs(2, sched, cap, 0.2, 0, noJitter), 5000);
198
+ assert.equal(computeExtractionRetryNextEligibleMs(3, sched, cap, 0.2, 0, noJitter), 30000);
199
+ // Past the schedule end → clamps to the last step, then to cap.
200
+ assert.equal(computeExtractionRetryNextEligibleMs(9, sched, cap, 0.2, 0, noJitter), 30000);
201
+ // A schedule value above the cap is clamped to the cap.
202
+ assert.equal(computeExtractionRetryNextEligibleMs(1, [999999], cap, 0.2, 0, noJitter), cap);
203
+ });
204
+
205
+ test("computeExtractionRetryNextEligibleMs: jitter stays within +/- ratio", () => {
206
+ const base = 10000;
207
+ const ratio = 0.2;
208
+ for (const r of [0, 0.25, 0.5, 0.75, 1]) {
209
+ const value = computeExtractionRetryNextEligibleMs(1, [base], base * 100, ratio, 0, () => r);
210
+ assert.ok(value >= base * (1 - ratio) - 1, `>= lower bound for rng=${r} (${value})`);
211
+ assert.ok(value <= base * (1 + ratio) + 1, `<= upper bound for rng=${r} (${value})`);
212
+ }
213
+ });
214
+
215
+ test("capExtractionRetryStateEntries: caps to newest and guards maxEntries<=0", () => {
216
+ const entries = Array.from({ length: 10 }, (_, i) => ({
217
+ fingerprint: `fp${i}`,
218
+ firstFailedAt: `2026-07-15T00:00:0${i}.000Z`,
219
+ }));
220
+ const capped = capExtractionRetryStateEntries(entries, 3);
221
+ assert.equal(capped.length, 3);
222
+ assert.deepEqual(
223
+ capped.map((e) => e.fingerprint),
224
+ ["fp7", "fp8", "fp9"],
225
+ );
226
+ // maxEntries<=0 must return [] (not ALL entries via slice(-0)).
227
+ assert.deepEqual(capExtractionRetryStateEntries(entries, 0), []);
228
+ assert.deepEqual(capExtractionRetryStateEntries(entries, -5), []);
229
+ });
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // Backoff eligibility gate
233
+ // ---------------------------------------------------------------------------
234
+
235
+ test("backoff: a provider_retryable failure parks the same fingerprint; extract() not called again", async () => {
236
+ const h = await makeHarness({
237
+ extractionRetryScheduleMs: [3_600_000],
238
+ extractionBreakerFailureThreshold: 100,
239
+ });
240
+ try {
241
+ const coord = h.newCoordinator();
242
+ h.setRespond(() => failureResult("provider_retryable"));
243
+ const r1 = await h.run(coord, "topic-a");
244
+ assert.equal(r1.status, "skipped");
245
+ assert.equal(h.engineCalls(), 1, "first attempt calls extract()");
246
+
247
+ const r2 = await h.run(coord, "topic-a");
248
+ assert.equal(r2.reason, "extraction_backoff");
249
+ assert.equal(h.engineCalls(), 1, "second attempt is suppressed by backoff");
250
+
251
+ // A distinct fingerprint is unaffected by another fingerprint's backoff.
252
+ const r3 = await h.run(coord, "topic-b");
253
+ assert.equal(h.engineCalls(), 2, "distinct fingerprint still runs");
254
+ assert.equal(r3.status, "skipped");
255
+ } finally {
256
+ await h.cleanup();
257
+ }
258
+ });
259
+
260
+ test("backoff: rewriting nextEligibleAt to the past re-enables the fingerprint (restart-safe hydration)", async () => {
261
+ const h = await makeHarness({
262
+ extractionRetryScheduleMs: [3_600_000],
263
+ extractionBreakerFailureThreshold: 100,
264
+ });
265
+ try {
266
+ h.setRespond(() => failureResult("provider_retryable"));
267
+ const coord = h.newCoordinator();
268
+ await h.run(coord, "topic-a");
269
+ assert.equal(h.engineCalls(), 1);
270
+
271
+ // Simulate the backoff window elapsing by rewinding the persisted state,
272
+ // then hydrate a FRESH coordinator (as a daemon restart would).
273
+ const storage = await h.storageForNs("default");
274
+ const meta = await storage.loadMeta();
275
+ assert.equal(meta.extractionRetryState?.length, 1, "failure persisted to meta.json");
276
+ meta.extractionRetryState = (meta.extractionRetryState ?? []).map((e) => ({
277
+ ...e,
278
+ nextEligibleAt: "2000-01-01T00:00:00.000Z",
279
+ }));
280
+ await storage.saveMeta(meta);
281
+
282
+ h.setRespond(() => successResult());
283
+ const coord2 = h.newCoordinator();
284
+ const r = await h.run(coord2, "topic-a");
285
+ assert.equal(h.engineCalls(), 2, "eligible-again fingerprint runs after restart hydration");
286
+ assert.equal(r.status, "completed");
287
+ } finally {
288
+ await h.cleanup();
289
+ }
290
+ });
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Circuit breaker
294
+ // ---------------------------------------------------------------------------
295
+
296
+ test("circuit breaker: opens after threshold failures and suppresses distinct fingerprints", async () => {
297
+ const h = await makeHarness({
298
+ extractionRetryScheduleMs: [3_600_000],
299
+ extractionBreakerFailureThreshold: 2,
300
+ extractionBreakerCooldownMs: 3_600_000,
301
+ });
302
+ try {
303
+ const coord = h.newCoordinator();
304
+ h.setRespond(() => failureResult("provider_retryable"));
305
+ await h.run(coord, "fp-1");
306
+ await h.run(coord, "fp-2");
307
+ assert.equal(h.engineCalls(), 2, "two distinct fingerprints failed, tripping the breaker");
308
+ assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
309
+
310
+ // A THIRD distinct fingerprint is short-circuited without calling extract().
311
+ const r = await h.run(coord, "fp-3");
312
+ assert.equal(r.reason, "provider_circuit_open");
313
+ assert.equal(h.engineCalls(), 2, "breaker-open suppresses distinct fingerprints too");
314
+ } finally {
315
+ await h.cleanup();
316
+ }
317
+ });
318
+
319
+ test("circuit breaker: a forced flush still calls extract() while the breaker is open and records the failure", async () => {
320
+ const h = await makeHarness({
321
+ extractionRetryScheduleMs: [3_600_000],
322
+ extractionBreakerFailureThreshold: 1,
323
+ extractionBreakerCooldownMs: 3_600_000,
324
+ });
325
+ try {
326
+ const coord = h.newCoordinator();
327
+ h.setRespond(() => failureResult("provider_retryable"));
328
+ await h.run(coord, "fp-1");
329
+ assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
330
+ assert.equal(h.engineCalls(), 1);
331
+
332
+ // Force-flush bypasses the gate (rule 18) but still records the failure.
333
+ const r = await h.run(coord, "fp-2", { force: true });
334
+ assert.equal(h.engineCalls(), 2, "forced attempt calls extract() despite open breaker");
335
+ assert.equal(r.status, "skipped");
336
+ const storage = await h.storageForNs("default");
337
+ const meta = await storage.loadMeta();
338
+ const fps = (meta.extractionRetryState ?? []).map((e) => e.fingerprint);
339
+ assert.equal(fps.length, 2, "forced failure was recorded into retry state");
340
+ } finally {
341
+ await h.cleanup();
342
+ }
343
+ });
344
+
345
+ test("circuit breaker: auth_config failure opens the breaker immediately (no hot loop)", async () => {
346
+ const h = await makeHarness({
347
+ extractionRetryScheduleMs: [3_600_000],
348
+ extractionBreakerFailureThreshold: 100, // far above one failure
349
+ extractionBreakerAuthCooldownMs: 3_600_000,
350
+ });
351
+ try {
352
+ const coord = h.newCoordinator();
353
+ h.setRespond(() => failureResult("auth_config"));
354
+ await h.run(coord, "fp-1");
355
+ assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
356
+
357
+ const r = await h.run(coord, "fp-2");
358
+ assert.equal(r.reason, "provider_circuit_open");
359
+ assert.equal(h.engineCalls(), 1, "one auth_config failure suppresses subsequent extraction");
360
+ } finally {
361
+ await h.cleanup();
362
+ }
363
+ });
364
+
365
+ test("recovery: half-open probe succeeds → breaker closes", async () => {
366
+ const h = await makeHarness({
367
+ extractionRetryScheduleMs: [3_600_000],
368
+ extractionBreakerFailureThreshold: 1,
369
+ extractionBreakerCooldownMs: 0, // cooldown already elapsed on the next call
370
+ });
371
+ try {
372
+ const coord = h.newCoordinator();
373
+ h.setRespond(() => failureResult("provider_retryable"));
374
+ await h.run(coord, "fp-1");
375
+ assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
376
+
377
+ // Cooldown 0 → next call flips open→half_open and allows one probe.
378
+ h.setRespond(() => successResult());
379
+ const r = await h.run(coord, "fp-2");
380
+ assert.equal(r.status, "completed", "half-open probe runs");
381
+ assert.equal(coord.getExtractionResilienceStatus().breaker.state, "closed");
382
+ assert.equal(coord.getExtractionResilienceStatus().breaker.consecutiveFailures, 0);
383
+ } finally {
384
+ await h.cleanup();
385
+ }
386
+ });
387
+
388
+ test("recovery: a FAILED half-open probe re-opens the breaker (cursor review)", async () => {
389
+ // An auth_config failure trips the breaker below the consecutive-failure
390
+ // threshold. After the auth cooldown elapses (half_open), a transient probe
391
+ // failure must re-open the breaker — not leave it stuck half_open where it
392
+ // no longer suppresses anything.
393
+ const h = await makeHarness({
394
+ extractionRetryScheduleMs: [3_600_000],
395
+ extractionBreakerFailureThreshold: 100, // auth trips below threshold
396
+ extractionBreakerAuthCooldownMs: 0, // half-open on the next call
397
+ extractionBreakerCooldownMs: 3_600_000, // a re-open parks for a long time
398
+ });
399
+ try {
400
+ const coord = h.newCoordinator();
401
+ h.setRespond(() => failureResult("auth_config"));
402
+ await h.run(coord, "fp-a"); // auth failure → breaker opens immediately
403
+ assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
404
+
405
+ // Auth cooldown 0 → this call flips open→half_open and probes; the probe
406
+ // fails transiently → the breaker must re-open with the transient cooldown.
407
+ h.setRespond(() => failureResult("provider_retryable"));
408
+ await h.run(coord, "fp-b");
409
+ assert.equal(
410
+ coord.getExtractionResilienceStatus().breaker.state,
411
+ "open",
412
+ "failed half-open probe re-opens the breaker",
413
+ );
414
+
415
+ // While re-opened, a third fingerprint is suppressed without calling extract().
416
+ const calls = h.engineCalls();
417
+ const r3 = await h.run(coord, "fp-c");
418
+ assert.equal(r3.reason, "provider_circuit_open", "re-opened breaker suppresses");
419
+ assert.equal(h.engineCalls(), calls, "no extract() call while re-opened");
420
+ } finally {
421
+ await h.cleanup();
422
+ }
423
+ });
424
+
425
+ test("recovery: a successful extract clears the fingerprint's retry state so it proceeds again", async () => {
426
+ const h = await makeHarness({
427
+ extractionRetryScheduleMs: [3_600_000],
428
+ extractionBreakerFailureThreshold: 100,
429
+ });
430
+ try {
431
+ const coord = h.newCoordinator();
432
+ h.setRespond(() => failureResult("provider_retryable"));
433
+ await h.run(coord, "fp-1"); // fail → parked
434
+ await h.run(coord, "fp-1"); // suppressed
435
+ assert.equal(h.engineCalls(), 1);
436
+
437
+ h.setRespond(() => successResult());
438
+ await h.run(coord, "fp-1", { force: true }); // forced probe succeeds → clears state
439
+ assert.equal(h.engineCalls(), 2);
440
+ assert.equal(coord.getExtractionResilienceStatus().backoffFingerprintCount, 0, "retry state cleared on success");
441
+
442
+ // Same fingerprint, non-forced, now proceeds because the entry cleared.
443
+ await h.run(coord, "fp-1");
444
+ assert.equal(h.engineCalls(), 3, "cleared fingerprint runs without the gate");
445
+ } finally {
446
+ await h.cleanup();
447
+ }
448
+ });
449
+
450
+ test("recovery: a successful FORCED flush on a fresh coordinator clears a persisted backoff entry (cursor/codex review)", async () => {
451
+ // A forced flush bypasses the retry gate and never hydrates the in-memory
452
+ // mirror, so the success-heal path must still clear the persisted meta entry
453
+ // — otherwise a stale backoff survives a successful forced flush and keeps
454
+ // blocking normal extraction until the timer expires.
455
+ const h = await makeHarness({
456
+ extractionRetryScheduleMs: [3_600_000],
457
+ extractionBreakerFailureThreshold: 100,
458
+ });
459
+ try {
460
+ const coordA = h.newCoordinator();
461
+ h.setRespond(() => failureResult("provider_retryable"));
462
+ await h.run(coordA, "fp-heal"); // fail → parked, persisted to meta.json
463
+ const storage = await h.storageForNs("default");
464
+ let meta = await storage.loadMeta();
465
+ assert.equal((meta.extractionRetryState ?? []).length, 1, "failure persisted");
466
+
467
+ // Fresh coordinator (mirror empty, namespace not hydrated) — forced success.
468
+ const coordB = h.newCoordinator();
469
+ h.setRespond(() => successResult());
470
+ await h.run(coordB, "fp-heal", { force: true });
471
+
472
+ meta = await storage.loadMeta();
473
+ assert.equal(
474
+ (meta.extractionRetryState ?? []).length,
475
+ 0,
476
+ "successful forced flush must clear the persisted backoff entry",
477
+ );
478
+ } finally {
479
+ await h.cleanup();
480
+ }
481
+ });
482
+
483
+ // ---------------------------------------------------------------------------
484
+ // Class-specific caps
485
+ // ---------------------------------------------------------------------------
486
+
487
+ test("parse_empty: long-parks a fingerprint after extractionParseEmptyMaxAttempts", async () => {
488
+ const h = await makeHarness({
489
+ extractionRetryScheduleMs: [1000],
490
+ extractionRetryMaxBackoffMs: 9_000_000,
491
+ extractionParseEmptyMaxAttempts: 2,
492
+ extractionBreakerFailureThreshold: 100,
493
+ extractionRetryJitterRatio: 0,
494
+ });
495
+ try {
496
+ const coord = h.newCoordinator();
497
+ h.setRespond(() => failureResult("parse_empty"));
498
+ // Force each attempt so the gate does not suppress; drive past the cap.
499
+ await h.run(coord, "fp-1", { force: true });
500
+ await h.run(coord, "fp-1", { force: true });
501
+ await h.run(coord, "fp-1", { force: true }); // attempt 3 > cap of 2 → long-park
502
+
503
+ const storage = await h.storageForNs("default");
504
+ const meta = await storage.loadMeta();
505
+ const entry = (meta.extractionRetryState ?? [])[0];
506
+ assert.ok(entry, "retry state entry exists");
507
+ assert.equal(entry.attempts, 3);
508
+ assert.equal(entry.lastFailureClass, "parse_empty");
509
+ const parkMs = Date.parse(entry.nextEligibleAt) - Date.now();
510
+ assert.ok(parkMs > 8_000_000, `long-parked to ~maxBackoff (got ${parkMs}ms)`);
511
+ assert.equal(h.recordedProcessedCount(), 0, "parse_empty fingerprint is never marked processed");
512
+ } finally {
513
+ await h.cleanup();
514
+ }
515
+ });
516
+
517
+ // ---------------------------------------------------------------------------
518
+ // Invariant guard (regression): never mark processed on failure
519
+ // ---------------------------------------------------------------------------
520
+
521
+ test("invariant: a failing extraction never records a processed fingerprint", async () => {
522
+ const h = await makeHarness({ extractionBreakerFailureThreshold: 100 });
523
+ try {
524
+ const coord = h.newCoordinator();
525
+ for (const cls of ["provider_retryable", "parse_empty", "auth_config"] as ExtractionFailureClass[]) {
526
+ h.setRespond(() => failureResult(cls));
527
+ await h.run(coord, `fp-${cls}`, { force: true });
528
+ }
529
+ assert.equal(h.recordedProcessedCount(), 0, "no processed fingerprint recorded across all failure classes");
530
+ } finally {
531
+ await h.cleanup();
532
+ }
533
+ });
534
+
535
+ // ---------------------------------------------------------------------------
536
+ // Config parity: extractionRetryEnabled=false restores pre-change behavior
537
+ // ---------------------------------------------------------------------------
538
+
539
+ test("extractionRetryEnabled=false: extractor is called on every observe with no gate", async () => {
540
+ const h = await makeHarness({
541
+ extractionRetryEnabled: false,
542
+ extractionBreakerFailureThreshold: 1,
543
+ });
544
+ try {
545
+ const coord = h.newCoordinator();
546
+ h.setRespond(() => failureResult("provider_retryable"));
547
+ await h.run(coord, "fp-1");
548
+ await h.run(coord, "fp-1");
549
+ await h.run(coord, "fp-1");
550
+ assert.equal(h.engineCalls(), 3, "no backoff/breaker gate when the feature is disabled");
551
+ assert.equal(coord.getExtractionResilienceStatus().breaker.state, "closed");
552
+ const storage = await h.storageForNs("default");
553
+ const meta = await storage.loadMeta();
554
+ assert.equal((meta.extractionRetryState ?? []).length, 0, "no retry state persisted when disabled");
555
+ } finally {
556
+ await h.cleanup();
557
+ }
558
+ });
559
+
560
+ // ---------------------------------------------------------------------------
561
+ // Cache coherence across instances
562
+ // ---------------------------------------------------------------------------
563
+
564
+ test("cache coherence: a failure recorded by one coordinator gates a second over the same meta", async () => {
565
+ const h = await makeHarness({
566
+ extractionRetryScheduleMs: [3_600_000],
567
+ extractionBreakerFailureThreshold: 100,
568
+ });
569
+ try {
570
+ const coordA = h.newCoordinator();
571
+ h.setRespond(() => failureResult("provider_retryable"));
572
+ await h.run(coordA, "shared-fp");
573
+ assert.equal(h.engineCalls(), 1);
574
+
575
+ // A second coordinator over the same memory dir hydrates from meta.json and
576
+ // honors the persisted backoff without calling extract().
577
+ const coordB = h.newCoordinator();
578
+ const r = await h.run(coordB, "shared-fp");
579
+ assert.equal(r.reason, "extraction_backoff");
580
+ assert.equal(h.engineCalls(), 1, "coordinator B honors coordinator A's persisted failure");
581
+ } finally {
582
+ await h.cleanup();
583
+ }
584
+ });
585
+
586
+ // ---------------------------------------------------------------------------
587
+ // Namespace isolation vs. provider-global breaker
588
+ // ---------------------------------------------------------------------------
589
+
590
+ test("namespace isolation: per-fingerprint backoff is namespace-scoped; breaker is provider-global", async () => {
591
+ const h = await makeHarness({
592
+ extractionRetryScheduleMs: [3_600_000],
593
+ extractionBreakerFailureThreshold: 100, // keep the breaker closed for the per-fp assertion
594
+ });
595
+ try {
596
+ const coord = h.newCoordinator();
597
+ h.setRespond(() => failureResult("provider_retryable"));
598
+ // Same fingerprint content, routed to namespace A → parked in A only.
599
+ await h.run(coord, "same-content", { writeNamespaceOverride: "nsA" });
600
+ assert.equal(h.engineCalls(), 1);
601
+ const rA = await h.run(coord, "same-content", { writeNamespaceOverride: "nsA" });
602
+ assert.equal(rA.reason, "extraction_backoff", "namespace A fingerprint is parked");
603
+ assert.equal(h.engineCalls(), 1);
604
+
605
+ // The identical content routed to namespace B is NOT gated by A's backoff.
606
+ const rB = await h.run(coord, "same-content", { writeNamespaceOverride: "nsB" });
607
+ assert.equal(h.engineCalls(), 2, "namespace B is not gated by namespace A's per-fingerprint backoff");
608
+ assert.equal(rB.status, "skipped");
609
+
610
+ // Meta isolation: each namespace persists only its own retry state.
611
+ const metaA = await (await h.storageForNs("nsA")).loadMeta();
612
+ const metaB = await (await h.storageForNs("nsB")).loadMeta();
613
+ assert.equal((metaA.extractionRetryState ?? []).length, 1);
614
+ assert.equal((metaB.extractionRetryState ?? []).length, 1);
615
+ } finally {
616
+ await h.cleanup();
617
+ }
618
+ });
619
+
620
+ test("namespace isolation: the provider breaker suppresses extraction across namespaces", async () => {
621
+ const h = await makeHarness({
622
+ extractionRetryScheduleMs: [3_600_000],
623
+ extractionBreakerFailureThreshold: 1,
624
+ extractionBreakerCooldownMs: 3_600_000,
625
+ });
626
+ try {
627
+ const coord = h.newCoordinator();
628
+ h.setRespond(() => failureResult("provider_retryable"));
629
+ await h.run(coord, "fp-a", { writeNamespaceOverride: "nsA" });
630
+ assert.equal(coord.getExtractionResilienceStatus().breaker.state, "open");
631
+
632
+ // A different namespace is still suppressed — the breaker is provider-global.
633
+ const r = await h.run(coord, "fp-b", { writeNamespaceOverride: "nsB" });
634
+ assert.equal(r.reason, "provider_circuit_open");
635
+ assert.equal(h.engineCalls(), 1, "provider-global breaker suppresses namespace B too");
636
+ } finally {
637
+ await h.cleanup();
638
+ }
639
+ });