mixdog 0.9.2 → 0.9.4

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 (417) hide show
  1. package/package.json +8 -3
  2. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  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/review-tasks.json +20 -0
  7. package/scripts/bench/round-codex.json +114 -0
  8. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  9. package/scripts/bench/round-mixdog-lead.json +269 -0
  10. package/scripts/bench/round-mixdog.json +126 -0
  11. package/scripts/bench/round-r10-bigsample.json +679 -0
  12. package/scripts/bench/round-r11-codexalign.json +257 -0
  13. package/scripts/bench/round-r4-codex.json +114 -0
  14. package/scripts/bench/round-r4-mixed.json +225 -0
  15. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  16. package/scripts/bench/round-r6-codex.json +114 -0
  17. package/scripts/bench/round-r6-solo.json +257 -0
  18. package/scripts/bench/round-r7-full.json +254 -0
  19. package/scripts/bench/round-r8-fulldefault.json +255 -0
  20. package/scripts/bench-run.mjs +215 -29
  21. package/scripts/build-tui.mjs +13 -1
  22. package/scripts/explore-bench.mjs +124 -0
  23. package/scripts/freevar-smoke.mjs +95 -0
  24. package/scripts/hook-bus-test.mjs +191 -0
  25. package/scripts/internal-comms-bench.mjs +1 -0
  26. package/scripts/internal-comms-smoke.mjs +10 -9
  27. package/scripts/mouse-probe.mjs +45 -0
  28. package/scripts/output-style-bench.mjs +13 -6
  29. package/scripts/output-style-smoke.mjs +4 -4
  30. package/scripts/path-suffix-test.mjs +57 -0
  31. package/scripts/provider-toolcall-test.mjs +7 -3
  32. package/scripts/recall-bench.mjs +207 -0
  33. package/scripts/recall-usecase-cases.json +18 -0
  34. package/scripts/recall-usecase-probe.json +6 -0
  35. package/scripts/session-bench.mjs +152 -6
  36. package/scripts/tool-smoke.mjs +30 -67
  37. package/scripts/tui-render-smoke.mjs +90 -0
  38. package/scripts/webhook-smoke.mjs +208 -0
  39. package/src/agents/debugger/AGENT.md +5 -2
  40. package/src/agents/heavy-worker/AGENT.md +21 -11
  41. package/src/agents/maintainer/AGENT.md +4 -0
  42. package/src/agents/reviewer/AGENT.md +3 -2
  43. package/src/agents/worker/AGENT.md +21 -11
  44. package/src/lib/rules-builder.cjs +4 -0
  45. package/src/mixdog-session-runtime.mjs +933 -3731
  46. package/src/output-styles/default.md +34 -9
  47. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  48. package/src/output-styles/minimal.md +4 -1
  49. package/src/output-styles/simple.md +22 -7
  50. package/src/repl.mjs +5 -5
  51. package/src/rules/agent/00-common.md +2 -0
  52. package/src/rules/agent/30-explorer.md +8 -11
  53. package/src/rules/lead/lead-brief.md +12 -0
  54. package/src/rules/lead/lead-tool.md +2 -9
  55. package/src/rules/shared/01-tool.md +11 -5
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  59. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  60. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  61. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  62. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  63. package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +63 -21
  66. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +97 -1343
  70. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  71. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +78 -10
  72. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
  73. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  74. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  75. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  76. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  77. package/src/runtime/agent/orchestrator/providers/gemini.mjs +44 -1014
  78. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +18 -4
  79. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  80. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  81. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  82. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  83. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +41 -1142
  89. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  90. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +303 -2119
  92. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +140 -995
  93. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  94. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  95. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  96. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  97. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
  99. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
  100. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  101. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  102. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  103. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  104. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  105. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  106. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  107. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  108. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  109. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +274 -0
  110. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  111. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  112. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  113. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  114. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +47 -0
  115. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  116. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  117. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  118. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  120. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  121. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  122. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  123. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  124. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  125. package/src/runtime/agent/orchestrator/session/loop.mjs +409 -1304
  126. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  127. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +230 -0
  128. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  129. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +149 -0
  130. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  131. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  132. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  133. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  134. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  135. package/src/runtime/agent/orchestrator/session/manager.mjs +226 -2114
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  137. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  138. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  141. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +33 -42
  142. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  143. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  144. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  145. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  146. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
  147. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  148. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  149. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -87
  152. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +78 -304
  155. package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -6
  156. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  157. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  158. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  159. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +551 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  161. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  163. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  164. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  165. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  166. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1080 -0
  167. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  168. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  169. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  170. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  171. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  172. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  173. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
  174. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  175. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  176. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  177. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  178. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  179. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  180. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  181. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  182. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  183. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  184. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -23
  185. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -74
  186. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  187. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  188. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  189. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  190. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  191. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  192. package/src/runtime/channels/backends/discord.mjs +12 -292
  193. package/src/runtime/channels/index.mjs +241 -894
  194. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  195. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  196. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  197. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  198. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  199. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  200. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  201. package/src/runtime/channels/lib/output-forwarder.mjs +9 -1
  202. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  203. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  204. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  205. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  206. package/src/runtime/channels/lib/telegram-format.mjs +19 -22
  207. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  208. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  209. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  210. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  211. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  212. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  213. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  214. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  215. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  216. package/src/runtime/channels/lib/webhook.mjs +36 -570
  217. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  218. package/src/runtime/channels/tool-defs.mjs +11 -130
  219. package/src/runtime/memory/index.mjs +258 -2050
  220. package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
  221. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  222. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  223. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  224. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  225. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  226. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  227. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  228. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  229. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  230. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  231. package/src/runtime/memory/lib/memory-cycle2.mjs +72 -837
  232. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  233. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  234. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  235. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  236. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  237. package/src/runtime/memory/lib/memory.mjs +20 -0
  238. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  239. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  240. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  241. package/src/runtime/memory/lib/recall-format.mjs +238 -0
  242. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  243. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  244. package/src/runtime/memory/tool-defs.mjs +6 -14
  245. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  246. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  247. package/src/runtime/search/lib/web-tools.mjs +24 -602
  248. package/src/runtime/shared/abort-controller.mjs +1 -1
  249. package/src/runtime/shared/atomic-file.mjs +26 -1
  250. package/src/runtime/shared/background-tasks.mjs +2 -3
  251. package/src/runtime/shared/buffered-appender.mjs +149 -0
  252. package/src/runtime/shared/launcher-control.mjs +2 -2
  253. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  254. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  255. package/src/runtime/shared/tool-primitives.mjs +308 -0
  256. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  257. package/src/runtime/shared/tool-surface.mjs +80 -898
  258. package/src/runtime/shared/transcript-writer.mjs +52 -2
  259. package/src/runtime/shared/update-checker.mjs +7 -4
  260. package/src/session-runtime/config-helpers.mjs +291 -0
  261. package/src/session-runtime/config-lifecycle.mjs +232 -0
  262. package/src/session-runtime/cwd-plugins.mjs +226 -0
  263. package/src/session-runtime/effort.mjs +128 -0
  264. package/src/session-runtime/fs-utils.mjs +10 -0
  265. package/src/session-runtime/mcp-glue.mjs +177 -0
  266. package/src/session-runtime/model-capabilities.mjs +130 -0
  267. package/src/session-runtime/model-recency.mjs +111 -0
  268. package/src/session-runtime/native-search.mjs +247 -0
  269. package/src/session-runtime/output-styles.mjs +126 -0
  270. package/src/session-runtime/plugin-mcp.mjs +114 -0
  271. package/src/session-runtime/prewarm.mjs +142 -0
  272. package/src/session-runtime/provider-models.mjs +278 -0
  273. package/src/session-runtime/provider-usage.mjs +120 -0
  274. package/src/session-runtime/quick-model-rows.mjs +170 -0
  275. package/src/session-runtime/quick-search-models.mjs +46 -0
  276. package/src/session-runtime/session-hooks.mjs +93 -0
  277. package/src/session-runtime/session-text.mjs +100 -0
  278. package/src/session-runtime/settings-api.mjs +319 -0
  279. package/src/session-runtime/statusline-route.mjs +35 -0
  280. package/src/session-runtime/tool-catalog.mjs +720 -0
  281. package/src/session-runtime/tool-defs.mjs +84 -0
  282. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  283. package/src/session-runtime/workflow.mjs +358 -0
  284. package/src/standalone/agent-tool/helpers.mjs +237 -0
  285. package/src/standalone/agent-tool/notify.mjs +107 -0
  286. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  287. package/src/standalone/agent-tool/render.mjs +152 -0
  288. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  289. package/src/standalone/agent-tool.mjs +155 -677
  290. package/src/standalone/channel-worker.mjs +7 -9
  291. package/src/standalone/explore-tool.mjs +40 -12
  292. package/src/standalone/hook-bus/config.mjs +207 -0
  293. package/src/standalone/hook-bus/constants.mjs +90 -0
  294. package/src/standalone/hook-bus/handlers.mjs +481 -0
  295. package/src/standalone/hook-bus/payload.mjs +31 -0
  296. package/src/standalone/hook-bus/rules.mjs +77 -0
  297. package/src/standalone/hook-bus.mjs +110 -746
  298. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  299. package/src/standalone/opencode-go-login.mjs +125 -0
  300. package/src/standalone/provider-admin.mjs +15 -19
  301. package/src/standalone/usage-dashboard.mjs +3 -1
  302. package/src/tui/App.jsx +1163 -7571
  303. package/src/tui/app/app-format.mjs +206 -0
  304. package/src/tui/app/channel-pickers.mjs +510 -0
  305. package/src/tui/app/clipboard.mjs +67 -0
  306. package/src/tui/app/core-memory-picker.mjs +210 -0
  307. package/src/tui/app/extension-pickers.mjs +506 -0
  308. package/src/tui/app/input-parsers.mjs +193 -0
  309. package/src/tui/app/maintenance-pickers.mjs +324 -0
  310. package/src/tui/app/model-options.mjs +330 -0
  311. package/src/tui/app/model-picker.mjs +365 -0
  312. package/src/tui/app/onboarding-steps.mjs +400 -0
  313. package/src/tui/app/project-picker.mjs +247 -0
  314. package/src/tui/app/provider-setup-picker.mjs +580 -0
  315. package/src/tui/app/resume-picker.mjs +55 -0
  316. package/src/tui/app/route-pickers.mjs +419 -0
  317. package/src/tui/app/settings-picker.mjs +490 -0
  318. package/src/tui/app/slash-commands.mjs +101 -0
  319. package/src/tui/app/slash-dispatch.mjs +427 -0
  320. package/src/tui/app/text-layout.mjs +46 -0
  321. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  322. package/src/tui/app/transcript-window.mjs +671 -0
  323. package/src/tui/app/use-mouse-input.mjs +460 -0
  324. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  325. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  326. package/src/tui/app/use-transcript-window.mjs +589 -0
  327. package/src/tui/components/ConfirmBar.jsx +1 -1
  328. package/src/tui/components/Picker.jsx +32 -4
  329. package/src/tui/components/PromptInput.jsx +259 -80
  330. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  331. package/src/tui/components/StatusLine.jsx +63 -12
  332. package/src/tui/components/TextEntryPanel.jsx +11 -0
  333. package/src/tui/components/ToolExecution.jsx +56 -588
  334. package/src/tui/components/TranscriptItem.jsx +105 -0
  335. package/src/tui/components/UsagePanel.jsx +18 -4
  336. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  337. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  338. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  339. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  340. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  341. package/src/tui/components/tool-output-format.mjs +2 -2
  342. package/src/tui/display-width.mjs +20 -3
  343. package/src/tui/dist/index.mjs +18034 -17188
  344. package/src/tui/engine/agent-envelope.mjs +296 -0
  345. package/src/tui/engine/agent-job-feed.mjs +133 -0
  346. package/src/tui/engine/boot-profile.mjs +21 -0
  347. package/src/tui/engine/labels.mjs +67 -0
  348. package/src/tui/engine/notice-text.mjs +112 -0
  349. package/src/tui/engine/notification-plan.mjs +76 -0
  350. package/src/tui/engine/queue-helpers.mjs +161 -0
  351. package/src/tui/engine/render-timing.mjs +17 -0
  352. package/src/tui/engine/session-stats.mjs +46 -0
  353. package/src/tui/engine/tool-approval.mjs +94 -0
  354. package/src/tui/engine/tool-call-fields.mjs +23 -0
  355. package/src/tui/engine/tool-card-results.mjs +234 -0
  356. package/src/tui/engine/tool-result-status.mjs +135 -0
  357. package/src/tui/engine/tool-result-text.mjs +126 -0
  358. package/src/tui/engine.mjs +405 -1385
  359. package/src/tui/figures.mjs +5 -0
  360. package/src/tui/index.jsx +105 -0
  361. package/src/tui/input-editing.mjs +60 -10
  362. package/src/tui/keyboard-protocol.mjs +2 -2
  363. package/src/tui/lib/voice-recorder.mjs +35 -19
  364. package/src/tui/markdown/format-token.mjs +11 -9
  365. package/src/tui/paste-attachments.mjs +38 -0
  366. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  367. package/src/tui/theme.mjs +6 -0
  368. package/src/tui/themes/base.mjs +2 -2
  369. package/src/tui/themes/kanagawa.mjs +4 -4
  370. package/src/tui/themes/teal.mjs +4 -5
  371. package/src/tui/themes/utils.mjs +1 -1
  372. package/src/ui/statusline-agents.mjs +213 -0
  373. package/src/ui/statusline-format.mjs +146 -0
  374. package/src/ui/statusline-segments.mjs +148 -0
  375. package/src/ui/statusline.mjs +77 -462
  376. package/src/ui/tool-card.mjs +0 -1
  377. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  378. package/src/workflows/default/WORKFLOW.md +16 -9
  379. package/src/workflows/sequential/WORKFLOW.md +16 -11
  380. package/src/workflows/solo/WORKFLOW.md +5 -1
  381. package/vendor/ink/build/display-width.js +19 -3
  382. package/vendor/ink/build/ink.js +103 -6
  383. package/vendor/ink/build/log-update.js +17 -3
  384. package/vendor/ink/build/wrap-text.js +125 -0
  385. package/scripts/_test-folder-dialog.mjs +0 -30
  386. package/scripts/fix-brief-fn.mjs +0 -35
  387. package/scripts/fix-format-tool-surface.mjs +0 -24
  388. package/scripts/fix-tool-exec-visible.mjs +0 -42
  389. package/scripts/patch-agent-brief.mjs +0 -48
  390. package/scripts/patch-app.mjs +0 -21
  391. package/scripts/patch-app2.mjs +0 -18
  392. package/scripts/patch-dist-brief.mjs +0 -96
  393. package/scripts/patch-tool-exec.mjs +0 -70
  394. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  395. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  396. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  397. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  398. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  399. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  400. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  401. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  402. package/src/standalone/agent-task-status.test.mjs +0 -76
  403. package/src/tui/components/tool-output-format.test.mjs +0 -399
  404. package/src/tui/display-width.test.mjs +0 -35
  405. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  406. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  407. package/src/tui/markdown/format-token.test.mjs +0 -354
  408. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  409. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  410. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  411. package/src/tui/prompt-history-store.test.mjs +0 -52
  412. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  413. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  414. package/src/ui/markdown.test.mjs +0 -70
  415. package/src/ui/statusline-context-label.test.mjs +0 -15
  416. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  417. 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';
