agent-nuvira 3.3.12 → 3.3.14

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 (458) hide show
  1. package/README.md +67 -1
  2. package/dist/agents/agents/planner.d.ts.map +1 -1
  3. package/dist/agents/agents/planner.js +9 -0
  4. package/dist/agents/agents/planner.js.map +1 -1
  5. package/dist/agents/agents/reasoner.d.ts.map +1 -1
  6. package/dist/agents/agents/reasoner.js +13 -0
  7. package/dist/agents/agents/reasoner.js.map +1 -1
  8. package/dist/agents/orchestrator.d.ts +31 -0
  9. package/dist/agents/orchestrator.d.ts.map +1 -1
  10. package/dist/agents/orchestrator.js +103 -135
  11. package/dist/agents/orchestrator.js.map +1 -1
  12. package/dist/cli/chat.d.ts +89 -0
  13. package/dist/cli/chat.d.ts.map +1 -1
  14. package/dist/cli/chat.js +798 -39
  15. package/dist/cli/chat.js.map +1 -1
  16. package/dist/cli/cli-program.d.ts.map +1 -1
  17. package/dist/cli/cli-program.js +20 -0
  18. package/dist/cli/cli-program.js.map +1 -1
  19. package/dist/cli/config.d.ts +12 -0
  20. package/dist/cli/config.d.ts.map +1 -1
  21. package/dist/cli/config.js +107 -3
  22. package/dist/cli/config.js.map +1 -1
  23. package/dist/cli/decisions.d.ts +28 -0
  24. package/dist/cli/decisions.d.ts.map +1 -0
  25. package/dist/cli/decisions.js +140 -0
  26. package/dist/cli/decisions.js.map +1 -0
  27. package/dist/cli/doctor.d.ts +6 -0
  28. package/dist/cli/doctor.d.ts.map +1 -1
  29. package/dist/cli/doctor.js +46 -0
  30. package/dist/cli/doctor.js.map +1 -1
  31. package/dist/cli/execute.d.ts.map +1 -1
  32. package/dist/cli/execute.js +6 -0
  33. package/dist/cli/execute.js.map +1 -1
  34. package/dist/cli/failover-runner.d.ts.map +1 -1
  35. package/dist/cli/failover-runner.js +19 -1
  36. package/dist/cli/failover-runner.js.map +1 -1
  37. package/dist/cli/feedback.d.ts.map +1 -1
  38. package/dist/cli/feedback.js +5 -1
  39. package/dist/cli/feedback.js.map +1 -1
  40. package/dist/cli/hooks.d.ts +30 -0
  41. package/dist/cli/hooks.d.ts.map +1 -0
  42. package/dist/cli/hooks.js +224 -0
  43. package/dist/cli/hooks.js.map +1 -0
  44. package/dist/cli/knowledge.d.ts +31 -0
  45. package/dist/cli/knowledge.d.ts.map +1 -0
  46. package/dist/cli/knowledge.js +223 -0
  47. package/dist/cli/knowledge.js.map +1 -0
  48. package/dist/cli/loop-executor.d.ts.map +1 -1
  49. package/dist/cli/loop-executor.js +92 -18
  50. package/dist/cli/loop-executor.js.map +1 -1
  51. package/dist/cli/model.d.ts +20 -0
  52. package/dist/cli/model.d.ts.map +1 -1
  53. package/dist/cli/model.js +294 -14
  54. package/dist/cli/model.js.map +1 -1
  55. package/dist/cli/omniroute-control.d.ts +88 -0
  56. package/dist/cli/omniroute-control.d.ts.map +1 -0
  57. package/dist/cli/omniroute-control.js +179 -0
  58. package/dist/cli/omniroute-control.js.map +1 -0
  59. package/dist/cli/omniroute.d.ts +21 -0
  60. package/dist/cli/omniroute.d.ts.map +1 -0
  61. package/dist/cli/omniroute.js +67 -0
  62. package/dist/cli/omniroute.js.map +1 -0
  63. package/dist/cli/rate.d.ts +78 -0
  64. package/dist/cli/rate.d.ts.map +1 -0
  65. package/dist/cli/rate.js +294 -0
  66. package/dist/cli/rate.js.map +1 -0
  67. package/dist/config/process-env.d.ts.map +1 -1
  68. package/dist/config/process-env.js +10 -0
  69. package/dist/config/process-env.js.map +1 -1
  70. package/dist/config/types.d.ts +8 -0
  71. package/dist/config/types.d.ts.map +1 -1
  72. package/dist/context/history.d.ts +21 -0
  73. package/dist/context/history.d.ts.map +1 -1
  74. package/dist/context/history.js +50 -0
  75. package/dist/context/history.js.map +1 -1
  76. package/dist/enterprise/vault.d.ts.map +1 -1
  77. package/dist/enterprise/vault.js +5 -21
  78. package/dist/enterprise/vault.js.map +1 -1
  79. package/dist/gateway/hook-contract.d.ts +206 -0
  80. package/dist/gateway/hook-contract.d.ts.map +1 -0
  81. package/dist/gateway/hook-contract.js +449 -0
  82. package/dist/gateway/hook-contract.js.map +1 -0
  83. package/dist/gateway/hooks.d.ts.map +1 -1
  84. package/dist/gateway/hooks.js +14 -0
  85. package/dist/gateway/hooks.js.map +1 -1
  86. package/dist/inference/gemini-adapter.d.ts.map +1 -1
  87. package/dist/inference/gemini-adapter.js +4 -0
  88. package/dist/inference/gemini-adapter.js.map +1 -1
  89. package/dist/inference/interface.d.ts +54 -0
  90. package/dist/inference/interface.d.ts.map +1 -1
  91. package/dist/inference/model-id-validation.d.ts +57 -0
  92. package/dist/inference/model-id-validation.d.ts.map +1 -0
  93. package/dist/inference/model-id-validation.js +127 -0
  94. package/dist/inference/model-id-validation.js.map +1 -0
  95. package/dist/inference/native-tools.d.ts +16 -1
  96. package/dist/inference/native-tools.d.ts.map +1 -1
  97. package/dist/inference/native-tools.js +34 -10
  98. package/dist/inference/native-tools.js.map +1 -1
  99. package/dist/inference/openai-compat-adapter.d.ts.map +1 -1
  100. package/dist/inference/openai-compat-adapter.js +12 -1
  101. package/dist/inference/openai-compat-adapter.js.map +1 -1
  102. package/dist/inference/provider-catalog.d.ts +11 -0
  103. package/dist/inference/provider-catalog.d.ts.map +1 -1
  104. package/dist/inference/provider-catalog.js +80 -0
  105. package/dist/inference/provider-catalog.js.map +1 -1
  106. package/dist/inference/route-resolver.d.ts +32 -8
  107. package/dist/inference/route-resolver.d.ts.map +1 -1
  108. package/dist/inference/route-resolver.js +175 -1
  109. package/dist/inference/route-resolver.js.map +1 -1
  110. package/dist/inference/tool-call-utils.d.ts.map +1 -1
  111. package/dist/inference/tool-call-utils.js +10 -8
  112. package/dist/inference/tool-call-utils.js.map +1 -1
  113. package/dist/inference/tools.d.ts +17 -1
  114. package/dist/inference/tools.d.ts.map +1 -1
  115. package/dist/inference/tools.js +69 -19
  116. package/dist/inference/tools.js.map +1 -1
  117. package/dist/learning/acceptance-model.d.ts +247 -0
  118. package/dist/learning/acceptance-model.d.ts.map +1 -0
  119. package/dist/learning/acceptance-model.js +708 -0
  120. package/dist/learning/acceptance-model.js.map +1 -0
  121. package/dist/learning/auto-router.d.ts +121 -24
  122. package/dist/learning/auto-router.d.ts.map +1 -1
  123. package/dist/learning/auto-router.js +374 -96
  124. package/dist/learning/auto-router.js.map +1 -1
  125. package/dist/learning/autonomy-policy.d.ts +12 -16
  126. package/dist/learning/autonomy-policy.d.ts.map +1 -1
  127. package/dist/learning/autonomy-policy.js +50 -48
  128. package/dist/learning/autonomy-policy.js.map +1 -1
  129. package/dist/learning/behavioural-labels.d.ts +86 -0
  130. package/dist/learning/behavioural-labels.d.ts.map +1 -0
  131. package/dist/learning/behavioural-labels.js +166 -0
  132. package/dist/learning/behavioural-labels.js.map +1 -0
  133. package/dist/learning/capability-evidence.d.ts +167 -0
  134. package/dist/learning/capability-evidence.d.ts.map +1 -0
  135. package/dist/learning/capability-evidence.js +210 -0
  136. package/dist/learning/capability-evidence.js.map +1 -0
  137. package/dist/learning/capability-readiness.d.ts +64 -0
  138. package/dist/learning/capability-readiness.d.ts.map +1 -0
  139. package/dist/learning/capability-readiness.js +74 -0
  140. package/dist/learning/capability-readiness.js.map +1 -0
  141. package/dist/learning/capability-types.d.ts +169 -0
  142. package/dist/learning/capability-types.d.ts.map +1 -0
  143. package/dist/learning/capability-types.js +20 -0
  144. package/dist/learning/capability-types.js.map +1 -0
  145. package/dist/learning/catalog-feed.d.ts +89 -0
  146. package/dist/learning/catalog-feed.d.ts.map +1 -0
  147. package/dist/learning/catalog-feed.js +211 -0
  148. package/dist/learning/catalog-feed.js.map +1 -0
  149. package/dist/learning/cli-intent-effects.d.ts +60 -0
  150. package/dist/learning/cli-intent-effects.d.ts.map +1 -0
  151. package/dist/learning/cli-intent-effects.js +153 -0
  152. package/dist/learning/cli-intent-effects.js.map +1 -0
  153. package/dist/learning/command-adaptation.d.ts +41 -0
  154. package/dist/learning/command-adaptation.d.ts.map +1 -0
  155. package/dist/learning/command-adaptation.js +122 -0
  156. package/dist/learning/command-adaptation.js.map +1 -0
  157. package/dist/learning/consent-picture.d.ts +42 -0
  158. package/dist/learning/consent-picture.d.ts.map +1 -0
  159. package/dist/learning/consent-picture.js +53 -0
  160. package/dist/learning/consent-picture.js.map +1 -0
  161. package/dist/learning/context-budget.d.ts +37 -6
  162. package/dist/learning/context-budget.d.ts.map +1 -1
  163. package/dist/learning/context-budget.js +38 -9
  164. package/dist/learning/context-budget.js.map +1 -1
  165. package/dist/learning/decision-log.d.ts +114 -0
  166. package/dist/learning/decision-log.d.ts.map +1 -0
  167. package/dist/learning/decision-log.js +296 -0
  168. package/dist/learning/decision-log.js.map +1 -0
  169. package/dist/learning/deliverable-class.d.ts.map +1 -1
  170. package/dist/learning/deliverable-class.js +19 -0
  171. package/dist/learning/deliverable-class.js.map +1 -1
  172. package/dist/learning/deliverable-corpus.d.ts +117 -0
  173. package/dist/learning/deliverable-corpus.d.ts.map +1 -0
  174. package/dist/learning/deliverable-corpus.js +176 -0
  175. package/dist/learning/deliverable-corpus.js.map +1 -0
  176. package/dist/learning/error-repair.d.ts +45 -2
  177. package/dist/learning/error-repair.d.ts.map +1 -1
  178. package/dist/learning/error-repair.js +95 -0
  179. package/dist/learning/error-repair.js.map +1 -1
  180. package/dist/learning/exhaustion-report.d.ts +136 -0
  181. package/dist/learning/exhaustion-report.d.ts.map +1 -0
  182. package/dist/learning/exhaustion-report.js +239 -0
  183. package/dist/learning/exhaustion-report.js.map +1 -0
  184. package/dist/learning/failure-bookkeeping.d.ts +16 -0
  185. package/dist/learning/failure-bookkeeping.d.ts.map +1 -1
  186. package/dist/learning/failure-bookkeeping.js +51 -5
  187. package/dist/learning/failure-bookkeeping.js.map +1 -1
  188. package/dist/learning/gate-friction.d.ts +39 -0
  189. package/dist/learning/gate-friction.d.ts.map +1 -0
  190. package/dist/learning/gate-friction.js +91 -0
  191. package/dist/learning/gate-friction.js.map +1 -0
  192. package/dist/learning/harness-narration.d.ts +34 -0
  193. package/dist/learning/harness-narration.d.ts.map +1 -0
  194. package/dist/learning/harness-narration.js +58 -0
  195. package/dist/learning/harness-narration.js.map +1 -0
  196. package/dist/learning/hub-skill-catalog.d.ts.map +1 -1
  197. package/dist/learning/hub-skill-catalog.js +1 -2
  198. package/dist/learning/hub-skill-catalog.js.map +1 -1
  199. package/dist/learning/hybrid-router.d.ts +65 -0
  200. package/dist/learning/hybrid-router.d.ts.map +1 -1
  201. package/dist/learning/hybrid-router.js +186 -0
  202. package/dist/learning/hybrid-router.js.map +1 -1
  203. package/dist/learning/knowledge-base.d.ts +350 -0
  204. package/dist/learning/knowledge-base.d.ts.map +1 -0
  205. package/dist/learning/knowledge-base.js +906 -0
  206. package/dist/learning/knowledge-base.js.map +1 -0
  207. package/dist/learning/knowledge-eval.d.ts +171 -0
  208. package/dist/learning/knowledge-eval.d.ts.map +1 -0
  209. package/dist/learning/knowledge-eval.js +196 -0
  210. package/dist/learning/knowledge-eval.js.map +1 -0
  211. package/dist/learning/knowledge-turn.d.ts +92 -0
  212. package/dist/learning/knowledge-turn.d.ts.map +1 -0
  213. package/dist/learning/knowledge-turn.js +190 -0
  214. package/dist/learning/knowledge-turn.js.map +1 -0
  215. package/dist/learning/learned-commands.d.ts +64 -0
  216. package/dist/learning/learned-commands.d.ts.map +1 -0
  217. package/dist/learning/learned-commands.js +108 -0
  218. package/dist/learning/learned-commands.js.map +1 -0
  219. package/dist/learning/lexical-search.d.ts +50 -0
  220. package/dist/learning/lexical-search.d.ts.map +1 -0
  221. package/dist/learning/lexical-search.js +111 -0
  222. package/dist/learning/lexical-search.js.map +1 -0
  223. package/dist/learning/machine-facts.d.ts +71 -0
  224. package/dist/learning/machine-facts.d.ts.map +1 -0
  225. package/dist/learning/machine-facts.js +164 -0
  226. package/dist/learning/machine-facts.js.map +1 -0
  227. package/dist/learning/markdown-outline.d.ts +73 -0
  228. package/dist/learning/markdown-outline.d.ts.map +1 -0
  229. package/dist/learning/markdown-outline.js +136 -0
  230. package/dist/learning/markdown-outline.js.map +1 -0
  231. package/dist/learning/model-first-router.d.ts +33 -1
  232. package/dist/learning/model-first-router.d.ts.map +1 -1
  233. package/dist/learning/model-first-router.js +123 -9
  234. package/dist/learning/model-first-router.js.map +1 -1
  235. package/dist/learning/model-identity.d.ts +80 -0
  236. package/dist/learning/model-identity.d.ts.map +1 -0
  237. package/dist/learning/model-identity.js +115 -0
  238. package/dist/learning/model-identity.js.map +1 -0
  239. package/dist/learning/model-registry.d.ts +141 -2
  240. package/dist/learning/model-registry.d.ts.map +1 -1
  241. package/dist/learning/model-registry.js +324 -38
  242. package/dist/learning/model-registry.js.map +1 -1
  243. package/dist/learning/model-scoring.d.ts.map +1 -1
  244. package/dist/learning/model-scoring.js +8 -1
  245. package/dist/learning/model-scoring.js.map +1 -1
  246. package/dist/learning/model-selection.d.ts +2 -1
  247. package/dist/learning/model-selection.d.ts.map +1 -1
  248. package/dist/learning/model-selection.js +76 -2
  249. package/dist/learning/model-selection.js.map +1 -1
  250. package/dist/learning/outcome-observation.d.ts +49 -0
  251. package/dist/learning/outcome-observation.d.ts.map +1 -0
  252. package/dist/learning/outcome-observation.js +95 -0
  253. package/dist/learning/outcome-observation.js.map +1 -0
  254. package/dist/learning/pair-entitlement.d.ts +184 -0
  255. package/dist/learning/pair-entitlement.d.ts.map +1 -0
  256. package/dist/learning/pair-entitlement.js +214 -0
  257. package/dist/learning/pair-entitlement.js.map +1 -0
  258. package/dist/learning/project-docs.d.ts +83 -0
  259. package/dist/learning/project-docs.d.ts.map +1 -0
  260. package/dist/learning/project-docs.js +132 -0
  261. package/dist/learning/project-docs.js.map +1 -0
  262. package/dist/learning/provider-fallback.d.ts +23 -1
  263. package/dist/learning/provider-fallback.d.ts.map +1 -1
  264. package/dist/learning/provider-fallback.js +24 -0
  265. package/dist/learning/provider-fallback.js.map +1 -1
  266. package/dist/learning/quota-ledger.d.ts +13 -0
  267. package/dist/learning/quota-ledger.d.ts.map +1 -1
  268. package/dist/learning/quota-ledger.js +28 -0
  269. package/dist/learning/quota-ledger.js.map +1 -1
  270. package/dist/learning/reasoning-trace.d.ts +65 -1
  271. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  272. package/dist/learning/reasoning-trace.js +123 -1
  273. package/dist/learning/reasoning-trace.js.map +1 -1
  274. package/dist/learning/requirement-probe.d.ts +55 -0
  275. package/dist/learning/requirement-probe.d.ts.map +1 -0
  276. package/dist/learning/requirement-probe.js +77 -0
  277. package/dist/learning/requirement-probe.js.map +1 -0
  278. package/dist/learning/resilient-call.d.ts +20 -1
  279. package/dist/learning/resilient-call.d.ts.map +1 -1
  280. package/dist/learning/resilient-call.js +42 -4
  281. package/dist/learning/resilient-call.js.map +1 -1
  282. package/dist/learning/response-usability.d.ts +85 -0
  283. package/dist/learning/response-usability.d.ts.map +1 -0
  284. package/dist/learning/response-usability.js +135 -0
  285. package/dist/learning/response-usability.js.map +1 -0
  286. package/dist/learning/router-bandit.d.ts +132 -0
  287. package/dist/learning/router-bandit.d.ts.map +1 -1
  288. package/dist/learning/router-bandit.js +253 -0
  289. package/dist/learning/router-bandit.js.map +1 -1
  290. package/dist/learning/routing-history.d.ts +41 -2
  291. package/dist/learning/routing-history.d.ts.map +1 -1
  292. package/dist/learning/routing-history.js.map +1 -1
  293. package/dist/learning/run-trace.d.ts +21 -0
  294. package/dist/learning/run-trace.d.ts.map +1 -1
  295. package/dist/learning/run-trace.js +38 -0
  296. package/dist/learning/run-trace.js.map +1 -1
  297. package/dist/learning/seeded-benchmark.d.ts +14 -0
  298. package/dist/learning/seeded-benchmark.d.ts.map +1 -1
  299. package/dist/learning/seeded-benchmark.js +43 -4
  300. package/dist/learning/seeded-benchmark.js.map +1 -1
  301. package/dist/learning/session-grant.d.ts +75 -0
  302. package/dist/learning/session-grant.d.ts.map +1 -0
  303. package/dist/learning/session-grant.js +112 -0
  304. package/dist/learning/session-grant.js.map +1 -0
  305. package/dist/learning/task-model-continuity.d.ts +73 -0
  306. package/dist/learning/task-model-continuity.d.ts.map +1 -0
  307. package/dist/learning/task-model-continuity.js +87 -0
  308. package/dist/learning/task-model-continuity.js.map +1 -0
  309. package/dist/learning/turn-feedback.d.ts +93 -0
  310. package/dist/learning/turn-feedback.d.ts.map +1 -0
  311. package/dist/learning/turn-feedback.js +127 -0
  312. package/dist/learning/turn-feedback.js.map +1 -0
  313. package/dist/learning/turn-report.d.ts +104 -2
  314. package/dist/learning/turn-report.d.ts.map +1 -1
  315. package/dist/learning/turn-report.js +161 -6
  316. package/dist/learning/turn-report.js.map +1 -1
  317. package/dist/learning/verdict-routing.d.ts +51 -0
  318. package/dist/learning/verdict-routing.d.ts.map +1 -0
  319. package/dist/learning/verdict-routing.js +122 -0
  320. package/dist/learning/verdict-routing.js.map +1 -0
  321. package/dist/mcp/client.d.ts.map +1 -1
  322. package/dist/mcp/client.js +5 -0
  323. package/dist/mcp/client.js.map +1 -1
  324. package/dist/mcp/manager.d.ts +10 -0
  325. package/dist/mcp/manager.d.ts.map +1 -1
  326. package/dist/mcp/manager.js +20 -0
  327. package/dist/mcp/manager.js.map +1 -1
  328. package/dist/mcp/mcp-discovery.d.ts +34 -0
  329. package/dist/mcp/mcp-discovery.d.ts.map +1 -0
  330. package/dist/mcp/mcp-discovery.js +71 -0
  331. package/dist/mcp/mcp-discovery.js.map +1 -0
  332. package/dist/mcp/mcp-schema-cache.d.ts +17 -0
  333. package/dist/mcp/mcp-schema-cache.d.ts.map +1 -1
  334. package/dist/mcp/mcp-schema-cache.js +47 -3
  335. package/dist/mcp/mcp-schema-cache.js.map +1 -1
  336. package/dist/mcp/types.d.ts +28 -0
  337. package/dist/mcp/types.d.ts.map +1 -1
  338. package/dist/mcp/types.js.map +1 -1
  339. package/dist/security/secret-scan.d.ts +156 -0
  340. package/dist/security/secret-scan.d.ts.map +1 -0
  341. package/dist/security/secret-scan.js +525 -0
  342. package/dist/security/secret-scan.js.map +1 -0
  343. package/dist/tools/ask-user.d.ts.map +1 -1
  344. package/dist/tools/ask-user.js +4 -0
  345. package/dist/tools/ask-user.js.map +1 -1
  346. package/dist/tools/capability-registry.d.ts +150 -0
  347. package/dist/tools/capability-registry.d.ts.map +1 -0
  348. package/dist/tools/capability-registry.js +400 -0
  349. package/dist/tools/capability-registry.js.map +1 -0
  350. package/dist/tools/child-agent-runtime.d.ts.map +1 -1
  351. package/dist/tools/child-agent-runtime.js +19 -5
  352. package/dist/tools/child-agent-runtime.js.map +1 -1
  353. package/dist/tools/coding-tools.d.ts +17 -5
  354. package/dist/tools/coding-tools.d.ts.map +1 -1
  355. package/dist/tools/coding-tools.js +80 -11
  356. package/dist/tools/coding-tools.js.map +1 -1
  357. package/dist/tools/command-memo.d.ts +116 -0
  358. package/dist/tools/command-memo.d.ts.map +1 -0
  359. package/dist/tools/command-memo.js +202 -0
  360. package/dist/tools/command-memo.js.map +1 -0
  361. package/dist/tools/git-tool.d.ts.map +1 -1
  362. package/dist/tools/git-tool.js +13 -3
  363. package/dist/tools/git-tool.js.map +1 -1
  364. package/dist/tools/harness-directive.d.ts +42 -0
  365. package/dist/tools/harness-directive.d.ts.map +1 -0
  366. package/dist/tools/harness-directive.js +45 -0
  367. package/dist/tools/harness-directive.js.map +1 -0
  368. package/dist/tools/loop-skill-hint.d.ts +74 -174
  369. package/dist/tools/loop-skill-hint.d.ts.map +1 -1
  370. package/dist/tools/loop-skill-hint.js +110 -536
  371. package/dist/tools/loop-skill-hint.js.map +1 -1
  372. package/dist/tools/mcp-client-tool.d.ts +2 -0
  373. package/dist/tools/mcp-client-tool.d.ts.map +1 -1
  374. package/dist/tools/mcp-client-tool.js +3 -0
  375. package/dist/tools/mcp-client-tool.js.map +1 -1
  376. package/dist/tools/modality/shared.d.ts +1 -4
  377. package/dist/tools/modality/shared.d.ts.map +1 -1
  378. package/dist/tools/modality/shared.js +7 -22
  379. package/dist/tools/modality/shared.js.map +1 -1
  380. package/dist/tools/plan-store.d.ts +44 -4
  381. package/dist/tools/plan-store.d.ts.map +1 -1
  382. package/dist/tools/plan-store.js +93 -9
  383. package/dist/tools/plan-store.js.map +1 -1
  384. package/dist/tools/registry.d.ts +76 -3
  385. package/dist/tools/registry.d.ts.map +1 -1
  386. package/dist/tools/registry.js +623 -15
  387. package/dist/tools/registry.js.map +1 -1
  388. package/dist/tools/remediation-ladder.d.ts +91 -0
  389. package/dist/tools/remediation-ladder.d.ts.map +1 -0
  390. package/dist/tools/remediation-ladder.js +316 -0
  391. package/dist/tools/remediation-ladder.js.map +1 -0
  392. package/dist/tools/run-cli.d.ts.map +1 -1
  393. package/dist/tools/run-cli.js +29 -4
  394. package/dist/tools/run-cli.js.map +1 -1
  395. package/dist/tools/run-terminal.d.ts +11 -0
  396. package/dist/tools/run-terminal.d.ts.map +1 -1
  397. package/dist/tools/run-terminal.js +289 -10
  398. package/dist/tools/run-terminal.js.map +1 -1
  399. package/dist/tools/skills-hub.d.ts +7 -0
  400. package/dist/tools/skills-hub.d.ts.map +1 -1
  401. package/dist/tools/skills-hub.js +11 -2
  402. package/dist/tools/skills-hub.js.map +1 -1
  403. package/dist/tools/step-artifact.d.ts +35 -0
  404. package/dist/tools/step-artifact.d.ts.map +1 -0
  405. package/dist/tools/step-artifact.js +154 -0
  406. package/dist/tools/step-artifact.js.map +1 -0
  407. package/dist/tools/tool-loop.d.ts +327 -13
  408. package/dist/tools/tool-loop.d.ts.map +1 -1
  409. package/dist/tools/tool-loop.js +891 -66
  410. package/dist/tools/tool-loop.js.map +1 -1
  411. package/dist/tools/toolsets.js +4 -4
  412. package/dist/tools/toolsets.js.map +1 -1
  413. package/dist/tools/worktree.d.ts +11 -2
  414. package/dist/tools/worktree.d.ts.map +1 -1
  415. package/dist/tools/worktree.js +11 -2
  416. package/dist/tools/worktree.js.map +1 -1
  417. package/dist/utils/atomic-store.d.ts +53 -0
  418. package/dist/utils/atomic-store.d.ts.map +1 -0
  419. package/dist/utils/atomic-store.js +178 -0
  420. package/dist/utils/atomic-store.js.map +1 -0
  421. package/dist/utils/binary-probe.d.ts +25 -0
  422. package/dist/utils/binary-probe.d.ts.map +1 -0
  423. package/dist/utils/binary-probe.js +48 -0
  424. package/dist/utils/binary-probe.js.map +1 -0
  425. package/dist/utils/workspace-path.d.ts +50 -0
  426. package/dist/utils/workspace-path.d.ts.map +1 -0
  427. package/dist/utils/workspace-path.js +86 -0
  428. package/dist/utils/workspace-path.js.map +1 -0
  429. package/dist/web-dashboard/chat-console.d.ts +24 -0
  430. package/dist/web-dashboard/chat-console.d.ts.map +1 -1
  431. package/dist/web-dashboard/chat-console.js +38 -0
  432. package/dist/web-dashboard/chat-console.js.map +1 -1
  433. package/dist/web-dashboard/project-context.d.ts +27 -2
  434. package/dist/web-dashboard/project-context.d.ts.map +1 -1
  435. package/dist/web-dashboard/project-context.js +32 -2
  436. package/dist/web-dashboard/project-context.js.map +1 -1
  437. package/dist/web-dashboard/server.d.ts +40 -0
  438. package/dist/web-dashboard/server.d.ts.map +1 -1
  439. package/dist/web-dashboard/server.js +733 -15
  440. package/dist/web-dashboard/server.js.map +1 -1
  441. package/dist/web-dashboard/src/types.d.ts +236 -1
  442. package/dist/web-dashboard/src/types.d.ts.map +1 -1
  443. package/dist/web-dashboard/workspace-guard.d.ts +18 -2
  444. package/dist/web-dashboard/workspace-guard.d.ts.map +1 -1
  445. package/dist/web-dashboard/workspace-guard.js +94 -12
  446. package/dist/web-dashboard/workspace-guard.js.map +1 -1
  447. package/dist/web-dashboard/workspace-resolution.d.ts +87 -0
  448. package/dist/web-dashboard/workspace-resolution.d.ts.map +1 -0
  449. package/dist/web-dashboard/workspace-resolution.js +105 -0
  450. package/dist/web-dashboard/workspace-resolution.js.map +1 -0
  451. package/package.json +1 -1
  452. package/src/web-dashboard/public/assets/index-tK8Y2qHB.css +1 -0
  453. package/src/web-dashboard/public/assets/index-zjbdJnO-.js +210 -0
  454. package/src/web-dashboard/public/assets/index-zjbdJnO-.js.map +1 -0
  455. package/src/web-dashboard/public/index.html +2 -2
  456. package/src/web-dashboard/public/assets/index-BrZcYhy6.js +0 -207
  457. package/src/web-dashboard/public/assets/index-BrZcYhy6.js.map +0 -1
  458. package/src/web-dashboard/public/assets/index-DsWczTa6.css +0 -1
