@remnic/core 9.69.85 → 9.69.87

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 (336) hide show
  1. package/dist/access-admin-ops-surface.js +45 -44
  2. package/dist/access-authorization-probe.js +46 -45
  3. package/dist/access-boundary.js +45 -44
  4. package/dist/access-cli.js +111 -110
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.d.ts.map +1 -1
  7. package/dist/access-extraction-force-flush.js +45 -44
  8. package/dist/access-http-briefing.js +46 -45
  9. package/dist/access-http-query.js +47 -46
  10. package/dist/access-http.js +67 -66
  11. package/dist/access-identity-continuity-surface.js +45 -44
  12. package/dist/access-lcm-surface.js +45 -44
  13. package/dist/access-mcp.d.ts.map +1 -1
  14. package/dist/access-mcp.js +59 -58
  15. package/dist/access-namespace-preflight.js +45 -44
  16. package/dist/access-observe-write-category.js +4 -4
  17. package/dist/access-observe-write-surface.js +45 -44
  18. package/dist/access-operations-batch.js +49 -48
  19. package/dist/access-operations.js +56 -55
  20. package/dist/access-recall-concurrency.js +45 -44
  21. package/dist/access-recall-diagnostics.js +4 -4
  22. package/dist/access-recall-response.js +45 -44
  23. package/dist/access-recall-surface.js +45 -44
  24. package/dist/access-schema.js +3 -3
  25. package/dist/access-service-helpers.d.ts +1 -0
  26. package/dist/access-service-helpers.d.ts.map +1 -1
  27. package/dist/access-service-helpers.js +1 -1
  28. package/dist/access-service.js +45 -44
  29. package/dist/access-surface-catalog.js +47 -46
  30. package/dist/access-surface-catalog.js.map +1 -1
  31. package/dist/active-recall.js +13 -12
  32. package/dist/active-recall.js.map +1 -1
  33. package/dist/{auto-sync-WEUDMXUW.js → auto-sync-6IPX2BAC.js} +5 -5
  34. package/dist/background-generation.js +2 -2
  35. package/dist/briefing.js +7 -7
  36. package/dist/buffer.js +4 -3
  37. package/dist/calibration.js +2 -2
  38. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  39. package/dist/causal-behavior.js +2 -2
  40. package/dist/causal-chain.js +2 -2
  41. package/dist/causal-consolidation.js +10 -10
  42. package/dist/causal-retrieval.js +2 -2
  43. package/dist/causal-trajectory-graph.js +3 -3
  44. package/dist/causal-trajectory.js +1 -1
  45. package/dist/{chunk-J3IWHCMF.js → chunk-2DT7PIDZ.js} +3 -3
  46. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  47. package/dist/{chunk-MFZACYZ6.js → chunk-37S3IMYB.js} +2 -2
  48. package/dist/{chunk-IBGEHKQ6.js → chunk-3C3J243X.js} +2 -2
  49. package/dist/{chunk-55FCXCXJ.js → chunk-3GQC3NMB.js} +4 -4
  50. package/dist/{chunk-QOPB6KE3.js → chunk-3TB2XQ3C.js} +2 -2
  51. package/dist/{chunk-3XQYM4RE.js → chunk-4BZP2B4U.js} +2 -2
  52. package/dist/{chunk-FYAU4S6E.js → chunk-4CDYONWO.js} +3 -3
  53. package/dist/{chunk-CRKWSSS4.js → chunk-4IEOGIPB.js} +2 -2
  54. package/dist/{chunk-W75BBBSY.js → chunk-4IIMMZZK.js} +11 -11
  55. package/dist/{chunk-Q3THVX75.js → chunk-4RL3JOB3.js} +2 -2
  56. package/dist/{chunk-7G6WUOLB.js → chunk-66DKTOBY.js} +2 -2
  57. package/dist/{chunk-IP5VLSLO.js → chunk-6L3PBSAR.js} +2 -2
  58. package/dist/{chunk-AZ2X5LNA.js → chunk-6LYAEM54.js} +2 -2
  59. package/dist/{chunk-RCQKAOOB.js → chunk-6NQ4LSMS.js} +40 -40
  60. package/dist/{chunk-WB2I2EQK.js → chunk-6TT6HRE4.js} +2 -2
  61. package/dist/{chunk-JX6USXZR.js → chunk-72JNNJJJ.js} +2 -2
  62. package/dist/{chunk-BUKK5SWA.js → chunk-7LCGFUMB.js} +4 -4
  63. package/dist/{chunk-OEBLZZKK.js → chunk-7XG7KMIW.js} +2 -2
  64. package/dist/{chunk-3GN2PEOT.js → chunk-AQK54Q7O.js} +2 -2
  65. package/dist/{chunk-3SG4FGDV.js → chunk-AWD36R4D.js} +5 -2
  66. package/dist/chunk-AWD36R4D.js.map +1 -0
  67. package/dist/{chunk-BOUFVFV2.js → chunk-AZSGSRIM.js} +2 -2
  68. package/dist/{chunk-N3MNDVJO.js → chunk-BFO3IR56.js} +2 -2
  69. package/dist/{chunk-2UPTRDGM.js → chunk-BJRAELDH.js} +3 -3
  70. package/dist/{chunk-IMAP236M.js → chunk-CS52MFDV.js} +2 -2
  71. package/dist/{chunk-UNJOB4DY.js → chunk-CUCWX3MO.js} +2 -2
  72. package/dist/{chunk-QB75LKQQ.js → chunk-CWHV574S.js} +32 -13
  73. package/dist/chunk-CWHV574S.js.map +1 -0
  74. package/dist/{chunk-DQISIT63.js → chunk-DZ42MDZE.js} +3 -3
  75. package/dist/{chunk-ML2MVAEZ.js → chunk-DZ525SNQ.js} +1 -1
  76. package/dist/{chunk-5ELENZTA.js → chunk-EDIH6X2P.js} +2 -2
  77. package/dist/{chunk-S42HZILI.js → chunk-EHS5MJEF.js} +2 -2
  78. package/dist/{chunk-IA3X2Y4K.js → chunk-EQ6YOIKO.js} +2 -2
  79. package/dist/{chunk-4V44AA7O.js → chunk-FAN73COB.js} +2 -2
  80. package/dist/{chunk-KERNN6BC.js → chunk-FB64XE3R.js} +2 -2
  81. package/dist/{chunk-SVZWVZ7E.js → chunk-FOH7AMMI.js} +72 -55
  82. package/dist/chunk-FOH7AMMI.js.map +1 -0
  83. package/dist/{chunk-S55WTPFJ.js → chunk-FRCESVV6.js} +7 -7
  84. package/dist/{chunk-GEX5NI36.js → chunk-HBBHL4YF.js} +2 -2
  85. package/dist/{chunk-2I6VHX6Q.js → chunk-HESDNYRB.js} +2 -2
  86. package/dist/{chunk-2U3R3DDH.js → chunk-ICZQG4XE.js} +9 -9
  87. package/dist/{chunk-DGAWEJVI.js → chunk-JGSQ6P4D.js} +2 -2
  88. package/dist/{chunk-TIQZNDZ5.js → chunk-JKMHZF3Q.js} +2 -2
  89. package/dist/{chunk-NAFPHFDM.js → chunk-JNLEJE4A.js} +2 -2
  90. package/dist/{chunk-IFCKHPY4.js → chunk-K4J4E7WV.js} +2 -2
  91. package/dist/{chunk-2QG4SLZI.js → chunk-KF7PQ7G2.js} +2 -2
  92. package/dist/{chunk-JXMECXMF.js → chunk-KFCMMDHH.js} +2 -2
  93. package/dist/{chunk-SQDKX4RW.js → chunk-KP7SANCF.js} +2 -2
  94. package/dist/{chunk-OM5BIPBR.js → chunk-L6UZEAID.js} +2 -2
  95. package/dist/{chunk-TEV2JGU5.js → chunk-LF5IGOOD.js} +15 -15
  96. package/dist/{chunk-QWUJWBZ5.js → chunk-LQG5O5EP.js} +2 -2
  97. package/dist/{chunk-NKFLHW7J.js → chunk-LS3XH3UE.js} +2 -2
  98. package/dist/{chunk-BJX3VQWZ.js → chunk-M536WNTQ.js} +2 -2
  99. package/dist/{chunk-EU3N5MK3.js → chunk-MQJL6CDZ.js} +3 -3
  100. package/dist/{chunk-VZBW6CDG.js → chunk-N4S4NUX5.js} +1 -1
  101. package/dist/{chunk-L2LBMKSI.js → chunk-NE6PHPLG.js} +10 -10
  102. package/dist/{chunk-VTPASHTO.js → chunk-NE7I2RCT.js} +2 -2
  103. package/dist/{chunk-OAOBL6OY.js → chunk-NR2ASHBL.js} +5 -5
  104. package/dist/{chunk-7UUKRS3J.js → chunk-P2WZHLUF.js} +3 -3
  105. package/dist/{chunk-IVH7INFM.js → chunk-P6WFK4ZJ.js} +6 -6
  106. package/dist/{chunk-CHDGJJV6.js → chunk-PUKTBULQ.js} +2 -2
  107. package/dist/{chunk-NAYBHNGN.js → chunk-QEV26L2F.js} +2 -2
  108. package/dist/{chunk-I4G4UWQA.js → chunk-QI2E6ISQ.js} +1 -1
  109. package/dist/{chunk-CK5U7JGH.js → chunk-QVS22WTO.js} +2 -2
  110. package/dist/{chunk-B47OHVFT.js → chunk-QWW2JDDX.js} +7 -7
  111. package/dist/{chunk-EJPF6XDQ.js → chunk-RDJ6C7NZ.js} +3 -3
  112. package/dist/{chunk-C2WWQ4OR.js → chunk-RMMA44HE.js} +1 -1
  113. package/dist/chunk-RMMA44HE.js.map +1 -0
  114. package/dist/{chunk-I53NAHZ4.js → chunk-S7GCT7NU.js} +3 -2
  115. package/dist/chunk-S7GCT7NU.js.map +1 -0
  116. package/dist/{chunk-RDEUG2M2.js → chunk-SOZUJCME.js} +6 -6
  117. package/dist/{chunk-T4QGUXCV.js → chunk-SPSRGEDQ.js} +2 -2
  118. package/dist/{chunk-TY446V34.js → chunk-SRZW7FNU.js} +2 -2
  119. package/dist/{chunk-SV72F4DS.js → chunk-SY5KDWGU.js} +2 -2
  120. package/dist/{chunk-H36X4NRH.js → chunk-TDYAIXHK.js} +5 -5
  121. package/dist/{chunk-ZTHGFTMW.js → chunk-TKC5LZ43.js} +2 -2
  122. package/dist/{chunk-6DTJBTHL.js → chunk-TM3RXL4G.js} +14 -2
  123. package/dist/chunk-TM3RXL4G.js.map +1 -0
  124. package/dist/{chunk-AXDOF4VI.js → chunk-UA5DX5C3.js} +2 -2
  125. package/dist/{chunk-JS3S2HVG.js → chunk-V2ES4WCZ.js} +2 -2
  126. package/dist/{chunk-5FNZ3G6X.js → chunk-VKQI47DJ.js} +4 -4
  127. package/dist/{chunk-JVY556FW.js → chunk-VLLD4TOB.js} +2 -2
  128. package/dist/{chunk-NMBX4D7P.js → chunk-VQNONC4E.js} +2 -2
  129. package/dist/{chunk-GZAGLCQH.js → chunk-VVDWZRZG.js} +2 -2
  130. package/dist/{chunk-T4BE5KJM.js → chunk-VX6GCKBS.js} +3 -3
  131. package/dist/{chunk-RIEX7NOZ.js → chunk-W4AOR52H.js} +2 -2
  132. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  133. package/dist/{chunk-BNPLDVM6.js → chunk-XTYX6V52.js} +2 -2
  134. package/dist/{chunk-FZVROZKC.js → chunk-XVZA6VXC.js} +4 -4
  135. package/dist/{chunk-RGMVMVMF.js → chunk-YBA52OKG.js} +2 -1
  136. package/dist/chunk-YBA52OKG.js.map +1 -0
  137. package/dist/{chunk-C7JAUNFC.js → chunk-YITN5WXI.js} +2 -2
  138. package/dist/{chunk-HS75MCQF.js → chunk-YRULQW5J.js} +2 -2
  139. package/dist/{chunk-DINRT75P.js → chunk-YSBL7VPO.js} +2 -2
  140. package/dist/{chunk-7QK5TUTW.js → chunk-YSJTCOHR.js} +2 -2
  141. package/dist/{chunk-QC56VZXC.js → chunk-YTQGGO3K.js} +67 -67
  142. package/dist/{chunk-HKFJ3PF3.js → chunk-ZI7QA4MK.js} +2 -2
  143. package/dist/chunk-ZJP66Q4I.js +48 -0
  144. package/dist/chunk-ZJP66Q4I.js.map +1 -0
  145. package/dist/cli.js +86 -85
  146. package/dist/coding/export-okf-codegraph.js +6 -6
  147. package/dist/coding/pre-action-gate.js +2 -2
  148. package/dist/compounding/engine.js +7 -7
  149. package/dist/config.js +13 -12
  150. package/dist/connectors/codex-materialize-runner.js +7 -7
  151. package/dist/connectors/index.js +7 -7
  152. package/dist/contradiction/index.js +3 -3
  153. package/dist/{contradiction-scan-5EJYRKD3.js → contradiction-scan-TAGMOUQ2.js} +3 -3
  154. package/dist/convergence-refresh.js +9 -9
  155. package/dist/corpus-watermark.js +9 -9
  156. package/dist/correction/correction-access-wiring.d.ts.map +1 -1
  157. package/dist/deep-recall-http-glue.js +4 -4
  158. package/dist/deep-recall-policy-llm.js +3 -3
  159. package/dist/deep-recall.js +2 -2
  160. package/dist/entity-retrieval.js +7 -7
  161. package/dist/external-wiki-access.js +46 -45
  162. package/dist/extraction-faithfulness.js +3 -3
  163. package/dist/extraction-judge.js +3 -3
  164. package/dist/extraction-liveness.d.ts +9 -0
  165. package/dist/extraction-liveness.d.ts.map +1 -1
  166. package/dist/extraction-liveness.js +2 -1
  167. package/dist/extraction-llm-health.d.ts +32 -0
  168. package/dist/extraction-llm-health.d.ts.map +1 -0
  169. package/dist/extraction-llm-health.js +17 -0
  170. package/dist/extraction-span-config.js +3 -2
  171. package/dist/extraction-span-materialize.js +2 -2
  172. package/dist/extraction.js +16 -15
  173. package/dist/fallback-llm.js +2 -2
  174. package/dist/{first-start-migration-GAT7G733.js → first-start-migration-7OATHJKB.js} +4 -4
  175. package/dist/{graph-edge-decay-PVKDKLQK.js → graph-edge-decay-KQGS3L3B.js} +6 -6
  176. package/dist/graph-jsonl.js +4 -4
  177. package/dist/graph-snapshot.js +4 -4
  178. package/dist/graph.js +5 -5
  179. package/dist/harmonic-retrieval.js +1 -1
  180. package/dist/index.js +136 -135
  181. package/dist/json-extract.d.ts.map +1 -1
  182. package/dist/json-extract.js +1 -1
  183. package/dist/lcm/engine.js +2 -2
  184. package/dist/lcm/index.js +5 -5
  185. package/dist/local-llm.d.ts.map +1 -1
  186. package/dist/local-llm.js +1 -1
  187. package/dist/location/index.js +9 -9
  188. package/dist/maintenance/memory-governance.js +7 -7
  189. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  190. package/dist/maintenance/rebuild-memory-projection.js +8 -8
  191. package/dist/memory-browse-operations.js +46 -45
  192. package/dist/memory-browse.js +3 -3
  193. package/dist/namespaces/migrate.js +8 -8
  194. package/dist/namespaces/storage.js +7 -7
  195. package/dist/operator-doctor-corpus.js +10 -10
  196. package/dist/operator-toolkit.js +25 -24
  197. package/dist/orchestration/extraction-run.d.ts.map +1 -1
  198. package/dist/orchestration/maintenance.js +17 -17
  199. package/dist/orchestrator.js +111 -110
  200. package/dist/recall-navigation-operations.js +46 -45
  201. package/dist/recall-navigation.js +45 -44
  202. package/dist/recall-planner-llm.js +2 -2
  203. package/dist/recall-state-view-admission.js +3 -3
  204. package/dist/recall-why-service.js +6 -6
  205. package/dist/recall-why.js +3 -3
  206. package/dist/{resolution-UYJU5JUA.js → resolution-DNX7FO7A.js} +4 -4
  207. package/dist/resolution-DNX7FO7A.js.map +1 -0
  208. package/dist/resume-bundles.js +14 -13
  209. package/dist/semantic-consolidation.js +8 -8
  210. package/dist/semantic-rule-promotion.js +7 -7
  211. package/dist/semantic-rule-verifier.js +7 -7
  212. package/dist/source-agent-qualifier.js +11 -10
  213. package/dist/standup/build.js +7 -7
  214. package/dist/storage.js +6 -6
  215. package/dist/summarizer.js +5 -5
  216. package/dist/support-passport/index.js +48 -47
  217. package/dist/transfer/backup.js +2 -2
  218. package/dist/transfer/capsule-export.js +2 -2
  219. package/dist/transfer/capsule-import.js +2 -2
  220. package/dist/transfer/export-okf.js +7 -7
  221. package/dist/verified-recall.js +7 -7
  222. package/dist/who-knows-cli.js +8 -8
  223. package/dist/who-knows.js +7 -7
  224. package/package.json +2 -2
  225. package/src/access-extraction-force-flush.ts +15 -7
  226. package/src/access-mcp.ts +90 -28
  227. package/src/access-service-health.test.ts +41 -5
  228. package/src/access-service-helpers.ts +1 -0
  229. package/src/access-service-observe-lcm-parity.test.ts +56 -24
  230. package/src/correction/correction-access-wiring.ts +36 -8
  231. package/src/correction/correction-classify-parse.test.ts +178 -0
  232. package/src/extraction-liveness.ts +13 -1
  233. package/src/extraction-llm-health.test.ts +103 -0
  234. package/src/extraction-llm-health.ts +71 -0
  235. package/src/json-extract.ts +6 -0
  236. package/src/local-llm-thinking.test.ts +12 -0
  237. package/src/local-llm.ts +1 -0
  238. package/src/orchestration/extraction-run.test.ts +55 -1
  239. package/src/orchestration/extraction-run.ts +13 -0
  240. package/dist/chunk-3SG4FGDV.js.map +0 -1
  241. package/dist/chunk-6DTJBTHL.js.map +0 -1
  242. package/dist/chunk-C2WWQ4OR.js.map +0 -1
  243. package/dist/chunk-I53NAHZ4.js.map +0 -1
  244. package/dist/chunk-QB75LKQQ.js.map +0 -1
  245. package/dist/chunk-RGMVMVMF.js.map +0 -1
  246. package/dist/chunk-SVZWVZ7E.js.map +0 -1
  247. /package/dist/{auto-sync-WEUDMXUW.js.map → auto-sync-6IPX2BAC.js.map} +0 -0
  248. /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  249. /package/dist/{chunk-J3IWHCMF.js.map → chunk-2DT7PIDZ.js.map} +0 -0
  250. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  251. /package/dist/{chunk-MFZACYZ6.js.map → chunk-37S3IMYB.js.map} +0 -0
  252. /package/dist/{chunk-IBGEHKQ6.js.map → chunk-3C3J243X.js.map} +0 -0
  253. /package/dist/{chunk-55FCXCXJ.js.map → chunk-3GQC3NMB.js.map} +0 -0
  254. /package/dist/{chunk-QOPB6KE3.js.map → chunk-3TB2XQ3C.js.map} +0 -0
  255. /package/dist/{chunk-3XQYM4RE.js.map → chunk-4BZP2B4U.js.map} +0 -0
  256. /package/dist/{chunk-FYAU4S6E.js.map → chunk-4CDYONWO.js.map} +0 -0
  257. /package/dist/{chunk-CRKWSSS4.js.map → chunk-4IEOGIPB.js.map} +0 -0
  258. /package/dist/{chunk-W75BBBSY.js.map → chunk-4IIMMZZK.js.map} +0 -0
  259. /package/dist/{chunk-Q3THVX75.js.map → chunk-4RL3JOB3.js.map} +0 -0
  260. /package/dist/{chunk-7G6WUOLB.js.map → chunk-66DKTOBY.js.map} +0 -0
  261. /package/dist/{chunk-IP5VLSLO.js.map → chunk-6L3PBSAR.js.map} +0 -0
  262. /package/dist/{chunk-AZ2X5LNA.js.map → chunk-6LYAEM54.js.map} +0 -0
  263. /package/dist/{chunk-RCQKAOOB.js.map → chunk-6NQ4LSMS.js.map} +0 -0
  264. /package/dist/{chunk-WB2I2EQK.js.map → chunk-6TT6HRE4.js.map} +0 -0
  265. /package/dist/{chunk-JX6USXZR.js.map → chunk-72JNNJJJ.js.map} +0 -0
  266. /package/dist/{chunk-BUKK5SWA.js.map → chunk-7LCGFUMB.js.map} +0 -0
  267. /package/dist/{chunk-OEBLZZKK.js.map → chunk-7XG7KMIW.js.map} +0 -0
  268. /package/dist/{chunk-3GN2PEOT.js.map → chunk-AQK54Q7O.js.map} +0 -0
  269. /package/dist/{chunk-BOUFVFV2.js.map → chunk-AZSGSRIM.js.map} +0 -0
  270. /package/dist/{chunk-N3MNDVJO.js.map → chunk-BFO3IR56.js.map} +0 -0
  271. /package/dist/{chunk-2UPTRDGM.js.map → chunk-BJRAELDH.js.map} +0 -0
  272. /package/dist/{chunk-IMAP236M.js.map → chunk-CS52MFDV.js.map} +0 -0
  273. /package/dist/{chunk-UNJOB4DY.js.map → chunk-CUCWX3MO.js.map} +0 -0
  274. /package/dist/{chunk-DQISIT63.js.map → chunk-DZ42MDZE.js.map} +0 -0
  275. /package/dist/{chunk-ML2MVAEZ.js.map → chunk-DZ525SNQ.js.map} +0 -0
  276. /package/dist/{chunk-5ELENZTA.js.map → chunk-EDIH6X2P.js.map} +0 -0
  277. /package/dist/{chunk-S42HZILI.js.map → chunk-EHS5MJEF.js.map} +0 -0
  278. /package/dist/{chunk-IA3X2Y4K.js.map → chunk-EQ6YOIKO.js.map} +0 -0
  279. /package/dist/{chunk-4V44AA7O.js.map → chunk-FAN73COB.js.map} +0 -0
  280. /package/dist/{chunk-KERNN6BC.js.map → chunk-FB64XE3R.js.map} +0 -0
  281. /package/dist/{chunk-S55WTPFJ.js.map → chunk-FRCESVV6.js.map} +0 -0
  282. /package/dist/{chunk-GEX5NI36.js.map → chunk-HBBHL4YF.js.map} +0 -0
  283. /package/dist/{chunk-2I6VHX6Q.js.map → chunk-HESDNYRB.js.map} +0 -0
  284. /package/dist/{chunk-2U3R3DDH.js.map → chunk-ICZQG4XE.js.map} +0 -0
  285. /package/dist/{chunk-DGAWEJVI.js.map → chunk-JGSQ6P4D.js.map} +0 -0
  286. /package/dist/{chunk-TIQZNDZ5.js.map → chunk-JKMHZF3Q.js.map} +0 -0
  287. /package/dist/{chunk-NAFPHFDM.js.map → chunk-JNLEJE4A.js.map} +0 -0
  288. /package/dist/{chunk-IFCKHPY4.js.map → chunk-K4J4E7WV.js.map} +0 -0
  289. /package/dist/{chunk-2QG4SLZI.js.map → chunk-KF7PQ7G2.js.map} +0 -0
  290. /package/dist/{chunk-JXMECXMF.js.map → chunk-KFCMMDHH.js.map} +0 -0
  291. /package/dist/{chunk-SQDKX4RW.js.map → chunk-KP7SANCF.js.map} +0 -0
  292. /package/dist/{chunk-OM5BIPBR.js.map → chunk-L6UZEAID.js.map} +0 -0
  293. /package/dist/{chunk-TEV2JGU5.js.map → chunk-LF5IGOOD.js.map} +0 -0
  294. /package/dist/{chunk-QWUJWBZ5.js.map → chunk-LQG5O5EP.js.map} +0 -0
  295. /package/dist/{chunk-NKFLHW7J.js.map → chunk-LS3XH3UE.js.map} +0 -0
  296. /package/dist/{chunk-BJX3VQWZ.js.map → chunk-M536WNTQ.js.map} +0 -0
  297. /package/dist/{chunk-EU3N5MK3.js.map → chunk-MQJL6CDZ.js.map} +0 -0
  298. /package/dist/{chunk-VZBW6CDG.js.map → chunk-N4S4NUX5.js.map} +0 -0
  299. /package/dist/{chunk-L2LBMKSI.js.map → chunk-NE6PHPLG.js.map} +0 -0
  300. /package/dist/{chunk-VTPASHTO.js.map → chunk-NE7I2RCT.js.map} +0 -0
  301. /package/dist/{chunk-OAOBL6OY.js.map → chunk-NR2ASHBL.js.map} +0 -0
  302. /package/dist/{chunk-7UUKRS3J.js.map → chunk-P2WZHLUF.js.map} +0 -0
  303. /package/dist/{chunk-IVH7INFM.js.map → chunk-P6WFK4ZJ.js.map} +0 -0
  304. /package/dist/{chunk-CHDGJJV6.js.map → chunk-PUKTBULQ.js.map} +0 -0
  305. /package/dist/{chunk-NAYBHNGN.js.map → chunk-QEV26L2F.js.map} +0 -0
  306. /package/dist/{chunk-I4G4UWQA.js.map → chunk-QI2E6ISQ.js.map} +0 -0
  307. /package/dist/{chunk-CK5U7JGH.js.map → chunk-QVS22WTO.js.map} +0 -0
  308. /package/dist/{chunk-B47OHVFT.js.map → chunk-QWW2JDDX.js.map} +0 -0
  309. /package/dist/{chunk-EJPF6XDQ.js.map → chunk-RDJ6C7NZ.js.map} +0 -0
  310. /package/dist/{chunk-RDEUG2M2.js.map → chunk-SOZUJCME.js.map} +0 -0
  311. /package/dist/{chunk-T4QGUXCV.js.map → chunk-SPSRGEDQ.js.map} +0 -0
  312. /package/dist/{chunk-TY446V34.js.map → chunk-SRZW7FNU.js.map} +0 -0
  313. /package/dist/{chunk-SV72F4DS.js.map → chunk-SY5KDWGU.js.map} +0 -0
  314. /package/dist/{chunk-H36X4NRH.js.map → chunk-TDYAIXHK.js.map} +0 -0
  315. /package/dist/{chunk-ZTHGFTMW.js.map → chunk-TKC5LZ43.js.map} +0 -0
  316. /package/dist/{chunk-AXDOF4VI.js.map → chunk-UA5DX5C3.js.map} +0 -0
  317. /package/dist/{chunk-JS3S2HVG.js.map → chunk-V2ES4WCZ.js.map} +0 -0
  318. /package/dist/{chunk-5FNZ3G6X.js.map → chunk-VKQI47DJ.js.map} +0 -0
  319. /package/dist/{chunk-JVY556FW.js.map → chunk-VLLD4TOB.js.map} +0 -0
  320. /package/dist/{chunk-NMBX4D7P.js.map → chunk-VQNONC4E.js.map} +0 -0
  321. /package/dist/{chunk-GZAGLCQH.js.map → chunk-VVDWZRZG.js.map} +0 -0
  322. /package/dist/{chunk-T4BE5KJM.js.map → chunk-VX6GCKBS.js.map} +0 -0
  323. /package/dist/{chunk-RIEX7NOZ.js.map → chunk-W4AOR52H.js.map} +0 -0
  324. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  325. /package/dist/{chunk-BNPLDVM6.js.map → chunk-XTYX6V52.js.map} +0 -0
  326. /package/dist/{chunk-FZVROZKC.js.map → chunk-XVZA6VXC.js.map} +0 -0
  327. /package/dist/{chunk-C7JAUNFC.js.map → chunk-YITN5WXI.js.map} +0 -0
  328. /package/dist/{chunk-HS75MCQF.js.map → chunk-YRULQW5J.js.map} +0 -0
  329. /package/dist/{chunk-DINRT75P.js.map → chunk-YSBL7VPO.js.map} +0 -0
  330. /package/dist/{chunk-7QK5TUTW.js.map → chunk-YSJTCOHR.js.map} +0 -0
  331. /package/dist/{chunk-QC56VZXC.js.map → chunk-YTQGGO3K.js.map} +0 -0
  332. /package/dist/{chunk-HKFJ3PF3.js.map → chunk-ZI7QA4MK.js.map} +0 -0
  333. /package/dist/{contradiction-scan-5EJYRKD3.js.map → contradiction-scan-TAGMOUQ2.js.map} +0 -0
  334. /package/dist/{graph-edge-decay-PVKDKLQK.js.map → extraction-llm-health.js.map} +0 -0
  335. /package/dist/{first-start-migration-GAT7G733.js.map → first-start-migration-7OATHJKB.js.map} +0 -0
  336. /package/dist/{resolution-UYJU5JUA.js.map → graph-edge-decay-KQGS3L3B.js.map} +0 -0
