@gajae-code/coding-agent 0.11.1 → 0.11.3

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 (413) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/launch-disposition.d.ts +23 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -0
  5. package/dist/types/cli/session-picker.d.ts +2 -1
  6. package/dist/types/cli/setup-cli.d.ts +1 -1
  7. package/dist/types/commands/mcp-serve.d.ts +33 -0
  8. package/dist/types/commands/mcp.d.ts +1 -1
  9. package/dist/types/commands/notify.d.ts +0 -2
  10. package/dist/types/commands/sdk.d.ts +1 -1
  11. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  12. package/dist/types/config/file-lock.d.ts +14 -4
  13. package/dist/types/config/keybindings.d.ts +42 -1
  14. package/dist/types/config/model-auth.d.ts +3 -0
  15. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  16. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  17. package/dist/types/config/model-equivalence.d.ts +20 -0
  18. package/dist/types/config/model-registry.d.ts +16 -15
  19. package/dist/types/config/model-resolver.d.ts +28 -18
  20. package/dist/types/config/models-config-schema.d.ts +11 -0
  21. package/dist/types/config/settings-schema.d.ts +141 -11
  22. package/dist/types/config/settings.d.ts +14 -11
  23. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  24. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  26. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  27. package/dist/types/discovery/helpers.d.ts +0 -6
  28. package/dist/types/edit/index.d.ts +3 -0
  29. package/dist/types/eval/py/env.d.ts +9 -0
  30. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  31. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  33. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  34. package/dist/types/extensibility/shared-events.d.ts +2 -0
  35. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  36. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  37. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  38. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  39. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  40. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  41. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  42. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  43. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  44. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  45. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  46. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  47. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  48. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  49. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  50. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  51. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  52. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  53. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  54. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  55. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  56. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  57. package/dist/types/goals/runtime.d.ts +1 -0
  58. package/dist/types/goals/state.d.ts +8 -0
  59. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  60. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  61. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  62. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  63. package/dist/types/hindsight/state.d.ts +11 -4
  64. package/dist/types/lsp/lspmux.d.ts +5 -3
  65. package/dist/types/lsp/path-trust.d.ts +1 -0
  66. package/dist/types/main.d.ts +4 -2
  67. package/dist/types/memories/storage.d.ts +1 -0
  68. package/dist/types/modes/action-registry.d.ts +29 -0
  69. package/dist/types/modes/components/chrome.d.ts +14 -0
  70. package/dist/types/modes/components/command-palette.d.ts +27 -0
  71. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  72. package/dist/types/modes/components/index.d.ts +2 -1
  73. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  74. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  75. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  76. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  77. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  78. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  79. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  80. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  81. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  82. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  83. package/dist/types/modes/components/status-line.d.ts +1 -94
  84. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  85. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  86. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  87. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  88. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  89. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  90. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  91. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  92. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  93. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  94. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  95. package/dist/types/modes/interactive-mode.d.ts +19 -17
  96. package/dist/types/modes/jobs-observer.d.ts +2 -0
  97. package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
  98. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  99. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  100. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  101. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  102. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  103. package/dist/types/modes/types.d.ts +20 -13
  104. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  105. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  106. package/dist/types/sdk/broker/broker.d.ts +78 -12
  107. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  108. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  109. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  110. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  111. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  112. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  113. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  114. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  115. package/dist/types/sdk/bus/config.d.ts +11 -0
  116. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  117. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  118. package/dist/types/sdk/bus/index.d.ts +29 -0
  119. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  120. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  121. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  122. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  123. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  124. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  125. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  126. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  127. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  128. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  129. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  130. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  131. package/dist/types/sdk/host/host.d.ts +2 -0
  132. package/dist/types/sdk/index.d.ts +1 -0
  133. package/dist/types/sdk/session-directory.d.ts +56 -0
  134. package/dist/types/sdk/session.d.ts +2 -0
  135. package/dist/types/session/agent-session.d.ts +82 -47
  136. package/dist/types/session/agent-storage.d.ts +7 -0
  137. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  138. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  139. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  140. package/dist/types/session/messages.d.ts +16 -0
  141. package/dist/types/session/session-manager-internal.d.ts +14 -0
  142. package/dist/types/session/session-manager.d.ts +82 -12
  143. package/dist/types/session/session-storage.d.ts +43 -3
  144. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  145. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  146. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  147. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  148. package/dist/types/system-prompt.d.ts +2 -11
  149. package/dist/types/task/executor.d.ts +3 -1
  150. package/dist/types/thinking.d.ts +7 -0
  151. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  152. package/dist/types/tools/ask.d.ts +64 -2
  153. package/dist/types/tools/bash.d.ts +3 -0
  154. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  155. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  156. package/dist/types/tools/cron.d.ts +2 -0
  157. package/dist/types/tools/fetch.d.ts +1 -0
  158. package/dist/types/tools/index.d.ts +30 -15
  159. package/dist/types/tools/output-meta.d.ts +1 -0
  160. package/dist/types/tools/read.d.ts +5 -0
  161. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  162. package/dist/types/tools/subagent.d.ts +1 -0
  163. package/dist/types/utils/event-bus.d.ts +1 -1
  164. package/dist/types/utils/file-mentions.d.ts +3 -3
  165. package/dist/types/web/search/index.d.ts +3 -1
  166. package/package.json +21 -9
  167. package/scripts/build-sdk-package-smoke.ts +34 -1
  168. package/scripts/generate-hotkeys-docs.ts +74 -0
  169. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  170. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  171. package/src/async/job-manager.ts +49 -6
  172. package/src/cli/config-cli.ts +75 -2
  173. package/src/cli/launch-disposition.ts +42 -0
  174. package/src/cli/notify-cli.ts +92 -26
  175. package/src/cli/session-picker.ts +20 -3
  176. package/src/cli/setup-cli.ts +1 -1
  177. package/src/cli.ts +1 -1
  178. package/src/commands/config.ts +1 -1
  179. package/src/commands/harness.ts +34 -106
  180. package/src/commands/launch.ts +6 -4
  181. package/src/commands/mcp-serve.ts +85 -8
  182. package/src/commands/mcp.ts +2 -2
  183. package/src/commands/notify.ts +8 -4
  184. package/src/commands/sdk.ts +61 -40
  185. package/src/commands/ultragoal.ts +4 -1
  186. package/src/config/atomic-yaml-patch.ts +80 -13
  187. package/src/config/file-lock.ts +105 -43
  188. package/src/config/keybindings.ts +64 -20
  189. package/src/config/model-auth.ts +9 -0
  190. package/src/config/model-bindings-applier.ts +139 -0
  191. package/src/config/model-discovery-manager.ts +224 -0
  192. package/src/config/model-equivalence.ts +66 -0
  193. package/src/config/model-registry.ts +169 -302
  194. package/src/config/model-resolver.ts +361 -369
  195. package/src/config/models-config-schema.ts +5 -4
  196. package/src/config/settings-schema.ts +214 -12
  197. package/src/config/settings.ts +458 -482
  198. package/src/coordinator-mcp/policy.ts +76 -11
  199. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  200. package/src/coordinator-mcp/question-state.ts +778 -0
  201. package/src/coordinator-mcp/server.ts +1702 -238
  202. package/src/debug/index.ts +1 -1
  203. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  204. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  205. package/src/discovery/helpers.ts +1 -50
  206. package/src/edit/index.ts +64 -0
  207. package/src/eval/py/env.ts +26 -0
  208. package/src/eval/py/kernel.ts +5 -3
  209. package/src/extensibility/extensions/loader.ts +9 -1
  210. package/src/extensibility/extensions/runner.ts +12 -3
  211. package/src/extensibility/extensions/types.ts +39 -1
  212. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  213. package/src/extensibility/hooks/runner.ts +2 -2
  214. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  215. package/src/extensibility/shared-events.ts +2 -0
  216. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  217. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  218. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  219. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  220. package/src/gjc-runtime/launch-tmux.ts +2 -5
  221. package/src/gjc-runtime/launch-worktree.ts +5 -5
  222. package/src/gjc-runtime/linux-proc.ts +71 -0
  223. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  224. package/src/gjc-runtime/session-resolution.ts +18 -3
  225. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  226. package/src/gjc-runtime/state-migrations.ts +1 -1
  227. package/src/gjc-runtime/state-runtime.ts +327 -249
  228. package/src/gjc-runtime/state-writer.ts +36 -40
  229. package/src/gjc-runtime/team-launch.ts +319 -0
  230. package/src/gjc-runtime/team-notify.ts +326 -0
  231. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  232. package/src/gjc-runtime/team-store.ts +828 -0
  233. package/src/gjc-runtime/team-workers.ts +962 -0
  234. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  235. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  236. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  237. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  238. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  239. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  240. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  241. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  242. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  243. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  244. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  245. package/src/goals/continuation-timeout-guard.ts +79 -0
  246. package/src/goals/runtime.ts +7 -5
  247. package/src/goals/state.ts +18 -0
  248. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  249. package/src/harness-control-plane/phase-rollup.ts +4 -7
  250. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  251. package/src/harness-control-plane/receipt-spool.ts +51 -7
  252. package/src/harness-control-plane/session-lease.ts +12 -130
  253. package/src/hindsight/backend.ts +26 -27
  254. package/src/hindsight/state.ts +101 -32
  255. package/src/hooks/skill-state.ts +17 -11
  256. package/src/internal-urls/docs-index.generated.ts +21 -18
  257. package/src/lsp/client.ts +1 -1
  258. package/src/lsp/config.ts +145 -26
  259. package/src/lsp/index.ts +1 -1
  260. package/src/lsp/lspmux.ts +39 -10
  261. package/src/lsp/path-trust.ts +81 -0
  262. package/src/main.ts +127 -30
  263. package/src/memories/index.ts +1 -0
  264. package/src/memories/storage.ts +6 -2
  265. package/src/modes/action-registry.ts +165 -0
  266. package/src/modes/components/chrome.ts +40 -0
  267. package/src/modes/components/command-palette.ts +168 -0
  268. package/src/modes/components/custom-editor.ts +1 -1
  269. package/src/modes/components/custom-provider-wizard.ts +51 -14
  270. package/src/modes/components/index.ts +2 -1
  271. package/src/modes/components/pet-selector.ts +11 -17
  272. package/src/modes/components/plan-preview-overlay.ts +295 -0
  273. package/src/modes/components/queue-mode-selector.ts +10 -27
  274. package/src/modes/components/queue-pane.ts +79 -0
  275. package/src/modes/components/read-tool-group.ts +13 -0
  276. package/src/modes/components/session-observer-overlay.ts +207 -772
  277. package/src/modes/components/sessions-dashboard.ts +181 -0
  278. package/src/modes/components/settings-selector.ts +1 -1
  279. package/src/modes/components/show-images-selector.ts +10 -24
  280. package/src/modes/components/status-line/types.ts +1 -1
  281. package/src/modes/components/status-line.ts +1 -969
  282. package/src/modes/components/tasks-pane.ts +73 -0
  283. package/src/modes/components/theme-selector.ts +11 -31
  284. package/src/modes/components/tool-execution.ts +22 -0
  285. package/src/modes/components/tool-status-header.ts +1056 -0
  286. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  287. package/src/modes/controllers/btw-controller.ts +1 -0
  288. package/src/modes/controllers/event-controller.ts +3 -2
  289. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  290. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  291. package/src/modes/controllers/input-controller.ts +764 -229
  292. package/src/modes/controllers/mode-gate.ts +24 -0
  293. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  294. package/src/modes/controllers/selector-controller.ts +225 -10
  295. package/src/modes/controllers/stt-controller.ts +125 -0
  296. package/src/modes/interactive-mode.ts +289 -1150
  297. package/src/modes/jobs-observer.ts +3 -0
  298. package/src/modes/prompt-action-autocomplete.ts +7 -0
  299. package/src/modes/runtime-init.ts +8 -0
  300. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  301. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  302. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  303. package/src/modes/tasks-aggregator.ts +199 -0
  304. package/src/modes/transcript-item-registry.ts +229 -0
  305. package/src/modes/types.ts +45 -18
  306. package/src/modes/utils/ui-helpers.ts +17 -9
  307. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  308. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  309. package/src/prompts/agents/architect.md +2 -8
  310. package/src/prompts/agents/critic.md +2 -8
  311. package/src/prompts/agents/executor.md +3 -0
  312. package/src/prompts/agents/planner.md +4 -12
  313. package/src/prompts/system/custom-system-prompt.md +1 -23
  314. package/src/prompts/system/plan-mode-approved.md +8 -0
  315. package/src/prompts/system/system-prompt.md +17 -96
  316. package/src/prompts/tools/bash.md +4 -4
  317. package/src/prompts/tools/browser.md +15 -52
  318. package/src/prompts/tools/find.md +2 -2
  319. package/src/prompts/tools/hashline.md +13 -55
  320. package/src/prompts/tools/read.md +3 -6
  321. package/src/prompts/tools/search-tool-bm25.md +0 -2
  322. package/src/prompts/tools/search.md +2 -4
  323. package/src/prompts/tools/skill-discovery.md +1 -2
  324. package/src/prompts/tools/task.md +1 -1
  325. package/src/runtime-mcp/manager.ts +1 -1
  326. package/src/sdk/broker/broker.ts +206 -78
  327. package/src/sdk/broker/discovery.ts +1 -1
  328. package/src/sdk/broker/ensure.ts +3 -1
  329. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  330. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  331. package/src/sdk/broker/lifecycle.ts +1399 -142
  332. package/src/sdk/broker/process-incarnation.ts +5 -13
  333. package/src/sdk/broker/session-index.ts +145 -56
  334. package/src/sdk/broker/state-version.ts +21 -0
  335. package/src/sdk/broker/transport.ts +1 -1
  336. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  337. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  338. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  339. package/src/sdk/bus/config-commands.ts +5 -4
  340. package/src/sdk/bus/config.ts +98 -0
  341. package/src/sdk/bus/conversation-store.ts +5 -29
  342. package/src/sdk/bus/daemon-paths.ts +2 -0
  343. package/src/sdk/bus/html-format.ts +20 -3
  344. package/src/sdk/bus/index.ts +782 -94
  345. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  346. package/src/sdk/bus/natives-augment.d.ts +26 -1
  347. package/src/sdk/bus/notification-service.ts +168 -20
  348. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  349. package/src/sdk/bus/recent-activity.ts +188 -74
  350. package/src/sdk/bus/rich-document.ts +258 -0
  351. package/src/sdk/bus/rich-render.ts +62 -70
  352. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  353. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  354. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  355. package/src/sdk/bus/threaded-inbound.ts +8 -3
  356. package/src/sdk/bus/threaded-render.ts +57 -1
  357. package/src/sdk/bus/topic-registry.ts +120 -13
  358. package/src/sdk/host/control/dispatch.ts +14 -2
  359. package/src/sdk/host/control/operations.ts +6 -1
  360. package/src/sdk/host/host.ts +44 -2
  361. package/src/sdk/host/query/handlers.ts +20 -22
  362. package/src/sdk/host/query/revision-store.ts +80 -57
  363. package/src/sdk/index.ts +1 -0
  364. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  365. package/src/sdk/session-directory.ts +141 -0
  366. package/src/sdk/session.ts +462 -480
  367. package/src/session/agent-session.ts +1700 -583
  368. package/src/session/agent-storage.ts +18 -0
  369. package/src/session/cache-economics.ts +25 -8
  370. package/src/session/fallback-chain-controller.ts +53 -3
  371. package/src/session/internal/managed-session-scope.ts +2226 -0
  372. package/src/session/internal/managed-session-storage.ts +453 -0
  373. package/src/session/messages.ts +91 -2
  374. package/src/session/session-manager-internal.ts +33 -0
  375. package/src/session/session-manager.ts +1112 -333
  376. package/src/session/session-storage.ts +444 -39
  377. package/src/session/volatile-context-pruning.ts +78 -0
  378. package/src/setup/credential-auto-import.ts +2 -1
  379. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  380. package/src/setup/provider-onboarding.ts +36 -61
  381. package/src/setup/provider-presets.json +5 -0
  382. package/src/skill-state/active-state.ts +3 -13
  383. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  384. package/src/skill-state/workflow-state-contract.ts +6 -41
  385. package/src/slash-commands/builtin-registry.ts +46 -11
  386. package/src/system-prompt.ts +37 -44
  387. package/src/task/agents.ts +19 -1
  388. package/src/task/executor.ts +90 -6
  389. package/src/task/index.ts +36 -21
  390. package/src/thinking.ts +21 -6
  391. package/src/tool-discovery/tool-index.ts +2 -134
  392. package/src/tools/ask.ts +157 -21
  393. package/src/tools/ast-edit.ts +1 -1
  394. package/src/tools/bash.ts +38 -0
  395. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  396. package/src/tools/browser/tab-supervisor.ts +282 -36
  397. package/src/tools/computer.ts +10 -3
  398. package/src/tools/cron.ts +30 -16
  399. package/src/tools/eval.ts +1 -2
  400. package/src/tools/fetch.ts +11 -4
  401. package/src/tools/index.ts +76 -24
  402. package/src/tools/output-meta.ts +19 -7
  403. package/src/tools/read.ts +219 -64
  404. package/src/tools/resource-gc.ts +2 -2
  405. package/src/tools/search-tool-bm25.ts +11 -74
  406. package/src/tools/subagent.ts +43 -2
  407. package/src/utils/event-bus.ts +9 -4
  408. package/src/utils/file-mentions.ts +4 -4
  409. package/src/web/search/index.ts +3 -3
  410. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  411. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  412. package/src/discovery/plugin-dir-roots.ts +0 -28
  413. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -4,11 +4,13 @@