@@ -37,10 +37,12 @@
37
37
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
38
38
  import { envBuff, resolveNuviraHome } from '../config/paths.js';
39
39
  import { formatCount } from '../utils/format.js';
40
+ import { stableJson, withFileLockSync, writeFileAtomicSync } from '../utils/atomic-store.js';
40
41
  import { join } from 'node:path';
41
42
  import { getVectorStore } from '../memory/vector-store.js';
42
43
  import { reasoningCapabilityFromAdvertised } from '../inference/reasoning-effort.js';
43
44
  import { isNonChatModel } from '../inference/model-catalog.js';
45
+ import { emptyCapabilityRecord, foldCallOutcome, foldLatency, foldVerification, } from './capability-evidence.js';
44
46
  import { getQuotaLedger } from './quota-ledger.js';
45
47
  import { getEventBus, EventNames } from '../observability/event-bus.js';
46
48
  import { appendChainedRecordFast, rechainRecords, writeHeadState, headOfLines, } from '../enterprise/audit-chain.js';
@@ -73,6 +75,14 @@ export const DEGRADED_UNAVAILABLE_THRESHOLD = 3;
73
75
  * permanently penalizes a recovered model in preferredModelsFor().
74
76
  */
75
77
  export const ERROR_RATE_HEAL_STEP = 0.1;
