@remnic/core 9.3.726 → 9.3.727

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 (318) hide show
  1. package/dist/access-boundary.d.ts +5 -5
  2. package/dist/access-boundary.js +20 -20
  3. package/dist/access-cli.js +42 -42
  4. package/dist/access-http.d.ts +20 -5
  5. package/dist/access-http.js +25 -25
  6. package/dist/access-mcp.d.ts +5 -5
  7. package/dist/access-mcp.js +24 -24
  8. package/dist/access-operations-batch.js +21 -21
  9. package/dist/access-operations.d.ts +5 -5
  10. package/dist/access-operations.js +23 -23
  11. package/dist/access-schema.js +3 -3
  12. package/dist/{access-service-B2PlPtxh.d.ts → access-service-Bgpjxo4f.d.ts} +36 -4
  13. package/dist/access-service.d.ts +5 -5
  14. package/dist/access-service.js +19 -19
  15. package/dist/access-surface-catalog.d.ts +5 -5
  16. package/dist/action-confidence.d.ts +1 -1
  17. package/dist/active-memory-bridge.d.ts +1 -1
  18. package/dist/active-recall.d.ts +1 -1
  19. package/dist/active-recall.js +3 -3
  20. package/dist/behavior-learner.d.ts +1 -1
  21. package/dist/behavior-signals.d.ts +1 -1
  22. package/dist/bootstrap.d.ts +4 -4
  23. package/dist/briefing.d.ts +1 -1
  24. package/dist/briefing.js +7 -7
  25. package/dist/buffer-surprise-report.d.ts +1 -1
  26. package/dist/buffer.d.ts +1 -1
  27. package/dist/calibration.d.ts +1 -1
  28. package/dist/capabilities.d.ts +1 -1
  29. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  30. package/dist/{catalog-CzUb-GyD.d.ts → catalog-bMczlO2G.d.ts} +1 -1
  31. package/dist/causal-behavior.d.ts +1 -1
  32. package/dist/causal-consolidation.d.ts +1 -1
  33. package/dist/causal-consolidation.js +8 -8
  34. package/dist/causal-trajectory-graph.d.ts +1 -1
  35. package/dist/{chunk-Q6D75LVH.js → chunk-27QQC7ZX.js} +75 -14
  36. package/dist/{chunk-Q6D75LVH.js.map → chunk-27QQC7ZX.js.map} +1 -1
  37. package/dist/{chunk-53EEQYE5.js → chunk-2KAYTPPT.js} +7 -7
  38. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  39. package/dist/{chunk-BIA7ELIB.js → chunk-36SEDFAU.js} +3 -3
  40. package/dist/{chunk-UQSVUEN2.js → chunk-37436GML.js} +2 -2
  41. package/dist/{chunk-RRHHQYJS.js → chunk-3TSTGI6F.js} +2 -2
  42. package/dist/{chunk-FO3HO52S.js → chunk-4H4F3BFL.js} +2 -2
  43. package/dist/{chunk-BEUCACTV.js → chunk-4NOI577P.js} +2 -2
  44. package/dist/{chunk-BBIE7W52.js → chunk-4NR2PDZ5.js} +2 -2
  45. package/dist/{chunk-TQQH2FTX.js → chunk-6PY5YJAV.js} +101 -15
  46. package/dist/{chunk-TQQH2FTX.js.map → chunk-6PY5YJAV.js.map} +1 -1
  47. package/dist/{chunk-PKMQK2WY.js → chunk-773DPI6T.js} +4 -4
  48. package/dist/{chunk-MQVKHFIU.js → chunk-7RLCCNHU.js} +8 -8
  49. package/dist/{chunk-XGTBNVIU.js → chunk-AAD2ZS6P.js} +11 -1
  50. package/dist/chunk-AAD2ZS6P.js.map +1 -0
  51. package/dist/{chunk-EZNDNMRH.js → chunk-AQLOP5F4.js} +2 -2
  52. package/dist/{chunk-UNNDFYUX.js → chunk-ARV3AUOM.js} +5 -5
  53. package/dist/{chunk-NPL7VDM6.js → chunk-ASKXIAHD.js} +2 -2
  54. package/dist/{chunk-K4ZNARDO.js → chunk-CBWI22JN.js} +2 -2
  55. package/dist/{chunk-ZOHMPFOB.js → chunk-CKFLLAIS.js} +3 -3
  56. package/dist/{chunk-DCKNMLEQ.js → chunk-CSRXQ6NB.js} +2 -2
  57. package/dist/{chunk-6AFVIOVZ.js → chunk-CTOQEZSN.js} +2 -2
  58. package/dist/{chunk-7NS7AWMZ.js → chunk-DKJCRGWM.js} +3 -3
  59. package/dist/{chunk-JPUT5NGH.js → chunk-DMQWV6SQ.js} +2 -2
  60. package/dist/{chunk-F3XZN24Z.js → chunk-DRUDC2UX.js} +2 -2
  61. package/dist/{chunk-KJR66XOM.js → chunk-EY6MDMHK.js} +2 -2
  62. package/dist/{chunk-3TUT4PWB.js → chunk-FVZ2ISIX.js} +2 -2
  63. package/dist/{chunk-C4X4QGIO.js → chunk-G6NKALRN.js} +5 -2
  64. package/dist/chunk-G6NKALRN.js.map +1 -0
  65. package/dist/{chunk-6SQQOJ4U.js → chunk-GKRLPHPU.js} +2 -2
  66. package/dist/{chunk-5NWYQUQC.js → chunk-HOHVIVDR.js} +2 -2
  67. package/dist/{chunk-6WAJQKBS.js → chunk-JYLFZWUX.js} +2 -2
  68. package/dist/{chunk-5W7CTZ2E.js → chunk-KQAFEZQX.js} +4 -4
  69. package/dist/{chunk-YTZ3VGC7.js → chunk-M4V2JVSZ.js} +2 -2
  70. package/dist/{chunk-PLG6JHQS.js → chunk-MJIHJ7OG.js} +38 -38
  71. package/dist/{chunk-TA3Y6GEX.js → chunk-MLKWJOAJ.js} +2 -2
  72. package/dist/{chunk-2NBP25BG.js → chunk-N3VNUM4Z.js} +171 -79
  73. package/dist/chunk-N3VNUM4Z.js.map +1 -0
  74. package/dist/{chunk-UKTDXTWX.js → chunk-NSA5ZRUL.js} +6 -6
  75. package/dist/{chunk-DQXPJ6HD.js → chunk-OEEW4KJA.js} +4 -4
  76. package/dist/{chunk-7AG54AF3.js → chunk-PUR2CKX2.js} +1 -1
  77. package/dist/chunk-PUR2CKX2.js.map +1 -0
  78. package/dist/{chunk-5OOA7SGX.js → chunk-R7IYU5DZ.js} +2 -2
  79. package/dist/{chunk-WGTWGDR5.js → chunk-SNKPUJHR.js} +5 -5
  80. package/dist/{chunk-BB57OZWC.js → chunk-UVPL565C.js} +2 -2
  81. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  82. package/dist/{chunk-OAHOL3ZC.js → chunk-XPHVAOBT.js} +111 -30
  83. package/dist/chunk-XPHVAOBT.js.map +1 -0
  84. package/dist/{chunk-V7CMKMHM.js → chunk-Y4YD5TYW.js} +3 -3
  85. package/dist/{chunk-MDBZTH7N.js → chunk-YKBHOBJK.js} +44 -10
  86. package/dist/chunk-YKBHOBJK.js.map +1 -0
  87. package/dist/{chunk-3MS3QVX6.js → chunk-YOLZS7NY.js} +2 -2
  88. package/dist/{chunk-6OIDDKAA.js → chunk-YTU7WKYN.js} +35 -9
  89. package/dist/chunk-YTU7WKYN.js.map +1 -0
  90. package/dist/{cli-IOlDHSBT.d.ts → cli-BLME420h.d.ts} +3 -3
  91. package/dist/cli.d.ts +6 -6
  92. package/dist/cli.js +47 -47
  93. package/dist/compat/codex-cli-fallback.d.ts +1 -0
  94. package/dist/compat/codex-cli-fallback.js +22 -0
  95. package/dist/compat/codex-cli-fallback.js.map +1 -0
  96. package/dist/compat/codex-thread-key.d.ts +1 -0
  97. package/dist/compat/codex-thread-key.js +18 -0
  98. package/dist/compat/codex-thread-key.js.map +1 -0
  99. package/dist/compounding/engine.d.ts +1 -1
  100. package/dist/compounding/engine.js +7 -7
  101. package/dist/compounding/preference-consolidator.d.ts +1 -1
  102. package/dist/compression-optimizer.d.ts +1 -1
  103. package/dist/config.d.ts +1 -1
  104. package/dist/config.js +3 -3
  105. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  106. package/dist/connectors/codex-materialize-runner.js +7 -7
  107. package/dist/connectors/codex-materialize.d.ts +1 -1
  108. package/dist/connectors/index.d.ts +1 -1
  109. package/dist/connectors/index.js +7 -7
  110. package/dist/consolidation-provenance-check.d.ts +1 -1
  111. package/dist/consolidation-undo.d.ts +1 -1
  112. package/dist/contradiction/index.d.ts +1 -1
  113. package/dist/conversation-index/backend.d.ts +1 -1
  114. package/dist/conversation-index/chunker.d.ts +1 -1
  115. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  116. package/dist/conversation-index/indexer.d.ts +1 -1
  117. package/dist/conversation-index/search.d.ts +1 -1
  118. package/dist/day-summary.d.ts +1 -1
  119. package/dist/delinearize.d.ts +1 -1
  120. package/dist/direct-answer-wiring.d.ts +1 -1
  121. package/dist/direct-answer.d.ts +1 -1
  122. package/dist/embedding-fallback.d.ts +1 -1
  123. package/dist/enrichment/index.d.ts +1 -1
  124. package/dist/entity-retrieval.d.ts +1 -1
  125. package/dist/entity-retrieval.js +7 -7
  126. package/dist/entity-schema.d.ts +1 -1
  127. package/dist/explicit-capture.d.ts +4 -4
  128. package/dist/extraction-faithfulness.d.ts +1 -1
  129. package/dist/extraction-faithfulness.js +2 -2
  130. package/dist/extraction-judge-telemetry.d.ts +1 -1
  131. package/dist/extraction-judge-training.d.ts +1 -1
  132. package/dist/extraction-judge.d.ts +1 -1
  133. package/dist/extraction.d.ts +1 -1
  134. package/dist/extraction.js +2 -2
  135. package/dist/faithfulness-config.d.ts +1 -0
  136. package/dist/faithfulness-config.js +1 -1
  137. package/dist/fallback-llm.d.ts +1 -1
  138. package/dist/graph-dashboard-diff.d.ts +1 -1
  139. package/dist/graph-dashboard-key.d.ts +1 -1
  140. package/dist/graph-dashboard-parser.d.ts +1 -1
  141. package/dist/graph-edge-reinforcement.d.ts +1 -1
  142. package/dist/graph-snapshot.d.ts +1 -1
  143. package/dist/graph.d.ts +1 -1
  144. package/dist/identity-continuity.d.ts +1 -1
  145. package/dist/importance.d.ts +1 -1
  146. package/dist/index.d.ts +9 -9
  147. package/dist/index.js +61 -61
  148. package/dist/intent.d.ts +1 -1
  149. package/dist/lcm/engine.d.ts +1 -1
  150. package/dist/lcm/engine.js +2 -2
  151. package/dist/lcm/index.d.ts +1 -1
  152. package/dist/lcm/index.js +7 -7
  153. package/dist/lcm/tools.d.ts +1 -1
  154. package/dist/lifecycle.d.ts +1 -1
  155. package/dist/live-connectors-runner.d.ts +1 -1
  156. package/dist/local-llm.d.ts +1 -1
  157. package/dist/local-model-endpoint.d.ts +9 -4
  158. package/dist/local-model-endpoint.js +1 -1
  159. package/dist/maintenance/memory-governance.d.ts +1 -1
  160. package/dist/maintenance/memory-governance.js +7 -7
  161. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  162. package/dist/maintenance/rebuild-memory-projection.js +8 -8
  163. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  164. package/dist/memory-action-policy.d.ts +1 -1
  165. package/dist/memory-cache.d.ts +1 -1
  166. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  167. package/dist/memory-projection-store.d.ts +1 -1
  168. package/dist/memory-provenance.d.ts +1 -1
  169. package/dist/memory-worth-outcomes.d.ts +1 -1
  170. package/dist/models-json.d.ts +1 -1
  171. package/dist/namespaces/migrate.d.ts +2 -2
  172. package/dist/namespaces/migrate.js +12 -12
  173. package/dist/namespaces/principal.d.ts +1 -1
  174. package/dist/namespaces/search.d.ts +1 -1
  175. package/dist/namespaces/search.js +4 -4
  176. package/dist/namespaces/storage.d.ts +2 -2
  177. package/dist/namespaces/storage.js +7 -7
  178. package/dist/native-knowledge.d.ts +1 -1
  179. package/dist/operator-toolkit.d.ts +1 -1
  180. package/dist/operator-toolkit.js +18 -18
  181. package/dist/orchestration/maintenance.d.ts +2 -2
  182. package/dist/orchestration/maintenance.js +9 -9
  183. package/dist/{orchestrator-C_OMSIMZ.d.ts → orchestrator-DqPAz8ot.d.ts} +3 -3
  184. package/dist/orchestrator.d.ts +4 -4
  185. package/dist/orchestrator.js +30 -30
  186. package/dist/patterns-cli.d.ts +1 -1
  187. package/dist/policy-runtime.d.ts +1 -1
  188. package/dist/provenance.d.ts +1 -1
  189. package/dist/provenance.js +1 -1
  190. package/dist/qmd-recall-cache.d.ts +1 -1
  191. package/dist/qmd.d.ts +1 -1
  192. package/dist/recall-disclosure-escalation.d.ts +1 -1
  193. package/dist/recall-explain-renderer.d.ts +1 -1
  194. package/dist/recall-explain-renderer.js +3 -3
  195. package/dist/recall-planner-llm.d.ts +1 -1
  196. package/dist/recall-state.d.ts +1 -1
  197. package/dist/recall-tag-filter.d.ts +1 -1
  198. package/dist/recall-xray-cli.d.ts +1 -1
  199. package/dist/recall-xray-cli.js +4 -4
  200. package/dist/recall-xray-renderer.d.ts +1 -1
  201. package/dist/recall-xray-renderer.js +3 -3
  202. package/dist/recall-xray.d.ts +1 -1
  203. package/dist/recall-xray.js +2 -2
  204. package/dist/resolve-auth-token.d.ts +1 -1
  205. package/dist/resume-bundles.js +4 -4
  206. package/dist/retrieval-agents.d.ts +1 -1
  207. package/dist/retrieval-tiers.d.ts +1 -1
  208. package/dist/routing/engine.d.ts +1 -1
  209. package/dist/routing/store.d.ts +1 -1
  210. package/dist/schemas.d.ts +22 -22
  211. package/dist/search/embed-helper.d.ts +1 -1
  212. package/dist/search/factory.d.ts +1 -1
  213. package/dist/search/factory.js +3 -3
  214. package/dist/search/index.d.ts +1 -1
  215. package/dist/search/index.js +7 -7
  216. package/dist/search/lancedb-backend.d.ts +1 -1
  217. package/dist/search/meilisearch-backend.d.ts +1 -1
  218. package/dist/search/noop-backend.d.ts +1 -1
  219. package/dist/search/orama-backend.d.ts +1 -1
  220. package/dist/search/port.d.ts +1 -1
  221. package/dist/search/remote-backend.d.ts +1 -1
  222. package/dist/{semantic-consolidation-B8LiYtvx.d.ts → semantic-consolidation-Cl15ksZy.d.ts} +1 -1
  223. package/dist/semantic-consolidation.d.ts +2 -2
  224. package/dist/semantic-consolidation.js +8 -8
  225. package/dist/semantic-rule-promotion.js +7 -7
  226. package/dist/semantic-rule-verifier.d.ts +1 -1
  227. package/dist/semantic-rule-verifier.js +7 -7
  228. package/dist/session-observer-bands.d.ts +1 -1
  229. package/dist/session-observer-state.d.ts +1 -1
  230. package/dist/shared-context/manager.d.ts +1 -1
  231. package/dist/signal.d.ts +1 -1
  232. package/dist/storage.d.ts +1 -1
  233. package/dist/storage.js +6 -6
  234. package/dist/summarizer.d.ts +1 -1
  235. package/dist/summary-snapshot.d.ts +1 -1
  236. package/dist/temporal-supersession.d.ts +1 -1
  237. package/dist/temporal-validity.d.ts +1 -1
  238. package/dist/threading.d.ts +1 -1
  239. package/dist/tier-migration.d.ts +1 -1
  240. package/dist/tier-routing.d.ts +1 -1
  241. package/dist/topics.d.ts +1 -1
  242. package/dist/transcript.d.ts +1 -1
  243. package/dist/transfer/backup.js +2 -2
  244. package/dist/transfer/capsule-export.js +2 -2
  245. package/dist/transfer/capsule-import.js +2 -2
  246. package/dist/transfer/types.d.ts +12 -12
  247. package/dist/trust-score-stage.d.ts +1 -1
  248. package/dist/trust-score.d.ts +1 -1
  249. package/dist/{types-DmTaqob2.d.ts → types-ChQb9D6s.d.ts} +8 -0
  250. package/dist/types.d.ts +1 -1
  251. package/dist/types.js +1 -1
  252. package/dist/utility-runtime.d.ts +1 -1
  253. package/dist/verified-recall.js +7 -7
  254. package/package.json +26 -2
  255. package/src/access-http.ts +68 -1
  256. package/src/admin/admin-surfaces.test.ts +289 -0
  257. package/src/admin/admin-surfaces.ts +138 -7
  258. package/src/bitemporal-dedup-backfill.test.ts +294 -0
  259. package/src/chat/chat-engine.test.ts +66 -1
  260. package/src/chat/chat-http.test.ts +119 -1
  261. package/src/chat/chat-http.ts +252 -97
  262. package/src/chat/chat-session.ts +143 -7
  263. package/src/compat/codex-cli-fallback.ts +23 -0
  264. package/src/compat/codex-subpath-aliases.test.ts +93 -0
  265. package/src/compat/codex-thread-key.ts +23 -0
  266. package/src/extraction-faithfulness.test.ts +240 -0
  267. package/src/extraction-faithfulness.ts +58 -6
  268. package/src/faithfulness-config.ts +16 -1
  269. package/src/local-model-endpoint.ts +24 -3
  270. package/src/orchestrator.ts +200 -8
  271. package/src/provenance-extraction.test.ts +266 -0
  272. package/src/provenance.ts +66 -16
  273. package/src/types.ts +8 -0
  274. package/dist/chunk-2NBP25BG.js.map +0 -1
  275. package/dist/chunk-6OIDDKAA.js.map +0 -1
  276. package/dist/chunk-7AG54AF3.js.map +0 -1
  277. package/dist/chunk-C4X4QGIO.js.map +0 -1
  278. package/dist/chunk-MDBZTH7N.js.map +0 -1
  279. package/dist/chunk-OAHOL3ZC.js.map +0 -1
  280. package/dist/chunk-XGTBNVIU.js.map +0 -1
  281. /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  282. /package/dist/{chunk-53EEQYE5.js.map → chunk-2KAYTPPT.js.map} +0 -0
  283. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  284. /package/dist/{chunk-BIA7ELIB.js.map → chunk-36SEDFAU.js.map} +0 -0
  285. /package/dist/{chunk-UQSVUEN2.js.map → chunk-37436GML.js.map} +0 -0
  286. /package/dist/{chunk-RRHHQYJS.js.map → chunk-3TSTGI6F.js.map} +0 -0
  287. /package/dist/{chunk-FO3HO52S.js.map → chunk-4H4F3BFL.js.map} +0 -0
  288. /package/dist/{chunk-BEUCACTV.js.map → chunk-4NOI577P.js.map} +0 -0
  289. /package/dist/{chunk-BBIE7W52.js.map → chunk-4NR2PDZ5.js.map} +0 -0
  290. /package/dist/{chunk-PKMQK2WY.js.map → chunk-773DPI6T.js.map} +0 -0
  291. /package/dist/{chunk-MQVKHFIU.js.map → chunk-7RLCCNHU.js.map} +0 -0
  292. /package/dist/{chunk-EZNDNMRH.js.map → chunk-AQLOP5F4.js.map} +0 -0
  293. /package/dist/{chunk-UNNDFYUX.js.map → chunk-ARV3AUOM.js.map} +0 -0
  294. /package/dist/{chunk-NPL7VDM6.js.map → chunk-ASKXIAHD.js.map} +0 -0
  295. /package/dist/{chunk-K4ZNARDO.js.map → chunk-CBWI22JN.js.map} +0 -0
  296. /package/dist/{chunk-ZOHMPFOB.js.map → chunk-CKFLLAIS.js.map} +0 -0
  297. /package/dist/{chunk-DCKNMLEQ.js.map → chunk-CSRXQ6NB.js.map} +0 -0
  298. /package/dist/{chunk-6AFVIOVZ.js.map → chunk-CTOQEZSN.js.map} +0 -0
  299. /package/dist/{chunk-7NS7AWMZ.js.map → chunk-DKJCRGWM.js.map} +0 -0
  300. /package/dist/{chunk-JPUT5NGH.js.map → chunk-DMQWV6SQ.js.map} +0 -0
  301. /package/dist/{chunk-F3XZN24Z.js.map → chunk-DRUDC2UX.js.map} +0 -0
  302. /package/dist/{chunk-KJR66XOM.js.map → chunk-EY6MDMHK.js.map} +0 -0
  303. /package/dist/{chunk-3TUT4PWB.js.map → chunk-FVZ2ISIX.js.map} +0 -0
  304. /package/dist/{chunk-6SQQOJ4U.js.map → chunk-GKRLPHPU.js.map} +0 -0
  305. /package/dist/{chunk-5NWYQUQC.js.map → chunk-HOHVIVDR.js.map} +0 -0
  306. /package/dist/{chunk-6WAJQKBS.js.map → chunk-JYLFZWUX.js.map} +0 -0
  307. /package/dist/{chunk-5W7CTZ2E.js.map → chunk-KQAFEZQX.js.map} +0 -0
  308. /package/dist/{chunk-YTZ3VGC7.js.map → chunk-M4V2JVSZ.js.map} +0 -0
  309. /package/dist/{chunk-PLG6JHQS.js.map → chunk-MJIHJ7OG.js.map} +0 -0
  310. /package/dist/{chunk-TA3Y6GEX.js.map → chunk-MLKWJOAJ.js.map} +0 -0
  311. /package/dist/{chunk-UKTDXTWX.js.map → chunk-NSA5ZRUL.js.map} +0 -0
  312. /package/dist/{chunk-DQXPJ6HD.js.map → chunk-OEEW4KJA.js.map} +0 -0
  313. /package/dist/{chunk-5OOA7SGX.js.map → chunk-R7IYU5DZ.js.map} +0 -0
  314. /package/dist/{chunk-WGTWGDR5.js.map → chunk-SNKPUJHR.js.map} +0 -0
  315. /package/dist/{chunk-BB57OZWC.js.map → chunk-UVPL565C.js.map} +0 -0
  316. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  317. /package/dist/{chunk-V7CMKMHM.js.map → chunk-Y4YD5TYW.js.map} +0 -0
  318. /package/dist/{chunk-3MS3QVX6.js.map → chunk-YOLZS7NY.js.map} +0 -0