@@ -114,7 +115,7 @@ export function normalizeGrokModelId(id) {
114
115
  }
115
116
  const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
116
117
  // Bump when the on-disk cache shape changes so stale-shape entries are
117
- // discarded instead of misread (mirrors openai-oauth's schema-version gate).
118
+ // discarded instead of misread.
118
119
  const GROK_MODEL_CACHE_SCHEMA_VERSION = 1;
119
120
  const DISCOVERY_TIMEOUT_MS = 15_000;
120
121
  const TOKEN_TIMEOUT_MS = 30_000;
@@ -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;
@@ -0,0 +1,32 @@
1
+ // Shared numeric/string primitives for provider usage-accounting modules.
2
+ //
3
+ // These are byte-for-byte identical helpers that were duplicated across
4
+ // api-usage.mjs, oauth-usage.mjs and opencode-go-usage.mjs. Extracting them
5
+ // here is behavior-preserving: each exported function reproduces the exact
6
+ // logic of the copies it replaces.
7
+ //
8
+ // NOTE ON `num`: api-usage.mjs and oauth-usage.mjs use the guarded variant
9
+ // below (treats null/undefined/'' as the fallback before Number()).
10
+ // opencode-go-usage.mjs uses an *unguarded* variant (Number('') === 0), which
11
+ // is observably different, so it keeps its own local `num` and is NOT wired to
12
+ // this module.
13
+
14
+ export function num(value, fallback = null) {
15
+ if (value === null || value === undefined || value === '') return fallback;
16
+ const n = Number(value);
17
+ return Number.isFinite(n) ? n : fallback;
18
+ }
19
+
20
+ export function round(value, digits = 4) {
21
+ const n = Number(value);
22
+ if (!Number.isFinite(n)) return null;
23
+ const scale = 10 ** digits;
24
+ return Math.round(n * scale) / scale;
25
+ }
26
+
27
+ // cleanString (oauth-usage.mjs) and clean (opencode-go-usage.mjs) were
28
+ // identical: trim strings, coerce empties/non-strings to null.
29
+ export function cleanString(value) {
30
+ const s = typeof value === 'string' ? value.trim() : '';
31
+ return s || null;
32
+ }
@@ -61,6 +61,33 @@ const _CATALOG_BEDROCK_PREFIXES = ['anthropic.', 'bedrock/anthropic.'];
61
61
  // azure_ai- or openrouter-namespaced).
