mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -24,6 +24,7 @@ import { join } from 'path';
24
24
  import { getPluginData } from '../config.mjs';
25
25
  import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
26
26
  import { enrichModels, getModelMetadataSync } from './model-catalog.mjs';
27
+ import { sanitizeModelList } from './model-list-sanitize.mjs';
27
28
  import { makeModelCache } from './model-cache.mjs';
28
29
  import { OpenAICompatProvider } from './openai-compat.mjs';
29
30
  import { createTimeoutSignal } from '../stall-policy.mjs';
@@ -473,6 +474,19 @@ function _sanitizeGrokModels(models) {
473
474
  // Image/video generation ids — excluded from "latest chat model" resolution.
474
475
  const NON_CHAT_MODEL_RE = /imagine|image|video/i;
475
476
 
477
+ // List-facing sanitizer: drop grok image/video generation ids (proxy-only
478
+ // chat models are kept), then apply the shared cross-provider sanitizer.
479
+ function _sanitizeGrokList(models) {
480
+ const base = _sanitizeGrokModels(models);
481
+ if (!Array.isArray(base)) return base;
482
+ const chatOnly = base.filter((m) => {
483
+ if (!m?.id) return false;
484
+ if (NON_CHAT_MODEL_RE.test(m.id) && !isProxyOnlyModel(m.id)) return false;
485
+ return true;
486
+ });
487
+ return sanitizeModelList(chatOnly, { provider: 'grok-oauth' });
488
+ }
489
+
476
490
  function _markLatestGrok(models) {
477
491
  let best = null;
478
492
  for (const m of models) {
@@ -683,7 +697,7 @@ export class GrokOAuthProvider {
683
697
  async listModels() {
684
698
  const cached = _modelCache.loadSync();
685
699
  if (cached) {
686
- const sanitized = _sanitizeGrokModels(cached);
700
+ const sanitized = _sanitizeGrokList(cached);
687
701
  if (sanitized !== cached) _modelCache.save(sanitized);
688
702
  return sanitized;
689
703
  }
@@ -693,7 +707,7 @@ export class GrokOAuthProvider {
693
707
  const items = await this._fetchAllModelItems();
694
708
  const normalized = items.map(_normalizeGrokModel).filter(Boolean);
695
709
  _markLatestGrok(normalized);
696
- const enriched = _sanitizeGrokModels(await enrichModels(normalized));
710
+ const enriched = _sanitizeGrokList(await enrichModels(normalized));
697
711
  _modelCache.save(enriched);
698
712
  return enriched;
699
713
  }
@@ -705,7 +719,7 @@ export class GrokOAuthProvider {
705
719
  const items = await this._fetchAllModelItems();
706
720
  const normalized = items.map(_normalizeGrokModel).filter(Boolean);
707
721
  _markLatestGrok(normalized);
708
- const enriched = _sanitizeGrokModels(await enrichModels(normalized));
722
+ const enriched = _sanitizeGrokList(await enrichModels(normalized));
709
723
  _modelCache.save(enriched);
710
724
  if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[grok-oauth] catalog refreshed (${enriched.length} models)\n`);
711
725
  return enriched;
@@ -18,6 +18,10 @@ import { existsSync, readFileSync } from 'fs';
18
18
  import { join } from 'path';
19
19
  import { getPluginData } from '../config.mjs';
20
20
  import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
21
+ import {
22
+ providerCachedModelMetadataSync,
23
+ providerUsesEndpointScopedLimits,
24
+ } from './provider-catalog-cache.mjs';
21
25
 
22
26
  const CATALOG_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
23
27
  const CATALOG_CACHE_FILE = 'litellm-catalog.json';
@@ -61,6 +65,33 @@ const _CATALOG_BEDROCK_PREFIXES = ['anthropic.', 'bedrock/anthropic.'];
61
65
  // azure_ai- or openrouter-namespaced).
62
66
  const _CATALOG_ENRICH_PREFIXES = ['anthropic/', 'openai/', 'gemini/', 'google/', 'xai/', 'deepseek/'];
63
67
 
68
+ // Provider hint → catalog prefixes to try (subset of _CATALOG_SIMPLE_PREFIXES /
69
+ // _CATALOG_ENRICH_PREFIXES). Keyed by the *mapped* models.dev provider id
70
+ // (see _modelsDevProviderId), so anthropic-oauth and anthropic share one
71
+ // entry, likewise grok-oauth/xai and gemini/google. A provider missing here
72
+ // (unknown/custom) gets bare-id lookup only — no prefix guessing across
73
+ // unrelated providers. No provider hint at all (mappedProvider null) keeps
74
+ // legacy behaviour: try every prefix.
75
+ const _PROVIDER_CATALOG_PREFIXES = {
76
+ openai: ['openai/'],
77
+ anthropic: ['anthropic/'],
78
+ google: ['gemini/', 'google/'],
79
+ xai: ['xai/'],
80
+ deepseek: ['deepseek/'],
81
+ azure: ['azure_ai/'],
82
+ };
83
+ function _prefixesForProvider(mappedProvider, allPrefixes) {
84
+ if (!mappedProvider) return allPrefixes;
85
+ const allowed = _PROVIDER_CATALOG_PREFIXES[mappedProvider];
86
+ if (!allowed) return [];
87
+ return allPrefixes.filter((p) => allowed.includes(p));
88
+ }
89
+ // Bedrock-style catalog keys (anthropic.<id>-v1:0) only ever describe
90
+ // Anthropic models; skip that lookup entirely for any other provider hint.
91
+ function _bedrockAllowed(mappedProvider) {
92
+ return !mappedProvider || mappedProvider === 'anthropic';
93
+ }
94
+
64
95
  // Polyfill for models the LiteLLM catalog does not list yet. Values mirror
65
96
  // the catalog row shape so _normalize works unchanged. Source: each provider's
66
97
  // official pricing page; do not extrapolate. Promotional discounts are
@@ -299,6 +330,33 @@ function _modelsDevMetadataSync(id, provider) {
299
330
  return _normalize(_modelsDevRowToOverride(row));
300
331
  }
301
332
 
333
+ // Raw models.dev catalog row accessor for the model-list sanitizer's
334
+ // data-driven staleness filter. Unlike _modelsDevMetadataSync this does NOT
335
+ // require a `cost` field (staleness only needs family/release_date) and
336
+ // returns the raw row untouched. Warms from disk one-shot if memory is cold;
337
+ // returns null when the catalog is unavailable so callers can skip filtering.
338
+ // `_test` (tests only) injects a fake catalog map without touching disk.
339
+ export function getModelsDevRowSync(id, provider, _test) {
340
+ const cat = _test || _mdCache || (warmModelsDevFromDiskSync(), _mdCache);
341
+ if (!cat) { void loadModelsDevCatalog(); return null; }
342
+ const pid = _modelsDevProviderId(provider);
343
+ if (!pid) return null;
344
+ const row = cat?.[pid]?.models?.[id];
345
+ return row || null;
346
+ }
347
+
348
+ // All models.dev rows for a provider (mapped id), keyed by model id. Used by
349
+ // the sanitizer's family-supersession pass to compare release dates across a
350
+ // provider's whole catalog. Returns null when the catalog is cold/unavailable.
351
+ export function getModelsDevProviderModelsSync(provider, _test) {
352
+ const cat = _test || _mdCache || (warmModelsDevFromDiskSync(), _mdCache);
353
+ if (!cat) { void loadModelsDevCatalog(); return null; }
354
+ const pid = _modelsDevProviderId(provider);
355
+ if (!pid) return null;
356
+ const models = cat?.[pid]?.models;
357
+ return (models && typeof models === 'object') ? models : null;
358
+ }
359
+
302
360
  /**
303
361
  * Sync lookup. Warm order:
304
362
  * 1. in-memory cache (hot path),
@@ -313,6 +371,7 @@ function _modelsDevMetadataSync(id, provider) {
313
371
  export function getModelMetadataSync(id, provider) {
314
372
  if (!id) return null;
315
373
  const mappedProvider = provider ? _modelsDevProviderId(provider) : null;
374
+ const providerNative = provider ? providerCachedModelMetadataSync(provider, id) : null;
316
375
  let meta = null;
317
376
  // 1. Manual overrides — authoritative + offline. Provider-guarded: when a
318
377
  // provider hint is given, an override is only honoured if it belongs to
@@ -323,16 +382,19 @@ export function getModelMetadataSync(id, provider) {
323
382
  if (ov && (!mappedProvider || _modelsDevProviderId(ov.litellm_provider) === mappedProvider)) {
324
383
  meta = _normalize(ov);
325
384
  }
385
+ const metaFromPricingOverride = meta !== null;
326
386
  // 2. LiteLLM community catalog (broad mainstream coverage).
327
387
  if (!_memCache) warmFromDiskSync();
328
388
  if (!meta && _memCache) {
329
389
  const catalog = _memCache;
330
- if (catalog[id]) meta = _normalize(catalog[id]);
331
- for (const prefix of _CATALOG_SIMPLE_PREFIXES) {
390
+ if (catalog[id] && (!mappedProvider || _modelsDevProviderId(catalog[id].litellm_provider) === mappedProvider)) {
391
+ meta = _normalize(catalog[id]);
392
+ }
393
+ for (const prefix of _prefixesForProvider(mappedProvider, _CATALOG_SIMPLE_PREFIXES)) {
332
394
  if (meta) break;
333
395
  if (catalog[prefix + id]) meta = _normalize(catalog[prefix + id]);
334
396
  }
335
- for (const prefix of _CATALOG_BEDROCK_PREFIXES) {
397
+ for (const prefix of (_bedrockAllowed(mappedProvider) ? _CATALOG_BEDROCK_PREFIXES : [])) {
336
398
  if (meta) break;
337
399
  const v1 = catalog[prefix + id + '-v1:0'];
338
400
  if (v1) meta = _normalize(v1);
@@ -343,7 +405,16 @@ export function getModelMetadataSync(id, provider) {
343
405
  // id, and add fields LiteLLM lacks, such as opencode-go reasoning_options.
344
406
  if (mappedProvider) {
345
407
  const md = _modelsDevMetadataSync(id, provider);
346
- if (md) meta = mergeModelMetadata(meta, md);
408
+ if (md) meta = mergeModelMetadata(meta, md, { preserveBaseCosts: metaFromPricingOverride });
409
+ }
410
+ if (providerUsesEndpointScopedLimits(provider) && !providerNative && meta) {
411
+ // OAuth/backend routes can expose smaller account/backend windows than
412
+ // the public API SKU. External catalogs remain useful for costs and
413
+ // capabilities, but their limits are not authoritative for these routes.
414
+ meta = { ...meta, contextWindow: null, outputTokens: null };
415
+ }
416
+ if (providerNative) {
417
+ meta = mergeModelMetadata(meta, providerNative, { preserveBaseCosts: true });
347
418
  }
348
419
  return meta;
349
420
  }
@@ -368,13 +439,23 @@ function _normalize(entry) {
368
439
  };
369
440
  }
370
441
 
371
- function mergeModelMetadata(base, overlay) {
442
+ function mergeModelMetadata(base, overlay, opts = {}) {
372
443
  if (!base) return overlay || null;
373
444
  if (!overlay) return base;
374
445
  return {
375
446
  ...base,
376
447
  contextWindow: overlay.contextWindow || base.contextWindow || null,
377
448
  outputTokens: overlay.outputTokens || base.outputTokens || null,
449
+ // Provider-scoped models.dev rates beat generic base (LiteLLM) rates
450
+ // when present — the overlay is the provider-scoped source, so a
451
+ // non-null overlay value always wins over the generic fallback —
452
+ // UNLESS base came from PRICING_OVERRIDES (preserveBaseCosts), which
453
+ // is a hand-verified, authoritative rate that must not be clobbered
454
+ // by a models.dev row for the same id.
455
+ inputCostPerM: (!opts.preserveBaseCosts && overlay.inputCostPerM != null) ? overlay.inputCostPerM : base.inputCostPerM,
456
+ outputCostPerM: (!opts.preserveBaseCosts && overlay.outputCostPerM != null) ? overlay.outputCostPerM : base.outputCostPerM,
457
+ cacheReadCostPerM: (!opts.preserveBaseCosts && overlay.cacheReadCostPerM != null) ? overlay.cacheReadCostPerM : base.cacheReadCostPerM,
458
+ cacheWriteCostPerM: (!opts.preserveBaseCosts && overlay.cacheWriteCostPerM != null) ? overlay.cacheWriteCostPerM : base.cacheWriteCostPerM,
378
459
  supportsVision: base.supportsVision || overlay.supportsVision,
379
460
  supportsFunctionCalling: base.supportsFunctionCalling || overlay.supportsFunctionCalling,
380
461
  supportsWebSearch: base.supportsWebSearch || overlay.supportsWebSearch,
@@ -401,23 +482,36 @@ export async function enrichModels(models) {
401
482
  const id = m.id || m.name;
402
483
  if (!id) return m;
403
484
  // Same lookup logic as getModelMetadata but inlined for speed.
404
- let entry = PRICING_OVERRIDES[id] || catalog[id];
485
+ const mappedProvider = m.provider ? _modelsDevProviderId(m.provider) : null;
486
+ // Provider-guarded, same as getModelMetadataSync: an override/catalog
487
+ // row is only honoured when it belongs to the hinted provider, so a
488
+ // model id shared across providers never leaks the wrong rate.
489
+ const ov = PRICING_OVERRIDES[id];
490
+ let entry = (ov && (!mappedProvider || _modelsDevProviderId(ov.litellm_provider) === mappedProvider)) ? ov : null;
491
+ if (!entry && catalog[id] && (!mappedProvider || _modelsDevProviderId(catalog[id].litellm_provider) === mappedProvider)) {
492
+ entry = catalog[id];
493
+ }
405
494
  if (!entry) {
406
- for (const prefix of _CATALOG_ENRICH_PREFIXES) {
495
+ for (const prefix of _prefixesForProvider(mappedProvider, _CATALOG_ENRICH_PREFIXES)) {
407
496
  if (catalog[prefix + id]) { entry = catalog[prefix + id]; break; }
408
497
  }
409
498
  }
410
- if (!entry) {
499
+ if (!entry && _bedrockAllowed(mappedProvider)) {
411
500
  for (const prefix of _CATALOG_BEDROCK_PREFIXES) {
412
501
  if (catalog[prefix + id + '-v1:0']) { entry = catalog[prefix + id + '-v1:0']; break; }
413
502
  }
414
503
  }
415
504
  let meta = entry ? _normalize(entry) : null;
505
+ const metaFromPricingOverride = entry != null && entry === ov;
416
506
  if (m.provider) {
417
- const pid = _modelsDevProviderId(m.provider);
418
- const row = pid ? _mdCache?.[pid]?.models?.[id] : null;
507
+ const row = mappedProvider ? _mdCache?.[mappedProvider]?.models?.[id] : null;
419
508
  const providerMeta = row ? _normalize(_modelsDevRowToOverride(row)) : null;
420
- if (providerMeta) meta = mergeModelMetadata(meta, providerMeta);
509
+ if (providerMeta) meta = mergeModelMetadata(meta, providerMeta, { preserveBaseCosts: metaFromPricingOverride });
510
+ const providerNative = providerCachedModelMetadataSync(m.provider, id);
511
+ if (providerUsesEndpointScopedLimits(m.provider) && !providerNative && meta) {
512
+ meta = { ...meta, contextWindow: null, outputTokens: null };
513
+ }
514
+ if (providerNative) meta = mergeModelMetadata(meta, providerNative, { preserveBaseCosts: true });
421
515
  }
422
516
  if (!meta) return m;
423
517
  return {
@@ -0,0 +1,356 @@
1
+ // Shared model-list sanitizer. Providers return raw /models catalogs that mix
2
+ // chat models with embeddings/tts/image/etc, keep long-deprecated legacy
3
+ // families, and expose the same model under many dated snapshot ids. This
4
+ // module narrows a provider's enriched model list down to current,
5
+ // chat-capable, coding-relevant models WITHOUT touching selection or
6
+ // default-model logic. Conservative by design: when a row's family/mode is
7
+ // unknown, KEEP it.
8
+ //
9
+ // Usage: `sanitizeModelList(await enrichModels(models), { provider })`.
10
+ // Input rows are the enriched shape ({ id, mode?, created?, ... }); output is
11
+ // a filtered + deduped subset of the SAME row objects (never mutated).
12
+
13
+ import { getModelsDevProviderModelsSync, getModelsDevRowSync } from './model-catalog.mjs';
14
+
15
+ // Id-level fallback for coding-unfit SKUs when the catalog row is missing.
16
+ // search-preview (grounded-search) and audio/realtime preview SKUs are
17
+ // unwanted for a coding agent even when chat-capable. This is applied
18
+ // regardless of an explicit mode:'chat' (unlike NON_CHAT_RE).
19
+ const CODING_UNFIT_ID_RE = /-search(-preview)?(\b|-|$)|-(audio|realtime)-preview/i;
20
+
21
+ // Known hosted providers get the full id-heuristic + legacy + dedupe
22
+ // treatment. Anything else (ollama/lmstudio/llamacpp/local, or any unknown
23
+ // custom provider name openai-compat forwards) is treated as CUSTOM: only the
24
+ // enriched-mode drop applies — no id regex, no legacy filter, no dedupe.
25
+ const HOSTED_PROVIDERS = new Set([
26
+ 'openai', 'openai-oauth', 'openai-ws', 'anthropic', 'anthropic-oauth',
27
+ 'gemini', 'google', 'xai', 'grok-oauth', 'deepseek', 'groq', 'mistral',
28
+ 'opencode-go', 'azure',
29
+ ]);
30
+
31
+ // (a) Non-chat modality ids. Matched as whole word-ish tokens on the id so we
32
+ // don't nuke unrelated substrings. NOTE: 'search'/'voice' intentionally
33
+ // excluded — chat SKUs like *-search-preview exist.
34
+ const NON_CHAT_RE = new RegExp(
35
+ '(^|[-_/\\s])(' +
36
+ [
37
+ 'embed', 'embedding', 'embeddings',
38
+ 'tts', 'stt', 'whisper', 'audio', 'speech', 'voice',
39
+ 'image', 'images', 'video', 'videos',
40
+ 'moderation', 'moderations', 'rerank', 'reranker', 'similarity',
41
+ 'dall[-_]?e', 'sora', 'imagen', 'imagine', 'veo', 'kling', 'runway',
42
+ 'realtime', 'transcribe', 'transcription', 'diarize',
43
+ 'guard', 'safety', 'classifier',
44
+ ].join('|') +
45
+ ')([-_/\\s]|$)',
46
+ 'i',
47
+ );
48
+
49
+ // (a2) Whole-id non-LLM SKUs that don't carry a modality token in a matchable
50
+ // position. These are exact families that OpenAI/others expose via /v1/models
51
+ // but are never chat/completion LLMs (image gen, TTS, embeddings, moderation).
52
+ // Anchored to the full id so version/date siblings all match.
53
+ const NON_LLM_ID_PATTERNS = [
54
+ /^tts(\b|-)/i, // tts-1, tts-1-hd
55
+ /^whisper(\b|-)/i, // whisper-1
56
+ /^dall[-_]?e(\b|-)/i, // dall-e-2, dall-e-3
57
+ /^(text-)?embedding/i, // text-embedding-3-large, embedding-*
58
+ /^text-embedding-ada/i, // text-embedding-ada-002
59
+ /^text-moderation(\b|-)/i, // text-moderation-*
60
+ /^omni-moderation(\b|-)/i, // omni-moderation-latest
61
+ /^gpt-image(\b|-)/i, // gpt-image-1, gpt-image-2
62
+ /^chatgpt-image(\b|-)/i, // chatgpt-image-latest
63
+ /^gpt-(4o|4o-mini|realtime|audio)?-?(tts|transcribe)(\b|-)/i, // gpt-4o-*-tts / -transcribe
64
+ /^gpt-audio(\b|-)/i, // gpt-audio, gpt-audio-mini, gpt-audio-1.5
65
+ /^gpt-realtime(\b|-)/i, // gpt-realtime*, gpt-realtime-mini
66
+ /-tts(\b|-)/i, // any *-tts SKU
67
+ ];
68
+
69
+ function _isNonLlmId(lid) {
70
+ return NON_LLM_ID_PATTERNS.some((re) => re.test(lid));
71
+ }
72
+
73
+ // (b) Legacy / deprecated families not useful for a coding agent. Current
74
+ // families (gpt-5.x, gpt-4.1, o1-pro, o3+, claude-4.x, gemini-2/3, grok-4,
75
+ // deepseek-v*) must NOT match. Anything not matched here is kept. Each entry
76
+ // is a full-id-anchored pattern with an explicit trailing boundary so version
77
+ // siblings (grok-4.20-beta, gpt-4.1) survive.
78
+ const LEGACY_PATTERNS = [
79
+ // OpenAI legacy — gpt-4 legacy = gpt-4o*, gpt-4-turbo/32k/0*, bare gpt-4.
80
+ // gpt-4.1* is CURRENT and deliberately not matched.
81
+ /^gpt-4(o|-turbo|-32k|-\d|$)/i,
82
+ /^chatgpt-4o(\b|-)/i,
83
+ /^gpt-3(\.5)?(\b|-)/i,
84
+ /^(text-)?(davinci|babbage|curie|ada)(\b|-)/i,
85
+ // o1 legacy = o1, o1-mini, o1-preview. o1-pro is CURRENT, not matched.
86
+ /^o1(-mini|-preview|-\d|$)/i,
87
+ /^o3-mini(\b|-)/i,
88
+ // Anthropic legacy
89
+ /^claude-(1|2|3|instant)(\b|-|\.)/i,
90
+ // Gemini legacy — gemini-1*, gemini-pro. gemini-2/3 not matched.
91
+ /^gemini-1(\b|-|\.)/i,
92
+ /^gemini-pro(\b|-)/i,
93
+ // Grok legacy — grok-1/2/3 and grok-beta. The digit boundary stops grok-3
94
+ // from catching grok-4.20-beta.
95
+ /^grok-(1|2|3)(\b|-|\.)/i,
96
+ /^grok-beta(\b|-)/i,
97
+ ];
98
+
99
+ function _isLegacy(lid) {
100
+ return LEGACY_PATTERNS.some((re) => re.test(lid));
101
+ }
102
+
103
+ function _isNonChatMode(row) {
104
+ const mode = typeof row?.mode === 'string' ? row.mode.trim().toLowerCase() : '';
105
+ if (!mode) return false; // unknown -> keep
106
+ return !['chat', 'completion', 'responses', 'messages'].includes(mode);
107
+ }
108
+
109
+ // (c) Canonical key: strip ONLY unambiguous date suffixes: -YYYY-MM-DD and
110
+ // -20YYMMDD (8-digit starting 20). Generic -0xxx is NOT stripped here — it
111
+ // collides with version ids (grok-4.20-0309, custom foo-0123). MMDD-style
112
+ // collapse is handled separately and conditionally.
113
+ function _canonicalKey(id) {
114
+ let key = String(id || '').trim().toLowerCase();
115
+ key = key.replace(/-\d{4}-\d{2}-\d{2}$/, ''); // -YYYY-MM-DD
116
+ key = key.replace(/-20\d{6}$/, ''); // -20YYMMDD
117
+ return key;
118
+ }
119
+
120
+ function _isDated(id) {
121
+ return _canonicalKey(id) !== String(id || '').trim().toLowerCase();
122
+ }
123
+
124
+ // MMDD-style suffix (-0309): only collapse when the undated alias exists in
125
+ // the same list AND both rows share identical context/output limits.
126
+ function _mmddBase(id) {
127
+ const lid = String(id || '').trim().toLowerCase();
128
+ const m = lid.match(/^(.*)-0\d{3}$/);
129
+ return m ? m[1] : null;
130
+ }
131
+ function _sameLimits(a, b) {
132
+ const cw = (r) => Number(r?.contextWindow) || 0;
133
+ const ot = (r) => Number(r?.outputTokens) || 0;
134
+ return cw(a) === cw(b) && ot(a) === ot(b);
135
+ }
136
+
137
+ // ── Data-driven auto-staleness (models.dev release_date + family) ────────────
138
+ // Parse a models.dev release_date ("YYYY-MM-DD" or "YYYY-MM") to a UTC epoch.
139
+ function _releaseEpoch(row) {
140
+ const rd = row && typeof row.release_date === 'string' ? row.release_date.trim() : '';
141
+ if (!rd) return null;
142
+ const m = rd.match(/^(\d{4})-(\d{2})(?:-(\d{2}))?$/);
143
+ if (!m) return null;
144
+ const t = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3] || '01'));
145
+ return Number.isFinite(t) ? t : null;
146
+ }
147
+
148
+ function _stalenessFamily(row, id) {
149
+ const modelId = String(id || row?.id || row?.name || '').trim().toLowerCase();
150
+ const claude = modelId.match(/(?:^|\/)claude-(opus|sonnet|haiku|fable)(?:-|$)/);
151
+ if (claude) return `claude-${claude[1]}`;
152
+ const family = row && typeof row.family === 'string' ? row.family.trim().toLowerCase() : '';
153
+ return family || null;
154
+ }
155
+
156
+ function _staleMonths() {
157
+ const raw = process.env.MIXDOG_MODEL_STALE_MONTHS;
158
+ if (raw == null || raw === '') return 12;
159
+ const n = Number(raw);
160
+ return Number.isFinite(n) ? n : 12;
161
+ }
162
+
163
+ // Apply catalog-driven staleness to the already-filtered `kept` rows.
164
+ // Rules (see slice brief): (1) family supersession by newer release_date,
165
+ // (2) absolute cut older than N months. Both consult the provider's
166
+ // models.dev catalog; rows not found in the catalog are KEPT (fallback to the
167
+ // static LEGACY_PATTERNS already applied earlier). Catalog cold → no-op.
168
+ // Guards: never empty a family via supersession; if the absolute cut would
169
+ // empty the whole provider list, skip the cut entirely.
170
+ function _applyAutoStaleness(kept, provider, testCatalog) {
171
+ const catModels = getModelsDevProviderModelsSync(provider, testCatalog);
172
+ if (!catModels || typeof catModels !== 'object') return kept; // cold/unavailable → skip
173
+
174
+ // Enrich each kept row with its catalog row (by id), if any.
175
+ const meta = kept.map((row) => {
176
+ const cr = catModels[row.id] || null;
177
+ return {
178
+ row,
179
+ cat: cr,
180
+ family: cr ? _stalenessFamily(cr, row.id) : null,
181
+ epoch: cr ? _releaseEpoch(cr) : null,
182
+ };
183
+ });
184
+
185
+ // (1) Family supersession: within a family, drop a row when the catalog has
186
+ // ANY model (kept or not) in the same family with a strictly newer date.
187
+ // Compute the newest release_date per family from the FULL catalog so a
188
+ // superseding model that isn't in this list still counts.
189
+ const familyNewest = new Map();
190
+ for (const [id, cr] of Object.entries(catModels)) {
191
+ const fam = _stalenessFamily(cr, id);
192
+ const ep = _releaseEpoch(cr);
193
+ if (!fam || ep == null) continue;
194
+ const prev = familyNewest.get(fam);
195
+ if (prev == null || ep > prev) familyNewest.set(fam, ep);
196
+ }
197
+
198
+ let dropped = new Set();
199
+ for (const m of meta) {
200
+ if (!m.cat || m.family == null || m.epoch == null) continue; // fallback: keep
201
+ const newest = familyNewest.get(m.family);
202
+ if (newest != null && newest > m.epoch) dropped.add(m.row);
203
+ }
204
+ // Never empty a family the user relies on: if supersession removed every
205
+ // in-list member of a family, restore the newest-in-list member.
206
+ const byFamilyInList = new Map();
207
+ for (const m of meta) {
208
+ if (m.family == null) continue;
209
+ if (!byFamilyInList.has(m.family)) byFamilyInList.set(m.family, []);
210
+ byFamilyInList.get(m.family).push(m);
211
+ }
212
+ for (const [, members] of byFamilyInList) {
213
+ if (!members.every((m) => dropped.has(m.row))) continue;
214
+ let best = null;
215
+ for (const m of members) {
216
+ if (!best || (m.epoch || 0) > (best.epoch || 0)) best = m;
217
+ }
218
+ if (best) dropped.delete(best.row);
219
+ }
220
+
221
+ // (2) Absolute cut: drop rows whose release_date is older than N months.
222
+ const months = _staleMonths();
223
+ if (months > 0) {
224
+ const cutoff = Date.now() - months * 30.4375 * 24 * 60 * 60 * 1000;
225
+ const cutDropped = new Set();
226
+ for (const m of meta) {
227
+ if (dropped.has(m.row)) continue;
228
+ if (!m.cat || m.epoch == null) continue; // fallback: keep
229
+ if (m.epoch < cutoff) cutDropped.add(m.row);
230
+ }
231
+ // If applying the absolute cut would empty the entire provider list, skip
232
+ // the cut (supersession drops still stand).
233
+ const survivorsAfterCut = kept.filter((r) => !dropped.has(r) && !cutDropped.has(r));
234
+ if (survivorsAfterCut.length > 0) {
235
+ for (const r of cutDropped) dropped.add(r);
236
+ }
237
+ }
238
+
239
+ return kept.filter((r) => !dropped.has(r));
240
+ }
241
+
242
+ // ── Coding-unfit filter (models.dev tool_call/modalities + id fallback) ──────
243
+ // HOSTED providers only, applied AFTER staleness, BEFORE dedupe. Data-driven:
244
+ // (1) catalog row exists and tool_call === false → drop (search-preview,
245
+ // roleplay, lightweight chat SKUs — regardless of mode:'chat');
246
+ // (2) catalog row exists and modalities.output is an array lacking 'text' →
247
+ // drop (non-text-emitting SKUs);
248
+ // (3) catalog row MISSING → id fallback CODING_UNFIT_ID_RE (search-preview /
249
+ // audio|realtime-preview), applied even for mode:'chat';
250
+ // (4) not in catalog + no id match → keep.
251
+ // Catalog cold/unavailable → id-fallback still applies (safe, id-only).
252
+ function _applyCodingUnfit(kept, provider, testCatalog) {
253
+ const out = [];
254
+ for (const row of kept) {
255
+ const id = row?.id;
256
+ if (!id) continue;
257
+ const cr = getModelsDevRowSync(id, provider, testCatalog);
258
+ if (cr) {
259
+ // (1) explicit non-tool-calling SKU
260
+ if (cr.tool_call === false) continue;
261
+ // (2) output modalities present but no 'text'
262
+ const outMods = cr?.modalities?.output;
263
+ if (Array.isArray(outMods) && !outMods.includes('text')) continue;
264
+ out.push(row);
265
+ continue;
266
+ }
267
+ // (3) catalog row missing → id-level fallback
268
+ if (CODING_UNFIT_ID_RE.test(String(id).toLowerCase())) continue;
269
+ // (4) keep
270
+ out.push(row);
271
+ }
272
+ return out;
273
+ }
274
+
275
+ /**
276
+ * Sanitize a provider model list.
277
+ * @param {Array<object>} models enriched rows ({ id, mode?, created?, ... })
278
+ * @param {{provider?: string}} [opts]
279
+ * @returns {Array<object>} filtered + deduped subset (same row objects)
280
+ */
281
+ export function sanitizeModelList(models, opts = {}) {
282
+ if (!Array.isArray(models)) return models;
283
+ const provider = String(opts?.provider || '').trim().toLowerCase();
284
+ const hosted = HOSTED_PROVIDERS.has(provider);
285
+
286
+ // CUSTOM/local providers (ollama, lmstudio, unknown names): apply ONLY the
287
+ // enriched-mode drop. No id heuristics, no legacy filter, no dedupe.
288
+ if (!hosted) {
289
+ return models.filter((row) => row?.id && !_isNonChatMode(row));
290
+ }
291
+
292
+ const kept = [];
293
+ for (const row of models) {
294
+ const id = row?.id;
295
+ if (!id) continue;
296
+ const lid = String(id).toLowerCase();
297
+ const modeChat = typeof row?.mode === 'string' && row.mode.trim().toLowerCase() === 'chat';
298
+ // (a0) hard non-LLM SKUs (image/tts/embeddings/moderation/realtime/audio):
299
+ // dropped even if the row falsely claims mode:'chat'. These families are
300
+ // never text-completion LLMs.
301
+ if (_isNonLlmId(lid)) continue;
302
+ // (a) enriched mode present and not chat-like
303
+ if (_isNonChatMode(row)) continue;
304
+ // (a) non-chat modality id — but an explicit mode:'chat' overrides id regex
305
+ if (!modeChat && NON_CHAT_RE.test(lid)) continue;
306
+ // (b) legacy/deprecated family
307
+ if (_isLegacy(lid)) continue;
308
+ kept.push(row);
309
+ }
310
+
311
+ // Data-driven auto-staleness (models.dev): applied AFTER the static drops
312
+ // above and BEFORE canonical dedupe. Catalog cold/unavailable → no-op.
313
+ // `opts._testCatalog` (tests only) injects a fake catalog map.
314
+ const staleFiltered = _applyAutoStaleness(kept, provider, opts?._testCatalog);
315
+
316
+ // Coding-unfit filter (models.dev tool_call/modalities + id fallback):
317
+ // AFTER staleness, BEFORE dedupe.
318
+ const codingFit = _applyCodingUnfit(staleFiltered, provider, opts?._testCatalog);
319
+
320
+ // (c) canonical dedupe on unambiguous date suffixes: prefer undated alias.
321
+ const byKey = new Map();
322
+ for (const row of codingFit) {
323
+ const key = _canonicalKey(row.id);
324
+ const prev = byKey.get(key);
325
+ if (!prev) { byKey.set(key, row); continue; }
326
+ byKey.set(key, _preferRow(prev, row));
327
+ }
328
+ let winners = new Set(byKey.values());
329
+
330
+ // Conditional MMDD collapse: drop a `-0xxx` row only when its undated alias
331
+ // survives in the winner set AND both share identical context/output limits.
332
+ const aliasByKey = new Map();
333
+ for (const row of winners) aliasByKey.set(_canonicalKey(row.id), row);
334
+ for (const row of [...winners]) {
335
+ const base = _mmddBase(row.id);
336
+ if (!base) continue;
337
+ const alias = aliasByKey.get(base);
338
+ if (alias && alias !== row && _sameLimits(alias, row)) winners.delete(row);
339
+ }
340
+
341
+ // Preserve original ordering of the winners.
342
+ return codingFit.filter((row) => winners.has(row));
343
+ }
344
+
345
+ function _preferRow(a, b) {
346
+ const aDated = _isDated(a.id);
347
+ const bDated = _isDated(b.id);
348
+ // Prefer the undated canonical alias.
349
+ if (aDated !== bDated) return aDated ? b : a;
350
+ if (!aDated && !bDated) return a; // both undated (shouldn't happen) keep first
351
+ // Both dated: keep the newest by `created`, else the lexically-latest id.
352
+ const ac = Number(a.created) || 0;
353
+ const bc = Number(b.created) || 0;
354
+ if (ac !== bc) return ac > bc ? a : b;
355
+ return String(b.id) > String(a.id) ? b : a;
356
+ }