@@ -0,0 +1,93 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import * as canonicalCliFallback from "../cli-fallback.js";
7
+ import * as canonicalThreadKey from "../thread-key.js";
8
+ import * as aliasCliFallback from "./codex-cli-fallback.js";
9
+ import * as aliasThreadKey from "./codex-thread-key.js";
10
+
11
+ const pkgJsonPath = fileURLToPath(new URL("../../package.json", import.meta.url));
12
+
13
+ test("codex-cli-fallback alias re-exports the same value symbols as cli-fallback", () => {
14
+ // export * forwards live bindings by reference, so the function objects and
15
+ // the test-hooks namespace are identical (===) across both subpaths.
16
+ assert.equal(
17
+ aliasCliFallback.callCodexCliFallback,
18
+ canonicalCliFallback.callCodexCliFallback,
19
+ );
20
+ assert.equal(
21
+ aliasCliFallback.setCodexCliFallbackRunnerForProcess,
22
+ canonicalCliFallback.setCodexCliFallbackRunnerForProcess,
23
+ );
24
+ assert.equal(
25
+ aliasCliFallback.__codexCliFallbackTestHooks,
26
+ canonicalCliFallback.__codexCliFallbackTestHooks,
27
+ );
28
+ });
29
+
30
+ test("codex-thread-key alias re-exports the same symbol as thread-key", () => {
31
+ assert.equal(
32
+ aliasThreadKey.CODEX_THREAD_KEY_PREFIX,
33
+ canonicalThreadKey.CODEX_THREAD_KEY_PREFIX,
34
+ );
35
+ assert.equal(aliasThreadKey.CODEX_THREAD_KEY_PREFIX, "codex-thread:");
36
+ });
37
+
38
+ test("every named runtime export of cli-fallback is forwarded by the alias", () => {
39
+ for (const key of Object.keys(canonicalCliFallback)) {
40
+ assert.equal(
41
+ key in aliasCliFallback,
42
+ true,
43
+ `alias missing forwarded export: ${key}`,
44
+ );
45
+ }
46
+ });
47
+
48
+ test("importing the old subpath resolves to the same symbol as the new subpath", async () => {
49
+ // @remnic/core self-references its own exports map. Under --conditions=
50
+ // remnic-source (set by preflight/test NODE_OPTIONS) the subpaths resolve to
51
+ // src/. Both specifiers land on the same module record, so the exported
52
+ // function is the same object reference.
53
+ const [byNew, byOld] = await Promise.all([
54
+ import("@remnic/core/cli-fallback"),
55
+ import("@remnic/core/codex-cli-fallback"),
56
+ ]);
57
+ assert.equal(byOld.callCodexCliFallback, byNew.callCodexCliFallback);
58
+
59
+ const [newKey, oldKey] = await Promise.all([
60
+ import("@remnic/core/thread-key"),
61
+ import("@remnic/core/codex-thread-key"),
62
+ ]);
63
+ assert.equal(oldKey.CODEX_THREAD_KEY_PREFIX, newKey.CODEX_THREAD_KEY_PREFIX);
64
+ });
65
+
66
+ test("the deprecated subpaths are registered with a removal timeline in the exports map", () => {
67
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as {
68
+ exports: Record<string, { deprecated?: string; import?: string }>;
69
+ };
70
+ const aliasEntries = [
71
+ "./codex-cli-fallback",
72
+ "./codex-cli-fallback.js",
73
+ "./codex-thread-key",
74
+ "./codex-thread-key.js",
75
+ ];
76
+ for (const subpath of aliasEntries) {
77
+ const entry = pkg.exports[subpath];
78
+ assert.ok(entry, `missing exports entry for ${subpath}`);
79
+ assert.ok(
80
+ entry.deprecated,
81
+ `exports entry for ${subpath} missing a deprecated field`,
82
+ );
83
+ assert.match(
84
+ entry.deprecated,
85
+ /v?10\.0\.0|remov/i,
86
+ `deprecated field for ${subpath} should state a removal timeline`,
87
+ );
88
+ assert.ok(
89
+ entry.import?.includes("/compat/"),
90
+ `exports entry for ${subpath} should target the compat re-export module`,
91
+ );
92
+ }
93
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Deprecated alias subpath `@remnic/core/codex-thread-key`.
3
+ *
4
+ * PR #1638 renamed `src/codex-thread-key.ts` -> `src/thread-key.ts` to
5
+ * graduate the host-prefixed name per CLAUDE.md rule 31. This thin re-export
6
+ * keeps the old subpath resolving during the compat window so external
7
+ * consumers importing `@remnic/core/codex-thread-key` do not hit
8
+ * ERR_PACKAGE_PATH_NOT_EXPORTED. It forwards every export to the canonical
9
+ * module -- no logic is duplicated.
10
+ *
11
+ * Removal timeline: v10.0.0 (see issue #1643).
12
+ * Import from `@remnic/core/thread-key` instead.
13
+ */
14
+ export * from "../thread-key.js";
15
+
16
+ process.emitWarning(
17
+ "@remnic/core/codex-thread-key is a deprecated alias; import from @remnic/core/thread-key instead. Removed in v10.0.0 (issue #1643).",
18
+ {
19
+ type: "DeprecationWarning",
20
+ code: "REMNIC_DEP_CORE_SUBPATH_CODEX_THREAD_KEY",
21
+ detail: "PR #1638 renamed codex-thread-key.ts to thread-key.ts.",
22
+ },
23
+ );
@@ -26,6 +26,7 @@ import type { FaithfulnessResult } from "./extraction-faithfulness.js";
26
26
  import type { FallbackLlmClient } from "./fallback-llm.js";
27
27
  import type { LocalLlmClient } from "./local-llm.js";
28
28
  import type { MemoryFrontmatter } from "./types.js";
29
+ import { callOpenAiCompatibleChat } from "./local-model-endpoint.js";
29
30
 
30
31
  // ---------------------------------------------------------------------------
31
32
  // Helpers — stub LLM clients
@@ -1429,3 +1430,242 @@ test("checkFaithfulnessBatch: local-endpoint failure does NOT leak the local mod
1429
1430
  );
1430
1431
  assert.equal(result.results[0]?.ok, true);
1431
1432
  });
1433
+
1434
+ // ---------------------------------------------------------------------------
1435
+ // Issue #1700 nits #5 + #6: probe signal forwarding + local parse-fallback
1436
+ // ---------------------------------------------------------------------------
1437
+
1438
+ test("callOpenAiCompatibleChat: a pre-aborted batch signal aborts the probe immediately, not after timeoutMs (#1700 nit #5)", async () => {
1439
+ // The batch signal is ALREADY aborted when the probe starts. With nit #5 the
1440
+ // probe forwards it and aborts instantly; without it the probe would wait for
1441
+ // its own timeoutMs (5000ms here). Assert the probe returns null fast.
1442
+ const endpoint = { baseUrl: "http://localhost:11434/v1", model: "remnic-faithfulness-gate-v1" };
1443
+ const batchController = new AbortController();
1444
+ batchController.abort();
1445
+ const spy = ((_url: string, init: RequestInit) =>
1446
+ new Promise<Response>((_resolve, reject) => {
1447
+ const sig = init.signal;
1448
+ if (!sig) return reject(new Error("no signal"));
1449
+ if (sig.aborted) return reject(new Error("aborted"));
1450
+ sig.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
1451
+ })) as unknown as typeof fetch;
1452
+ const start = Date.now();
1453
+ const result = await callOpenAiCompatibleChat(
1454
+ endpoint,
1455
+ [{ role: "user", content: "hi" }],
1456
+ { timeoutMs: 5000, signal: batchController.signal },
1457
+ spy,
1458
+ );
1459
+ const elapsed = Date.now() - start;
1460
+ assert.equal(result, null, "a pre-aborted signal must fail the probe closed (null)");
1461
+ assert.ok(
1462
+ elapsed < 1000,
1463
+ `probe must abort immediately on a pre-aborted signal (${elapsed}ms), not wait timeoutMs`,
1464
+ );
1465
+ });
1466
+
1467
+ test("checkFaithfulnessBatch: default surfaces malformed_output for a 200-with-garbage local response (#1700 nit #6)", async () => {
1468
+ // extractionFaithfulnessLocalParseFallback unset (default false): a local
1469
+ // endpoint returning 200 with non-verdict JSON surfaces malformed_output so a
1470
+ // misconfigured endpoint is visible. The configured chain must NOT run.
1471
+ const inputs = [{ factText: "Fact", quote: "Quote" }];
1472
+ const config = parseConfig({
1473
+ extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
1474
+ extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
1475
+ extractionFaithfulnessTimeoutMs: 5000,
1476
+ });
1477
+ const fallbackCalls: Array<{ messages: unknown; options: unknown }> = [];
1478
+ const { fetch: garbageFetch } = fakeFetchFor("http://localhost:11434/v1", () => ({
1479
+ model: "remnic-faithfulness-gate-v1",
1480
+ choices: [{ message: { content: "this is not a verdict array" } }],
1481
+ }));
1482
+ const result = await checkFaithfulnessBatch(
1483
+ inputs,
1484
+ config,
1485
+ null,
1486
+ stubFallbackLlm(JSON.stringify([{ index: 0, verdict: "entailed" }]), fallbackCalls),
1487
+ garbageFetch,
1488
+ );
1489
+ assert.equal(fallbackCalls.length, 0, "chain must NOT run when flag is off — garbage surfaces loudly");
1490
+ assert.equal(result.results[0]?.ok, false);
1491
+ if (!result.results[0]?.ok) {
1492
+ assert.equal(result.results[0].error.code, "malformed_output");
1493
+ }
1494
+ });
1495
+
1496
+ test("checkFaithfulnessBatch: extractionFaithfulnessLocalParseFallback falls back to the chain on local parse failure (#1700 nit #6)", async () => {
1497
+ // Flag ON: the same 200-with-garbage local response now falls through to the
1498
+ // configured chain instead of surfacing malformed_output (resilient fallback).
1499
+ const inputs = [{ factText: "Fact", quote: "Quote" }];
1500
+ const config = parseConfig({
1501
+ extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
1502
+ extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
1503
+ extractionFaithfulnessTimeoutMs: 5000,
1504
+ extractionFaithfulnessLocalParseFallback: true,
1505
+ });
1506
+ const fallbackCalls: Array<{ messages: unknown; options: unknown }> = [];
1507
+ const { fetch: garbageFetch } = fakeFetchFor("http://localhost:11434/v1", () => ({
1508
+ model: "remnic-faithfulness-gate-v1",
1509
+ choices: [{ message: { content: "this is not a verdict array" } }],
1510
+ }));
1511
+ const result = await checkFaithfulnessBatch(
1512
+ inputs,
1513
+ config,
1514
+ null,
1515
+ stubFallbackLlm(JSON.stringify([{ index: 0, verdict: "entailed" }]), fallbackCalls),
1516
+ garbageFetch,
1517
+ );
1518
+ assert.equal(
1519
+ fallbackCalls.length,
1520
+ 1,
1521
+ "chain MUST run when flag is on and the local response is unparseable",
1522
+ );
1523
+ assert.equal(result.results[0]?.ok, true);
1524
+ if (result.results[0]?.ok) {
1525
+ assert.equal(result.results[0].verdict, "entailed", "fallback chain verdict is used");
1526
+ }
1527
+ });
1528
+
1529
+ test("checkFaithfulnessBatch: extractionFaithfulnessLocalParseFallback does NOT fall back when the local response is valid (#1700 nit #6 regression)", async () => {
1530
+ // Flag ON but the local response IS a valid verdict array → use it, do not
1531
+ // call the chain. Guards against the flag over-triggering on good responses.
1532
+ const inputs = [{ factText: "Fact", quote: "Quote" }];
1533
+ const config = parseConfig({
1534
+ extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
1535
+ extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
1536
+ extractionFaithfulnessTimeoutMs: 5000,
1537
+ extractionFaithfulnessLocalParseFallback: true,
1538
+ });
1539
+ const fallbackCalls: Array<{ messages: unknown; options: unknown }> = [];
1540
+ const { fetch: goodFetch } = fakeFetchFor("http://localhost:11434/v1", () => ({
1541
+ model: "remnic-faithfulness-gate-v1",
1542
+ choices: [{ message: { content: JSON.stringify([{ index: 0, verdict: "contradicted" }]) } }],
1543
+ }));
1544
+ const result = await checkFaithfulnessBatch(
1545
+ inputs,
1546
+ config,
1547
+ null,
1548
+ stubFallbackLlm(JSON.stringify([{ index: 0, verdict: "entailed" }]), fallbackCalls),
1549
+ goodFetch,
1550
+ );
1551
+ assert.equal(fallbackCalls.length, 0, "chain must NOT run when the local response is valid, even with the flag on");
1552
+ assert.equal(result.results[0]?.ok, true);
1553
+ if (result.results[0]?.ok) {
1554
+ assert.equal(result.results[0].verdict, "contradicted", "valid local verdict is used");
1555
+ }
1556
+ });
1557
+
1558
+ test("checkFaithfulnessBatch: extractionFaithfulnessLocalParseFallback falls back when the local response is a PARTIAL verdict array (codex P2 PRRT_kwDORJXyws6O6zwZ)", async () => {
1559
+ // Flag ON: a local endpoint that returns a valid-but-INCOMPLETE verdict
1560
+ // array (1 of N entries) must fall through to the configured chain, NOT be
1561
+ // accepted as-is. parseFaithfulnessResponse is truthy as soon as ONE entry
1562
+ // is valid, so the previous guard accepted the partial response and the
1563
+ // missing indexes surfaced as malformed_output downstream -- defeating the
1564
+ // resilient fallback. Accept the local response only when it covers every
1565
+ // expected index.
1566
+ const inputs = [
1567
+ { factText: "Fact A", quote: "Quote A" },
1568
+ { factText: "Fact B", quote: "Quote B" },
1569
+ ];
1570
+ const config = parseConfig({
1571
+ extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
1572
+ extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
1573
+ extractionFaithfulnessTimeoutMs: 5000,
1574
+ extractionFaithfulnessLocalParseFallback: true,
1575
+ });
1576
+ const fallbackCalls: Array<{ messages: unknown; options: unknown }> = [];
1577
+ // Local endpoint returns only index 0 -- index 1 is missing (partial).
1578
+ const { fetch: partialFetch } = fakeFetchFor("http://localhost:11434/v1", () => ({
1579
+ model: "remnic-faithfulness-gate-v1",
1580
+ choices: [{ message: { content: JSON.stringify([{ index: 0, verdict: "contradicted" }]) } }],
1581
+ }));
1582
+ const result = await checkFaithfulnessBatch(
1583
+ inputs,
1584
+ config,
1585
+ null,
1586
+ stubFallbackLlm(
1587
+ JSON.stringify([
1588
+ { index: 0, verdict: "entailed" },
1589
+ { index: 1, verdict: "unsupported" },
1590
+ ]),
1591
+ fallbackCalls,
1592
+ ),
1593
+ partialFetch,
1594
+ );
1595
+ assert.equal(
1596
+ fallbackCalls.length,
1597
+ 1,
1598
+ "a partial local verdict array must fall through to the configured chain",
1599
+ );
1600
+ // The chain's complete verdict array is used for both indexes.
1601
+ assert.equal(result.results[0]?.ok && result.results[0].verdict, "entailed");
1602
+ assert.equal(result.results[1]?.ok && result.results[1].verdict, "unsupported");
1603
+ });
1604
+
1605
+ test("checkFaithfulnessBatch: extractionFaithfulnessLocalParseFallback falls back when local indexes are fractional/non-contiguous (codex P2 PRRT_kwDORJXyws6O7PfY)", async () => {
1606
+ // Flag ON: a malformed local response with expectedCount distinct but
1607
+ // NON-INTEGER indexes (e.g. 0 and 0.5 for a two-item batch) must NOT be
1608
+ // accepted. parseFaithfulnessResponse used to admit any numeric index, so a
1609
+ // size-only check passed while index 1 stayed missing -> malformed_output.
1610
+ // parseEntries now rejects non-integer indexes, and the gate requires every
1611
+ // integer key 0..N-1 before accepting the local response.
1612
+ const inputs = [
1613
+ { factText: "Fact A", quote: "Quote A" },
1614
+ { factText: "Fact B", quote: "Quote B" },
1615
+ ];
1616
+ const config = parseConfig({
1617
+ extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
1618
+ extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
1619
+ extractionFaithfulnessTimeoutMs: 5000,
1620
+ extractionFaithfulnessLocalParseFallback: true,
1621
+ });
1622
+ const fallbackCalls: Array<{ messages: unknown; options: unknown }> = [];
1623
+ // Local endpoint returns indexes 0 and 0.5 -- two distinct keys, but index 1
1624
+ // is missing (fractional index is invalid).
1625
+ const { fetch: fracFetch } = fakeFetchFor("http://localhost:11434/v1", () => ({
1626
+ model: "remnic-faithfulness-gate-v1",
1627
+ choices: [{ message: { content: JSON.stringify([
1628
+ { index: 0, verdict: "contradicted" },
1629
+ { index: 0.5, verdict: "entailed" },
1630
+ ]) } }],
1631
+ }));
1632
+ const result = await checkFaithfulnessBatch(
1633
+ inputs,
1634
+ config,
1635
+ null,
1636
+ stubFallbackLlm(
1637
+ JSON.stringify([
1638
+ { index: 0, verdict: "entailed" },
1639
+ { index: 1, verdict: "unsupported" },
1640
+ ]),
1641
+ fallbackCalls,
1642
+ ),
1643
+ fracFetch,
1644
+ );
1645
+ assert.equal(
1646
+ fallbackCalls.length,
1647
+ 1,
1648
+ "a fractional/non-contiguous local response must fall through to the chain",
1649
+ );
1650
+ assert.equal(result.results[0]?.ok && result.results[0].verdict, "entailed");
1651
+ assert.equal(result.results[1]?.ok && result.results[1].verdict, "unsupported");
1652
+ });
1653
+
1654
+ test('parseConfig: extractionFaithfulnessLocalParseFallback coerces CLI string "true" (#1700 review — coerceBool parity)', () => {
1655
+ // A CLI override reaches the parser as the string "true"; === true would
1656
+ // silently leave the flag disabled. coerceBool must turn it on (gotcha 36).
1657
+ const onByString = parseConfig({
1658
+ extractionFaithfulnessLocalParseFallback: "true",
1659
+ });
1660
+ assert.equal(onByString.extractionFaithfulnessLocalParseFallback, true);
1661
+ const onByBool = parseConfig({
1662
+ extractionFaithfulnessLocalParseFallback: true,
1663
+ });
1664
+ assert.equal(onByBool.extractionFaithfulnessLocalParseFallback, true);
1665
+ const offDefault = parseConfig({});
1666
+ assert.equal(offDefault.extractionFaithfulnessLocalParseFallback, false);
1667
+ const offByString = parseConfig({
1668
+ extractionFaithfulnessLocalParseFallback: "false",
1669
+ });
1670
+ assert.equal(offByString.extractionFaithfulnessLocalParseFallback, false);
1671
+ });
@@ -220,6 +220,27 @@ export function parseFaithfulnessResponse(
220
220
  return null;
221
221
  }
222
222
 
223
+ /**
224
+ * True when `map` contains an entry for every integer index in
225
+ * [0, expectedCount). The local parse-fallback gate uses this to accept a
226
+ * local model-lab response only when it is COMPLETE: size alone is
227
+ * insufficient because a malformed response with duplicate/fractional indexes
228
+ * can yield expectedCount distinct map keys without covering index 0..N-1
229
+ * (codex P2 PRRT_kwDORJXyws6O7PfY). parseEntries now rejects non-integer
230
+ * indexes so the two checks agree, but the explicit coverage loop documents
231
+ * the invariant and stays robust to any future parser relaxation.
232
+ */
233
+ function coversAllIndexes(
234
+ map: Map<number, ParsedFaithfulnessEntry>,
235
+ expectedCount: number,
236
+ ): boolean {
237
+ if (map.size < expectedCount) return false;
238
+ for (let i = 0; i < expectedCount; i++) {
239
+ if (!map.has(i)) return false;
240
+ }
241
+ return true;
242
+ }
243
+
223
244
  function parseEntries(
224
245
  data: unknown,
225
246
  expectedCount: number,
@@ -243,7 +264,7 @@ function parseEntries(
243
264
  if (!entry || typeof entry !== "object") continue;
244
265
  const obj = entry as Record<string, unknown>;
245
266
  const idx = typeof obj.index === "number" ? obj.index : undefined;
246
- if (idx === undefined || idx < 0 || idx >= expectedCount) continue;
267
+ if (idx === undefined || !Number.isInteger(idx) || idx < 0 || idx >= expectedCount) continue;
247
268
  const verdictRaw = typeof obj.verdict === "string" ? obj.verdict : undefined;
248
269
  if (!verdictRaw || !VALID_VERDICTS.has(verdictRaw)) continue;
249
270
  const rationale =
@@ -279,6 +300,7 @@ interface LlmCallResult {
279
300
  async function callFaithfulnessLlm(
280
301
  systemPrompt: string,
281
302
  userPrompt: string,
303
+ expectedCount: number,
282
304
  config: PluginConfig,
283
305
  localLlm: LocalLlmClient | null,
284
306
  fallbackLlm: FallbackLlmClient | null,
@@ -311,15 +333,44 @@ async function callFaithfulnessLlm(
311
333
  const result = await callOpenAiCompatibleChat(
312
334
  localEndpoint,
313
335
  messages,
314
- { temperature: 0.1, maxTokens: 2048, responseFormatJson: true, timeoutMs: probeBudgetMs },
336
+ {
337
+ temperature: 0.1,
338
+ maxTokens: 2048,
339
+ responseFormatJson: true,
340
+ timeoutMs: probeBudgetMs,
341
+ // Forward the batch signal so the probe aborts the instant the batch
342
+ // budget elapses, not after probeBudgetMs (issue #1700 nit #5).
343
+ ...(signal ? { signal } : {}),
344
+ },
315
345
  fetchImpl,
316
346
  );
317
347
  if (result?.content) {
318
- return { content: result.content, modelUsed: result.modelUsed };
348
+ // Issue #1700 nit #6: pre-validate the local endpoint response shape.
349
+ // Default (extractionFaithfulnessLocalParseFallback=false) returns the
350
+ // content as-is -- a 200-with-garbage local response surfaces
351
+ // malformed_output, alerting the operator to a misconfigured endpoint.
352
+ // When the operator opts into resilient fallback, an unparseable OR
353
+ // PARTIAL local response falls through to the configured chain instead
354
+ // of surfacing. parseFaithfulnessResponse is truthy as soon as ONE entry
355
+ // is valid, so accept the local response only when the parsed verdict
356
+ // map covers every expected index -- otherwise the missing indexes would
357
+ // surface as malformed_output downstream, defeating the resilient
358
+ // fallback (codex P2 PRRT_kwDORJXyws6O6zwZ).
359
+ if (!config.extractionFaithfulnessLocalParseFallback) {
360
+ return { content: result.content, modelUsed: result.modelUsed };
361
+ }
362
+ const parsedLocal = parseFaithfulnessResponse(result.content, expectedCount);
363
+ if (parsedLocal && coversAllIndexes(parsedLocal, expectedCount)) {
364
+ return { content: result.content, modelUsed: result.modelUsed };
365
+ }
366
+ log.debug(
367
+ "extraction-faithfulness: local endpoint returned incomplete/unparseable output; falling back to configured chain",
368
+ );
369
+ } else {
370
+ log.debug(
371
+ "extraction-faithfulness: local model-lab endpoint unavailable, trying configured chain",
372
+ );
319
373
  }
320
- log.debug(
321
- "extraction-faithfulness: local model-lab endpoint unavailable, trying configured chain",
322
- );
323
374
  }
324
375
 
325
376
  // extractionFaithfulnessModel is the LOCAL served model's name (e.g.
@@ -532,6 +583,7 @@ export async function checkFaithfulnessBatch(
532
583
  const callPromise = callFaithfulnessLlm(
533
584
  FAITHFULNESS_SYSTEM_PROMPT,
534
585
  userPrompt,
586
+ checkableInputs.length,
535
587
  config,
536
588
  localLlm,
537
589
  fallbackLlm,
@@ -17,7 +17,7 @@
17
17
  * `config.ts` spread the result in one line.
18
18
  */
19
19
 
20
- import { coerceNumber } from "./connectors/coerce.js";
20
+ import { coerceBool, coerceNumber } from "./connectors/coerce.js";
21
21
 
22
22
  /**
23
23
  * Parse a value as a strict integer >= `min`, throwing on anything else.
@@ -67,6 +67,7 @@ export function parseFaithfulnessGateConfig(cfg: Record<string, unknown>): {
67
67
  extractionFaithfulnessBaseUrl: string;
68
68
  extractionFaithfulnessContextChars: number;
69
69
  extractionFaithfulnessTimeoutMs: number;
70
+ extractionFaithfulnessLocalParseFallback: boolean;
70
71
  } {
71
72
  const gateValue = cfg.extractionFaithfulnessGate;
72
73
  let extractionFaithfulnessGate: "off" | "shadow" | "enforce";
@@ -105,12 +106,26 @@ export function parseFaithfulnessGateConfig(cfg: Record<string, unknown>): {
105
106
  60_000,
106
107
  );
107
108
 
109
+ // Issue #1700 nit #6: resilient-fallback toggle for the local model-lab
110
+ // endpoint. Default false preserves the loud-signal behavior (a 200-with-
111
+ // garbage local response surfaces malformed_output so a misconfigured
112
+ // endpoint is visible); true falls back to the configured chain on local
113
+ // parse failure. Byte-identical pre-feature when unset (rule 39).
114
+ // coerceBool for CLI-string parity: a CLI override like
115
+ // --config extractionFaithfulnessLocalParseFallback=true reaches here as the
116
+ // string "true"; === true would silently leave the flag disabled. Matches
117
+ // every other CLI-settable boolean in config.ts (gotcha 36, codex P2
118
+ // PRRT_kwDORJXyws6O6gDQ / cursor BUGBOT e273b22c).
119
+ const extractionFaithfulnessLocalParseFallback =
120
+ coerceBool(cfg.extractionFaithfulnessLocalParseFallback) === true;
121
+
108
122
  return {
109
123
  extractionFaithfulnessGate,
110
124
  extractionFaithfulnessModel,
111
125
  extractionFaithfulnessBaseUrl,
112
126
  extractionFaithfulnessContextChars,
113
127
  extractionFaithfulnessTimeoutMs,
128
+ extractionFaithfulnessLocalParseFallback,
114
129
  };
115
130
  }
116
131
 
@@ -42,10 +42,15 @@ export interface EndpointChatMessage {
42
42
 
43
43
  /** Options for a single local-endpoint chat call. */
44
44
  export interface EndpointChatOptions {
45
- /** Per-call timeout in ms. The caller's batch AbortSignal is NOT forwarded
46
- * (matches the local-LLM precedent: each attempt is bounded by its own
47
- * timer so one slow server can't starve the batch). */
45
+ /** Per-call timeout in ms. Bounds each attempt so one slow server can't
46
+ * starve the batch (matches the local-LLM precedent). */
48
47
  timeoutMs: number;
48
+ /** Optional batch AbortSignal forwarded into the probe (issue #1700 nit #5).
49
+ * When the batch budget elapses (or the caller aborts), the in-flight probe
50
+ * aborts immediately instead of waiting for its own `timeoutMs` — a
51
+ * micro-optimization, not a correctness fix (the outer race already bounds
52
+ * the total). Undefined preserves the prior per-attempt-only behavior. */
53
+ signal?: AbortSignal;
49
54
  /** Sampling temperature — classification wants low entropy (0.0–0.2). */
50
55
  temperature?: number;
51
56
  /** Max output tokens. */
@@ -119,6 +124,21 @@ export async function callOpenAiCompatibleChat(
119
124
  const url = joinBaseUrl(endpoint.baseUrl, "/chat/completions");
120
125
  const controller = new AbortController();
121
126
  const timer = setTimeout(() => controller.abort(), options.timeoutMs);
127
+ // Forward the batch AbortSignal into the probe (issue #1700 nit #5): if the
128
+ // batch budget already elapsed, abort the in-flight probe immediately rather
129
+ // than waiting for the probe's own timeoutMs. AbortSignal.any is ES2024
130
+ // (forbidden here); wire the listener manually + guard the already-aborted
131
+ // case. The fetch still uses the probe's controller, so clearing the timer
132
+ // below is the only cleanup needed.
133
+ const batchSignal = options.signal;
134
+ const onBatchAbort = () => controller.abort();
135
+ if (batchSignal) {
136
+ if (batchSignal.aborted) {
137
+ controller.abort();
138
+ } else {
139
+ batchSignal.addEventListener("abort", onBatchAbort, { once: true });
140
+ }
141
+ }
122
142
  try {
123
143
  const body: Record<string, unknown> = {
124
144
  model: endpoint.model,
@@ -150,6 +170,7 @@ export async function callOpenAiCompatibleChat(
150
170
  return { content, modelUsed: extractModel(parsed) ?? endpoint.model };
151
171
  } finally {
152
172
  clearTimeout(timer);
173
+ if (batchSignal) batchSignal.removeEventListener("abort", onBatchAbort);
153
174
  }
154
175
  }
155
176