62
62
  const _CATALOG_ENRICH_PREFIXES = ['anthropic/', 'openai/', 'gemini/', 'google/', 'xai/', 'deepseek/'];
63
63
 
64
+ // Provider hint → catalog prefixes to try (subset of _CATALOG_SIMPLE_PREFIXES /
65
+ // _CATALOG_ENRICH_PREFIXES). Keyed by the *mapped* models.dev provider id
66
+ // (see _modelsDevProviderId), so anthropic-oauth and anthropic share one
67
+ // entry, likewise grok-oauth/xai and gemini/google. A provider missing here
68
+ // (unknown/custom) gets bare-id lookup only — no prefix guessing across
69
+ // unrelated providers. No provider hint at all (mappedProvider null) keeps
70
+ // legacy behaviour: try every prefix.
71
+ const _PROVIDER_CATALOG_PREFIXES = {
72
+ openai: ['openai/'],
73
+ anthropic: ['anthropic/'],
74
+ google: ['gemini/', 'google/'],
75
+ xai: ['xai/'],
76
+ deepseek: ['deepseek/'],
77
+ azure: ['azure_ai/'],
78
+ };
79
+ function _prefixesForProvider(mappedProvider, allPrefixes) {
80
+ if (!mappedProvider) return allPrefixes;
81
+ const allowed = _PROVIDER_CATALOG_PREFIXES[mappedProvider];
82
+ if (!allowed) return [];
83
+ return allPrefixes.filter((p) => allowed.includes(p));
84
+ }
85
+ // Bedrock-style catalog keys (anthropic.<id>-v1:0) only ever describe
86
+ // Anthropic models; skip that lookup entirely for any other provider hint.
87
+ function _bedrockAllowed(mappedProvider) {
88
+ return !mappedProvider || mappedProvider === 'anthropic';
89
+ }
90
+
64
91
  // Polyfill for models the LiteLLM catalog does not list yet. Values mirror