78
+ /**
79
+ * How long a pair that resolved with an UNUSABLE (empty) response is parked.
80
+ *
81
+ * fix_model_routing P1/P2. Short enough to self-heal (a genuinely transient
82
+ * empty), long enough that the same broken pair is not re-picked on the next
83
+ * turn. Model-scoped: the provider's healthy siblings stay routable.
84
+ */
85
+ export const EMPTY_RESPONSE_PARK_MS = 120_000;
76
86
  function memoryDir() {
77
87
  return envBuff('MEMORY_DIR') || DEFAULT_MEMORY_DIR;
78
88
  }
@@ -114,13 +124,6 @@ function actionLogPath() {
114
124
  function entryKey(provider, model) {
115
125
  return `${provider}|${model || 'default'}`;
116
126
  }
117
- /**
118
- * Does an `unavailable` REASON mean the provider/model pair cannot exist?
119
- *
120
- * Deliberately narrow: it must be an explicit "this model does not exist on
121
- * this endpoint", never a permission/per-account answer (a 403 or a quota
122
- * denial can be repaired, so the pair must stay reachable).
123
- */
124
127
  /**
125
128
  * Does an `unavailable` REASON mean the provider/model pair cannot exist?
126
129
  *
@@ -130,19 +133,59 @@ function entryKey(provider, model) {
130
133
  */
131
134
  const MODEL_NOT_FOUND_REASON_RE = /model not found|not in live model list|does not exist|no such model|model_not_found/i;
132
135
  /**
133
- * Is this entry a DEAD PAIR?
136
+ * Is this entry a NONEXISTENT pair — the provider cannot serve this id at all?
134
137
  *
135
138
  * The flag is the primary signal, but the reason check heals data written
136
139
  * BEFORE the flag existed (and by any surface): an entry sitting at
137
140
  * `unavailable` with a not-found reason IS a retired pair, and without this the
138
141
  * live `local/gemini-3.1-flash-lite` entry would keep being offered until it
139
142
  * failed one more time.
143
+ *
144
+ * EXPORTED for `learning/pair-entitlement.ts`, which needs the SAME verdict the
145
+ * dead-pair machinery uses. Two predicates for one question is how the registry
146
+ * and the router start disagreeing about which pairs are offerable.
140
147
  */
141
- function isDeadEntry(e) {
148
+ export function isNonexistentPair(e) {
142
149
  if (e.deadPair)
143
150
  return true;
144
151
  return e.status === 'unavailable' && MODEL_NOT_FOUND_REASON_RE.test(e.lastError ?? '');
145
152
  }
153
+ /**
154
+ * Does an `unavailable` REASON describe an ACCOUNT-level refusal that a fresh
155
+ * catalogue listing cannot refute?
156
+ *
157
+ * The complement of {@link MODEL_NOT_FOUND_REASON_RE}, and deliberately disjoint
158
+ * from it. "This model does not exist on this endpoint" IS refuted by the
159
+ * provider listing the model again — but "this account cannot pay for this
160
+ * call" and "this key is not authorized" are facts about the ACCOUNT, and a
161
+ * public model list says nothing about them. Collapsing the two is what let a
162
+ * metadata probe re-arm a pair `recordCall` had already proven dead (see the
163
+ * note in `markListed`).
164
+ *
165
+ * Narrow on purpose: transient refusals (`rate-limit`, quota parks, 5xx) are
166
+ * NOT entitlement failures — a listing, a park expiry or a recovered key may
167
+ * legitimately re-open those, and `quotaParkedUntil` already models the clock.
168
+ *
169
+ * EXPORTED for `learning/pair-entitlement.ts` — see `isNonexistentPair`.
170
+ */
171
+ export function isEntitlementFailure(lastError) {
172
+ if (!lastError)
173
+ return false;
174
+ if (MODEL_NOT_FOUND_REASON_RE.test(lastError))
175
+ return false;
176
+ const t = lastError.toLowerCase();
177
+ return (t.includes('credit-exhausted') ||
178
+ t.includes('insufficient credit') ||
179
+ t.includes('payment required') ||
180
+ t.includes('billing') ||
181
+ t.includes('unauthorized') ||
182
+ t.includes('forbidden') ||
183
+ t.includes('invalid api key') ||
184
+ t.includes('invalid_api_key') ||
185
+ t.includes('authentication failed') ||
186
+ /\bauth\b/.test(t) ||
187
+ /\b40[123]\b/.test(t));
188
+ }
146
189
  /**
147
190
  * The config value `'default'` is a SENTINEL meaning "use the provider's
148
191
  * default model" — it is NOT a model id. Telemetry must never track it: a
@@ -156,6 +199,71 @@ export function isSentinelModel(model) {
156
199
  function emptyState() {
157
200
  return { version: CURRENT_VERSION, entries: {}, updatedAt: Date.now() };
158
201
  }
202
+ /** A deep copy of registry state, so a snapshot cannot be mutated through aliasing. */
203
+ function cloneRegistryData(data) {
204
+ return JSON.parse(JSON.stringify(data));
205
+ }
206
+ /**
207
+ * Merge three views of the registry into the one that should be on disk.
208
+ *
209
+ * The problem it solves, measured (2026-10-07): the singleton loads the mirror
210
+ * once at construction and `persist()` writes its WHOLE entry map back, so a
211
+ * long-lived process flushes a snapshot of every model it never touched —
212
+ * silently reverting what the dashboard, the gateway or a CLI run verified in
213
+ * the meantime. On this machine that flipped a strict-pin verdict from
214
+ * `credit-exhausted` back to `unverified` between two runs of one command, which
215
+ * re-armed the exact pre-flight F6 had just closed.
216
+ *
217
+ * The resolution needs no bookkeeping in the mutators, because `boot` already
218
+ * records what this process was told:
219
+ *
220
+ * - an entry this process CHANGED since boot (`memory` differs from `boot`) is
221
+ * this process's knowledge, and wins;
222
+ * - an entry it never touched is whatever `disk` now holds, because another
223
+ * process may have learned something about it;
224
+ * - an entry present at boot but ABSENT from `memory` was deliberately dropped
225
+ * (`pruneAbsentModels`), so the deletion wins — otherwise the prune would
226
+ * undo itself the moment the next persist re-added it from disk;
227
+ * - an entry in neither `boot` nor `memory` but on `disk` is a peer's new row,
228
+ * and is adopted.
229
+ *
230
+ * Comparing with `stableJson` (not a bare deep-equal) is deliberate: two entries
231
+ * built by different code paths can be structurally identical yet serialize with
232
+ * different key order, which would read as "changed" and clobber the peer.
233
+ */
234
+ export function mergeRegistryMirror(boot, memory, disk) {
235
+ const entries = {};
236
+ const keys = new Set([
237
+ ...Object.keys(boot.entries),
238
+ ...Object.keys(memory.entries),
239
+ ...Object.keys(disk.entries),
240
+ ]);
241
+ for (const key of keys) {
242
+ const mine = memory.entries[key];
243
+ const base = boot.entries[key];
244
+ const theirs = disk.entries[key];
245
+ if (!mine) {
246
+ if (base)
247
+ continue; // this process removed it — the deletion is the update
248
+ if (theirs)
249
+ entries[key] = theirs; // a peer's new row
250
+ continue;
251
+ }
252
+ const changedHere = !base || stableJson(mine) !== stableJson(base);
253
+ if (changedHere) {
254
+ entries[key] = mine;
255
+ continue;
256
+ }
257
+ // Unchanged HERE, so the file decides — including its ABSENCE. Re-adding a
258
+ // key the file no longer has would resurrect a peer's deletion (a prune, or
259
+ // a `reset`), which is the same lost-update bug pointing the other way.
260
+ if (theirs === undefined)
261
+ continue;
262
+ entries[key] = theirs;
263
+ }
264
+ // Scalars (version, updatedAt) come from this process; only the entry map merges.
265
+ return { ...disk, ...memory, entries };
266
+ }
159
267
  /**
160
268
  * Aggregate raw action-telemetry entries into the per-action dashboard view.
161
269
  * Pure + sync — the dashboard server calls this on the raw JSONL lines, and
@@ -303,6 +411,13 @@ export function aggregateActionTelemetry(entries, options = {}) {
303
411
  */
304
412
  export class ModelRegistry {
305
413
  data;
414
+ /**
415
+ * The registry as this process loaded it, kept so `persist()` can tell WHICH
416
+ * entries this process actually changed (see `mergeRegistryMirror`). Without
417
+ * it, a persist flushes every entry it merely READ back over whatever other
418
+ * processes have learned since — the measured lost-update bug.
419
+ */
420
+ bootSnapshot;
306
421
  /** Cached VectorStore for the enterprise mirror (null until first mirror). */
307
422
  vectorStore = null;
308
423
  /** Whether the vector mirror has been confirmed usable. */
@@ -311,6 +426,7 @@ export class ModelRegistry {
311
426
  actionLogCount = -1;
312
427
  constructor() {
313
428
  this.data = this.loadMirror();
429
+ this.bootSnapshot = cloneRegistryData(this.data);
314
430
  }
315
431
  // ─── Synchronous read path (lightning fast — no I/O, no network) ─────────
316
432
  /**
@@ -385,7 +501,7 @@ export class ModelRegistry {
385
501
  getDeadPairs() {
386
502
  const out = [];
387
503
  for (const e of Object.values(this.data.entries)) {
388
- if (isDeadEntry(e))
504
+ if (isNonexistentPair(e))
389
505
  out.push({ provider: e.provider, model: e.model });
390
506
  }
391
507
  return out;
@@ -395,7 +511,7 @@ export class ModelRegistry {
395
511
  if (!model || model === 'default')
396
512
  return false;
397
513
  const entry = this.data.entries[entryKey(provider, model)];
398
- return !!entry && isDeadEntry(entry);
514
+ return !!entry && isNonexistentPair(entry);
399
515
  }
400
516
  /** Providers that currently have at least one verified, usable model. Sync. */
401
517
  getUsableProviders(now = Date.now()) {
@@ -497,6 +613,23 @@ export class ModelRegistry {
497
613
  getEntry(provider, model) {
498
614
  return this.data.entries[entryKey(provider, model)];
499
615
  }
616
+ /**
617
+ * Every verified, usable pair across ALL providers (A1).
618
+ *
619
+ * `getVerifiedModels(provider)` answers "what works on THIS provider"; this
620
+ * answers "what works anywhere", which is the question a pin pre-flight has to
621
+ * ask: when a pair is refused, the useful next sentence is "the same model is
622
+ * verified on <other provider>". Built on the SAME `isUsable()` gate as
623
+ * routing, so a suggestion here can never name a pair the router would reject.
624
+ *
625
+ * The `'default'` sentinel is excluded — it is a config marker ("use the
626
+ * provider's default"), not a model id, and suggesting it would be nonsense.
627
+ */
628
+ getAllUsablePairs(now = Date.now()) {
629
+ return Object.values(this.data.entries)
630
+ .filter((e) => !isSentinelModel(e.model) && this.isUsable(e.provider, e.model, now))
631
+ .map((e) => ({ provider: e.provider, model: e.model }));
632
+ }
500
633
  /**
501
634
  * The learned reasoning-parameter support for a provider × model, or
502
635
  * `undefined` when nothing has been established (callers treat absent as
@@ -593,7 +726,9 @@ export class ModelRegistry {
593
726
  }
594
727
  /**
595
728
  * listModels probe: mark the model as seen (unverified unless already
596
- * verified). Does NOT downgrade a verified entry — real verification wins.
729
+ * verified). Does NOT downgrade a verified entry — real verification wins —
730
+ * and does NOT clear a definitive ACCOUNT-level refusal (see
731
+ * `isEntitlementFailure`).
597
732
  * Accepts either bare ids (legacy callers) or full model descriptors; when
598
733
  * a descriptor carries the provider-advertised context window, it is
599
734
  * recorded so the router's context preflight can use the LIVE value.
@@ -623,10 +758,24 @@ export class ModelRegistry {
623
758
  existing.reasoningCapability = reasoningCapability;
624
759
  continue;
625
760
  }
761
+ // ── A DEFINITIVE ENTITLEMENT REFUSAL SURVIVES A CATALOGUE LISTING ───────
762
+ // Being in the provider's model list proves the provider SERVES the model.
763
+ // It proves nothing about whether THIS ACCOUNT may use it — and those are
764
+ // different axes, the same distinction `errorRate`/`partialRate` already
765
+ // draw below. Measured (2026-10-06): `openrouter|deepseek/deepseek-v4.1-flash`
766
+ // is in OpenRouter's public catalogue, while the account behind the key had
767
+ // never purchased credits. `recordCall` had learned the definitive
768
+ // `credit-exhausted` verdict; a catalogue refresh re-listed the provider,
769
+ // this loop reset the entry to `unverified`, and the pin that one run
770
+ // refused before any network call was offered again by the next — which got
771
+ // a raw `402 Insufficient credits` body back. Same pin, same registry,
772
+ // opposite outcomes, decided by a metadata probe. An entitlement verdict
773
+ // only real traffic can clear.
774
+ const entitled = existing?.status === 'unavailable' && isEntitlementFailure(existing?.lastError);
626
775
  this.data.entries[key] = {
627
776
  provider,
628
777
  model,
629
- status: 'unverified',
778
+ status: entitled ? 'unavailable' : 'unverified',
630
779
  lastVerifiedAt: existing?.lastVerifiedAt || 0,
631
780
  lastProbedAt: now,
632
781
  lastUsedAt: existing?.lastUsedAt || 0,
@@ -642,6 +791,7 @@ export class ModelRegistry {
642
791
  quotaParkedUntil: existing?.quotaParkedUntil || 0,
643
792
  reasoningCapability,
644
793
  source: 'probe',
794
+ capability: existing?.capability,
645
795
  lastError: existing?.lastError,
646
796
  // The provider's OWN model list is authoritative about what it serves:
647
797
  // a pair that appears in a fresh list is not a dead pair, so the flag is
@@ -718,6 +868,11 @@ export class ModelRegistry {
718
868
  // Reasoning-parameter support is model metadata, independent of the verify
719
869
  // event — it survives a re-verify exactly like contextWindowTokens does.
720
870
  reasoningCapability: existing?.reasoningCapability,
871
+ // Bundle 3b: the measured scorecard is model metadata too. A verification
872
+ // proves the pair ANSWERS; it says nothing about how well it does the work,
873
+ // so the accuracy samples must not be reset by it (nor by any availability
874
+ // flip — see markListed/markUnavailable).
875
+ capability: existing?.capability,
721
876
  };
722
877
  this.persist();
723
878
  // A GENUINE promotion (was not verified → now verified) is a state change
@@ -734,6 +889,60 @@ export class ModelRegistry {
734
889
  this.appendActionLog({ timestamp: now, action, provider, model, outcome: 'verified', latencyMs, costUsd, callId });
735
890
  }
736
891
  }
892
+ /**
893
+ * Fold one observation into this pair's capability scorecard (Bundle 3b).
894
+ *
895
+ * Silent and best-effort by design: a scorecard write must never fail a call or
896
+ * an availability write. The record is model metadata — created on first
897
+ * observation, never reset by an availability flip (see `markVerified`).
898
+ */
899
+ foldCapability(provider, model, observation) {
900
+ if (isSentinelModel(model))
901
+ return;
902
+ try {
903
+ const key = entryKey(provider, model);
904
+ const entry = this.data.entries[key];
905
+ if (!entry)
906
+ return;
907
+ const now = observation.now ?? Date.now();
908
+ let record = entry.capability ?? emptyCapabilityRecord(now);
909
+ if (observation.verification !== undefined) {
910
+ record = foldVerification(record, observation.verification, now);
911
+ }
912
+ if (observation.ok !== undefined) {
913
+ record = foldCallOutcome(record, observation.ok, now);
914
+ }
915
+ if (observation.latencyMs !== undefined) {
916
+ record = foldLatency(record, observation.latencyMs, now);
917
+ }
918
+ if (record === entry.capability)
919
+ return;
920
+ entry.capability = record;
921
+ this.persist();
922
+ }
923
+ catch {
924
+ // Best-effort — the scorecard is learning, never a gate.
925
+ }
926
+ }
927
+ /**
928
+ * Record a completed TURN's verification verdict for a pair (Bundle 3b).
929
+ *
930
+ * The accuracy parameter's real feed: `TurnReport.verification` is derived from
931
+ * recorded tool/plan evidence and cannot be talked up by the model, so the
932
+ * sample this writes is an observation rather than a claim. `blocked` and
933
+ * `not-applicable` contribute NOTHING — a wall the run hit, and a turn with
934
+ * nothing to check, are both silent about the model.
935
+ *
936
+ * A pair with no entry yet is skipped rather than invented: the scorecard hangs
937
+ * off a registry row, and creating one from a turn would claim the pair exists.
938
+ */
939
+ recordCapabilityEvidence(provider, model, verification) {
940
+ this.foldCapability(provider, model, { verification });
941
+ }
942
+ /** This pair's measured scorecard, or undefined when nothing was measured. */
943
+ getCapability(provider, model) {
944
+ return this.data.entries[entryKey(provider, model)]?.capability;
945
+ }
737
946
  /**
738
947
  * M2.2: record EXACT tokens from a provider-reported usage payload. The
739
948
  * per-call token EMAs (α=0.3, matching latency) feed getMeasuredUsage(),
@@ -821,6 +1030,7 @@ export class ModelRegistry {
821
1030
  reasoningCapability: existing?.reasoningCapability,
822
1031
  quotaParkedUntil: Math.max(existing?.quotaParkedUntil || 0, quotaParkedUntil),
823
1032
  source,
1033
+ capability: existing?.capability,
824
1034
  lastError: reason,
825
1035
  // A "model not found" answer is definitive for this provider × model, so
826
1036
  // the pair is retired from every candidate pool (see `deadPair`). The flag
@@ -990,6 +1200,13 @@ export class ModelRegistry {
990
1200
  if (ok) {
991
1201
  this.markVerified(provider, model, 'telemetry', latencyMs, action, costUsd, callId);
992
1202
  this.data.entries[key].lastUsedAt = now;
1203
+ // Bundle 3b: every real call is also capability evidence — how the pair held
1204
+ // up (robustness) and how fast it answered (performance). Folded HERE (the
1205
+ // single telemetry entry point) and AFTER markVerified, because markVerified
1206
+ // is what creates the row for a pair's first-ever call — folding earlier
1207
+ // would silently drop the first observation of every model. Availability is
1208
+ // not touched: this is evidence about the model, not a status write.
1209
+ this.foldCapability(provider, model, { ok, latencyMs, now });
993
1210
  // P4 M4.4: a clean success heals mid-stream flakiness (the provider
994
1211
  // demonstrably finishes). Decay the EMA toward 0 — never hard-reset,
995
1212
  // so a single success doesn't erase a flaky streak. markVerified already
@@ -1055,8 +1272,41 @@ export class ModelRegistry {
1055
1272
  entry.providerParked = false;
1056
1273
  flipped = true;
1057
1274
  }
1275
+ else if (errorType === 'empty-response') {
1276
+ // fix_model_routing P1/P2 — the provider RESOLVED but carried nothing.
1277
+ // This used to be recorded as a SUCCESS (the walk had no notion of a
1278
+ // usable response), so a model that answers every turn with an empty body
1279
+ // kept `verified` and `errorRate 0` and the router kept choosing it. The
1280
+ // error-rate bump above already sinks it in scoring; the park is the hard
1281
+ // stop, and it is MODEL-scoped so the provider's healthy siblings remain
1282
+ // routable and the pair returns automatically after the window.
1283
+ entry.lastError = 'empty-response (no text, no tool call)';
1284
+ entry.quotaParkedUntil = Math.max(entry.quotaParkedUntil, now + EMPTY_RESPONSE_PARK_MS);
1285
+ entry.providerParked = false;
1286
+ flipped = true;
1287
+ }
1288
+ else if (errorType === 'credit-exhausted') {
1289
+ // DEFINITIVE, and NOT a cooldown (A4). The account cannot pay, so there is
1290
+ // no window to wait out — a park would expire and the router would offer
1291
+ // the pair again, which is precisely how `openrouter/deepseek-v4.1-flash`
1292
+ // kept being chosen and kept answering 402. Demoted like `auth`: it takes
1293
+ // a funded key (or `models unblock` / a re-probe) to restore.
1294
+ //
1295
+ // This is honest about WHAT failed: the check is on the PAIR because that
1296
+ // is the granularity the registry stores, but the sentence names the
1297
+ // account, not the model — a different host may serve this same model,
1298
+ // and the failover walk is free to try one.
1299
+ entry.status = 'unavailable';
1300
+ entry.lastError = 'credit-exhausted (that provider account cannot pay for this call)';
1301
+ entry.quotaParkedUntil = 0;
1302
+ entry.providerParked = false;
1303
+ flipped = true;
1304
+ }
1058
1305
  this.data.entries[key] = entry;
1059
1306
  this.persist();
1307
+ // Bundle 3b — a FAILED call is capability evidence too (robustness), and the
1308
+ // row now exists on both paths (markVerified above, or the entry built here).
1309
+ this.foldCapability(provider, model, { ok, latencyMs, now });
1060
1310
  if (flipped)
1061
1311
  this.emitUpdated([provider], `telemetry failure (${errorType})`, 'telemetry');
1062
1312
  if (action) {
@@ -1413,7 +1663,11 @@ export class ModelRegistry {
1413
1663
  * Only call this with an AUTHORITATIVE live list (the refresh probe of a
1414
1664
  * keyless/local runner). Entries whose model is NOT in the list are handled:
1415
1665
  * - UNVERIFIED / UNAVAILABLE entries are DELETED entirely (never checked
1416
- * again — they have no learned value worth keeping).
1666
+ * again — they have no learned value worth keeping), EXCEPT an entry whose
1667
+ * unavailability is an ACCOUNT-level refusal (`isEntitlementFailure`): that
1668
+ * one is the only record that this key cannot pay or is not authorized, so
1669
+ * deleting it loses the verdict and the next model listing re-creates the
1670
+ * pair as `unverified` — re-arming exactly the pin A2 exists to refuse.
1417
1671
  * - VERIFIED entries are DEMOTED to `unavailable` with reason "model
1418
1672
  * deleted from local system" instead of being hard-deleted — a partial
1419
1673
  * listModels response (a model mid-pull, a gateway hiccup) must not
@@ -1433,6 +1687,14 @@ export class ModelRegistry {
1433
1687
  continue;
1434
1688
  if (e.status === 'verified')
1435
1689
  demotedKeys.push(key);
1690
+ // Absence from a model list is evidence about the CATALOGUE, not about the
1691
+ // account. Deleting an entitlement verdict hands the pair straight back to
1692
+ // the next `markListed` as a fresh `unverified` entry (observed live: the
1693
+ // `credit-exhausted` verdict on `openrouter/deepseek/deepseek-v4.1-flash`
1694
+ // was gone after a refresh, so the strict pre-flight stopped firing and a
1695
+ // run that had refused before any call sent the request and got a raw 402).
1696
+ else if (isEntitlementFailure(e.lastError))
1697
+ continue;
1436
1698
  else
1437
1699
  removedKeys.push(key);
1438
1700
  }
@@ -1491,19 +1753,38 @@ export class ModelRegistry {
1491
1753
  */
1492
1754
  reloadFromMirror() {
1493
1755
  this.data = this.loadMirror();
1756
+ this.bootSnapshot = cloneRegistryData(this.data);
1494
1757
  }
1495
1758
  /**
1496
1759
  * Persist: JSON mirror synchronously (canonical, guaranteed), then mirror to
1497
1760
  * the VectorStore namespace asynchronously (best-effort, auto-tiers to JSON
1498
1761
  * when FAISS/native aren't installed — so it can never throw).
1499
1762
  */
1500
- persist() {
1763
+ persist(opts = {}) {
1501
1764
  this.data.updatedAt = Date.now();
1502
1765
  const dir = memoryDir();
1766
+ const path = mirrorPath();
1503
1767
  try {
1504
- if (!existsSync(dir))
1505
- mkdirSync(dir, { recursive: true });
1506
- writeFileSync(mirrorPath(), JSON.stringify(this.data, null, 2), 'utf-8');
1768
+ mkdirSync(dir, { recursive: true });
1769
+ // The read-merge-write runs UNDER the lock, so two processes cannot
1770
+ // interleave it: the second one to run re-reads what the first wrote and
1771
+ // merges rather than overwriting. `writeFileAtomicSync` then makes the
1772
+ // publication all-or-nothing, so a concurrent READER can never parse a
1773
+ // half-written file (which every loader here turns into an empty state
1774
+ // that the next persist would write back).
1775
+ const merged = withFileLockSync(`${path}.lock`, () => {
1776
+ // A deliberate wipe (`reset`) must not adopt a peer's concurrent row —
1777
+ // the operator asked for an empty registry, so it is written as-is.
1778
+ const next = opts.replace
1779
+ ? this.data
1780
+ : mergeRegistryMirror(this.bootSnapshot, this.data, this.loadMirror());
1781
+ writeFileAtomicSync(path, JSON.stringify(next, null, 2));
1782
+ return next;
1783
+ });
1784
+ // Adopt the merge, and re-baseline: this process's view is now the file's,
1785
+ // so the NEXT persist compares against it instead of against boot.
1786
+ this.data = merged.value;
1787
+ this.bootSnapshot = cloneRegistryData(this.data);
1507
1788
  }
1508
1789
  catch {
1509
1790
  // Best-effort — a failed mirror write must never break routing.
@@ -1525,27 +1806,31 @@ export class ModelRegistry {
1525
1806
  mirrorToVector(dir) {
1526
1807
  try {
1527
1808
  const indexPath = join(dir, 'vectors-model-registry.json');
1528
- if (!existsSync(dir))
1529
- mkdirSync(dir, { recursive: true });
1530
- let entries = {};
1531
- try {
1532
- if (existsSync(indexPath)) {
1533
- const raw = JSON.parse(readFileSync(indexPath, 'utf-8'));
1534
- if (raw && typeof raw === 'object' && raw.entries && typeof raw.entries === 'object') {
1535
- entries = raw.entries;
1809
+ mkdirSync(dir, { recursive: true });
1810
+ // Same cross-process shape as `persist()`: the read-merge-write is one
1811
+ // critical section, and the publication is atomic. Two processes mirroring
1812
+ // this namespace concurrently would otherwise drop each other's snapshot.
1813
+ withFileLockSync(`${indexPath}.lock`, () => {
1814
+ let entries = {};
1815
+ try {
1816
+ if (existsSync(indexPath)) {
1817
+ const raw = JSON.parse(readFileSync(indexPath, 'utf-8'));
1818
+ if (raw && typeof raw === 'object' && raw.entries && typeof raw.entries === 'object') {
1819
+ entries = raw.entries;
1820
+ }
1536
1821
  }
1537
1822
  }
1538
- }
1539
- catch {
1540
- // Corrupt/missing file — start from an empty index.
1541
- }
1542
- entries[VECTOR_SNAPSHOT_ID] = {
1543
- id: VECTOR_SNAPSHOT_ID,
1544
- vector: [1], // 1-dim placeholder — we never search, only store.
1545
- metadata: { snapshot: this.data },
1546
- createdAt: Date.now(),
1547
- };
1548
- writeFileSync(indexPath, JSON.stringify({ entries, version: 2 }, null, 2), 'utf-8');
1823
+ catch {
1824
+ // Corrupt/missing file — start from an empty index.
1825
+ }
1826
+ entries[VECTOR_SNAPSHOT_ID] = {
1827
+ id: VECTOR_SNAPSHOT_ID,
1828
+ vector: [1], // 1-dim placeholder — we never search, only store.
1829
+ metadata: { snapshot: this.data },
1830
+ createdAt: Date.now(),
1831
+ };
1832
+ writeFileAtomicSync(indexPath, JSON.stringify({ entries, version: 2 }, null, 2));
1833
+ });
1549
1834
  this.vectorMirrored = true;
1550
1835
  }
1551
1836
  catch {
@@ -1684,7 +1969,8 @@ export class ModelRegistry {
1684
1969
  /** Clear the registry (CLI / tests). */
1685
1970
  reset() {
1686
1971
  this.data = emptyState();
1687
- this.persist(); // persist() also overwrites the vector snapshot with the empty state.
1972
+ // Also overwrites the vector snapshot with the empty state.
1973
+ this.persist({ replace: true });
1688
1974
  }
1689
1975
  }
1690
1976
  // ─── Singleton ──────────────────────────────────────────────────────────────