@@ -433,14 +433,13 @@ test("#2128: successful LCM flush does not bind per-call project context", async
433
433
  test("#2128: extraction force-flush uses observe's scoped target even when LCM is disabled", async () => {
434
434
  const probe = makeParityProbe(withSelfPolicyPrefix("pi-geek"));
435
435
  const service = new EngramAccessService(probe.orch);
436
- const deadlineMs = Date.now() + 10_000;
437
436
  const abortController = new AbortController();
438
437
  probe.lcmEngine.enabled = false;
439
438
 
440
439
  const response = await service.extractionForceFlush({
441
440
  sessionKey: "pi-geek:force-flush",
442
441
  projectTag: "Acme/Webshop",
443
- deadlineMs,
442
+ deadlineMs: 10_000,
444
443
  abortSignal: abortController.signal,
445
444
  authenticatedPrincipal: "pi-geek",
446
445
  });
@@ -460,7 +459,11 @@ test("#2128: extraction force-flush uses observe's scoped target even when LCM i
460
459
  false,
461
460
  "force-flush must let the orchestrator discover all session buffer keys",
462
461
  );
463
- assert.equal(call.options.extractionDeadlineMs, deadlineMs);
462
+ // #3140: the duration budget arrives at the orchestrator as an absolute deadline.
463
+ assert.ok(
464
+ (call.options.extractionDeadlineMs ?? 0) >= Date.now() + 9_000,
465
+ "the orchestrator deadline is the duration converted at force-flush entry",
466
+ );
464
467
  assert.equal(call.options.failOnExtractionFailure, true);
465
468
  assert.equal(call.options.abortSignal, abortController.signal);
466
469
  assert.equal(call.options.writeNamespaceOverride, expectedNamespace);
@@ -573,7 +576,7 @@ test("#2128: pending extraction barriers stay isolated by authenticated principa
573
576
  sessionKey,
574
577
  namespace: "team",
575
578
  authenticatedPrincipal: "alice",
576
- deadlineMs: Date.now() + 500,
579
+ deadlineMs: 500,
577
580
  });
578
581
  await new Promise<void>((resolve) => setImmediate(resolve));
579
582
  pending.get("alice")?.();
@@ -583,7 +586,7 @@ test("#2128: pending extraction barriers stay isolated by authenticated principa
583
586
  sessionKey,
584
587
  namespace: "team",
585
588
  authenticatedPrincipal: "bob",
586
- deadlineMs: Date.now() + 500,
589
+ deadlineMs: 500,
587
590
  });
588
591
  pending.get("bob")?.();
589
592
  assert.equal((await bobFlush).flushed, true);
@@ -600,7 +603,7 @@ test("#2128: chained observes remain behind one force-flush barrier", async () =
600
603
  const sessionKey = "chained-observes";
601
604
 
602
605
  await service.observe(observeRequest({ sessionKey, skipExtraction: false }));
603
- const flushPromise = service.extractionForceFlush({ sessionKey, deadlineMs: Date.now() + 500 });
606
+ const flushPromise = service.extractionForceFlush({ sessionKey, deadlineMs: 500 });
604
607
  await new Promise<void>((resolve) => setImmediate(resolve));
605
608
  await service.observe(observeRequest({ sessionKey, skipExtraction: false }));
606
609
  assert.equal(resolvers.length, 2);
@@ -669,7 +672,7 @@ test("#2128: force-flush deadline cancels the pending observe extraction", async
669
672
  await assert.rejects(
670
673
  service.extractionForceFlush({
671
674
  sessionKey,
672
- deadlineMs: Date.now() + 25,
675
+ deadlineMs: 25,
673
676
  }),
674
677
  (error: unknown) =>
675
678
  error instanceof EngramAccessInputError &&
@@ -893,7 +896,7 @@ test("#2128: post-flush retained cleanup is best effort and receives lifecycle g
893
896
  },
894
897
  };
895
898
  const service = new EngramAccessService(probe.orch);
896
- const deadlineMs = Date.now() + 10_000;
899
+ const deadlineMs = 10_000;
897
900
 
898
901
  const response = await service.extractionForceFlush({
899
902
  sessionKey: "pi-geek:cleanup-best-effort",
@@ -903,11 +906,15 @@ test("#2128: post-flush retained cleanup is best effort and receives lifecycle g
903
906
  });
904
907
 
905
908
  assert.equal(response.flushed, true);
906
- assert.deepEqual(cleanupCalls, [{
907
- sessionKey: "pi-geek:cleanup-best-effort",
908
- ownerPrincipal: "pi-geek",
909
- options: { abortSignal: abortController.signal, deadlineMs },
910
- }]);
909
+ assert.equal(cleanupCalls.length, 1);
910
+ // #3140: cleanup receives the duration converted to an absolute deadline.
911
+ assert.equal(cleanupCalls[0]?.sessionKey, "pi-geek:cleanup-best-effort");
912
+ assert.equal(cleanupCalls[0]?.ownerPrincipal, "pi-geek");
913
+ assert.equal(cleanupCalls[0]?.options?.abortSignal, abortController.signal);
914
+ assert.ok(
915
+ (cleanupCalls[0]?.options?.deadlineMs ?? 0) >= Date.now() + 9_000,
916
+ "cleanup deadline is the duration converted at force-flush entry",
917
+ );
911
918
  });
912
919
 
913
920
  test("#2128: post-flush retained cleanup stops on abort or deadline", async (t) => {
@@ -982,7 +989,11 @@ test("#2128: post-flush retained cleanup stops on abort or deadline", async (t)
982
989
  /replay extraction deadline exceeded \(retained_turn_cleanup\)/,
983
990
  );
984
991
  });
985
- test("#2206: an expired force-flush deadline stops before scope resolution", async () => {
992
+ test("#3140: a duration deadline reaches scope resolution and aborts it when it elapses", async () => {
993
+ // Regression for issue #3140: `deadlineMs` is a BUDGET from now. The old
994
+ // absolute-epoch reading compared a relative value (e.g. 30000) against
995
+ // Date.now(), so a live-session force flush was rejected INSTANTLY with
996
+ // `replay extraction deadline exceeded (scope_resolution)` before any work.
986
997
  const probe = makeParityProbe({ namespacesEnabled: false } as Partial<PluginConfig>);
987
998
  const service = new EngramAccessService(probe.orch);
988
999
  let scopeResolutionStarted = false;
@@ -991,17 +1002,23 @@ test("#2206: an expired force-flush deadline stops before scope resolution", asy
991
1002
  };
992
1003
  internals.resolveMemoryScopePlan = async () => {
993
1004
  scopeResolutionStarted = true;
994
- throw new Error("scope resolution must not start");
1005
+ return new Promise<never>(() => {});
995
1006
  };
996
1007
 
1008
+ const started = Date.now();
997
1009
  await assert.rejects(
998
1010
  service.extractionForceFlush({
999
- sessionKey: "expired-scope-resolution",
1000
- deadlineMs: 1,
1011
+ sessionKey: "duration-deadline-scope-resolution",
1012
+ deadlineMs: 30,
1001
1013
  }),
1002
1014
  /scope_resolution/,
1003
1015
  );
1004
- assert.equal(scopeResolutionStarted, false);
1016
+ assert.equal(scopeResolutionStarted, true, "the budget lets scope resolution start");
1017
+ assert.ok(
1018
+ Date.now() - started >= 25,
1019
+ "the flush waits out the budget instead of failing instantly",
1020
+ );
1021
+ assert.equal(probe.extractionForceFlushCalls.length, 0);
1005
1022
  });
1006
1023
 
1007
1024
  test("#2206: abort during scope resolution cancels pre-resolution observe preparations", async () => {
@@ -1089,8 +1106,10 @@ test("#2206: scope-resolution deadline cancels preparations by raw projectTag an
1089
1106
  await observeScopeGate;
1090
1107
  return resolveMemoryScopePlan(request);
1091
1108
  }
1109
+ // Blocked scope work, as a live session's pending observe preparations
1110
+ // would hold it (#3140): the duration budget elapses while it hangs.
1092
1111
  flushScopeStarted = true;
1093
- throw new Error("scope resolution must not start");
1112
+ return new Promise<never>(() => {});
1094
1113
  };
1095
1114
 
1096
1115
  const observe = service.observe(observeRequest({
@@ -1104,12 +1123,12 @@ test("#2206: scope-resolution deadline cancels preparations by raw projectTag an
1104
1123
  service.extractionForceFlush({
1105
1124
  sessionKey: "pi-geek:raw-hint-deadline",
1106
1125
  authenticatedPrincipal: "pi-geek",
1107
- deadlineMs: Date.now() - 1,
1126
+ deadlineMs: 1,
1108
1127
  ...rawHintCase,
1109
1128
  }),
1110
1129
  /scope_resolution/,
1111
1130
  );
1112
- assert.equal(flushScopeStarted, false);
1131
+ assert.equal(flushScopeStarted, true);
1113
1132
  releaseObserveScope();
1114
1133
  const response = await observe;
1115
1134
  assert.equal(response.extractionQueued, false);
@@ -1132,12 +1151,19 @@ test("#2128: aborted or expired extraction force-flush never touches a buffer",
1132
1151
  }),
1133
1152
  /extraction force-flush aborted/,
1134
1153
  );
1154
+ // #3140: the expired-at-entry scenario is gone (durations always start in
1155
+ // the future); a TINY budget elapsing while scope work is blocked still
1156
+ // rejects with the same error and never reaches the buffer.
1157
+ const internals = service as unknown as {
1158
+ resolveMemoryScopePlan: (request: unknown) => Promise<never>;
1159
+ };
1160
+ internals.resolveMemoryScopePlan = async () => new Promise<never>(() => {});
1135
1161
  await assert.rejects(
1136
1162
  () =>
1137
1163
  service.extractionForceFlush({
1138
1164
  sessionKey: "pi-geek:expired-flush",
1139
1165
  authenticatedPrincipal: "pi-geek",
1140
- deadlineMs: Date.now() - 1,
1166
+ deadlineMs: 1,
1141
1167
  }),
1142
1168
  (error: unknown) =>
1143
1169
  error instanceof EngramAccessInputError &&
@@ -1167,13 +1193,19 @@ test("#2128: self-deps wires force-flush cancellation into the observe tracker",
1167
1193
  cancellations.push({ sessionKey, principal, namespace });
1168
1194
  };
1169
1195
 
1196
+ // #3140: a tiny budget elapsing while scope work is blocked still cancels
1197
+ // the pending observe preparations via the same deadline hook.
1198
+ const internals = service as unknown as {
1199
+ resolveMemoryScopePlan: (request: unknown) => Promise<never>;
1200
+ };
1201
+ internals.resolveMemoryScopePlan = async () => new Promise<never>(() => {});
1170
1202
  await assert.rejects(
1171
1203
  () =>
1172
1204
  service.extractionForceFlush({
1173
1205
  sessionKey: "pi-geek:cancel-hook",
1174
1206
  namespace: "pi-geek",
1175
1207
  authenticatedPrincipal: "pi-geek",
1176
- deadlineMs: Date.now() - 1,
1208
+ deadlineMs: 1,
1177
1209
  }),
1178
1210
  (error: unknown) =>
1179
1211
  error instanceof EngramAccessInputError &&
@@ -1198,7 +1230,7 @@ test("#2128: late extraction deadline is surfaced as an access input error", asy
1198
1230
  service.extractionForceFlush({
1199
1231
  sessionKey: "pi-geek:late-deadline",
1200
1232
  authenticatedPrincipal: "pi-geek",
1201
- deadlineMs: Date.now() + 10_000,
1233
+ deadlineMs: 10_000,
1202
1234
  }),
1203
1235
  (error: unknown) =>
1204
1236
  error instanceof EngramAccessInputError &&
@@ -30,6 +30,7 @@ import { writeFileAtomically } from "../maintenance/atomic-file.js";
30
30
  import type { Orchestrator } from "../orchestrator.js";
31
31
  import type { MemoryFile, MemoryStatus, PluginConfig } from "../types.js";
32
32
  import { stripAttributesSuffix } from "../structured-attributes.js";
33
+ import { extractJsonCandidates } from "../json-extract.js";
33
34
  import { supersessionKeysForFact } from "../temporal-supersession.js";
34
35
  import { computeContentHash } from "../content-hash.js";
35
36
  import { sanitizeMemoryContent } from "../sanitize.js";
@@ -1010,16 +1011,43 @@ function parseClassifyResponse(
1010
1011
  raw: string,
1011
1012
  candidates: PlannerCandidate[],
1012
1013
  ): LlmClassificationResult {
1013
- let parsed: unknown;
1014
- try {
1015
- parsed = JSON.parse(raw);
1016
- } catch {
1017
- return fallbackClassification(candidates, "LLM returned non-JSON response");
1014
+ // Correction plans can mutate memory: accept one schema-valid, verbatim
1015
+ // response, never an arbitrary example or a fence-rewritten patch payload.
1016
+ // The response envelope gates candidacy; actions need only ≥1 valid entry
1017
+ // (an empty list is fine, an all-malformed list is unusable). Malformed
1018
+ // siblings are dropped after selection, so one bad action cannot discard
1019
+ // the whole plan; distinct eligible responses stay ambiguous (fail closed).
1020
+ const responses = new Map<string, Record<string, unknown>>();
1021
+ let sawJson = false;
1022
+ for (const candidate of new Set([raw.trim(), ...extractJsonCandidates(raw)])) {
1023
+ if (!raw.includes(candidate)) continue;
1024
+ try {
1025
+ const parsed: unknown = JSON.parse(candidate);
1026
+ sawJson = true;
1027
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
1028
+ const value = parsed as Record<string, unknown>;
1029
+ if (!isClassification(value.classification) ||
1030
+ typeof value.confidence !== "number" || !Number.isFinite(value.confidence) ||
1031
+ value.confidence < 0 || value.confidence > 1 || !Array.isArray(value.actions)) continue;
1032
+ const hasUsable = value.actions.some((action) => {
1033
+ try {
1034
+ validateCorrectionAction(action);
1035
+ return true;
1036
+ } catch {
1037
+ return false;
1038
+ }
1039
+ });
1040
+ if (hasUsable || value.actions.length === 0) responses.set(JSON.stringify(value), value);
1041
+ } catch {
1042
+ // Keep scanning after malformed JSON, not after mere syntax success.
1043
+ }
1018
1044
  }
1019
- if (!parsed || typeof parsed !== "object") {
1020
- return fallbackClassification(candidates, "LLM returned non-object response");
1045
+ if (responses.size !== 1) {
1046
+ return fallbackClassification(candidates, responses.size > 1
1047
+ ? "LLM returned ambiguous correction responses"
1048
+ : sawJson ? "LLM returned invalid correction response" : "LLM returned non-JSON response");
1021
1049
  }
1022
- const obj = parsed as Record<string, unknown>;
1050
+ const obj = responses.values().next().value as Record<string, unknown>;
1023
1051
  const classification = isClassification(obj.classification) ? obj.classification : "outdated";
1024
1052
  const confidence = typeof obj.confidence === "number" ? Math.min(1, Math.max(0, obj.confidence)) : 0.5;
1025
1053
  const rawActions = Array.isArray(obj.actions) ? obj.actions : [];
@@ -0,0 +1,178 @@
1
+ /**
2
+ * parseClassifyResponse regression tests (issue #3134).
3
+ *
4
+ * Proves the correction planner's single LLM-response parse chokepoint:
5
+ * - fenced JSON responses (```json … ```) parse into real actions instead of
6
+ * degrading to the deterministic fallback (#3134, same class as #1514);
7
+ * - genuinely non-JSON responses still return the byte-identical
8
+ * deterministic fallback (rule 13);
9
+ * - bare JSON keeps parsing through the same candidate chain.
10
+ */
11
+ import assert from "node:assert/strict";
12
+ import { test } from "node:test";
13
+
14
+ import { parseClassifyResponse } from "./correction-access-wiring.js";
15
+ import type { PlannerCandidate } from "./correction-planner.js";
16
+
17
+ const CANDIDATES: PlannerCandidate[] = [
18
+ { memoryId: "m1", path: "facts/m1.md", content: "m1 content", excerpt: "m1 excerpt", score: 1 },
19
+ ];
20
+
21
+ const CLASSIFY_JSON = JSON.stringify({
22
+ classification: "wrong",
23
+ confidence: 0.9,
24
+ actions: [{ kind: "edit", memoryId: "m1", patch: "m1 corrected content" }],
25
+ relevance: [{ memoryId: "m1", why: "names the corrected fact" }],
26
+ });
27
+
28
+ test("#3134 fenced json classify response parses into actions instead of fallback", () => {
29
+ const result = parseClassifyResponse(["```json", CLASSIFY_JSON, "```"].join("\n"), CANDIDATES);
30
+ assert.equal(result.fallback, undefined, "a fenced response must not degrade to the fallback");
31
+ assert.equal(result.classification, "wrong");
32
+ assert.equal(result.confidence, 0.9);
33
+ assert.deepEqual(result.actions, [{ kind: "edit", memoryId: "m1", patch: "m1 corrected content" }]);
34
+ assert.deepEqual(result.relevance, [{ memoryId: "m1", why: "names the corrected fact" }]);
35
+ assert.deepEqual(result.warnings, []);
36
+ });
37
+
38
+ test("#3134 fenced response without a language tag parses", () => {
39
+ const result = parseClassifyResponse(["```", CLASSIFY_JSON, "```"].join("\n"), CANDIDATES);
40
+ assert.equal(result.fallback, undefined);
41
+ assert.equal(result.actions.length, 1);
42
+ });
43
+
44
+ test("#3134 prose-wrapped fenced response with trailing prose parses", () => {
45
+ const result = parseClassifyResponse(
46
+ ["Here is the classification:", "```json", CLASSIFY_JSON, "```", "Done."].join("\n"),
47
+ CANDIDATES
48
+ );
49
+ assert.equal(result.fallback, undefined);
50
+ assert.equal(result.actions.length, 1);
51
+ });
52
+
53
+ test("#3134 bare JSON with an inner markdown code block keeps its patch content verbatim", () => {
54
+ const patch = "```\nconst fixed = true;\n```";
55
+ const result = parseClassifyResponse(
56
+ JSON.stringify({
57
+ classification: "wrong",
58
+ confidence: 0.8,
59
+ actions: [{ kind: "edit", memoryId: "m1", patch }],
60
+ }),
61
+ CANDIDATES
62
+ );
63
+ assert.equal(result.fallback, undefined, "valid bare JSON must not degrade to the fallback");
64
+ assert.deepEqual(result.actions, [{ kind: "edit", memoryId: "m1", patch }], "inner code-block delimiters must survive verbatim");
65
+ });
66
+
67
+ test("#3134 outer-fenced JSON with an inner code block parses with the patch verbatim", () => {
68
+ const patch = "```py\nprint(\"fixed\")\n```";
69
+ const inner = JSON.stringify({
70
+ classification: "wrong",
71
+ confidence: 0.8,
72
+ actions: [{ kind: "edit", memoryId: "m1", patch }],
73
+ });
74
+ const result = parseClassifyResponse(["```json", inner, "```"].join("\n"), CANDIDATES);
75
+ assert.equal(result.fallback, undefined, "an outer fence plus an inner code block must not degrade to the fallback");
76
+ assert.deepEqual(result.actions, [{ kind: "edit", memoryId: "m1", patch }], "inner code-block delimiters must survive verbatim");
77
+ });
78
+
79
+ test("#3134 genuinely non-JSON response still returns the deterministic fallback", () => {
80
+ const result = parseClassifyResponse("I'm sorry, I cannot classify that request.", CANDIDATES);
81
+ assert.deepEqual(result, {
82
+ classification: "outdated",
83
+ confidence: 0,
84
+ actions: [],
85
+ relevance: [{ memoryId: "m1", why: "located for review" }],
86
+ warnings: ["LLM returned non-JSON response"],
87
+ fallback: true,
88
+ });
89
+ });
90
+
91
+ test("#3134 empty response still returns the deterministic fallback", () => {
92
+ const result = parseClassifyResponse("", CANDIDATES);
93
+ assert.equal(result.fallback, true);
94
+ assert.deepEqual(result.warnings, ["LLM returned non-JSON response"]);
95
+ });
96
+
97
+ test("#3134 bare JSON keeps parsing through the same candidate chain", () => {
98
+ const result = parseClassifyResponse(CLASSIFY_JSON, CANDIDATES);
99
+ assert.equal(result.fallback, undefined);
100
+ assert.equal(result.classification, "wrong");
101
+ assert.equal(result.confidence, 0.9);
102
+ assert.equal(result.actions.length, 1);
103
+ });
104
+
105
+ test("#3134 bare JSON object with no actions still parses without fallback", () => {
106
+ const result = parseClassifyResponse(
107
+ JSON.stringify({ classification: "incomplete", confidence: 0.4, actions: [] }),
108
+ CANDIDATES
109
+ );
110
+ assert.equal(result.fallback, undefined);
111
+ assert.equal(result.classification, "incomplete");
112
+ assert.equal(result.actions.length, 0);
113
+ });
114
+
115
+ test("#3145 skips unrelated JSON objects before the correction response", () => {
116
+ const result = parseClassifyResponse("Example: {}\nAnswer: " + CLASSIFY_JSON, CANDIDATES);
117
+ assert.equal(result.fallback, undefined);
118
+ assert.equal(result.classification, "wrong");
119
+ assert.equal(result.actions.length, 1);
120
+ });
121
+
122
+ test("#3145 rejects ambiguous valid correction drafts instead of choosing an example", () => {
123
+ const other = JSON.stringify({ classification: "outdated", confidence: 0.99,
124
+ actions: [{ kind: "edit", memoryId: "m1", patch: "wrong example" }] });
125
+ const result = parseClassifyResponse(other + "\nActual answer: " + CLASSIFY_JSON, CANDIDATES);
126
+ assert.equal(result.fallback, true);
127
+ assert.equal(result.confidence, 0);
128
+ assert.deepEqual(result.actions, []);
129
+ });
130
+
131
+ test("#3145 skips malformed correction actions before a valid response", () => {
132
+ const malformed = JSON.stringify({ classification: "wrong", confidence: 0.9,
133
+ actions: [{ kind: "invented-action" }] });
134
+ const result = parseClassifyResponse(malformed + "\n" + CLASSIFY_JSON, CANDIDATES);
135
+ assert.equal(result.fallback, undefined);
136
+ assert.deepEqual(result.actions, [{ kind: "edit", memoryId: "m1", patch: "m1 corrected content" }]);
137
+ });
138
+
139
+ test("#3145 keeps the usable action when a sibling action is malformed", () => {
140
+ const mixed = JSON.stringify({
141
+ classification: "outdated",
142
+ confidence: 0.8,
143
+ actions: [
144
+ { kind: "edit", memoryId: "m1", patch: "m1 corrected content" },
145
+ { kind: "invented-action" },
146
+ ],
147
+ });
148
+ const result = parseClassifyResponse(mixed, CANDIDATES);
149
+ assert.equal(result.fallback, undefined, "one malformed sibling must not discard the whole plan");
150
+ assert.deepEqual(result.actions, [{ kind: "edit", memoryId: "m1", patch: "m1 corrected content" }]);
151
+ assert.equal(
152
+ result.warnings.filter((w) => w.startsWith("dropped malformed action")).length,
153
+ 1,
154
+ "exactly the malformed sibling is dropped with a warning",
155
+ );
156
+ });
157
+
158
+ test("#3145 a partially valid draft is ambiguous against a distinct valid draft", () => {
159
+ const partialDraft = JSON.stringify({
160
+ classification: "outdated",
161
+ confidence: 0.8,
162
+ actions: [
163
+ { kind: "edit", memoryId: "m1", patch: "partial example" },
164
+ { kind: "invented-action" },
165
+ ],
166
+ });
167
+ const result = parseClassifyResponse(partialDraft + "\n" + CLASSIFY_JSON, CANDIDATES);
168
+ assert.equal(result.fallback, true);
169
+ assert.equal(result.confidence, 0);
170
+ assert.deepEqual(result.actions, []);
171
+ });
172
+
173
+ test("#3145 an unrelated standalone object never becomes a plausible correction", () => {
174
+ const result = parseClassifyResponse("{}", CANDIDATES);
175
+ assert.equal(result.fallback, true);
176
+ assert.equal(result.confidence, 0);
177
+ assert.deepEqual(result.actions, []);
178
+ });
@@ -19,6 +19,7 @@
19
19
  * the verdict never diverges across code paths.
20
20
  */
21
21
  import { coerceBool, coerceNumber } from "./connectors/coerce.js";
22
+ import { getExtractionLlmHealth } from "./extraction-llm-health.js";
22
23
  import { log } from "./logger.js";
23
24
  import type { OperatorDoctorCheck } from "./operator-doctor-types.js";
24
25
  import type { PluginConfig } from "./types.js";
@@ -121,6 +122,15 @@ export interface ExtractionLivenessStatus {
121
122
  degradedReason: string | null;
122
123
  watermarkPending?: boolean;
123
124
  watermarkScope: "aggregate";
125
+ /**
126
+ * Extraction-LLM availability (issue #3140): outcome of the last extraction
127
+ * attempt. `lastFailureReason` is non-null exactly while `llmReachable` is
128
+ * false. Populated by the daemon-wide status readers (`/health`, doctor,
129
+ * stats), not by the pure evaluators below.
130
+ */
131
+ llmReachable?: boolean;
132
+ lastFailureReason?: string | null;
133
+ lastFailureAt?: string | null;
124
134
  }
125
135
 
126
136
  interface ExtractionLivenessOrchestratorLike {
@@ -380,7 +390,9 @@ export async function computeExtractionLivenessStatus(
380
390
  nowMs,
381
391
  });
382
392
  throttle?.maybeWarn(status, config.staleWindowMs, nowMs);
383
- return status;
393
+ // Extraction-LLM availability (#3140): a silent no_models / LLM-unreachable
394
+ // outage must be visible on /health (and doctor/stats) without debug logs.
395
+ return { ...status, ...getExtractionLlmHealth() };
384
396
  }
385
397
 
386
398
  /**
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Extraction-LLM health surfacing (issue #3140).
3
+ *
4
+ * Covers the three #3140 surfaces:
5
+ * 1. the singleton tracker that `/health.extraction` reads for LLM
6
+ * availability + last failure reason,
7
+ * 2. the ERROR-level, per-reason-deduplicated failure event emitted by the
8
+ * extraction run instead of debug-only lines,
9
+ * 3. the runExtraction integration recording failures/successes.
10
+ */
11
+
12
+ import assert from "node:assert/strict";
13
+ import test from "node:test";
14
+
15
+ import {
16
+ emitExtractionLlmFailureError,
17
+ getExtractionLlmHealth,
18
+ recordExtractionLlmFailure,
19
+ recordExtractionLlmSuccess,
20
+ resetExtractionLlmHealthForTests,
21
+ } from "./extraction-llm-health.js";
22
+ import { log } from "./logger.js";
23
+
24
+ test("tracker: starts reachable with no failure recorded", () => {
25
+ resetExtractionLlmHealthForTests();
26
+ try {
27
+ assert.deepEqual(getExtractionLlmHealth(), {
28
+ llmReachable: true,
29
+ lastFailureReason: null,
30
+ lastFailureAt: null,
31
+ });
32
+ } finally {
33
+ resetExtractionLlmHealthForTests();
34
+ }
35
+ });
36
+
37
+ test("tracker: a failure marks the extractor unavailable with the last reason (health payload, non-debug)", () => {
38
+ resetExtractionLlmHealthForTests();
39
+ try {
40
+ recordExtractionLlmFailure("no_models", 1_000);
41
+ const health = getExtractionLlmHealth();
42
+ assert.equal(health.llmReachable, false);
43
+ assert.equal(health.lastFailureReason, "no_models");
44
+ assert.equal(health.lastFailureAt, new Date(1_000).toISOString());
45
+
46
+ recordExtractionLlmFailure("local_llm_unavailable", 2_000);
47
+ assert.equal(getExtractionLlmHealth().lastFailureReason, "local_llm_unavailable");
48
+ } finally {
49
+ resetExtractionLlmHealthForTests();
50
+ }
51
+ });
52
+
53
+ test("tracker: a later success clears the failure so llmReachable and the reason agree", () => {
54
+ resetExtractionLlmHealthForTests();
55
+ try {
56
+ recordExtractionLlmFailure("no_models");
57
+ recordExtractionLlmSuccess();
58
+ assert.deepEqual(getExtractionLlmHealth(), {
59
+ llmReachable: true,
60
+ lastFailureReason: null,
61
+ lastFailureAt: null,
62
+ });
63
+ } finally {
64
+ resetExtractionLlmHealthForTests();
65
+ }
66
+ });
67
+
68
+ test("dedup: the first failure of a reason logs at ERROR level, repeats are suppressed", () => {
69
+ resetExtractionLlmHealthForTests();
70
+ const errors: string[] = [];
71
+ const debugs: string[] = [];
72
+ const originalError = log.error;
73
+ const originalDebug = log.debug;
74
+ log.error = (message: unknown) => {
75
+ errors.push(String(message));
76
+ };
77
+ log.debug = (message: unknown) => {
78
+ debugs.push(String(message));
79
+ };
80
+ try {
81
+ assert.equal(emitExtractionLlmFailureError("no_models", "auth_config", 0), true);
82
+ assert.equal(errors.length, 1);
83
+ assert.match(errors[0] ?? "", /no_models/);
84
+
85
+ // Same reason inside the window: suppressed to debug, no second ERROR.
86
+ assert.equal(emitExtractionLlmFailureError("no_models", "auth_config", 60_000), false);
87
+ assert.equal(errors.length, 1);
88
+ assert.equal(debugs.length, 1);
89
+
90
+ // A different reason emits its own first ERROR.
91
+ assert.equal(emitExtractionLlmFailureError("local_llm_unavailable", undefined, 60_000), true);
92
+ assert.equal(errors.length, 2);
93
+ assert.match(errors[1] ?? "", /local_llm_unavailable/);
94
+
95
+ // Outside the window the same reason emits again.
96
+ assert.equal(emitExtractionLlmFailureError("no_models", undefined, 3_600_000), true);
97
+ assert.equal(errors.length, 3);
98
+ } finally {
99
+ log.error = originalError;
100
+ log.debug = originalDebug;
101
+ resetExtractionLlmHealthForTests();
102
+ }
103
+ });
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Extraction-LLM availability tracking surfaced on `/health` (issue #3140).
3
+ *
4
+ * A failing extraction LLM (unreachable endpoint, gateway with no models)
5
+ * used to surface only as debug-level lines while extraction silently skipped
6
+ * persistence and the buffer grew for days. The extraction run records every
7
+ * attempt outcome here; the access `/health` payload reads the last outcome so
8
+ * an unavailable extractor is visible without debug logs, and each distinct
9
+ * failure reason logs at ERROR level at most once per window instead of once
10
+ * per failed flush.
11
+ */
12
+
13
+ import { log } from "./logger.js";
14
+
15
+ export interface ExtractionLlmHealth {
16
+ /** Whether the LAST extraction attempt completed without a failure marker. */
17
+ llmReachable: boolean;
18
+ /** `failureReason` of the last failed attempt; null while reachable. */
19
+ lastFailureReason: string | null;
20
+ /** ISO timestamp of the last failed attempt; null while reachable. */
21
+ lastFailureAt: string | null;
22
+ }
23
+
24
+ /** One ERROR line per distinct reason per window; repeats stay at debug. */
25
+ const DEDUP_WINDOW_MS = 3_600_000;
26
+
27
+ let lastFailureReason: string | null = null;
28
+ let lastFailureAtMs: number | null = null;
29
+ const lastErrorEmitByReason = new Map<string, number>();
30
+
31
+ export function recordExtractionLlmSuccess(): void {
32
+ lastFailureReason = null;
33
+ lastFailureAtMs = null;
34
+ }
35
+
36
+ export function recordExtractionLlmFailure(reason: string, atMs: number = Date.now()): void {
37
+ lastFailureReason = reason;
38
+ lastFailureAtMs = atMs;
39
+ }
40
+
41
+ export function getExtractionLlmHealth(): ExtractionLlmHealth {
42
+ return {
43
+ llmReachable: lastFailureReason === null,
44
+ lastFailureReason,
45
+ lastFailureAt: lastFailureAtMs === null ? null : new Date(lastFailureAtMs).toISOString(),
46
+ };
47
+ }
48
+
49
+ /**
50
+ * ERROR-level, deduplicated failure event (issue #3140): the first failure of
51
+ * a given reason inside the window logs at error level; repeats inside the
52
+ * window log at debug so the backoff retry loop cannot flood the transcript
53
+ * while the outage stays visible. Returns whether the error line was emitted.
54
+ */
55
+ export function emitExtractionLlmFailureError(reason: string, detail?: string, nowMs: number = Date.now()): boolean {
56
+ const suffix = detail ? `: ${detail}` : "";
57
+ const last = lastErrorEmitByReason.get(reason);
58
+ if (last !== undefined && nowMs - last < DEDUP_WINDOW_MS) {
59
+ log.debug(`extraction failure repeat suppressed for ${DEDUP_WINDOW_MS / 60_000}m: ${reason}${suffix}`);
60
+ return false;
61
+ }
62
+ lastErrorEmitByReason.set(reason, nowMs);
63
+ log.error(`extraction failed with no durable outputs (buffer retained for retry): ${reason}${suffix}`);
64
+ return true;
65
+ }
66
+
67
+ /** Test seam: restore pristine singleton state. */
68
+ export function resetExtractionLlmHealthForTests(): void {
69
+ recordExtractionLlmSuccess();
70
+ lastErrorEmitByReason.clear();
71
+ }
@@ -21,6 +21,12 @@ export function extractJsonCandidates(text: string): string[] {
21
21
  const cleaned = stripCodeFences(trimmed);
22
22
  const candidates: string[] = [];
23
23
 
24
+ // Verbatim balanced blocks FIRST: stripCodeFences rewrites every ``` pair, so
25
+ // a stripped candidate can parse while carrying altered content (e.g. a
26
+ // patch value that embeds an inner code block). A string-aware balanced scan
27
+ // of the unstripped text yields the payload unmodified; stripped candidates
28
+ // remain as later fallbacks for shapes the raw scan cannot cut.
29
+ candidates.push(...scanBalancedJsonBlocks(trimmed));
24
30
  if (cleaned.length > 0) candidates.push(cleaned);
25
31
  candidates.push(...scanBalancedJsonBlocks(cleaned));
26
32