65
92
  // the catalog row shape so _normalize works unchanged. Source: each provider's
66
93
  // official pricing page; do not extrapolate. Promotional discounts are
@@ -299,6 +326,33 @@ function _modelsDevMetadataSync(id, provider) {
299
326
  return _normalize(_modelsDevRowToOverride(row));
300
327
  }
301
328
 
329
+ // Raw models.dev catalog row accessor for the model-list sanitizer's
330
+ // data-driven staleness filter. Unlike _modelsDevMetadataSync this does NOT
331
+ // require a `cost` field (staleness only needs family/release_date) and
332
+ // returns the raw row untouched. Warms from disk one-shot if memory is cold;
333
+ // returns null when the catalog is unavailable so callers can skip filtering.
334
+ // `_test` (tests only) injects a fake catalog map without touching disk.
335
+ export function getModelsDevRowSync(id, provider, _test) {
336
+ const cat = _test || _mdCache || (warmModelsDevFromDiskSync(), _mdCache);
337
+ if (!cat) { void loadModelsDevCatalog(); return null; }
338
+ const pid = _modelsDevProviderId(provider);
339
+ if (!pid) return null;
340
+ const row = cat?.[pid]?.models?.[id];
341
+ return row || null;
342
+ }
343
+
344
+ // All models.dev rows for a provider (mapped id), keyed by model id. Used by
345
+ // the sanitizer's family-supersession pass to compare release dates across a
346
+ // provider's whole catalog. Returns null when the catalog is cold/unavailable.
347
+ export function getModelsDevProviderModelsSync(provider, _test) {
348
+ const cat = _test || _mdCache || (warmModelsDevFromDiskSync(), _mdCache);
349
+ if (!cat) { void loadModelsDevCatalog(); return null; }
350
+ const pid = _modelsDevProviderId(provider);
351
+ if (!pid) return null;
352
+ const models = cat?.[pid]?.models;
353
+ return (models && typeof models === 'object') ? models : null;
354
+ }
355
+
302
356
  /**
303
357
  * Sync lookup. Warm order:
304
358
  * 1. in-memory cache (hot path),
@@ -323,16 +377,19 @@ export function getModelMetadataSync(id, provider) {
323
377
  if (ov && (!mappedProvider || _modelsDevProviderId(ov.litellm_provider) === mappedProvider)) {
324
378
  meta = _normalize(ov);
325
379
  }
380
+ const metaFromPricingOverride = meta !== null;
326
381
  // 2. LiteLLM community catalog (broad mainstream coverage).
327
382
  if (!_memCache) warmFromDiskSync();
328
383
  if (!meta && _memCache) {
329
384
  const catalog = _memCache;
330
- if (catalog[id]) meta = _normalize(catalog[id]);
331
- for (const prefix of _CATALOG_SIMPLE_PREFIXES) {
385
+ if (catalog[id] && (!mappedProvider || _modelsDevProviderId(catalog[id].litellm_provider) === mappedProvider)) {
386
+ meta = _normalize(catalog[id]);
387
+ }
388
+ for (const prefix of _prefixesForProvider(mappedProvider, _CATALOG_SIMPLE_PREFIXES)) {
332
389
  if (meta) break;
333
390
  if (catalog[prefix + id]) meta = _normalize(catalog[prefix + id]);
334
391
  }
335
- for (const prefix of _CATALOG_BEDROCK_PREFIXES) {
392
+ for (const prefix of (_bedrockAllowed(mappedProvider) ? _CATALOG_BEDROCK_PREFIXES : [])) {
336
393
  if (meta) break;
337
394
  const v1 = catalog[prefix + id + '-v1:0'];
338
395
  if (v1) meta = _normalize(v1);
@@ -343,7 +400,7 @@ export function getModelMetadataSync(id, provider) {
343
400
  // id, and add fields LiteLLM lacks, such as opencode-go reasoning_options.
344
401
  if (mappedProvider) {
345
402
  const md = _modelsDevMetadataSync(id, provider);
346
- if (md) meta = mergeModelMetadata(meta, md);
403
+ if (md) meta = mergeModelMetadata(meta, md, { preserveBaseCosts: metaFromPricingOverride });
347
404
  }
348
405
  return meta;
349
406
  }
@@ -368,13 +425,23 @@ function _normalize(entry) {
368
425
  };
369
426
  }
370
427
 
371
- function mergeModelMetadata(base, overlay) {
428
+ function mergeModelMetadata(base, overlay, opts = {}) {
372
429
  if (!base) return overlay || null;
373
430
  if (!overlay) return base;
374
431
  return {
375
432
  ...base,
376
433
  contextWindow: overlay.contextWindow || base.contextWindow || null,
377
434
  outputTokens: overlay.outputTokens || base.outputTokens || null,
435
+ // Provider-scoped models.dev rates beat generic base (LiteLLM) rates
436
+ // when present — the overlay is the provider-scoped source, so a
437
+ // non-null overlay value always wins over the generic fallback —
438
+ // UNLESS base came from PRICING_OVERRIDES (preserveBaseCosts), which
439
+ // is a hand-verified, authoritative rate that must not be clobbered
440
+ // by a models.dev row for the same id.
441
+ inputCostPerM: (!opts.preserveBaseCosts && overlay.inputCostPerM != null) ? overlay.inputCostPerM : base.inputCostPerM,
442
+ outputCostPerM: (!opts.preserveBaseCosts && overlay.outputCostPerM != null) ? overlay.outputCostPerM : base.outputCostPerM,
443
+ cacheReadCostPerM: (!opts.preserveBaseCosts && overlay.cacheReadCostPerM != null) ? overlay.cacheReadCostPerM : base.cacheReadCostPerM,
444
+ cacheWriteCostPerM: (!opts.preserveBaseCosts && overlay.cacheWriteCostPerM != null) ? overlay.cacheWriteCostPerM : base.cacheWriteCostPerM,
378
445
  supportsVision: base.supportsVision || overlay.supportsVision,
379
446
  supportsFunctionCalling: base.supportsFunctionCalling || overlay.supportsFunctionCalling,
380
447
  supportsWebSearch: base.supportsWebSearch || overlay.supportsWebSearch,
@@ -401,23 +468,31 @@ export async function enrichModels(models) {
401
468
  const id = m.id || m.name;
402
469
  if (!id) return m;
403
470
  // Same lookup logic as getModelMetadata but inlined for speed.
404
- let entry = PRICING_OVERRIDES[id] || catalog[id];
471
+ const mappedProvider = m.provider ? _modelsDevProviderId(m.provider) : null;
472
+ // Provider-guarded, same as getModelMetadataSync: an override/catalog
473
+ // row is only honoured when it belongs to the hinted provider, so a
474
+ // model id shared across providers never leaks the wrong rate.
475
+ const ov = PRICING_OVERRIDES[id];
476
+ let entry = (ov && (!mappedProvider || _modelsDevProviderId(ov.litellm_provider) === mappedProvider)) ? ov : null;
477
+ if (!entry && catalog[id] && (!mappedProvider || _modelsDevProviderId(catalog[id].litellm_provider) === mappedProvider)) {
478
+ entry = catalog[id];
479
+ }
405
480
  if (!entry) {
406
- for (const prefix of _CATALOG_ENRICH_PREFIXES) {
481
+ for (const prefix of _prefixesForProvider(mappedProvider, _CATALOG_ENRICH_PREFIXES)) {
407
482
  if (catalog[prefix + id]) { entry = catalog[prefix + id]; break; }
408
483
  }
409
484
  }
410
- if (!entry) {
485
+ if (!entry && _bedrockAllowed(mappedProvider)) {
411
486
  for (const prefix of _CATALOG_BEDROCK_PREFIXES) {
412
487
  if (catalog[prefix + id + '-v1:0']) { entry = catalog[prefix + id + '-v1:0']; break; }
413
488
  }
414
489
  }
415
490
  let meta = entry ? _normalize(entry) : null;
491
+ const metaFromPricingOverride = entry != null && entry === ov;
416
492
  if (m.provider) {
417
- const pid = _modelsDevProviderId(m.provider);
418
- const row = pid ? _mdCache?.[pid]?.models?.[id] : null;
493
+ const row = mappedProvider ? _mdCache?.[mappedProvider]?.models?.[id] : null;
419
494
  const providerMeta = row ? _normalize(_modelsDevRowToOverride(row)) : null;
420
- if (providerMeta) meta = mergeModelMetadata(meta, providerMeta);
495
+ if (providerMeta) meta = mergeModelMetadata(meta, providerMeta, { preserveBaseCosts: metaFromPricingOverride });
421
496
  }
422
497
  if (!meta) return m;
423
498
  return {
@@ -0,0 +1,348 @@
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 _staleMonths() {
149
+ const raw = process.env.MIXDOG_MODEL_STALE_MONTHS;
150
+ if (raw == null || raw === '') return 12;
151
+ const n = Number(raw);
152
+ return Number.isFinite(n) ? n : 12;
153
+ }
154
+
155
+ // Apply catalog-driven staleness to the already-filtered `kept` rows.
156
+ // Rules (see slice brief): (1) family supersession by newer release_date,
157
+ // (2) absolute cut older than N months. Both consult the provider's
158
+ // models.dev catalog; rows not found in the catalog are KEPT (fallback to the
159
+ // static LEGACY_PATTERNS already applied earlier). Catalog cold → no-op.
160
+ // Guards: never empty a family via supersession; if the absolute cut would
161
+ // empty the whole provider list, skip the cut entirely.
162
+ function _applyAutoStaleness(kept, provider, testCatalog) {
163
+ const catModels = getModelsDevProviderModelsSync(provider, testCatalog);
164
+ if (!catModels || typeof catModels !== 'object') return kept; // cold/unavailable → skip
165
+
166
+ // Enrich each kept row with its catalog row (by id), if any.
167
+ const meta = kept.map((row) => {
168
+ const cr = catModels[row.id] || null;
169
+ return {
170
+ row,
171
+ cat: cr,
172
+ family: cr && typeof cr.family === 'string' ? cr.family.toLowerCase() : null,
173
+ epoch: cr ? _releaseEpoch(cr) : null,
174
+ };
175
+ });
176
+
177
+ // (1) Family supersession: within a family, drop a row when the catalog has
178
+ // ANY model (kept or not) in the same family with a strictly newer date.
179
+ // Compute the newest release_date per family from the FULL catalog so a
180
+ // superseding model that isn't in this list still counts.
181
+ const familyNewest = new Map();
182
+ for (const cr of Object.values(catModels)) {
183
+ const fam = cr && typeof cr.family === 'string' ? cr.family.toLowerCase() : null;
184
+ const ep = _releaseEpoch(cr);
185
+ if (!fam || ep == null) continue;
186
+ const prev = familyNewest.get(fam);
187
+ if (prev == null || ep > prev) familyNewest.set(fam, ep);
188
+ }
189
+
190
+ let dropped = new Set();
191
+ for (const m of meta) {
192
+ if (!m.cat || m.family == null || m.epoch == null) continue; // fallback: keep
193
+ const newest = familyNewest.get(m.family);
194
+ if (newest != null && newest > m.epoch) dropped.add(m.row);
195
+ }
196
+ // Never empty a family the user relies on: if supersession removed every
197
+ // in-list member of a family, restore the newest-in-list member.
198
+ const byFamilyInList = new Map();
199
+ for (const m of meta) {
200
+ if (m.family == null) continue;
201
+ if (!byFamilyInList.has(m.family)) byFamilyInList.set(m.family, []);
202
+ byFamilyInList.get(m.family).push(m);
203
+ }
204
+ for (const [, members] of byFamilyInList) {
205
+ if (!members.every((m) => dropped.has(m.row))) continue;
206
+ let best = null;
207
+ for (const m of members) {
208
+ if (!best || (m.epoch || 0) > (best.epoch || 0)) best = m;
209
+ }
210
+ if (best) dropped.delete(best.row);
211
+ }
212
+
213
+ // (2) Absolute cut: drop rows whose release_date is older than N months.
214
+ const months = _staleMonths();
215
+ if (months > 0) {
216
+ const cutoff = Date.now() - months * 30.4375 * 24 * 60 * 60 * 1000;
217
+ const cutDropped = new Set();
218
+ for (const m of meta) {
219
+ if (dropped.has(m.row)) continue;
220
+ if (!m.cat || m.epoch == null) continue; // fallback: keep
221
+ if (m.epoch < cutoff) cutDropped.add(m.row);
222
+ }
223
+ // If applying the absolute cut would empty the entire provider list, skip
224
+ // the cut (supersession drops still stand).
225
+ const survivorsAfterCut = kept.filter((r) => !dropped.has(r) && !cutDropped.has(r));
226
+ if (survivorsAfterCut.length > 0) {
227
+ for (const r of cutDropped) dropped.add(r);
228
+ }
229
+ }
230
+
231
+ return kept.filter((r) => !dropped.has(r));
232
+ }
233
+
234
+ // ── Coding-unfit filter (models.dev tool_call/modalities + id fallback) ──────
235
+ // HOSTED providers only, applied AFTER staleness, BEFORE dedupe. Data-driven:
236
+ // (1) catalog row exists and tool_call === false → drop (search-preview,
237
+ // roleplay, lightweight chat SKUs — regardless of mode:'chat');
238
+ // (2) catalog row exists and modalities.output is an array lacking 'text' →
239
+ // drop (non-text-emitting SKUs);
240
+ // (3) catalog row MISSING → id fallback CODING_UNFIT_ID_RE (search-preview /
241
+ // audio|realtime-preview), applied even for mode:'chat';
242
+ // (4) not in catalog + no id match → keep.
243
+ // Catalog cold/unavailable → id-fallback still applies (safe, id-only).
244
+ function _applyCodingUnfit(kept, provider, testCatalog) {
245
+ const out = [];
246
+ for (const row of kept) {
247
+ const id = row?.id;
248
+ if (!id) continue;
249
+ const cr = getModelsDevRowSync(id, provider, testCatalog);
250
+ if (cr) {
251
+ // (1) explicit non-tool-calling SKU
252
+ if (cr.tool_call === false) continue;
253
+ // (2) output modalities present but no 'text'
254
+ const outMods = cr?.modalities?.output;
255
+ if (Array.isArray(outMods) && !outMods.includes('text')) continue;
256
+ out.push(row);
257
+ continue;
258
+ }
259
+ // (3) catalog row missing → id-level fallback
260
+ if (CODING_UNFIT_ID_RE.test(String(id).toLowerCase())) continue;
261
+ // (4) keep
262
+ out.push(row);
263
+ }
264
+ return out;
265
+ }
266
+
267
+ /**
268
+ * Sanitize a provider model list.
269
+ * @param {Array<object>} models enriched rows ({ id, mode?, created?, ... })
270
+ * @param {{provider?: string}} [opts]
271
+ * @returns {Array<object>} filtered + deduped subset (same row objects)
272
+ */
273
+ export function sanitizeModelList(models, opts = {}) {
274
+ if (!Array.isArray(models)) return models;
275
+ const provider = String(opts?.provider || '').trim().toLowerCase();
276
+ const hosted = HOSTED_PROVIDERS.has(provider);
277
+
278
+ // CUSTOM/local providers (ollama, lmstudio, unknown names): apply ONLY the
279
+ // enriched-mode drop. No id heuristics, no legacy filter, no dedupe.
280
+ if (!hosted) {
281
+ return models.filter((row) => row?.id && !_isNonChatMode(row));
282
+ }
283
+
284
+ const kept = [];
285
+ for (const row of models) {
286
+ const id = row?.id;
287
+ if (!id) continue;
288
+ const lid = String(id).toLowerCase();
289
+ const modeChat = typeof row?.mode === 'string' && row.mode.trim().toLowerCase() === 'chat';
290
+ // (a0) hard non-LLM SKUs (image/tts/embeddings/moderation/realtime/audio):
291
+ // dropped even if the row falsely claims mode:'chat'. These families are
292
+ // never text-completion LLMs.
293
+ if (_isNonLlmId(lid)) continue;
294
+ // (a) enriched mode present and not chat-like
295
+ if (_isNonChatMode(row)) continue;
296
+ // (a) non-chat modality id — but an explicit mode:'chat' overrides id regex
297
+ if (!modeChat && NON_CHAT_RE.test(lid)) continue;
298
+ // (b) legacy/deprecated family
299
+ if (_isLegacy(lid)) continue;
300
+ kept.push(row);
301
+ }
302
+
303
+ // Data-driven auto-staleness (models.dev): applied AFTER the static drops
304
+ // above and BEFORE canonical dedupe. Catalog cold/unavailable → no-op.
305
+ // `opts._testCatalog` (tests only) injects a fake catalog map.
306
+ const staleFiltered = _applyAutoStaleness(kept, provider, opts?._testCatalog);
307
+
308
+ // Coding-unfit filter (models.dev tool_call/modalities + id fallback):
309
+ // AFTER staleness, BEFORE dedupe.
310
+ const codingFit = _applyCodingUnfit(staleFiltered, provider, opts?._testCatalog);
311
+
312
+ // (c) canonical dedupe on unambiguous date suffixes: prefer undated alias.
313
+ const byKey = new Map();
314
+ for (const row of codingFit) {
315
+ const key = _canonicalKey(row.id);
316
+ const prev = byKey.get(key);
317
+ if (!prev) { byKey.set(key, row); continue; }
318
+ byKey.set(key, _preferRow(prev, row));
319
+ }
320
+ let winners = new Set(byKey.values());
321
+
322
+ // Conditional MMDD collapse: drop a `-0xxx` row only when its undated alias
323
+ // survives in the winner set AND both share identical context/output limits.
324
+ const aliasByKey = new Map();
325
+ for (const row of winners) aliasByKey.set(_canonicalKey(row.id), row);
326
+ for (const row of [...winners]) {
327
+ const base = _mmddBase(row.id);
328
+ if (!base) continue;
329
+ const alias = aliasByKey.get(base);
330
+ if (alias && alias !== row && _sameLimits(alias, row)) winners.delete(row);
331
+ }
332
+
333
+ // Preserve original ordering of the winners.
334
+ return codingFit.filter((row) => winners.has(row));
335
+ }
336
+
337
+ function _preferRow(a, b) {
338
+ const aDated = _isDated(a.id);
339
+ const bDated = _isDated(b.id);
340
+ // Prefer the undated canonical alias.
341
+ if (aDated !== bDated) return aDated ? b : a;
342
+ if (!aDated && !bDated) return a; // both undated (shouldn't happen) keep first
343
+ // Both dated: keep the newest by `created`, else the lexically-latest id.
344
+ const ac = Number(a.created) || 0;
345
+ const bc = Number(b.created) || 0;
346
+ if (ac !== bc) return ac > bc ? a : b;
347
+ return String(b.id) > String(a.id) ? b : a;
348
+ }