4
4
 
5
5
  import { ThinkingLevel } from "@gajae-code/agent-core";
6
6
  import { type Api, DEFAULT_MODEL_PER_PROVIDER, type KnownProvider, type Model, modelsAreEqual } from "@gajae-code/ai";
7
- import { fuzzyMatch } from "@gajae-code/tui";
7
+
8
8
  import { logger } from "@gajae-code/utils";
9
9
  import chalk from "chalk";
10
- import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
11
- import { isAuthenticated, kNoAuth, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
10
+ import { parseThinkingLevel, resolveThinkingLevelForModel, splitSelectorThinkingSuffix } from "../thinking";
11
+ import { isAuthenticatedOrKeyless } from "./model-auth";
12
+ import { compareEquivalentModelVariants } from "./model-equivalence";
13
+ import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
12
14
  import { type ModelSelectorValue, normalizeModelSelectorValue } from "./model-selector-value";
13
15
  import type { Settings } from "./settings";
14
16
 
@@ -35,6 +37,9 @@ export interface ScopedModel extends ScopedModelSelection {
35
37
  explicitThinkingLevel: boolean;
36
38
  }
37
39
 
40
+ export type { SelectorThinkingSuffix } from "../thinking";
41
+ export { splitSelectorThinkingSuffix } from "../thinking";
42
+
38
43
  /**
39
44
  * Parse a model string in "provider/modelId" format.
40
45
  * Returns undefined if the format is invalid.
@@ -44,18 +49,11 @@ export function parseModelString(
44
49
  ): { provider: string; id: string; thinkingLevel?: ThinkingLevel } | undefined {
45
50
  const slashIdx = modelStr.indexOf("/");
46
51
  if (slashIdx <= 0) return undefined;
47
- const id = modelStr.slice(slashIdx + 1);
48
52
  const provider = modelStr.slice(0, slashIdx);
49
- // Strip valid thinking level suffix (e.g., "Anthropic model-sonnet-4-6:high" -> id "Anthropic model-sonnet-4-6", thinkingLevel "high")
50
- const colonIdx = id.lastIndexOf(":");
51
- if (colonIdx !== -1) {
52
- const suffix = id.slice(colonIdx + 1);
53
- const thinkingLevel = parseThinkingLevel(suffix);
54
- if (thinkingLevel) {
55
- return { provider, id: id.slice(0, colonIdx), thinkingLevel };
56
- }
57
- }
58
- return { provider, id };
53
+ const suffix = splitSelectorThinkingSuffix(modelStr);
54
+ return suffix.thinkingLevel
55
+ ? { provider, id: suffix.selector.slice(slashIdx + 1), thinkingLevel: suffix.thinkingLevel }
56
+ : { provider, id: modelStr.slice(slashIdx + 1) };
59
57
  }
60
58
 
61
59
  /**
@@ -123,15 +121,22 @@ function cloneModelWithRequestedId(model: Model<Api>, requestedId: string): Mode
123
121
  };
124
122
  }
125
123
 
126
- const kProviderModelIndex = Symbol("model-resolver.providerIndex");
127
- type ModelsWithProviderIndex = readonly Model<Api>[] & {
128
- [kProviderModelIndex]?: Map<string, Model<Api> | null>;
129
- };
124
+ const providerModelIndexes = new WeakMap<
125
+ readonly Model<Api>[],
126
+ { fingerprint: string; models: readonly Model<Api>[]; index: Map<string, Model<Api> | null> }
127
+ >();
128
+
129
+ function modelFingerprint(availableModels: readonly Model<Api>[]): string {
130
+ return availableModels.map(model => `${model.provider}\u0000${model.id}`).join("\u0001");
131
+ }
130
132
 
131
133
  function getProviderModelIndex(availableModels: readonly Model<Api>[]): Map<string, Model<Api> | null> {
132
- const tagged = availableModels as ModelsWithProviderIndex;
133
- const cached = tagged[kProviderModelIndex];
134
- if (cached) return cached;
134
+ const fingerprint = modelFingerprint(availableModels);
135
+ const cached = providerModelIndexes.get(availableModels);
136
+ if (cached?.fingerprint === fingerprint && cached.models.every((model, index) => model === availableModels[index])) {
137
+ return cached.index;
138
+ }
139
+
135
140
  const index = new Map<string, Model<Api> | null>();
136
141
  for (const m of availableModels) {
137
142
  const key = `${m.provider.toLowerCase()}\u0000${m.id.toLowerCase()}`;
@@ -141,7 +146,8 @@ function getProviderModelIndex(availableModels: readonly Model<Api>[]): Map<stri
141
146
  index.set(key, m);
142
147
  }
143
148
  }
144
- tagged[kProviderModelIndex] = index;
149
+ providerModelIndexes.set(availableModels, { fingerprint, models: [...availableModels], index });
150
+
145
151
  return index;
146
152
  }
147
153
 
@@ -158,13 +164,10 @@ export function resolveProviderModelReference(
158
164
  return undefined;
159
165
  }
160
166
 
161
- // Prefer an exact provider/model id match before falling back to the
162
- // case-insensitive index. Some provider catalogs intentionally carry both a
163
- // machine-id form (for example `minimax-m3`) and a display-cased alias
164
- // (`MiniMax-M3`). The lowercase index correctly treats those aliases as
165
- // ambiguous for fuzzy/case-insensitive lookup, but an exact selector should
166
- // still resolve deterministically.
167
- const caseExact = availableModels.find(m => m.provider === trimmedProvider && m.id === trimmedModelId);
167
+ // Preserve exact provider/id replacement semantics before case-insensitive
168
+ // ambiguity handling. Custom entries can intentionally replace a bundled
169
+ // model while a differently-cased catalog alias remains present.
170
+ const caseExact = availableModels.find(model => model.provider === trimmedProvider && model.id === trimmedModelId);
168
171
  if (caseExact) return caseExact;
169
172
 
170
173
  const index = getProviderModelIndex(availableModels);
@@ -201,12 +204,12 @@ export interface ModelMatchPreferences {
201
204
  }
202
205
 
203
206
  export type CanonicalModelRegistry = Partial<
204
- Pick<ModelRegistry, "resolveCanonicalModel" | "getCanonicalVariants" | "getCanonicalId">
207
+ Pick<ModelRegistry, "resolveCanonicalModel" | "getCanonicalVariants" | "getCanonicalId" | "seedCanonicalVariant">
205
208
  >;
206
209
  export type ModelLookupRegistry = Pick<ModelRegistry, "getAvailable"> & Partial<CanonicalModelRegistry>;
207
210
  type CliModelRegistry = Pick<ModelRegistry, "getAll"> & Partial<CanonicalModelRegistry>;
208
- type InitialModelRegistry = Pick<ModelRegistry, "getAvailable" | "find">;
209
- type RestorableModelRegistry = Pick<ModelRegistry, "getAvailable" | "find" | "getApiKey">;
211
+ type InitialModelRegistry = Pick<ModelRegistry, "getAvailable">;
212
+ type RestorableModelRegistry = Pick<ModelRegistry, "getAvailable" | "getApiKey">;
210
213
 
211
214
  interface ModelPreferenceContext {
212
215
  modelUsageRank: Map<string, number>;
@@ -215,68 +218,99 @@ interface ModelPreferenceContext {
215
218
  modelOrder: Map<string, number>;
216
219
  }
217
220
 
221
+ const preferenceContexts = new WeakMap<
222
+ readonly Model<Api>[],
223
+ {
224
+ fingerprint: string;
225
+ models: readonly Model<Api>[];
226
+ cacheKey: string;
227
+ context: ModelPreferenceContext;
228
+ }
229
+ >();
230
+
231
+ function modelPreferenceKey(model: Pick<Model<Api>, "provider" | "id">): string {
232
+ return `${model.provider}/${model.id}`.toLowerCase();
233
+ }
234
+
235
+ function preferenceCacheKey(preferences: ModelMatchPreferences | undefined): string {
236
+ return JSON.stringify([preferences?.usageOrder ?? [], preferences?.deprioritizeProviders ?? ["openrouter"]]);
237
+ }
238
+
218
239
  function buildPreferenceContext(
219
240
  availableModels: Model<Api>[],
220
241
  preferences: ModelMatchPreferences | undefined,
221
242
  ): ModelPreferenceContext {
243
+ const fingerprint = modelFingerprint(availableModels);
244
+ const cacheKey = preferenceCacheKey(preferences);
245
+ const cached = preferenceContexts.get(availableModels);
246
+ if (
247
+ cached?.fingerprint === fingerprint &&
248
+ cached.cacheKey === cacheKey &&
249
+ cached.models.every((model, index) => model === availableModels[index])
250
+ ) {
251
+ return cached.context;
252
+ }
253
+
222
254
  const modelUsageRank = new Map<string, number>();
223
255
  const providerUsageRank = new Map<string, number>();
224
256
  const usageOrder = preferences?.usageOrder ?? [];
225
257
  for (let i = 0; i < usageOrder.length; i += 1) {
226
258
  const key = usageOrder[i];
227
- if (!modelUsageRank.has(key)) {
228
- modelUsageRank.set(key, i);
229
- }
259
+ if (!modelUsageRank.has(key.toLowerCase())) modelUsageRank.set(key.toLowerCase(), i);
230
260
  const parsed = parseModelString(key);
231
- if (parsed && !providerUsageRank.has(parsed.provider)) {
232
- providerUsageRank.set(parsed.provider, i);
261
+ if (parsed && !providerUsageRank.has(parsed.provider.toLowerCase())) {
262
+ providerUsageRank.set(parsed.provider.toLowerCase(), i);
233
263
  }
234
264
  }
235
265
 
236
- const deprioritizedProviders = new Set(preferences?.deprioritizeProviders ?? ["openrouter"]);
266
+ const deprioritizedProviders = new Set(
267
+ (preferences?.deprioritizeProviders ?? ["openrouter"]).map(provider => provider.toLowerCase()),
268
+ );
269
+
237
270
  const modelOrder = new Map<string, number>();
238
271
  for (let i = 0; i < availableModels.length; i += 1) {
239
272
  modelOrder.set(formatModelString(availableModels[i]), i);
240
273
  }
274
+ const context = { modelUsageRank, providerUsageRank, deprioritizedProviders, modelOrder };
275
+ preferenceContexts.set(availableModels, {
276
+ fingerprint,
277
+ models: [...availableModels],
278
+ cacheKey,
279
+ context,
280
+ });
241
281
 
242
- return { modelUsageRank, providerUsageRank, deprioritizedProviders, modelOrder };
282
+ return context;
243
283
  }
244
284
 
245
285
  function pickPreferredModel(candidates: Model<Api>[], context: ModelPreferenceContext): Model<Api> {
246
286
  if (candidates.length <= 1) return candidates[0];
247
287
  return [...candidates].sort((a, b) => {
248
- const aKey = formatModelString(a);
249
- const bKey = formatModelString(b);
250
- const aUsage = context.modelUsageRank.get(aKey);
251
- const bUsage = context.modelUsageRank.get(bKey);
288
+ const aUsage = context.modelUsageRank.get(modelPreferenceKey(a));
289
+ const bUsage = context.modelUsageRank.get(modelPreferenceKey(b));
290
+
252
291
  if (aUsage !== undefined || bUsage !== undefined) {
253
292
  return (aUsage ?? Number.POSITIVE_INFINITY) - (bUsage ?? Number.POSITIVE_INFINITY);
254
293
  }
255
294
 
256
- const aProviderUsage = context.providerUsageRank.get(a.provider);
257
- const bProviderUsage = context.providerUsageRank.get(b.provider);
295
+ const aProviderUsage = context.providerUsageRank.get(a.provider.toLowerCase());
296
+ const bProviderUsage = context.providerUsageRank.get(b.provider.toLowerCase());
297
+
258
298
  if (aProviderUsage !== undefined || bProviderUsage !== undefined) {
259
299
  return (aProviderUsage ?? Number.POSITIVE_INFINITY) - (bProviderUsage ?? Number.POSITIVE_INFINITY);
260
300
  }
261
301
 
262
- // Prefer vision-capable variants over configured provider/registration order
263
- // so an ambiguous id never resolves to a text-only namesake when a
264
- // vision-capable variant of the same id is available.
265
- const aVision = a.input.includes("image") ? 0 : 1;
266
- const bVision = b.input.includes("image") ? 0 : 1;
267
- if (aVision !== bVision) {
268
- return aVision - bVision;
302
+ const variantRank = compareEquivalentModelVariants(a, b, { finalize: false });
303
+ if (variantRank !== 0) {
304
+ return variantRank;
269
305
  }
270
306
 
271
- const aDeprioritized = context.deprioritizedProviders.has(a.provider);
272
- const bDeprioritized = context.deprioritizedProviders.has(b.provider);
307
+ const aDeprioritized = context.deprioritizedProviders.has(a.provider.toLowerCase());
308
+ const bDeprioritized = context.deprioritizedProviders.has(b.provider.toLowerCase());
273
309
  if (aDeprioritized !== bDeprioritized) {
274
310
  return aDeprioritized ? 1 : -1;
275
311
  }
276
312
 
277
- const aOrder = context.modelOrder.get(aKey) ?? 0;
278
- const bOrder = context.modelOrder.get(bKey) ?? 0;
279
- return aOrder - bOrder;
313
+ return compareEquivalentModelVariants(a, b, { modelOrder: context.modelOrder });
280
314
  })[0];
281
315
  }
282
316
 
@@ -321,6 +355,7 @@ function findExactCanonicalModelMatch(
321
355
  modelReference: string,
322
356
  availableModels: Model<Api>[],
323
357
  modelRegistry: CanonicalModelRegistry | undefined,
358
+ sessionId?: string,
324
359
  ): Model<Api> | undefined {
325
360
  if (!modelRegistry) {
326
361
  return undefined;
@@ -332,6 +367,35 @@ function findExactCanonicalModelMatch(
332
367
  return modelRegistry.resolveCanonicalModel?.(trimmedReference, {
333
368
  availableOnly: false,
334
369
  candidates: availableModels,
370
+ sessionId,
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Resolve an unqualified exact alias through its canonical record so it uses
376
+ * the registry's equivalent-variant ranking rather than generic fuzzy policy.
377
+ */
378
+ function findExactEquivalentModelMatch(
379
+ modelReference: string,
380
+ availableModels: Model<Api>[],
381
+ modelRegistry: CanonicalModelRegistry | undefined,
382
+ sessionId?: string,
383
+ ): Model<Api> | undefined {
384
+ if (!modelRegistry?.getCanonicalId || !modelRegistry.resolveCanonicalModel) return undefined;
385
+ const trimmedReference = modelReference.trim();
386
+ if (!trimmedReference || trimmedReference.includes("/")) return undefined;
387
+ const exactMatches = availableModels.filter(model => model.id.toLowerCase() === trimmedReference.toLowerCase());
388
+ if (exactMatches.length === 0) return undefined;
389
+ const canonicalIds = new Set(
390
+ exactMatches
391
+ .map(model => modelRegistry.getCanonicalId!(model))
392
+ .filter((canonicalId): canonicalId is string => Boolean(canonicalId)),
393
+ );
394
+ if (canonicalIds.size !== 1) return undefined;
395
+ return modelRegistry.resolveCanonicalModel([...canonicalIds][0]!, {
396
+ availableOnly: false,
397
+ candidates: availableModels,
398
+ sessionId,
335
399
  });
336
400
  }
337
401
 
@@ -343,7 +407,7 @@ function tryMatchModel(
343
407
  modelPattern: string,
344
408
  availableModels: Model<Api>[],
345
409
  context: ModelPreferenceContext,
346
- options?: { modelRegistry?: CanonicalModelRegistry },
410
+ options?: { modelRegistry?: CanonicalModelRegistry; sessionId?: string },
347
411
  ): Model<Api> | undefined {
348
412
  // Explicit provider/model selectors always bypass canonical coalescing.
349
413
  const exactRefMatch = findExactModelReferenceMatch(modelPattern, availableModels);
@@ -352,11 +416,26 @@ function tryMatchModel(
352
416
  }
353
417
 
354
418
  // Exact canonical ids coalesce provider variants before bare-id matching.
355
- const exactCanonicalMatch = findExactCanonicalModelMatch(modelPattern, availableModels, options?.modelRegistry);
419
+ const exactCanonicalMatch = findExactCanonicalModelMatch(
420
+ modelPattern,
421
+ availableModels,
422
+ options?.modelRegistry,
423
+ options?.sessionId,
424
+ );
356
425
  if (exactCanonicalMatch) {
357
426
  return exactCanonicalMatch;
358
427
  }
359
428
 
429
+ const exactEquivalentMatch = findExactEquivalentModelMatch(
430
+ modelPattern,
431
+ availableModels,
432
+ options?.modelRegistry,
433
+ options?.sessionId,
434
+ );
435
+ if (exactEquivalentMatch) {
436
+ return exactEquivalentMatch;
437
+ }
438
+
360
439
  // Exact ID match (case-insensitive) — this must happen before provider-scoped
361
440
  // fuzzy matching so raw IDs that contain slashes (for example OpenRouter model
362
441
  // IDs like "openai/gpt-4o:extended") still resolve as IDs instead of being
@@ -365,41 +444,19 @@ function tryMatchModel(
365
444
  if (exactMatches.length > 0) {
366
445
  return pickPreferredModel(exactMatches, context);
367
446
  }
368
- // Check for provider/modelId format — fuzzy match within provider only.
447
+ // Provider-qualified selectors are exact-only. Case-only duplicate catalog
448
+ // entries still rank deterministically within the explicitly named provider.
369
449
  const slashIndex = modelPattern.indexOf("/");
370
450
  if (slashIndex !== -1) {
371
- const provider = modelPattern.substring(0, slashIndex);
372
- const modelId = modelPattern.substring(slashIndex + 1);
373
- const providerModels = availableModels.filter(m => m.provider.toLowerCase() === provider.toLowerCase());
374
- if (providerModels.length === 0) {
375
- // The prefix is not a known provider in this candidate set, so treat the
376
- // slash as part of the raw model ID and continue with generic matching.
377
- } else {
378
- const scored = providerModels
379
- .map(model => ({ model, match: fuzzyMatch(modelId, model.id) }))
380
- .filter(entry => entry.match.matches);
381
- if (scored.length === 0) {
382
- return undefined;
383
- }
384
-
385
- scored.sort((a, b) => {
386
- if (a.match.score !== b.match.score) return a.match.score - b.match.score;
387
- const aKey = formatModelString(a.model);
388
- const bKey = formatModelString(b.model);
389
- const aUsage = context.modelUsageRank.get(aKey) ?? Number.POSITIVE_INFINITY;
390
- const bUsage = context.modelUsageRank.get(bKey) ?? Number.POSITIVE_INFINITY;
391
- if (aUsage !== bUsage) return aUsage - bUsage;
392
-
393
- const aProviderUsage = context.providerUsageRank.get(a.model.provider) ?? Number.POSITIVE_INFINITY;
394
- const bProviderUsage = context.providerUsageRank.get(b.model.provider) ?? Number.POSITIVE_INFINITY;
395
- if (aProviderUsage !== bProviderUsage) return aProviderUsage - bProviderUsage;
396
-
397
- const aOrder = context.modelOrder.get(aKey) ?? 0;
398
- const bOrder = context.modelOrder.get(bKey) ?? 0;
399
- return aOrder - bOrder;
400
- });
401
- return scored[0]?.model;
402
- }
451
+ const provider = modelPattern.slice(0, slashIndex).trim().toLowerCase();
452
+ const modelId = modelPattern
453
+ .slice(slashIndex + 1)
454
+ .trim()
455
+ .toLowerCase();
456
+ const providerExactMatches = availableModels.filter(
457
+ model => model.provider.toLowerCase() === provider && model.id.toLowerCase() === modelId,
458
+ );
459
+ return providerExactMatches.length > 0 ? pickPreferredModel(providerExactMatches, context) : undefined;
403
460
  }
404
461
 
405
462
  // No exact match - fall back to partial matching
@@ -441,84 +498,80 @@ export interface ParsedModelResult {
441
498
  explicitThinkingLevel: boolean;
442
499
  }
443
500
 
501
+ export interface ResolveSelectorOptions {
502
+ allowInvalidThinkingSelectorFallback?: boolean;
503
+ modelRegistry?: CanonicalModelRegistry;
504
+ preferences?: ModelMatchPreferences;
505
+ sessionId?: string;
506
+ }
507
+
444
508
  /**
445
- * Parse a pattern to extract model and thinking level.
446
- * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix).
447
- *
448
- * Algorithm:
449
- * 1. Try to match full pattern as a model
450
- * 2. If found, return it with undefined thinking level
451
- * 3. If not found and has colons, split on last colon:
452
- * - If suffix is valid thinking level, use it and recurse on prefix
453
- * - If suffix is invalid, warn and recurse on prefix
454
- *
455
- * @internal Exported for testing
509
+ * Resolve one selector through ordered exact, canonical, bare-id, provider-fuzzy,
510
+ * substring/alias, and glob stages. Thinking is split only after a full selector
511
+ * cannot resolve, which preserves OpenRouter route suffixes in concrete IDs.
456
512
  */
457
- function parseModelPatternWithContext(
458
- pattern: string,
459
- availableModels: Model<Api>[],
460
- context: ModelPreferenceContext,
461
- options?: { allowInvalidThinkingSelectorFallback?: boolean; modelRegistry?: CanonicalModelRegistry },
513
+ export function resolveSelector(
514
+ selector: string,
515
+ candidates: Model<Api>[],
516
+ options?: ResolveSelectorOptions,
462
517
  ): ParsedModelResult {
463
- // Try exact match first
464
- const exactMatch = tryMatchModel(pattern, availableModels, context, options);
465
- if (exactMatch) {
466
- return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
467
- }
468
-
469
- // No match - try splitting on last colon if present
470
- const lastColonIndex = pattern.lastIndexOf(":");
471
- if (lastColonIndex === -1) {
472
- // No colons, pattern simply doesn't match any model
518
+ const context = buildPreferenceContext(candidates, options?.preferences);
519
+ const exact = tryMatchModel(selector, candidates, context, options);
520
+ if (exact) return { model: exact, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
521
+ const glob = findGlobMatch(selector, candidates, context);
522
+ if (glob) return { model: glob, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
523
+
524
+ const suffix = splitSelectorThinkingSuffix(selector);
525
+ if (
526
+ suffix.selector === selector ||
527
+ (suffix.invalidSuffix !== undefined && !(options?.allowInvalidThinkingSelectorFallback ?? true))
528
+ ) {
473
529
  return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
474
530
  }
475
531
 
476
- const prefix = pattern.substring(0, lastColonIndex);
477
- const suffix = pattern.substring(lastColonIndex + 1);
478
-
479
- const parsedThinkingLevel = parseThinkingLevel(suffix);
480
- if (parsedThinkingLevel) {
481
- // Valid thinking level - recurse on prefix and use this level
482
- const result = parseModelPatternWithContext(prefix, availableModels, context, options);
483
- if (result.model) {
484
- // Only use this thinking level if no warning from inner recursion
485
- const explicitThinkingLevel = !result.warning;
486
- return {
487
- model: result.model,
488
- thinkingLevel: explicitThinkingLevel ? parsedThinkingLevel : undefined,
489
- warning: result.warning,
490
- explicitThinkingLevel,
491
- };
492
- }
493
- return result;
494
- }
495
-
496
- const allowFallback = options?.allowInvalidThinkingSelectorFallback ?? true;
497
- if (!allowFallback) {
498
- return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
532
+ const model =
533
+ tryMatchModel(suffix.selector, candidates, context, options) ??
534
+ findGlobMatch(suffix.selector, candidates, context);
535
+ if (!model) return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
536
+ if (suffix.thinkingLevel) {
537
+ return { model, thinkingLevel: suffix.thinkingLevel, warning: undefined, explicitThinkingLevel: true };
499
538
  }
539
+ return {
540
+ model,
541
+ thinkingLevel: undefined,
542
+ warning: `Invalid thinking level "${suffix.invalidSuffix}" in pattern "${selector}". Using default instead.`,
543
+ explicitThinkingLevel: false,
544
+ };
545
+ }
500
546
 
501
- // Invalid suffix - recurse on prefix and warn
502
- const result = parseModelPatternWithContext(prefix, availableModels, context, options);
503
- if (result.model) {
504
- return {
505
- model: result.model,
506
- thinkingLevel: undefined,
507
- warning: `Invalid thinking level "${suffix}" in pattern "${pattern}". Using default instead.`,
508
- explicitThinkingLevel: false,
509
- };
510
- }
511
- return result;
547
+ function findGlobMatch(
548
+ selector: string,
549
+ candidates: Model<Api>[],
550
+ context: ModelPreferenceContext,
551
+ ): Model<Api> | undefined {
552
+ if (!selector.includes("*") && !selector.includes("?") && !selector.includes("[")) return undefined;
553
+ const glob = new Bun.Glob(selector.toLowerCase());
554
+ return pickPreferredModel(
555
+ candidates.filter(
556
+ model => glob.match(formatModelString(model).toLowerCase()) || glob.match(model.id.toLowerCase()),
557
+ ),
558
+ context,
559
+ );
512
560
  }
513
561
 
562
+ /** @internal Exported for testing and legacy adapters. */
563
+
514
564
  export function parseModelPattern(
515
565
  pattern: string,
516
566
  availableModels: Model<Api>[],
517
567
  preferences?: ModelMatchPreferences,
518
- options?: { allowInvalidThinkingSelectorFallback?: boolean; modelRegistry?: CanonicalModelRegistry },
568
+ options?: {
569
+ allowInvalidThinkingSelectorFallback?: boolean;
570
+ modelRegistry?: CanonicalModelRegistry;
571
+ sessionId?: string;
572
+ },
519
573
  ): ParsedModelResult {
520
- const context = buildPreferenceContext(availableModels, preferences);
521
- return parseModelPatternWithContext(pattern, availableModels, context, options);
574
+ return resolveSelector(pattern, availableModels, { ...options, preferences });
522
575
  }
523
576
 
524
577
  const PREFIX_MODEL_ROLE = "pi/";
@@ -547,11 +600,11 @@ function resolveConfiguredRolePattern(value: string, settings?: Settings): strin
547
600
  const normalized = value.trim();
548
601
  if (!normalized) return undefined;
549
602
 
550
- const lastColonIndex = normalized.lastIndexOf(":");
551
- const thinkingLevel =
552
- lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
553
- const aliasCandidate = thinkingLevel ? normalized.slice(0, lastColonIndex) : normalized;
603
+ const suffix = splitSelectorThinkingSuffix(normalized);
604
+ const thinkingLevel = suffix.thinkingLevel;
605
+ const aliasCandidate = thinkingLevel ? suffix.selector : normalized;
554
606
  const role = getModelRoleAlias(aliasCandidate);
607
+
555
608
  if (!role) return [normalized];
556
609
 
557
610
  const configured = settings?.getModelRole(role);
@@ -626,7 +679,12 @@ export interface ResolvedModelRoleValue {
626
679
  export function resolveModelRoleValue(
627
680
  roleValue: ModelSelectorValue | undefined,
628
681
  availableModels: Model<Api>[],
629
- options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences; modelRegistry?: CanonicalModelRegistry },
682
+ options?: {
683
+ settings?: Settings;
684
+ matchPreferences?: ModelMatchPreferences;
685
+ modelRegistry?: CanonicalModelRegistry;
686
+ sessionId?: string;
687
+ },
630
688
  ): ResolvedModelRoleValue {
631
689
  const effectivePatterns = normalizeModelPatternList(roleValue).flatMap(
632
690
  pattern => resolveConfiguredRolePattern(pattern, options?.settings) ?? [],
@@ -639,6 +697,7 @@ export function resolveModelRoleValue(
639
697
  for (const effectivePattern of effectivePatterns) {
640
698
  const resolved = parseModelPattern(effectivePattern, availableModels, options?.matchPreferences, {
641
699
  modelRegistry: options?.modelRegistry,
700
+ sessionId: options?.sessionId,
642
701
  });
643
702
  if (resolved.model) {
644
703
  return {
@@ -669,9 +728,8 @@ export function extractExplicitThinkingSelector(
669
728
  let current = normalized;
670
729
  while (!visited.has(current)) {
671
730
  visited.add(current);
672
- const lastColonIndex = current.lastIndexOf(":");
673
- const thinkingSelector =
674
- lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(current.slice(lastColonIndex + 1)) : undefined;
731
+ const thinkingSelector = splitSelectorThinkingSuffix(current).thinkingLevel;
732
+
675
733
  if (thinkingSelector) {
676
734
  return thinkingSelector;
677
735
  }
@@ -693,12 +751,7 @@ export function resolveModelFromString(
693
751
  matchPreferences?: ModelMatchPreferences,
694
752
  modelRegistry?: CanonicalModelRegistry,
695
753
  ): Model<Api> | undefined {
696
- const parsed = parseModelString(value);
697
- if (parsed) {
698
- const exact = available.find(model => model.provider === parsed.provider && model.id === parsed.id);
699
- if (exact) return exact;
700
- }
701
- return parseModelPattern(value, available, matchPreferences, { modelRegistry }).model;
754
+ return resolveSelector(value, available, { preferences: matchPreferences, modelRegistry }).model;
702
755
  }
703
756
 
704
757
  /**
@@ -735,6 +788,7 @@ export function resolveModelOverride(
735
788
  modelPatterns: string[],
736
789
  modelRegistry: ModelLookupRegistry,
737
790
  settings?: Settings,
791
+ sessionId?: string,
738
792
  ): { model?: Model<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
739
793
  if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
740
794
  const availableModels = modelRegistry.getAvailable();
@@ -744,6 +798,7 @@ export function resolveModelOverride(
744
798
  settings,
745
799
  matchPreferences,
746
800
  modelRegistry,
801
+ sessionId,
747
802
  });
748
803
  if (model) {
749
804
  return { model, thinkingLevel, explicitThinkingLevel };
@@ -781,7 +836,12 @@ export async function resolveModelChainWithAuth(
781
836
  const skips: Array<{ selector: string; reason: string }> = [];
782
837
  for (let activeIndex = 0; activeIndex < modelPatterns.length; activeIndex += 1) {
783
838
  const selector = modelPatterns[activeIndex];
784
- const candidate = resolveModelRoleValue(selector, availableModels, { settings, matchPreferences, modelRegistry });
839
+ const candidate = resolveModelRoleValue(selector, availableModels, {
840
+ settings,
841
+ matchPreferences,
842
+ modelRegistry,
843
+ sessionId,
844
+ });
785
845
  if (!candidate.model) {
786
846
  skips.push({ selector, reason: "unknown_model" });
787
847
  continue;
@@ -794,7 +854,7 @@ export async function resolveModelChainWithAuth(
794
854
  }
795
855
  }
796
856
  const key = await modelRegistry.getApiKey(candidate.model, sessionId);
797
- if (key === kNoAuth || isAuthenticated(key)) {
857
+ if (isAuthenticatedOrKeyless(key)) {
798
858
  return { ...candidate, activeIndex, skips };
799
859
  }
800
860
  skips.push({ selector, reason: "unauthenticated" });
@@ -828,8 +888,9 @@ export async function resolveModelOverrideWithAuthFallback(
828
888
  parentActiveModelPattern: string | undefined,
829
889
  modelRegistry: ModelLookupRegistry & Pick<ModelRegistry, "getApiKey">,
830
890
  settings?: Settings,
831
- sessionId?: string,
891
+ authSessionId?: string,
832
892
  options?: ModelChainResolutionOptions,
893
+ canonicalSessionId?: string,
833
894
  ): Promise<{
834
895
  model?: Model<Api>;
835
896
  thinkingLevel?: ThinkingLevel;
@@ -847,8 +908,20 @@ export async function resolveModelOverrideWithAuthFallback(
847
908
  let requestedResolution: ResolvedModelRoleValue | undefined;
848
909
  const skips: Array<{ selector: string; reason: string }> = [];
849
910
  let activeIndex = 0;
911
+ const canonicalScope = canonicalSessionId ?? authSessionId;
912
+ if (canonicalScope && parentActiveModelPattern) {
913
+ const parentActiveModel = resolveModelOverride([parentActiveModelPattern], modelRegistry, settings).model;
914
+ if (parentActiveModel) {
915
+ modelRegistry.seedCanonicalVariant?.(canonicalScope, parentActiveModel);
916
+ }
917
+ }
850
918
  for (const pattern of modelPatterns) {
851
- const candidate = resolveModelRoleValue(pattern, availableModels, { settings, matchPreferences, modelRegistry });
919
+ const candidate = resolveModelRoleValue(pattern, availableModels, {
920
+ settings,
921
+ matchPreferences,
922
+ modelRegistry,
923
+ sessionId: canonicalScope,
924
+ });
852
925
  if (!requestedModel && candidate.model) {
853
926
  requestedModel = candidate.model;
854
927
  requestedResolution = candidate;
@@ -866,19 +939,19 @@ export async function resolveModelOverrideWithAuthFallback(
866
939
  continue;
867
940
  }
868
941
  }
869
- const key = await modelRegistry.getApiKey(candidate.model, sessionId);
870
- if (key === kNoAuth || isAuthenticated(key)) {
942
+ const key = await modelRegistry.getApiKey(candidate.model, authSessionId);
943
+ if (isAuthenticatedOrKeyless(key)) {
871
944
  return { ...candidate, requestedModel: candidate.model, authFallbackUsed: false, activeIndex, skips };
872
945
  }
873
946
  skips.push({ selector: pattern, reason: "unauthenticated" });
874
947
  activeIndex += 1;
875
948
  }
876
949
  const fallback = parentActiveModelPattern
877
- ? resolveModelOverride([parentActiveModelPattern], modelRegistry, settings)
950
+ ? resolveModelOverride([parentActiveModelPattern], modelRegistry, settings, authSessionId)
878
951
  : { explicitThinkingLevel: false };
879
952
  if (fallback.model) {
880
- const fallbackKey = await modelRegistry.getApiKey(fallback.model, sessionId);
881
- if (fallbackKey === kNoAuth || isAuthenticated(fallbackKey)) {
953
+ const fallbackKey = await modelRegistry.getApiKey(fallback.model, authSessionId);
954
+ if (isAuthenticatedOrKeyless(fallbackKey)) {
882
955
  const isParentSubstitution = requestedModel === undefined || !modelsAreEqual(fallback.model, requestedModel);
883
956
  return {
884
957
  ...fallback,
@@ -923,21 +996,20 @@ function resolveExactCanonicalScopePattern(
923
996
  modelRegistry: Pick<ModelRegistry, "getCanonicalVariants">,
924
997
  availableModels: Model<Api>[],
925
998
  ): { models: Model<Api>[]; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } | undefined {
926
- const lastColonIndex = pattern.lastIndexOf(":");
927
- let canonicalId = pattern;
928
- let thinkingLevel: ThinkingLevel | undefined;
929
- let explicitThinkingLevel = false;
930
-
931
- if (lastColonIndex !== -1) {
932
- const suffix = pattern.substring(lastColonIndex + 1);
933
- const parsedThinkingLevel = parseThinkingLevel(suffix);
934
- if (parsedThinkingLevel) {
935
- canonicalId = pattern.substring(0, lastColonIndex);
936
- thinkingLevel = parsedThinkingLevel;
937
- explicitThinkingLevel = true;
938
- }
999
+ // Exact concrete selectors must win before a suffix is interpreted as thinking.
1000
+ // This preserves canonical/OpenRouter model IDs that themselves contain colons.
1001
+ if (
1002
+ findExactModelReferenceMatch(pattern, availableModels) ||
1003
+ (pattern.includes(":") && availableModels.some(model => model.id.toLowerCase() === pattern.trim().toLowerCase()))
1004
+ ) {
1005
+ return undefined;
939
1006
  }
940
1007
 
1008
+ const suffix = splitSelectorThinkingSuffix(pattern);
1009
+ const canonicalId = suffix.thinkingLevel ? suffix.selector : pattern;
1010
+ const thinkingLevel = suffix.thinkingLevel;
1011
+ const explicitThinkingLevel = thinkingLevel !== undefined;
1012
+
941
1013
  const variants = modelRegistry
942
1014
  .getCanonicalVariants(canonicalId, { availableOnly: true, candidates: availableModels })
943
1015
  .map(variant => variant.model);
@@ -949,43 +1021,39 @@ function resolveExactCanonicalScopePattern(
949
1021
  }
950
1022
 
951
1023
  /**
952
- * Resolve model patterns to actual Model objects with optional thinking levels
953
- * Format: "pattern:level" where :level is optional
954
- * For each pattern, finds all matching models and picks the best version:
955
- * 1. Prefer alias (e.g., Anthropic model-sonnet-4-5) over dated versions (Anthropic model-sonnet-4-5-20250929)
956
- * 2. If no alias, pick the latest dated version
957
- *
958
- * Supports models with colons in their IDs (e.g., OpenRouter's model:exacto).
959
- * The algorithm tries to match the full pattern first, then progressively
960
- * strips colon-suffixes to find a match.
1024
+ * Resolve model patterns to actual Model objects with optional thinking levels.
1025
+ * A `:level` suffix is interpreted only after the complete selector fails to
1026
+ * resolve, preserving concrete model IDs that contain colon-bearing route suffixes.
1027
+ * For each non-glob pattern, alias IDs are preferred over dated versions; otherwise
1028
+ * the latest dated version is selected.
961
1029
  */
1030
+
962
1031
  export async function resolveModelScope(
963
1032
  patterns: string[],
964
1033
  modelRegistry: Pick<ModelRegistry, "getAvailable" | "getCanonicalVariants">,
965
1034
  preferences?: ModelMatchPreferences,
966
1035
  ): Promise<ScopedModel[]> {
967
1036
  const availableModels = modelRegistry.getAvailable();
968
- const context = buildPreferenceContext(availableModels, preferences);
1037
+
969
1038
  const scopedModels: ScopedModel[] = [];
1039
+ const addScopedModel = (selection: ScopedModel): void => {
1040
+ const duplicate = scopedModels.some(
1041
+ existing =>
1042
+ modelsAreEqual(existing.model, selection.model) &&
1043
+ existing.thinkingLevel === selection.thinkingLevel &&
1044
+ existing.explicitThinkingLevel === selection.explicitThinkingLevel,
1045
+ );
1046
+ if (!duplicate) scopedModels.push(selection);
1047
+ };
970
1048
 
971
1049
  for (const pattern of patterns) {
972
1050
  // Check if pattern contains glob characters
973
1051
  if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
974
- // Extract optional thinking level suffix (e.g., "provider/*:high")
975
- const colonIdx = pattern.lastIndexOf(":");
976
- let globPattern = pattern;
977
- let thinkingLevel: ThinkingLevel | undefined;
978
- let explicitThinkingLevel = false;
979
-
980
- if (colonIdx !== -1) {
981
- const suffix = pattern.substring(colonIdx + 1);
982
- const parsedThinkingLevel = parseThinkingLevel(suffix);
983
- if (parsedThinkingLevel) {
984
- thinkingLevel = parsedThinkingLevel;
985
- explicitThinkingLevel = true;
986
- globPattern = pattern.substring(0, colonIdx);
987
- }
988
- }
1052
+ // Extract the optional thinking suffix once, preserving route suffixes.
1053
+ const suffix = splitSelectorThinkingSuffix(pattern);
1054
+ const globPattern = suffix.thinkingLevel ? suffix.selector : pattern;
1055
+ const thinkingLevel = suffix.thinkingLevel;
1056
+ const explicitThinkingLevel = thinkingLevel !== undefined;
989
1057
 
990
1058
  // Match against "provider/modelId" format OR just model ID
991
1059
  // This allows "*sonnet*" to match without requiring "anthropic/*sonnet*"
@@ -1001,42 +1069,37 @@ export async function resolveModelScope(
1001
1069
  }
1002
1070
 
1003
1071
  for (const model of matchingModels) {
1004
- if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
1005
- scopedModels.push({
1006
- model,
1007
- thinkingLevel: explicitThinkingLevel
1008
- ? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
1009
- : thinkingLevel,
1010
- explicitThinkingLevel,
1011
- });
1012
- }
1072
+ addScopedModel({
1073
+ model,
1074
+ thinkingLevel: explicitThinkingLevel
1075
+ ? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
1076
+ : thinkingLevel,
1077
+ explicitThinkingLevel,
1078
+ });
1013
1079
  }
1080
+
1014
1081
  continue;
1015
1082
  }
1016
1083
 
1017
1084
  const exactCanonical = resolveExactCanonicalScopePattern(pattern, modelRegistry, availableModels);
1018
1085
  if (exactCanonical) {
1019
1086
  for (const model of exactCanonical.models) {
1020
- if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
1021
- scopedModels.push({
1022
- model,
1023
- thinkingLevel: exactCanonical.explicitThinkingLevel
1024
- ? (resolveThinkingLevelForModel(model, exactCanonical.thinkingLevel) ??
1025
- exactCanonical.thinkingLevel)
1026
- : exactCanonical.thinkingLevel,
1027
- explicitThinkingLevel: exactCanonical.explicitThinkingLevel,
1028
- });
1029
- }
1087
+ addScopedModel({
1088
+ model,
1089
+ thinkingLevel: exactCanonical.explicitThinkingLevel
1090
+ ? (resolveThinkingLevelForModel(model, exactCanonical.thinkingLevel) ?? exactCanonical.thinkingLevel)
1091
+ : exactCanonical.thinkingLevel,
1092
+ explicitThinkingLevel: exactCanonical.explicitThinkingLevel,
1093
+ });
1030
1094
  }
1095
+
1031
1096
  continue;
1032
1097
  }
1033
1098
 
1034
- const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPatternWithContext(
1035
- pattern,
1036
- availableModels,
1037
- context,
1038
- { modelRegistry },
1039
- );
1099
+ const { model, thinkingLevel, warning, explicitThinkingLevel } = resolveSelector(pattern, availableModels, {
1100
+ modelRegistry,
1101
+ preferences,
1102
+ });
1040
1103
 
1041
1104
  if (warning) {
1042
1105
  logger.warn(warning);
@@ -1047,16 +1110,13 @@ export async function resolveModelScope(
1047
1110
  continue;
1048
1111
  }
1049
1112
 
1050
- // Avoid duplicates
1051
- if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
1052
- scopedModels.push({
1053
- model,
1054
- thinkingLevel: explicitThinkingLevel
1055
- ? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
1056
- : thinkingLevel,
1057
- explicitThinkingLevel,
1058
- });
1059
- }
1113
+ addScopedModel({
1114
+ model,
1115
+ thinkingLevel: explicitThinkingLevel
1116
+ ? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
1117
+ : thinkingLevel,
1118
+ explicitThinkingLevel,
1119
+ });
1060
1120
  }
1061
1121
 
1062
1122
  return scopedModels;
@@ -1100,9 +1160,7 @@ export interface ResolveCliModelResult {
1100
1160
  error: string | undefined;
1101
1161
  }
1102
1162
 
1103
- /**
1104
- * Resolve a single model from CLI flags.
1105
- */
1163
+ /** Resolve a single model from CLI flags through the staged selector resolver. */
1106
1164
  export function resolveCliModel(options: {
1107
1165
  cliProvider?: string;
1108
1166
  cliModel?: string;
@@ -1110,10 +1168,7 @@ export function resolveCliModel(options: {
1110
1168
  preferences?: ModelMatchPreferences;
1111
1169
  }): ResolveCliModelResult {
1112
1170
  const { cliProvider, cliModel, modelRegistry, preferences } = options;
1113
-
1114
- if (!cliModel) {
1115
- return { model: undefined, selector: undefined, warning: undefined, error: undefined };
1116
- }
1171
+ if (!cliModel) return { model: undefined, selector: undefined, warning: undefined, error: undefined };
1117
1172
 
1118
1173
  const availableModels = modelRegistry.getAll();
1119
1174
  if (availableModels.length === 0) {
@@ -1125,12 +1180,9 @@ export function resolveCliModel(options: {
1125
1180
  };
1126
1181
  }
1127
1182
 
1128
- const providerMap = new Map<string, string>();
1129
- for (const model of availableModels) {
1130
- providerMap.set(model.provider.toLowerCase(), model.provider);
1131
- }
1132
-
1133
- let provider = cliProvider ? providerMap.get(cliProvider.toLowerCase()) : undefined;
1183
+ const provider = cliProvider
1184
+ ? availableModels.find(model => model.provider.toLowerCase() === cliProvider.toLowerCase())?.provider
1185
+ : undefined;
1134
1186
  if (cliProvider && !provider) {
1135
1187
  return {
1136
1188
  model: undefined,
@@ -1140,117 +1192,55 @@ export function resolveCliModel(options: {
1140
1192
  };
1141
1193
  }
1142
1194
 
1143
- const trimmedModel = cliModel.trim();
1144
- if (!provider) {
1145
- const lower = trimmedModel.toLowerCase();
1146
- // When input has provider/id format (e.g. "zai/glm-5"), prefer decomposed
1147
- // provider+id match over flat id match. Without this, a model with id
1148
- // "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai"
1149
- // with id "glm-5", because Array.find returns the first catalog hit.
1150
- const slashIdx = lower.indexOf("/");
1151
- let exact: (typeof availableModels)[number] | undefined;
1152
- if (slashIdx !== -1) {
1153
- const prefix = lower.substring(0, slashIdx);
1154
- const suffix = trimmedModel.substring(slashIdx + 1);
1155
- exact = resolveProviderModelReference(prefix, suffix, availableModels);
1156
- }
1157
- if (!exact && !trimmedModel.includes(":")) {
1158
- const canonicalMatch = modelRegistry.resolveCanonicalModel?.(trimmedModel, { availableOnly: false });
1159
- if (canonicalMatch) {
1160
- return {
1161
- model: canonicalMatch,
1162
- selector: modelRegistry.getCanonicalId?.(canonicalMatch) ?? trimmedModel,
1163
- warning: undefined,
1164
- thinkingLevel: undefined,
1165
- error: undefined,
1166
- };
1167
- }
1168
- }
1169
- if (!exact) {
1170
- exact = availableModels.find(
1171
- model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower,
1172
- );
1173
- }
1174
- if (exact) {
1175
- return {
1176
- model: exact,
1177
- selector: formatModelString(exact),
1178
- warning: undefined,
1179
- thinkingLevel: undefined,
1180
- error: undefined,
1181
- };
1182
- }
1183
- }
1184
-
1185
- let pattern = trimmedModel;
1186
-
1187
- if (!provider) {
1188
- const slashIndex = cliModel.indexOf("/");
1189
- if (slashIndex !== -1) {
1190
- const maybeProvider = cliModel.substring(0, slashIndex);
1191
- const canonical = providerMap.get(maybeProvider.toLowerCase());
1192
- if (canonical) {
1193
- provider = canonical;
1194
- pattern = cliModel.substring(slashIndex + 1);
1195
- }
1196
- }
1197
- } else {
1198
- const prefix = `${provider}/`;
1199
- if (cliModel.toLowerCase().startsWith(prefix.toLowerCase())) {
1200
- pattern = cliModel.substring(prefix.length);
1201
- }
1202
- }
1203
-
1204
- if (provider) {
1205
- const exactProviderMatch = resolveProviderModelReference(provider, pattern, availableModels);
1206
- if (exactProviderMatch) {
1207
- return {
1208
- model: exactProviderMatch,
1209
- selector: formatModelString(exactProviderMatch),
1210
- warning: undefined,
1211
- thinkingLevel: undefined,
1212
- error: undefined,
1213
- };
1214
- }
1215
- }
1216
-
1217
- const candidates = provider ? availableModels.filter(model => model.provider === provider) : availableModels;
1218
- const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, preferences, {
1219
- allowInvalidThinkingSelectorFallback: false,
1220
- modelRegistry,
1221
- });
1195
+ const modelInput = cliModel.trim();
1196
+ const providerPrefix = provider ? `${provider}/` : "";
1197
+ const pattern =
1198
+ provider && modelInput.toLowerCase().startsWith(providerPrefix.toLowerCase())
1199
+ ? modelInput.slice(providerPrefix.length)
1200
+ : modelInput;
1201
+ const candidates = provider
1202
+ ? availableModels.filter(model => model.provider.toLowerCase() === provider.toLowerCase())
1203
+ : availableModels;
1204
+ const selectorInput = provider ? `${provider}/${pattern}` : pattern;
1205
+ const exactProviderResolved = provider
1206
+ ? resolveSelector(selectorInput, candidates, {
1207
+ allowInvalidThinkingSelectorFallback: false,
1208
+ preferences,
1209
+ })
1210
+ : undefined;
1211
+ const resolved = exactProviderResolved?.model
1212
+ ? exactProviderResolved
1213
+ : resolveSelector(provider ? pattern : selectorInput, candidates, {
1214
+ allowInvalidThinkingSelectorFallback: false,
1215
+ modelRegistry: provider ? undefined : modelRegistry,
1216
+ preferences,
1217
+ });
1222
1218
 
1223
- if (!model) {
1224
- const display = provider ? `${provider}/${pattern}` : cliModel;
1219
+ if (!resolved.model) {
1225
1220
  return {
1226
1221
  model: undefined,
1227
1222
  selector: undefined,
1228
1223
  thinkingLevel: undefined,
1229
- warning,
1230
- error: `Model "${display}" not found. Use --list-models to see available models.`,
1224
+ warning: resolved.warning,
1225
+ error: `Model "${selectorInput}" not found. Use --list-models to see available models.`,
1231
1226
  };
1232
1227
  }
1233
1228
 
1234
- let selector = provider ? formatModelString(model) : undefined;
1235
- if (!provider) {
1236
- const lastColonIndex = pattern.lastIndexOf(":");
1237
- const canonicalCandidate =
1238
- lastColonIndex !== -1 && parseThinkingLevel(pattern.substring(lastColonIndex + 1))
1239
- ? pattern.substring(0, lastColonIndex)
1240
- : pattern;
1241
- if (!canonicalCandidate.includes("/")) {
1242
- const canonicalResolved = modelRegistry.resolveCanonicalModel?.(canonicalCandidate, { availableOnly: false });
1243
- if (canonicalResolved && canonicalResolved.provider === model.provider && canonicalResolved.id === model.id) {
1244
- selector = modelRegistry.getCanonicalId?.(canonicalResolved) ?? canonicalCandidate;
1245
- }
1246
- }
1247
- }
1248
-
1229
+ const suffix = splitSelectorThinkingSuffix(selectorInput);
1230
+ const canonicalSelector = suffix.selector.includes("/") ? undefined : suffix.selector;
1231
+ const canonicalModel = canonicalSelector
1232
+ ? modelRegistry.resolveCanonicalModel?.(canonicalSelector, { availableOnly: false, candidates: availableModels })
1233
+ : undefined;
1249
1234
  return {
1250
- model,
1251
- selector,
1252
- thinkingLevel,
1253
- warning,
1235
+ model: resolved.model,
1236
+ selector:
1237
+ canonicalModel &&
1238
+ canonicalModel.provider === resolved.model.provider &&
1239
+ canonicalModel.id === resolved.model.id
1240
+ ? (modelRegistry.getCanonicalId?.(canonicalModel) ?? canonicalSelector)
1241
+ : formatModelString(resolved.model),
1242
+ thinkingLevel: resolved.thinkingLevel,
1243
+ warning: resolved.warning,
1254
1244
  error: undefined,
1255
1245
  };
1256
1246
  }
@@ -1295,7 +1285,8 @@ export async function findInitialModel(options: {
1295
1285
 
1296
1286
  // 1. CLI args take priority
1297
1287
  if (cliProvider && cliModel) {
1298
- const found = modelRegistry.find(cliProvider, cliModel);
1288
+ const found = resolveProviderModelReference(cliProvider, cliModel, modelRegistry.getAvailable());
1289
+
1299
1290
  if (!found) {
1300
1291
  console.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));
1301
1292
  process.exit(1);
@@ -1319,7 +1310,8 @@ export async function findInitialModel(options: {
1319
1310
 
1320
1311
  // 3. Try saved default from settings
1321
1312
  if (defaultProvider && defaultModelId) {
1322
- const found = modelRegistry.find(defaultProvider, defaultModelId);
1313
+ const found = resolveProviderModelReference(defaultProvider, defaultModelId, modelRegistry.getAvailable());
1314
+
1323
1315
  if (found) {
1324
1316
  model = found;
1325
1317
  thinkingLevel = resolveThinkingLevelForModel(found, defaultThinkingSelector);
@@ -1358,7 +1350,7 @@ export async function restoreModelFromSession(
1358
1350
  shouldPrintMessages: boolean,
1359
1351
  modelRegistry: RestorableModelRegistry,
1360
1352
  ): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
1361
- const restoredModel = modelRegistry.find(savedProvider, savedModelId);
1353
+ const restoredModel = resolveProviderModelReference(savedProvider, savedModelId, modelRegistry.getAvailable());
1362
1354
 
1363
1355
  // Check if restored model exists and has a valid API key
1364
1356
  const hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel)) : false;