synabun 2.0.5 → 2.0.9

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 (527) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +2 -1
  3. package/THIRD-PARTY-LICENSES.md +13 -0
  4. package/docs/whatsapp.md +151 -16
  5. package/hooks/claude-code/post-plan.mjs +6 -30
  6. package/hooks/claude-code/session-start.mjs +1 -4
  7. package/hooks/claude-code/shared.mjs +112 -75
  8. package/lib/child-path.js +303 -0
  9. package/lib/claude-hooks.js +2 -1
  10. package/lib/data-home-migration.js +2 -0
  11. package/lib/desktop-app.js +265 -0
  12. package/lib/external-tools.js +7 -3
  13. package/lib/log-redact.js +102 -0
  14. package/lib/package-processes.js +670 -0
  15. package/lib/packaged-runtime.js +138 -11
  16. package/lib/runtime-dependencies.js +225 -0
  17. package/mcp-server/dist/http.d.ts +3 -1
  18. package/mcp-server/dist/http.d.ts.map +1 -1
  19. package/mcp-server/dist/http.js +15 -4
  20. package/mcp-server/dist/http.js.map +1 -1
  21. package/mcp-server/dist/index.d.ts.map +1 -1
  22. package/mcp-server/dist/index.js +9 -6
  23. package/mcp-server/dist/index.js.map +1 -1
  24. package/mcp-server/dist/services/browser-bench.d.ts +1 -1
  25. package/mcp-server/dist/services/browser-bench.d.ts.map +1 -1
  26. package/mcp-server/dist/services/browser-bench.js +1 -1
  27. package/mcp-server/dist/services/browser-bench.js.map +1 -1
  28. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  29. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  30. package/mcp-server/dist/services/budget-judgments.js +153 -0
  31. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  32. package/mcp-server/dist/services/categories.d.ts +35 -2
  33. package/mcp-server/dist/services/categories.d.ts.map +1 -1
  34. package/mcp-server/dist/services/categories.js +77 -33
  35. package/mcp-server/dist/services/categories.js.map +1 -1
  36. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  37. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  38. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  39. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  40. package/mcp-server/dist/services/desktop-bench.d.ts +7 -7
  41. package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -1
  42. package/mcp-server/dist/services/desktop-bench.js +1 -1
  43. package/mcp-server/dist/services/desktop-bench.js.map +1 -1
  44. package/mcp-server/dist/services/identity.d.ts +11 -0
  45. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  46. package/mcp-server/dist/services/identity.js +24 -0
  47. package/mcp-server/dist/services/identity.js.map +1 -1
  48. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  49. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  50. package/mcp-server/dist/services/neural-interface.js +34 -2
  51. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  52. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  53. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  54. package/mcp-server/dist/services/typesafe-config.js +4 -1
  55. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  56. package/mcp-server/dist/tools/agents.d.ts +11 -8
  57. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  58. package/mcp-server/dist/tools/agents.js +35 -14
  59. package/mcp-server/dist/tools/agents.js.map +1 -1
  60. package/mcp-server/dist/tools/bluesky.js +1 -1
  61. package/mcp-server/dist/tools/bluesky.js.map +1 -1
  62. package/mcp-server/dist/tools/browser-advanced.d.ts +1 -1
  63. package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -1
  64. package/mcp-server/dist/tools/browser-advanced.js +1 -1
  65. package/mcp-server/dist/tools/browser-advanced.js.map +1 -1
  66. package/mcp-server/dist/tools/browser-batch.d.ts +1 -1
  67. package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -1
  68. package/mcp-server/dist/tools/browser-batch.js +1 -1
  69. package/mcp-server/dist/tools/browser-batch.js.map +1 -1
  70. package/mcp-server/dist/tools/browser-cheatsheet.d.ts +1 -1
  71. package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -1
  72. package/mcp-server/dist/tools/browser-cheatsheet.js +1 -1
  73. package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -1
  74. package/mcp-server/dist/tools/browser-interact.d.ts +1 -1
  75. package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -1
  76. package/mcp-server/dist/tools/browser-interact.js +1 -1
  77. package/mcp-server/dist/tools/browser-interact.js.map +1 -1
  78. package/mcp-server/dist/tools/browser-navigate.d.ts +1 -1
  79. package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -1
  80. package/mcp-server/dist/tools/browser-navigate.js +1 -1
  81. package/mcp-server/dist/tools/browser-navigate.js.map +1 -1
  82. package/mcp-server/dist/tools/browser-observe.d.ts +1 -1
  83. package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -1
  84. package/mcp-server/dist/tools/browser-observe.js +1 -1
  85. package/mcp-server/dist/tools/browser-observe.js.map +1 -1
  86. package/mcp-server/dist/tools/card-tools.d.ts +1 -1
  87. package/mcp-server/dist/tools/card-tools.d.ts.map +1 -1
  88. package/mcp-server/dist/tools/card-tools.js +1 -1
  89. package/mcp-server/dist/tools/card-tools.js.map +1 -1
  90. package/mcp-server/dist/tools/category.d.ts +1 -1
  91. package/mcp-server/dist/tools/category.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/category.js +5 -1
  93. package/mcp-server/dist/tools/category.js.map +1 -1
  94. package/mcp-server/dist/tools/choice.d.ts +1 -1
  95. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/choice.js +1 -1
  97. package/mcp-server/dist/tools/choice.js.map +1 -1
  98. package/mcp-server/dist/tools/computer.d.ts +1 -1
  99. package/mcp-server/dist/tools/computer.d.ts.map +1 -1
  100. package/mcp-server/dist/tools/computer.js +1 -1
  101. package/mcp-server/dist/tools/computer.js.map +1 -1
  102. package/mcp-server/dist/tools/discord-channel.d.ts +1 -1
  103. package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -1
  104. package/mcp-server/dist/tools/discord-channel.js +1 -1
  105. package/mcp-server/dist/tools/discord-channel.js.map +1 -1
  106. package/mcp-server/dist/tools/discord-guild.d.ts +1 -1
  107. package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -1
  108. package/mcp-server/dist/tools/discord-guild.js +1 -1
  109. package/mcp-server/dist/tools/discord-guild.js.map +1 -1
  110. package/mcp-server/dist/tools/discord-member.d.ts +1 -1
  111. package/mcp-server/dist/tools/discord-member.d.ts.map +1 -1
  112. package/mcp-server/dist/tools/discord-member.js +1 -1
  113. package/mcp-server/dist/tools/discord-member.js.map +1 -1
  114. package/mcp-server/dist/tools/discord-message.d.ts +1 -1
  115. package/mcp-server/dist/tools/discord-message.d.ts.map +1 -1
  116. package/mcp-server/dist/tools/discord-message.js +1 -1
  117. package/mcp-server/dist/tools/discord-message.js.map +1 -1
  118. package/mcp-server/dist/tools/discord-onboarding.d.ts +1 -1
  119. package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -1
  120. package/mcp-server/dist/tools/discord-onboarding.js +1 -1
  121. package/mcp-server/dist/tools/discord-onboarding.js.map +1 -1
  122. package/mcp-server/dist/tools/discord-role.d.ts +1 -1
  123. package/mcp-server/dist/tools/discord-role.d.ts.map +1 -1
  124. package/mcp-server/dist/tools/discord-role.js +1 -1
  125. package/mcp-server/dist/tools/discord-role.js.map +1 -1
  126. package/mcp-server/dist/tools/discord-thread.d.ts +1 -1
  127. package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -1
  128. package/mcp-server/dist/tools/discord-thread.js +1 -1
  129. package/mcp-server/dist/tools/discord-thread.js.map +1 -1
  130. package/mcp-server/dist/tools/discord-webhook.d.ts +1 -1
  131. package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -1
  132. package/mcp-server/dist/tools/discord-webhook.js +1 -1
  133. package/mcp-server/dist/tools/discord-webhook.js.map +1 -1
  134. package/mcp-server/dist/tools/fb-groups.d.ts +1 -1
  135. package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -1
  136. package/mcp-server/dist/tools/fb-groups.js +1 -1
  137. package/mcp-server/dist/tools/fb-groups.js.map +1 -1
  138. package/mcp-server/dist/tools/forget.d.ts +1 -1
  139. package/mcp-server/dist/tools/forget.d.ts.map +1 -1
  140. package/mcp-server/dist/tools/forget.js +1 -1
  141. package/mcp-server/dist/tools/forget.js.map +1 -1
  142. package/mcp-server/dist/tools/git-tools.d.ts +1 -1
  143. package/mcp-server/dist/tools/git-tools.d.ts.map +1 -1
  144. package/mcp-server/dist/tools/git-tools.js +1 -1
  145. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  146. package/mcp-server/dist/tools/gsc/experience.d.ts +1 -1
  147. package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -1
  148. package/mcp-server/dist/tools/gsc/experience.js +1 -1
  149. package/mcp-server/dist/tools/gsc/experience.js.map +1 -1
  150. package/mcp-server/dist/tools/gsc/indexing.d.ts +1 -1
  151. package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -1
  152. package/mcp-server/dist/tools/gsc/indexing.js +1 -1
  153. package/mcp-server/dist/tools/gsc/indexing.js.map +1 -1
  154. package/mcp-server/dist/tools/gsc/inspect.d.ts +1 -1
  155. package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -1
  156. package/mcp-server/dist/tools/gsc/inspect.js +1 -1
  157. package/mcp-server/dist/tools/gsc/inspect.js.map +1 -1
  158. package/mcp-server/dist/tools/gsc/navigate.d.ts +1 -1
  159. package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -1
  160. package/mcp-server/dist/tools/gsc/navigate.js +1 -1
  161. package/mcp-server/dist/tools/gsc/navigate.js.map +1 -1
  162. package/mcp-server/dist/tools/gsc/performance.d.ts +1 -1
  163. package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -1
  164. package/mcp-server/dist/tools/gsc/performance.js +1 -1
  165. package/mcp-server/dist/tools/gsc/performance.js.map +1 -1
  166. package/mcp-server/dist/tools/gsc/settings.d.ts +1 -1
  167. package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -1
  168. package/mcp-server/dist/tools/gsc/settings.js +1 -1
  169. package/mcp-server/dist/tools/gsc/settings.js.map +1 -1
  170. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  171. package/mcp-server/dist/tools/image-tools.d.ts.map +1 -1
  172. package/mcp-server/dist/tools/image-tools.js +1 -1
  173. package/mcp-server/dist/tools/image-tools.js.map +1 -1
  174. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +1 -1
  175. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -1
  176. package/mcp-server/dist/tools/leonardo-browser-tools.js +1 -1
  177. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -1
  178. package/mcp-server/dist/tools/loop.d.ts +1 -1
  179. package/mcp-server/dist/tools/loop.d.ts.map +1 -1
  180. package/mcp-server/dist/tools/loop.js +1 -1
  181. package/mcp-server/dist/tools/loop.js.map +1 -1
  182. package/mcp-server/dist/tools/memories.d.ts +1 -1
  183. package/mcp-server/dist/tools/memories.d.ts.map +1 -1
  184. package/mcp-server/dist/tools/memories.js +1 -1
  185. package/mcp-server/dist/tools/memories.js.map +1 -1
  186. package/mcp-server/dist/tools/morelogin.d.ts +1 -1
  187. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  188. package/mcp-server/dist/tools/morelogin.js +1 -1
  189. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  190. package/mcp-server/dist/tools/profile.d.ts +1 -1
  191. package/mcp-server/dist/tools/profile.d.ts.map +1 -1
  192. package/mcp-server/dist/tools/profile.js +1 -1
  193. package/mcp-server/dist/tools/profile.js.map +1 -1
  194. package/mcp-server/dist/tools/recall.d.ts +1 -1
  195. package/mcp-server/dist/tools/recall.d.ts.map +1 -1
  196. package/mcp-server/dist/tools/recall.js +1 -1
  197. package/mcp-server/dist/tools/recall.js.map +1 -1
  198. package/mcp-server/dist/tools/reflect.d.ts +1 -1
  199. package/mcp-server/dist/tools/reflect.d.ts.map +1 -1
  200. package/mcp-server/dist/tools/reflect.js +1 -1
  201. package/mcp-server/dist/tools/reflect.js.map +1 -1
  202. package/mcp-server/dist/tools/remember.d.ts +1 -1
  203. package/mcp-server/dist/tools/remember.d.ts.map +1 -1
  204. package/mcp-server/dist/tools/remember.js +13 -11
  205. package/mcp-server/dist/tools/remember.js.map +1 -1
  206. package/mcp-server/dist/tools/restore.d.ts +1 -1
  207. package/mcp-server/dist/tools/restore.d.ts.map +1 -1
  208. package/mcp-server/dist/tools/restore.js +1 -1
  209. package/mcp-server/dist/tools/restore.js.map +1 -1
  210. package/mcp-server/dist/tools/style-guide-tools.d.ts +1 -1
  211. package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -1
  212. package/mcp-server/dist/tools/style-guide-tools.js +1 -1
  213. package/mcp-server/dist/tools/style-guide-tools.js.map +1 -1
  214. package/mcp-server/dist/tools/sync.d.ts +1 -1
  215. package/mcp-server/dist/tools/sync.d.ts.map +1 -1
  216. package/mcp-server/dist/tools/sync.js +1 -1
  217. package/mcp-server/dist/tools/sync.js.map +1 -1
  218. package/mcp-server/dist/tools/tictactoe-tools.d.ts +1 -1
  219. package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -1
  220. package/mcp-server/dist/tools/tictactoe-tools.js +1 -1
  221. package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -1
  222. package/mcp-server/dist/tools/utils.d.ts +1 -1
  223. package/mcp-server/dist/tools/utils.d.ts.map +1 -1
  224. package/mcp-server/dist/tools/utils.js +1 -1
  225. package/mcp-server/dist/tools/utils.js.map +1 -1
  226. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  227. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  228. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  229. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  230. package/mcp-server/dist/tools/whiteboard-tools.d.ts +3 -3
  231. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  232. package/mcp-server/dist/tools/whiteboard-tools.js +1 -1
  233. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  234. package/mcp-server/dist/tools/youtube/analytics.d.ts +1 -1
  235. package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -1
  236. package/mcp-server/dist/tools/youtube/analytics.js +1 -1
  237. package/mcp-server/dist/tools/youtube/analytics.js.map +1 -1
  238. package/mcp-server/dist/tools/youtube/download.d.ts +1 -1
  239. package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -1
  240. package/mcp-server/dist/tools/youtube/download.js +1 -1
  241. package/mcp-server/dist/tools/youtube/download.js.map +1 -1
  242. package/mcp-server/dist/tools/youtube/media.d.ts +1 -1
  243. package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -1
  244. package/mcp-server/dist/tools/youtube/media.js +1 -1
  245. package/mcp-server/dist/tools/youtube/media.js.map +1 -1
  246. package/mcp-server/dist/tools/youtube/metadata.d.ts +1 -1
  247. package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -1
  248. package/mcp-server/dist/tools/youtube/metadata.js +1 -1
  249. package/mcp-server/dist/tools/youtube/metadata.js.map +1 -1
  250. package/mcp-server/dist/tools/youtube/navigate.d.ts +1 -1
  251. package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -1
  252. package/mcp-server/dist/tools/youtube/navigate.js +1 -1
  253. package/mcp-server/dist/tools/youtube/navigate.js.map +1 -1
  254. package/mcp-server/dist/tools/youtube/playlist.d.ts +1 -1
  255. package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -1
  256. package/mcp-server/dist/tools/youtube/playlist.js +1 -1
  257. package/mcp-server/dist/tools/youtube/playlist.js.map +1 -1
  258. package/mcp-server/dist/tools/youtube/schedule.d.ts +1 -1
  259. package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -1
  260. package/mcp-server/dist/tools/youtube/schedule.js +1 -1
  261. package/mcp-server/dist/tools/youtube/schedule.js.map +1 -1
  262. package/mcp-server/dist/tools/youtube/source.d.ts +1 -1
  263. package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -1
  264. package/mcp-server/dist/tools/youtube/source.js +1 -1
  265. package/mcp-server/dist/tools/youtube/source.js.map +1 -1
  266. package/mcp-server/dist/tools/youtube/upload.d.ts +1 -1
  267. package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -1
  268. package/mcp-server/dist/tools/youtube/upload.js +4 -9
  269. package/mcp-server/dist/tools/youtube/upload.js.map +1 -1
  270. package/mcp-server/dist/tui.js +18 -5
  271. package/mcp-server/dist/tui.js.map +1 -1
  272. package/mcp-server/dist/version.d.ts +5 -0
  273. package/mcp-server/dist/version.d.ts.map +1 -0
  274. package/mcp-server/dist/version.js +21 -0
  275. package/mcp-server/dist/version.js.map +1 -0
  276. package/mcp-server/package-lock.json +3 -3
  277. package/mcp-server/package.json +3 -3
  278. package/mcp-server/run.mjs +15 -9
  279. package/mcp-server/src/http.ts +18 -3
  280. package/mcp-server/src/index.ts +9 -6
  281. package/mcp-server/src/services/browser-bench.ts +1 -1
  282. package/mcp-server/src/services/budget-judgments.ts +183 -0
  283. package/mcp-server/src/services/categories.ts +77 -32
  284. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  285. package/mcp-server/src/services/desktop-bench.ts +1 -1
  286. package/mcp-server/src/services/identity.ts +27 -0
  287. package/mcp-server/src/services/neural-interface.ts +34 -2
  288. package/mcp-server/src/services/typesafe-config.ts +4 -1
  289. package/mcp-server/src/tools/agents.ts +34 -15
  290. package/mcp-server/src/tools/bluesky.ts +1 -1
  291. package/mcp-server/src/tools/browser-advanced.ts +1 -1
  292. package/mcp-server/src/tools/browser-batch.ts +1 -1
  293. package/mcp-server/src/tools/browser-cheatsheet.ts +1 -1
  294. package/mcp-server/src/tools/browser-interact.ts +1 -1
  295. package/mcp-server/src/tools/browser-navigate.ts +1 -1
  296. package/mcp-server/src/tools/browser-observe.ts +1 -1
  297. package/mcp-server/src/tools/card-tools.ts +1 -1
  298. package/mcp-server/src/tools/category.ts +6 -1
  299. package/mcp-server/src/tools/choice.ts +1 -1
  300. package/mcp-server/src/tools/computer.ts +1 -1
  301. package/mcp-server/src/tools/discord-channel.ts +1 -1
  302. package/mcp-server/src/tools/discord-guild.ts +1 -1
  303. package/mcp-server/src/tools/discord-member.ts +1 -1
  304. package/mcp-server/src/tools/discord-message.ts +1 -1
  305. package/mcp-server/src/tools/discord-onboarding.ts +1 -1
  306. package/mcp-server/src/tools/discord-role.ts +1 -1
  307. package/mcp-server/src/tools/discord-thread.ts +1 -1
  308. package/mcp-server/src/tools/discord-webhook.ts +1 -1
  309. package/mcp-server/src/tools/fb-groups.ts +1 -1
  310. package/mcp-server/src/tools/forget.ts +1 -1
  311. package/mcp-server/src/tools/git-tools.ts +1 -1
  312. package/mcp-server/src/tools/gsc/experience.ts +1 -1
  313. package/mcp-server/src/tools/gsc/indexing.ts +1 -1
  314. package/mcp-server/src/tools/gsc/inspect.ts +1 -1
  315. package/mcp-server/src/tools/gsc/navigate.ts +1 -1
  316. package/mcp-server/src/tools/gsc/performance.ts +1 -1
  317. package/mcp-server/src/tools/gsc/settings.ts +1 -1
  318. package/mcp-server/src/tools/image-tools.ts +1 -1
  319. package/mcp-server/src/tools/leonardo-browser-tools.ts +1 -1
  320. package/mcp-server/src/tools/loop.ts +1 -1
  321. package/mcp-server/src/tools/memories.ts +1 -1
  322. package/mcp-server/src/tools/morelogin.ts +1 -1
  323. package/mcp-server/src/tools/profile.ts +1 -1
  324. package/mcp-server/src/tools/recall.ts +1 -1
  325. package/mcp-server/src/tools/reflect.ts +1 -1
  326. package/mcp-server/src/tools/remember.ts +13 -12
  327. package/mcp-server/src/tools/restore.ts +1 -1
  328. package/mcp-server/src/tools/style-guide-tools.ts +1 -1
  329. package/mcp-server/src/tools/sync.ts +1 -1
  330. package/mcp-server/src/tools/tictactoe-tools.ts +1 -1
  331. package/mcp-server/src/tools/utils.ts +1 -1
  332. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  333. package/mcp-server/src/tools/whiteboard-tools.ts +1 -1
  334. package/mcp-server/src/tools/youtube/analytics.ts +1 -1
  335. package/mcp-server/src/tools/youtube/download.ts +1 -1
  336. package/mcp-server/src/tools/youtube/media.ts +1 -1
  337. package/mcp-server/src/tools/youtube/metadata.ts +1 -1
  338. package/mcp-server/src/tools/youtube/navigate.ts +1 -1
  339. package/mcp-server/src/tools/youtube/playlist.ts +1 -1
  340. package/mcp-server/src/tools/youtube/schedule.ts +1 -1
  341. package/mcp-server/src/tools/youtube/source.ts +1 -1
  342. package/mcp-server/src/tools/youtube/upload.ts +4 -12
  343. package/mcp-server/src/tui.ts +24 -10
  344. package/mcp-server/src/version.ts +22 -0
  345. package/neural-interface/i18n/en.json +645 -57
  346. package/neural-interface/i18n/pt-BR.json +644 -56
  347. package/neural-interface/lib/account-usage.js +163 -0
  348. package/neural-interface/lib/agent-sandbox-elevation.js +102 -0
  349. package/neural-interface/lib/agent-sandbox-setting.js +57 -0
  350. package/neural-interface/lib/assistant-api.js +274 -52
  351. package/neural-interface/lib/assistant-brains/claude.js +15 -2
  352. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  353. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  354. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  355. package/neural-interface/lib/assistant-budget-split.js +408 -0
  356. package/neural-interface/lib/assistant-budget.js +94 -95
  357. package/neural-interface/lib/assistant-config.js +62 -14
  358. package/neural-interface/lib/assistant-dispatch.js +432 -125
  359. package/neural-interface/lib/assistant-jev.js +26 -3
  360. package/neural-interface/lib/assistant-persona.js +68 -11
  361. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  362. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  363. package/neural-interface/lib/assistant-route-gate-codex.js +3 -1
  364. package/neural-interface/lib/assistant-route-gate.js +19 -3
  365. package/neural-interface/lib/assistant-router.js +373 -42
  366. package/neural-interface/lib/assistant-runtime.js +312 -48
  367. package/neural-interface/lib/assistant-user-writer.js +4 -2
  368. package/neural-interface/lib/augmented-path.js +317 -53
  369. package/neural-interface/lib/claude-accounts.js +342 -31
  370. package/neural-interface/lib/claude-agent-bridge.js +90 -3
  371. package/neural-interface/lib/claude-executable.js +6 -1
  372. package/neural-interface/lib/claude-model-catalog.js +3 -3
  373. package/neural-interface/lib/claude-panel-auth.js +221 -0
  374. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  375. package/neural-interface/lib/cli-install-api.js +137 -0
  376. package/neural-interface/lib/cli-install.js +797 -0
  377. package/neural-interface/lib/cli-path.js +130 -0
  378. package/neural-interface/lib/cli-resolver.js +744 -0
  379. package/neural-interface/lib/cli-update-planner.js +143 -7
  380. package/neural-interface/lib/codex-account-auth.js +180 -0
  381. package/neural-interface/lib/codex-accounts.js +502 -0
  382. package/neural-interface/lib/codex-capabilities.js +2 -1
  383. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  384. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  385. package/neural-interface/lib/codex-panel-auth.js +62 -0
  386. package/neural-interface/lib/codex-session-settings.js +1 -1
  387. package/neural-interface/lib/codex-settings-schema.js +122 -0
  388. package/neural-interface/lib/desktop/config.js +3 -0
  389. package/neural-interface/lib/exec-loop-command.js +3 -1
  390. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  391. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  392. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  393. package/neural-interface/lib/git-bash/install.js +529 -0
  394. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  395. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  396. package/neural-interface/lib/git-bash/manager.js +268 -0
  397. package/neural-interface/lib/git-bash/pins.js +206 -0
  398. package/neural-interface/lib/git-bash/pins.json +34 -0
  399. package/neural-interface/lib/git-bash/resolver.js +194 -0
  400. package/neural-interface/lib/git-read.js +34 -2
  401. package/neural-interface/lib/install-details.js +163 -0
  402. package/neural-interface/lib/mcp-installer.js +14 -2
  403. package/neural-interface/lib/native-loop-providers.js +22 -4
  404. package/neural-interface/lib/native-loop-runtime.js +9 -4
  405. package/neural-interface/lib/os-path.js +319 -0
  406. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  407. package/neural-interface/lib/provider-panel-terminal-auth.js +96 -0
  408. package/neural-interface/lib/pty-host/core.js +2 -1
  409. package/neural-interface/lib/pty-host/manager.js +27 -2
  410. package/neural-interface/lib/remote-policy.js +20 -11
  411. package/neural-interface/lib/session-indexer.js +3 -1
  412. package/neural-interface/lib/session-title-generator.js +6 -3
  413. package/neural-interface/lib/system-about.js +340 -0
  414. package/neural-interface/lib/system-activity.js +66 -0
  415. package/neural-interface/lib/terminal-env.js +11 -3
  416. package/neural-interface/lib/terminal-path.js +55 -0
  417. package/neural-interface/lib/virtual-socket.js +12 -1
  418. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  419. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  420. package/neural-interface/lib/whatsapp/commands.js +13 -14
  421. package/neural-interface/lib/whatsapp/config.js +134 -20
  422. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  423. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  424. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  425. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  426. package/neural-interface/lib/whatsapp/manager.js +92 -32
  427. package/neural-interface/lib/whatsapp/paths.js +11 -1
  428. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  429. package/neural-interface/lib/whatsapp/redact.js +63 -0
  430. package/neural-interface/lib/whatsapp/service.js +343 -91
  431. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  432. package/neural-interface/package-lock.json +2 -2
  433. package/neural-interface/package.json +1 -1
  434. package/neural-interface/public/index.html +2 -0
  435. package/neural-interface/public/index2d.html +2 -0
  436. package/neural-interface/public/offline.html +16 -1
  437. package/neural-interface/public/onboarding.html +96 -2
  438. package/neural-interface/public/shared/account-i18n.js +7 -0
  439. package/neural-interface/public/shared/account-menu.js +542 -0
  440. package/neural-interface/public/shared/account-usage.js +79 -0
  441. package/neural-interface/public/shared/account-view.js +336 -0
  442. package/neural-interface/public/shared/agent-runtime-options.js +28 -8
  443. package/neural-interface/public/shared/assistant/asst-account-menu.js +298 -0
  444. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  445. package/neural-interface/public/shared/assistant/asst-api.js +39 -2
  446. package/neural-interface/public/shared/assistant/asst-brain-picker.js +288 -32
  447. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  448. package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
  449. package/neural-interface/public/shared/assistant/asst-menu.js +219 -80
  450. package/neural-interface/public/shared/assistant/asst-model-chip.js +135 -0
  451. package/neural-interface/public/shared/assistant/asst-panel.js +87 -16
  452. package/neural-interface/public/shared/assistant/asst-render.js +38 -7
  453. package/neural-interface/public/shared/assistant/asst-route.js +72 -14
  454. package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
  455. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  456. package/neural-interface/public/shared/assistant/asst-state.js +1 -1
  457. package/neural-interface/public/shared/assistant/asst-styles.js +160 -29
  458. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  459. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  460. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  461. package/neural-interface/public/shared/cdx/cdx-panel.js +49 -108
  462. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  463. package/neural-interface/public/shared/cdx/cdx-styles.js +172 -111
  464. package/neural-interface/public/shared/cdx/cdx-tabs.js +264 -664
  465. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  466. package/neural-interface/public/shared/claude-account-view.js +3 -0
  467. package/neural-interface/public/shared/cli-install-view.js +392 -0
  468. package/neural-interface/public/shared/cli-status.js +282 -22
  469. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  470. package/neural-interface/public/shared/cp/cp-styles.js +46 -0
  471. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  472. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  473. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  474. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  475. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  476. package/neural-interface/public/shared/editor/editor-panel.js +1 -1
  477. package/neural-interface/public/shared/explorer/fx-main.js +1 -1
  478. package/neural-interface/public/shared/git-bash-view.js +145 -0
  479. package/neural-interface/public/shared/html-shell.js +1 -0
  480. package/neural-interface/public/shared/i18n.js +19 -1
  481. package/neural-interface/public/shared/install-details-view.js +86 -0
  482. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -1
  483. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +36 -1
  484. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
  485. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +169 -2
  486. package/neural-interface/public/shared/pwa-install-page.js +3 -1
  487. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  488. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  489. package/neural-interface/public/shared/settings/settings-ia.js +15 -0
  490. package/neural-interface/public/shared/showcase-privacy.js +490 -0
  491. package/neural-interface/public/shared/storage.js +30 -3
  492. package/neural-interface/public/shared/styles.css +281 -32
  493. package/neural-interface/public/shared/synabun-mascot.js +48 -2
  494. package/neural-interface/public/shared/ui-claude-panel.js +262 -217
  495. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  496. package/neural-interface/public/shared/ui-explorer.js +2 -0
  497. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  498. package/neural-interface/public/shared/ui-judgments.js +10 -1
  499. package/neural-interface/public/shared/ui-loading.js +30 -6
  500. package/neural-interface/public/shared/ui-menubar.js +13 -3
  501. package/neural-interface/public/shared/ui-navbar.js +3 -1
  502. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  503. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  504. package/neural-interface/public/shared/ui-sessions.js +9 -3
  505. package/neural-interface/public/shared/ui-settings.js +55 -3
  506. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  507. package/neural-interface/public/shared/ui-sync.js +8 -0
  508. package/neural-interface/public/shared/ui-terminal.js +17 -0
  509. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  510. package/neural-interface/public/shared/ui-update.js +598 -7
  511. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  512. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  513. package/neural-interface/public/shared/worker-end.js +2 -0
  514. package/neural-interface/public/sw.js +1 -1
  515. package/neural-interface/public/variant/2d/main.js +10 -1
  516. package/neural-interface/public/variant/3d/main.js +10 -1
  517. package/neural-interface/scripts/rebuild-pty.js +14 -2
  518. package/neural-interface/server.js +1384 -679
  519. package/npm-shrinkwrap.json +4424 -0
  520. package/package.json +57 -2
  521. package/postinstall.js +61 -47
  522. package/preuninstall.js +8 -35
  523. package/setup.js +80 -47
  524. package/skills/synabun/modules/idea.md +1 -1
  525. package/skills/synabun/modules/schedule.md +1 -1
  526. package/updater.cmd +2 -2
  527. package/updater.mjs +97 -3
@@ -35,6 +35,9 @@ import { randomBytes } from 'node:crypto';
35
35
  import { CATALOG_PROVIDERS, TIERS, defaultIsHidden, defaultModelRow, findModel, hiddenModelId, priceText, providerAllHidden, providerEmpty, rowIsHidden, rowMakes } from './assistant-catalog.js';
36
36
  import { effectiveRouting, normalizeRouteMode } from './assistant-config.js';
37
37
  import { normalizeEffort } from './effort-levels.js';
38
+ import { hasRouteAccounts, routeAccountText } from './assistant-route-accounts.js';
39
+ import { chooseUsageAccount } from './account-usage.js';
40
+ import { PROVIDER_LABELS, coversWork, dryProviders, isFreePrice, listPriceOf, rowOf } from './assistant-budget-split.js';
38
41
 
39
42
  export const TASK_CLASS_META = Object.freeze({
40
43
  chat: { label: 'Chat', description: 'Conversation and questions answered from knowledge or memory', defaultKind: 'direct' },
@@ -55,7 +58,7 @@ export const TASK_CLASS_META = Object.freeze({
55
58
  });
56
59
  export const TASK_CLASSES = Object.freeze(Object.keys(TASK_CLASS_META));
57
60
  export const PREFERENCE_KEYS = Object.freeze([...TASK_CLASSES, 'vision']);
58
- export const ROUTE_REASONS = Object.freeze(['always-ask', 'low_confidence', 'no_confidence', 'model_missing', 'model_unknown', 'model_disabled', 'model_unavailable', 'needs_vision', 'needs_output', 'no_capable_model']);
61
+ export const ROUTE_REASONS = Object.freeze(['always-ask', 'low_confidence', 'no_confidence', 'model_missing', 'model_unknown', 'model_disabled', 'model_unavailable', 'needs_vision', 'needs_output', 'no_capable_model', 'budget_exhausted']);
59
62
  export const MODEL_DISABLED_TEXT = "Disabled in the Assistant's Models list.";
60
63
  const VISION_CLASSES = new Set(['computer']);
61
64
  const MEDIUM_WORDS = { image: 'images', video: 'videos' };
@@ -92,6 +95,7 @@ function clip(value, max = 200) {
92
95
  const text = String(value ?? '').replace(/\s+/g, ' ').trim();
93
96
  return text.length > max ? `${text.slice(0, max - 1)}…` : text;
94
97
  }
98
+ const usdText = (value) => `$${(Number(value) || 0).toFixed(2)}`;
95
99
  function routeError(code, message, status = 400, extra = {}) {
96
100
  const error = new Error(message);
97
101
  error.code = code;
@@ -103,7 +107,7 @@ function tierRank(tier) { return TIER_RANK[tier] ?? 1; }
103
107
  function outPrice(row) { const n = Number(row?.price?.output); return Number.isFinite(n) ? n : Number.POSITIVE_INFINITY; }
104
108
  function sameTarget(a, b) {
105
109
  if (!a || !b) return false;
106
- return a.kind === b.kind && a.provider === b.provider && String(a.model || '') === String(b.model || '') && String(a.effort || '') === String(b.effort || '');
110
+ return a.kind === b.kind && a.provider === b.provider && String(a.model || '') === String(b.model || '') && String(a.effort || '') === String(b.effort || '') && String(a.accountId || '') === String(b.accountId || '');
107
111
  }
108
112
 
109
113
  /**
@@ -215,6 +219,7 @@ export function normalizeRouteRequest(body = {}, { brain = {} } = {}) {
215
219
  proposals.push({
216
220
  kind, provider, model,
217
221
  effort: raw.effort ? String(raw.effort).trim().slice(0, 40) : null,
222
+ ...(raw.accountId || raw.account_id ? { accountId: String(raw.accountId || raw.account_id).slice(0, 64) } : {}),
218
223
  reason: raw.reason ? clip(raw.reason, 240) : null,
219
224
  });
220
225
  }
@@ -241,6 +246,7 @@ export function describeTarget(target, { catalog = null, brain = {}, brainInfo =
241
246
  const unlistedClaude = target.provider === 'claude-code' && match === 'unlisted';
242
247
  return {
243
248
  kind: target.kind, provider: target.provider, model: target.model || null, effort: target.effort || null,
249
+ ...(Object.hasOwn(target, 'accountId') ? { accountId: target.accountId, account: target.account || null, accountError: target.accountError || null } : {}),
244
250
  label, tier: info?.tier || row?.tier || null,
245
251
  vision: info ? info.vision ?? null : (row ? row.vision ?? null : (unlistedClaude ? true : null)),
246
252
  // What it generates besides text (image / video creation needs a true one).
@@ -272,6 +278,7 @@ function preferenceTarget(pref, brain, catalog = null, { dispatchOnly = false }
272
278
  const kind = here && !dispatchOnly ? 'direct' : 'dispatch';
273
279
  const model = pref.model || (here && dispatchOnly ? brain.model || null : null);
274
280
  const target = { kind, provider: pref.provider, model, effort: pref.effort || null };
281
+ if (hasRouteAccounts(pref.provider) && pref.accountId) target.accountId = pref.accountId;
275
282
  // A remembered route onto a model the user since disabled is not applied.
276
283
  return catalog && isDisabled(catalog, target) ? null : target;
277
284
  }
@@ -338,6 +345,122 @@ function sightFallback(catalog, provider, { brain = {}, routing = {}, taskClass
338
345
  return alt ? { kind: 'dispatch', provider: alt.provider, model: alt.row.id, effort: null } : null;
339
346
  }
340
347
 
348
+ // ── the session budget (assistant-budget-split.js coverage) ──────────────────
349
+
350
+ /** Does `target` run on a free model (the catalog says so)? Free models need no share of the budget. */
351
+ export function targetIsFree(catalog, target) {
352
+ if (!target?.provider) return false;
353
+ return isFreePrice(listPriceOf(rowOf(catalog, target.provider, target.model || null), { provider: target.provider, model: target.model || null }));
354
+ }
355
+ /**
356
+ * Can the session's budget pay for `target` now? A direct target is the brain's own work (it may use its
357
+ * reserve), a dispatch a worker's; a free model needs only the session to be under its budget.
358
+ * `coverage` null (No limit, or no budget wired): always.
359
+ */
360
+ export function budgetCovers(coverage, catalog, target) {
361
+ if (!coverage || !target?.provider) return true;
362
+ return coversWork(coverage, { provider: target.provider, forBrain: target.kind === 'direct', free: targetIsFree(catalog, target) });
363
+ }
364
+ /** "codex share used up" / "the session budget is spent". */
365
+ function dryText(coverage, provider) {
366
+ if (!coverage || coverage.leftUsd < 0.01) return 'the session budget is spent';
367
+ return `${provider} share used up`;
368
+ }
369
+ /**
370
+ * With a budget, a task whose provider has no share left moves to the next capable provider with money
371
+ * (the user chose Re-route): the brain's next proposal first, then the same tier on another provider
372
+ * (its saved model for that class, else its default model, else its cheapest model of that tier), then a
373
+ * free capable model, and last any capable model with share left, the closest tier first (cheapest
374
+ * within it), so a task is declined only when nothing at all can pay. Every capability rule holds for
375
+ * each candidate (`capable`). → { target } (moved or not), or { declined: true } when nothing can pay.
376
+ */
377
+ function budgetReroute({ target, request, catalog, brain, routing, coverage, capable }) {
378
+ const covers = (t) => budgetCovers(coverage, catalog, t);
379
+ if (covers(target)) return { target, moved: false };
380
+ const providerOk = (p) => coverage.providers?.[p]?.eligible !== false;
381
+ const tried = new Set();
382
+ const key = (t) => `${t.kind}:${t.provider}:${t.model || ''}`;
383
+ const pick = (t) => {
384
+ if (!t?.provider || tried.has(key(t))) return null;
385
+ tried.add(key(t));
386
+ return providerOk(t.provider) && capable(t) && covers(t) ? t : null;
387
+ };
388
+ // 1. The brain's next proposals.
389
+ for (const p of (request.proposals || []).slice(1)) {
390
+ const t = pick({ kind: p.kind === 'direct' && p.provider === brain.provider && !request.dispatchOnly ? 'direct' : 'dispatch', provider: p.provider, model: p.model || null, effort: p.effort || null, ...(p.accountId ? { accountId: p.accountId } : {}) });
391
+ if (t) return { target: t, moved: true, step: 'proposal' };
392
+ }
393
+ // 2. The same tier on another provider.
394
+ const tierOf = (t) => (t?.model ? findModel(catalog, t.provider, t.model)?.row?.tier : defaultModelRow(catalog, t?.provider)?.tier) || null;
395
+ const tier = tierOf(target);
396
+ for (const provider of CATALOG_PROVIDERS) {
397
+ if (provider === target.provider || !providerOk(provider)) continue;
398
+ const pref = routing?.preferences?.[request.taskClass];
399
+ const candidates = [];
400
+ if (pref?.provider === provider) candidates.push({ kind: 'dispatch', provider, model: pref.model || null, effort: pref.effort || null, ...(pref.accountId ? { accountId: pref.accountId } : {}) });
401
+ const own = defaultModelRow(catalog, provider);
402
+ if (own && (!tier || own.tier === tier)) candidates.push({ kind: 'dispatch', provider, model: own.id, effort: null });
403
+ visibleRows(catalog, provider).filter((row) => row.status !== 'unavailable' && (!tier || row.tier === tier))
404
+ .sort((a, b) => outPrice(a) - outPrice(b)).forEach((row) => candidates.push({ kind: 'dispatch', provider, model: row.id, effort: null }));
405
+ for (const candidate of candidates) {
406
+ const t = pick(candidate);
407
+ if (t) return { target: t, moved: true, step: 'same_tier' };
408
+ }
409
+ }
410
+ // 3. A free capable model, anywhere.
411
+ for (const provider of CATALOG_PROVIDERS) {
412
+ if (!providerOk(provider)) continue;
413
+ const free = visibleRows(catalog, provider).filter((row) => row.status !== 'unavailable' && isFreePrice(row.price || null));
414
+ for (const row of free) {
415
+ const t = pick({ kind: 'dispatch', provider, model: row.id, effort: null });
416
+ if (t) return { target: t, moved: true, step: 'free' };
417
+ }
418
+ }
419
+ // 4. Any capable model with share left: the closest tier first, the cheapest within it.
420
+ const distance = (row) => (tier ? Math.abs(tierRank(row.tier) - tierRank(tier)) : 0);
421
+ const rest = CATALOG_PROVIDERS.filter(providerOk)
422
+ .flatMap((provider) => visibleRows(catalog, provider).filter((row) => row.status !== 'unavailable').map((row) => ({ provider, row })))
423
+ .sort((a, b) => distance(a.row) - distance(b.row) || outPrice(a.row) - outPrice(b.row));
424
+ for (const { provider, row } of rest) {
425
+ const t = pick({ kind: 'dispatch', provider, model: row.id, effort: null });
426
+ if (t) return { target: t, moved: true, step: 'closest_tier' };
427
+ }
428
+ return { declined: true, dry: dryProviders(coverage) };
429
+ }
430
+
431
+ /**
432
+ * The capability rules a target of `request` must meet: not disabled, a listed model, the medium it must
433
+ * make (image / video creation), sight (design: a confirmed vision on an available row; other vision
434
+ * work: not a confirmed blind one). → (target) => boolean
435
+ */
436
+ export function capableFor(request, { catalog = null, brain = {}, brainInfo = null } = {}) {
437
+ const medium = request?.needsOutput || null;
438
+ const visionOnly = request?.visionOnly === true;
439
+ return (target) => {
440
+ if (!target?.provider || isDisabled(catalog, target)) return false;
441
+ if (target.model && findModel(catalog, target.provider, target.model) === null) return false;
442
+ if (medium && !targetMakes(catalog, target, medium)) return false;
443
+ if (request?.needsVision) {
444
+ if (visionOnly ? designFit(catalog, target, { brain, brainInfo }) !== 'ok' : describeTarget(target, { catalog, brain, brainInfo })?.vision === false) return false;
445
+ }
446
+ return true;
447
+ };
448
+ }
449
+ /**
450
+ * An approved route whose provider ran out of share after approval: move the dispatch the same way
451
+ * agent_route would (budgetReroute). → { target, correction } or { declined, dry }.
452
+ */
453
+ export function budgetMove({ target, request, catalog = null, brain = {}, brainInfo = null, routing = {}, coverage = null }) {
454
+ if (!coverage || budgetCovers(coverage, catalog, target)) return { target, correction: null };
455
+ const moved = budgetReroute({ target, request, catalog, brain, routing, coverage, capable: capableFor(request, { catalog, brain, brainInfo }) });
456
+ if (moved.declined) return { declined: true, dry: moved.dry };
457
+ const next = effortChecked(request.dispatchOnly && moved.target.kind !== 'dispatch' ? { ...moved.target, kind: 'dispatch' } : moved.target, catalog);
458
+ return {
459
+ target: next,
460
+ correction: { field: 'route', from: routeText(target), to: routeText(next), reason: 'budget_share', step: moved.step, text: `budget: ${dryText(coverage, target.provider)} -> ${next.provider}/${next.model || 'default'}` },
461
+ };
462
+ }
463
+
341
464
  /**
342
465
  * Decide whether the user must be asked. Returns
343
466
  * { ask, reasons[], auto: target|null, proposal: target, corrections[], source }.
@@ -345,12 +468,19 @@ function sightFallback(catalog, provider, { brain = {}, routing = {}, taskClass
345
468
  * with no model in the catalog that makes the medium, and for design with none
346
469
  * that can see, it returns { ask: false, unavailable: 'no_capable_model', auto: null }
347
470
  * in every mode.
471
+ *
472
+ * `budget` ({ coverage }: assistant-budget-split.js coverageOf, null under No limit): a target whose
473
+ * provider has no share left moves to the next capable provider with money (budgetReroute) and the
474
+ * corrections say so (reason budget_share); with a budget the saved routes yield to this. When nothing
475
+ * can pay: { ask: false, unavailable: 'budget_exhausted', dry }. `budgetFrom` names the target that was
476
+ * moved or could not be paid, so the caller can divide the budget again and ask once more.
348
477
  */
349
- export function evaluateRoute({ mode, routing = {}, request, brain = {}, brainInfo = null, catalog = null }) {
478
+ export function evaluateRoute({ mode, routing = {}, request, brain = {}, brainInfo = null, catalog = null, budget = null }) {
350
479
  const reasons = [];
351
480
  const corrections = [];
352
481
  const first = request.proposals[0];
353
482
  let proposal = { kind: first.kind, provider: first.provider, model: first.model, effort: first.effort };
483
+ if (first.accountId) proposal.accountId = first.accountId;
354
484
  let source = 'brain';
355
485
  if (request.taskClass === 'chat') {
356
486
  return { ask: false, reasons, auto: { kind: 'direct', provider: brain.provider, model: brain.model || null, effort: brain.effort || null }, proposal, corrections, source };
@@ -370,11 +500,33 @@ export function evaluateRoute({ mode, routing = {}, request, brain = {}, brainIn
370
500
  const fit = (target) => designFit(catalog, target, { brain, brainInfo });
371
501
  // Too blind for this task: design needs a confirmed true (null never counts) on an available row; other vision work refuses a confirmed false.
372
502
  const tooBlind = (target) => (visionOnly ? fit(target) !== 'ok' : sees(target) === false);
503
+ // The session budget: a target whose provider has no share left moves (every capability rule holds).
504
+ const coverage = budget?.coverage || null;
505
+ const capable = capableFor(request, { catalog, brain, brainInfo });
506
+ /** → { target } (the same one, or moved with a budget_share correction), or { declined } (nothing can pay). */
507
+ const applyBudget = (target) => {
508
+ if (!coverage || !target) return { target };
509
+ const moved = budgetReroute({ target, request, catalog, brain, routing, coverage, capable });
510
+ if (moved.declined) return { declined: true, dry: moved.dry, from: target };
511
+ if (!moved.moved) return { target };
512
+ const next = effortChecked(dispatchOnly && moved.target.kind !== 'dispatch' ? { ...moved.target, kind: 'dispatch' } : moved.target, catalog, corrections);
513
+ corrections.push({
514
+ field: 'route', from: routeText(target), to: routeText(next), reason: 'budget_share', step: moved.step,
515
+ text: `budget: ${dryText(coverage, target.provider)} -> ${next.provider}/${next.model || 'default'}`,
516
+ });
517
+ return { target: next, from: target };
518
+ };
519
+ const budgetDeclined = (outcome, extra = {}) => ({
520
+ ask: false, unavailable: 'budget_exhausted', reasons: ['budget_exhausted'], auto: null, proposal, corrections, source, bound: false,
521
+ dry: outcome.dry || [], budgetFrom: outcome.from || null, ...extra,
522
+ });
373
523
  const remembered = preferenceTarget(routing.preferences?.[request.taskClass], brain, null, { dispatchOnly });
374
524
  const disabled = !!remembered && isDisabled(catalog, remembered);
375
525
  // A saved route onto a model that cannot make the medium (or, for design, is not known to see) is not applied either.
376
526
  const incapable = !!remembered && !disabled && (!targetMakes(catalog, remembered, medium) || (visionOnly && tooBlind(remembered)));
377
527
  const pref = remembered && !disabled && !incapable ? remembered : null;
528
+ // In either asking mode, a proposal of the saved target carries its saved account too.
529
+ if (pref && sameTarget({ ...proposal, accountId: null, effort: null }, { ...pref, accountId: null, effort: null }) && (!pref.effort || pref.effort === proposal.effort) && (pref.accountId || proposal.accountId)) proposal.accountId = pref.accountId || null;
378
530
  // Omitted model: a remembered preference for the class fills it.
379
531
  if (!proposal.model && proposal.kind === 'dispatch') {
380
532
  if (pref && (pref.provider === proposal.provider || !first.model)) { proposal = { ...pref, effort: proposal.effort || pref.effort }; source = 'remembered'; }
@@ -395,11 +547,16 @@ export function evaluateRoute({ mode, routing = {}, request, brain = {}, brainIn
395
547
  else if (request.confidence < (routing.askBelow ?? 0.75)) reasons.push('low_confidence');
396
548
 
397
549
  const effective = normalizeRouteMode(mode) || 'ask-unsure';
398
- if (effective === 'always-ask') return { ask: true, reasons: ['always-ask', ...reasons], auto: null, proposal, corrections, source };
399
- if (effective === 'ask-unsure') {
400
- const unsure = reasons.filter((r) => r !== 'no_confidence' || source !== 'remembered');
401
- if (unsure.length) return { ask: true, reasons: unsure, auto: null, proposal, corrections, source };
402
- return { ask: false, reasons, auto: proposal, proposal, corrections, source };
550
+ if (effective === 'always-ask' || effective === 'ask-unsure') {
551
+ // The suggestion a card shows (or the pick that runs) is one the budget can pay for.
552
+ const budgeted = applyBudget(proposal);
553
+ if (budgeted.declined) return budgetDeclined(budgeted);
554
+ const budgetFrom = budgeted.from || null;
555
+ if (budgetFrom) { proposal = budgeted.target; source = 'budget'; }
556
+ if (effective === 'always-ask') return { ask: true, reasons: ['always-ask', ...reasons], auto: null, proposal, corrections, source, budgetFrom };
557
+ const unsure = reasons.filter((r) => r !== 'no_confidence' || (source !== 'remembered' && source !== 'budget'));
558
+ if (unsure.length) return { ask: true, reasons: unsure, auto: null, proposal, corrections, source, budgetFrom };
559
+ return { ask: false, reasons, auto: proposal, proposal, corrections, source, budgetFrom };
403
560
  }
404
561
  // never: the user's saved route for the class binds (the brain only classifies);
405
562
  // without an enabled one, run the brain's pick, correcting what cannot run.
@@ -407,7 +564,7 @@ export function evaluateRoute({ mode, routing = {}, request, brain = {}, brainIn
407
564
  const bound = !!pref;
408
565
  if (bound) {
409
566
  // Provider and model from the saved route; its effort when set, else the brain's.
410
- auto = { kind: pref.kind, provider: pref.provider, model: pref.model || (pref.kind === 'direct' ? brain.model || null : null), effort: pref.effort || proposal.effort || null };
567
+ auto = { ...pref, model: pref.model || (pref.kind === 'direct' ? brain.model || null : null), effort: pref.effort || proposal.effort || null };
411
568
  const pinnedPref = pinnedDefault(catalog, auto);
412
569
  if (pinnedPref) auto = { ...auto, model: pinnedPref.id };
413
570
  if (!sameTarget({ ...auto, effort: null }, { ...proposal, effort: null }) || (pref.effort && pref.effort !== proposal.effort)) {
@@ -485,10 +642,14 @@ export function evaluateRoute({ mode, routing = {}, request, brain = {}, brainIn
485
642
  // A corrected model or provider re-checks the effort.
486
643
  auto = effortChecked(auto, catalog, corrections);
487
644
  if (!bound && corrections.some((c) => c.reason !== 'remembered_disabled' && c.reason !== 'remembered_incapable')) source = 'corrected';
488
- return { ask: false, reasons, auto, proposal, corrections, source, bound };
645
+ // With a budget the saved route yields: a provider with no share left moves the task on.
646
+ const budgeted = applyBudget(auto);
647
+ if (budgeted.declined) return budgetDeclined(budgeted);
648
+ if (budgeted.from) { auto = budgeted.target; source = 'budget'; }
649
+ return { ask: false, reasons, auto, proposal, corrections, source, bound, budgetFrom: budgeted.from || null };
489
650
  }
490
651
 
491
- function optionFor(id, rawTarget, badge, { catalog, brain, brainInfo, needsVision, needsOutput = null, visionOnly = false, reason = null }) {
652
+ function optionFor(id, rawTarget, badge, { catalog, brain, brainInfo, needsVision, needsOutput = null, visionOnly = false, reason = null, coverage = null }) {
492
653
  const target = effortChecked(rawTarget, catalog);
493
654
  const d = describeTarget(target, { catalog, brain, brainInfo });
494
655
  const here = target.kind === 'direct';
@@ -497,6 +658,7 @@ function optionFor(id, rawTarget, badge, { catalog, brain, brainInfo, needsVisio
497
658
  const fit = visionOnly ? designFit(catalog, target, { brain, brainInfo }) : 'ok';
498
659
  const option = {
499
660
  id, kind: target.kind, provider: target.provider, model: target.model || null, effort: target.effort || null,
661
+ ...(Object.hasOwn(target, 'accountId') ? { accountId: target.accountId } : {}),
500
662
  // A design card shows sight as design judges it (an unlisted claude-* id is unknown there).
501
663
  label, badge, tier: d.tier, vision: fit === 'unknown' ? null : d.vision, price: d.price, contextWindow: d.contextWindow,
502
664
  reason: reason || null, warning: null, disabled: false, disabledReason: null,
@@ -507,6 +669,12 @@ function optionFor(id, rawTarget, badge, { catalog, brain, brainInfo, needsVisio
507
669
  else if (fit !== 'ok') { option.disabled = true; option.disabledReason = fitText(fit); }
508
670
  else if (needsVision && d.vision === false) { option.disabled = true; option.disabledReason = 'This model cannot see images, and this task needs to.'; }
509
671
  else if (needsVision && d.vision === null) option.warning = 'Image support unknown for this model.';
672
+ // The session budget: a provider with no share left cannot take the task (a free model still can).
673
+ if (!option.disabled && coverage && !budgetCovers(coverage, catalog, target)) {
674
+ option.disabled = true;
675
+ option.budget = 'share_used_up';
676
+ option.disabledReason = coverage.leftUsd < 0.01 ? "Budget: this session's budget is spent." : `Budget: the ${providerLabel(target.provider)} share of this session's budget is used up.`;
677
+ }
510
678
  if (here && brainInfo?.contextWindow && d.contextWindow && d.contextWindow < brainInfo.contextWindow) {
511
679
  option.warning = `Smaller context (${Math.round(d.contextWindow / 1000)}k) than the current model; a long conversation may not fit.`;
512
680
  }
@@ -556,19 +724,21 @@ function strongestClaude(catalog) {
556
724
  * the medium disabled, and — when the suggestion cannot — the model that would
557
725
  * run it instead (badge "capable"). Design: the same with "can see".
558
726
  */
559
- export function buildRouteOptions({ request, evaluation, brain = {}, brainInfo = null, catalog = null, routing = {} }) {
727
+ export function buildRouteOptions({ request, evaluation, brain = {}, brainInfo = null, catalog = null, routing = {}, coverage = null }) {
560
728
  const medium = request.needsOutput || null;
561
729
  const dispatchOnly = request.dispatchOnly === true;
562
730
  const visionOnly = request.visionOnly === true;
563
- const ctx = { catalog, brain, brainInfo, needsVision: request.needsVision, needsOutput: medium, visionOnly };
731
+ const ctx = { catalog, brain, brainInfo, needsVision: request.needsVision, needsOutput: medium, visionOnly, coverage };
564
732
  const options = [];
565
733
  const push = (option) => { if (!options.some((o) => sameTarget(o, option))) options.push(option); };
566
734
  const hereTarget = { kind: 'direct', provider: brain.provider, model: brain.model || null, effort: brain.effort || null };
567
735
  request.proposals.forEach((p, index) => {
568
736
  const target = { kind: p.kind, provider: p.provider, model: p.model || (index === 0 ? evaluation.proposal?.model || null : null), effort: p.effort };
569
- if (index === 0 && evaluation.proposal) Object.assign(target, { model: evaluation.proposal.model, effort: evaluation.proposal.effort, provider: evaluation.proposal.provider, kind: evaluation.proposal.kind });
737
+ if (index === 0 && evaluation.proposal) Object.assign(target, evaluation.proposal);
570
738
  push(optionFor(`s${index + 1}`, target, 'suggested', { ...ctx, reason: p.reason }));
571
739
  });
740
+ // The session budget moved the suggestion: the brain's own pick stays on the card, disabled with why.
741
+ if (evaluation.budgetFrom && coverage) push(optionFor('b0', evaluation.budgetFrom, 'suggested', { ...ctx, reason: request.proposals[0]?.reason || null }));
572
742
  if (!dispatchOnly) push(optionFor('here', hereTarget, 'current', ctx));
573
743
  const pref = preferenceTarget(routing.preferences?.[request.taskClass], brain, catalog, { dispatchOnly });
574
744
  if (pref) push(optionFor('r1', pref, 'remembered', ctx));
@@ -627,9 +797,9 @@ export function targetFromAnswer({ card, response = {}, catalog = null, brain =
627
797
  // Image / video creation and design always run on a worker: a "here" pick is a dispatch onto that model.
628
798
  const dispatchOnly = card.dispatchOnly === true;
629
799
  if (option) {
630
- if (option.disabled) throw routeError('ROUTE_INVALID', option.disabledReason || 'That option is not available for this task.');
800
+ if (option.disabled && !option.accountError) throw routeError('ROUTE_INVALID', option.disabledReason || 'That option is not available for this task.');
631
801
  const corrections = [];
632
- const target = effortChecked({ kind: dispatchOnly ? 'dispatch' : option.kind, provider: option.provider, model: option.model || (dispatchOnly && option.kind === 'direct' ? brain.model || null : null), effort: option.effort }, catalog, corrections);
802
+ const target = effortChecked({ kind: dispatchOnly ? 'dispatch' : option.kind, provider: option.provider, model: option.model || (dispatchOnly && option.kind === 'direct' ? brain.model || null : null), effort: option.effort, ...(option.accountId ? { accountId: option.accountChoice || option.accountId, ...(option.accountKind ? { accountKind: dispatchOnly ? 'dispatch' : option.accountKind } : {}) } : {}) }, catalog, corrections);
633
803
  ensureAvailable(target);
634
804
  return { target, optionId, remember, corrections };
635
805
  }
@@ -681,7 +851,7 @@ export function escalationFor({ run, catalog = null, routing = {}, depth = 0 })
681
851
  // just dropped) the retry stands: the launch checks it against a fresh catalog.
682
852
  const retryable = !sight || !catalog || designFit(catalog, { kind: 'dispatch', provider, model: run.model || null }) === 'ok';
683
853
  if (cause === 'transient' && !isDisabled(catalog, { provider, model: run.model }) && retryable) {
684
- return { reason, cause, kind: 'retry', from, to: describeTarget(effortChecked({ kind: 'dispatch', provider, model: run.model || null, effort: run.effort || null }, catalog), { catalog }), depth: depth + 1 };
854
+ return { reason, cause, kind: 'retry', from, to: describeTarget(effortChecked({ kind: 'dispatch', provider, model: run.model || null, effort: run.effort || null, ...(run.accountId ? { accountId: run.accountId } : {}) }, catalog), { catalog }), depth: depth + 1 };
685
855
  }
686
856
  let to = null;
687
857
  const ladder = Array.isArray(routing.ladders?.[provider]) ? routing.ladders[provider] : null;
@@ -697,13 +867,14 @@ export function escalationFor({ run, catalog = null, routing = {}, depth = 0 })
697
867
  }
698
868
  if (!to && !medium) {
699
869
  const pref = routing.preferences?.complex;
700
- if (pref?.provider && !(pref.provider === provider && pref.model === run.model) && !isDisabled(catalog, pref) && canRun({ kind: 'dispatch', provider: pref.provider, model: pref.model || null })) to = { kind: 'dispatch', provider: pref.provider, model: pref.model || null, effort: pref.effort || null };
870
+ if (pref?.provider && !(pref.provider === provider && pref.model === run.model) && !isDisabled(catalog, pref) && canRun({ kind: 'dispatch', provider: pref.provider, model: pref.model || null })) to = { kind: 'dispatch', provider: pref.provider, model: pref.model || null, effort: pref.effort || null, ...(pref.accountId ? { accountId: pref.accountId } : {}) };
701
871
  }
702
872
  if (!to && provider !== 'claude-code' && !medium) {
703
873
  const opus = strongestClaude(catalog);
704
874
  if (opus && canRun({ kind: 'dispatch', provider: 'claude-code', model: opus.id })) to = { kind: 'dispatch', provider: 'claude-code', model: opus.id, effort: 'high' };
705
875
  }
706
876
  if (!to) return null;
877
+ if (to.provider === provider && !to.accountId && run.accountId) to.accountId = run.accountId;
707
878
  return { reason, cause, kind: 'escalate', from, to: describeTarget(effortChecked(to, catalog), { catalog }), depth: depth + 1 };
708
879
  }
709
880
 
@@ -722,6 +893,9 @@ export function escalationFor({ run, catalog = null, routing = {}, depth = 0 })
722
893
  * declineHeld(runId, reason), routed(sessionId, result), routeFailed(sessionId, error) }
723
894
  * @param {object} [deps.clarifier] assistant-clarify.js — { blocking(sessionId, { independent }) }: while the
724
895
  * user has not answered the brain's questions, agent_route answers "clarifying"
896
+ * @param {object} [deps.budget] assistant-budget-planner.js — the session budget: the split is made at the
897
+ * session's first actionable route, a provider out of share is a checkpoint, and
898
+ * with a budget a task moves to a provider with money left (the saved routes yield)
725
899
  */
726
900
  export function createAssistantRouter({
727
901
  catalog,
@@ -732,6 +906,9 @@ export function createAssistantRouter({
732
906
  randomId = () => randomBytes(6).toString('hex'),
733
907
  log = () => {},
734
908
  clarifier = null,
909
+ listAccounts = null,
910
+ accountUsage = null,
911
+ budget = null,
735
912
  } = {}) {
736
913
  if (!catalog) throw new Error('createAssistantRouter requires a catalog');
737
914
  const cards = new Map(); // routeId → card
@@ -749,9 +926,108 @@ export function createAssistantRouter({
749
926
  return normalizeRouteMode(session?.routingMode) || routing().defaultMode;
750
927
  }
751
928
  async function catalogValue() {
929
+ if (accountUsage) await accountUsage.refresh();
752
930
  try { return await catalog.full(); } catch (error) { log('router:catalog-error', error?.message || String(error)); return catalog.peek?.() || { models: {} }; }
753
931
  }
754
932
  function brainInfoFor(brain) { try { return catalog.brainInfo(brain); } catch { return null; } }
933
+ /** Whether the session runs under a budget now (No limit, or no planner: false). */
934
+ function budgeted() { try { return !!budget?.limited?.(); } catch { return false; } }
935
+ function coverageOf(sessionId) { try { return budgeted() ? budget.coverage(sessionId) : null; } catch { return null; } }
936
+ /** The shares a route result carries: { mode, amountUsd, leftUsd, providers: [{ provider, shareUsd, spentUsd, leftUsd }] }, null under No limit. */
937
+ function compactBudget(sessionId) {
938
+ let view = null;
939
+ try { view = budget?.view?.(sessionId) || null; } catch { view = null; }
940
+ if (!view) return null;
941
+ if (view.unlimited) return { mode: 'unlimited' };
942
+ return {
943
+ mode: 'limited', amountUsd: view.amountUsd, spentUsd: view.spentUsd, leftUsd: view.leftUsd, source: view.split?.source || null,
944
+ providers: (view.providers || []).map((row) => ({ provider: row.provider, shareUsd: row.shareUsd, spentUsd: row.spentUsd, leftUsd: row.workerLeftUsd, brain: row.isBrain || undefined })),
945
+ dry: view.dry || [],
946
+ };
947
+ }
948
+ /** A task the budget planner keeps for its next split (the route was approved). */
949
+ function noteBudgetTask(sessionId, request, target) {
950
+ if (!budgeted() || !target?.provider || request?.taskClass === 'chat') return;
951
+ try { budget.noteTask(sessionId, { taskClass: request.taskClass, summary: request.summary || null, provider: target.provider, model: target.model || null }); } catch { /* the split goes on without it */ }
952
+ }
953
+ /**
954
+ * evaluateRoute under the session budget. The split is made at the session's first actionable route
955
+ * (the planner asks Jev, else its fallback). A target whose provider has no share left is a checkpoint:
956
+ * the unspent budget is divided again, then the route is evaluated once more on the new shares (where
957
+ * it may still move to another provider, or be declined). No limit: exactly evaluateRoute.
958
+ */
959
+ async function evaluateWithBudget(sessionId, plan) {
960
+ if (!budgeted() || plan.request.taskClass === 'chat') return evaluateRoute(plan);
961
+ const first = plan.request.proposals[0] || {};
962
+ const task = { taskClass: plan.request.taskClass, summary: plan.request.summary || null, provider: first.provider || null, model: first.model || null };
963
+ try { await budget.ensureSplit(sessionId, { task }); } catch { /* the coverage below works without a split */ }
964
+ let evaluation = evaluateRoute({ ...plan, budget: { coverage: coverageOf(sessionId) } });
965
+ const from = evaluation.budgetFrom;
966
+ if (from) {
967
+ try { await budget.ensureCovered(sessionId, { provider: from.provider, forBrain: from.kind === 'direct', free: targetIsFree(plan.catalog, from), task: { ...task, provider: from.provider, model: from.model || null } }); } catch { /* evaluated on the shares as they are */ }
968
+ evaluation = evaluateRoute({ ...plan, budget: { coverage: coverageOf(sessionId) } });
969
+ }
970
+ evaluation.coverage = coverageOf(sessionId);
971
+ return evaluation;
972
+ }
973
+ /** What the brain is told when no provider can pay for the task. */
974
+ function budgetDeclinedText(sessionId, dry) {
975
+ const view = compactBudget(sessionId);
976
+ const money = view?.mode === 'limited' ? ` (${usdText(view.leftUsd)} of the ${usdText(view.amountUsd)} budget left)` : '';
977
+ return `No provider has budget left for this task: ${dry.length ? `the ${dry.join(', ')} share${dry.length === 1 ? ' is' : 's are'} used up` : 'the session budget is spent'} and no unspent budget could be moved${money}. Do not start or dispatch it. Tell the user in one line that this session's budget is used up for this work, and that they can raise it or switch to No limit (Assistant → Budget).`;
978
+ }
979
+ /** Declined because no provider can pay (every mode, no card). */
980
+ function budgetDeclinedResult({ sessionId, request, mode, cat, brain, brainInfo, evaluation }) {
981
+ const routeId = `route-${randomId()}`;
982
+ const dry = evaluation.dry || [];
983
+ const route = {
984
+ routeId, status: 'declined', sessionId, taskClass: request.taskClass, summary: request.summary, confidence: request.confidence,
985
+ target: null, decidedBy: 'budget', remembered: false, corrections: evaluation.corrections || [], mode, runIds: [], reasonCode: 'budget_exhausted', dry,
986
+ reason: dry.length ? `Budget: the ${dry.join(', ')} share${dry.length === 1 ? ' is' : 's are'} used up.` : "Budget: this session's budget is spent.",
987
+ };
988
+ call('routeEvent', sessionId, 'declined', routeView(route, { catalog: cat, brain, brainInfo }));
989
+ return {
990
+ ok: true, status: 'declined', reason: 'budget_exhausted', dry, routeId, mode, source: 'budget', target: null,
991
+ remembered: false, continuation: false, corrections: evaluation.corrections || [], runIds: [], expiresAt: null,
992
+ budget: compactBudget(sessionId), next: budgetDeclinedText(sessionId, dry),
993
+ };
994
+ }
995
+ function liveAccounts(cat) { return accountUsage ? accountUsage.list() : listAccounts ? listAccounts() : cat?.accounts || {}; }
996
+ function accountTarget(target, brain, cat, { check = true, fallback = null, select = true } = {}) {
997
+ if (!hasRouteAccounts(target?.provider)) return target;
998
+ const accounts = liveAccounts(cat);
999
+ // Older hosts/test catalogs that have no account service retain their dispatch validation.
1000
+ if (!Object.hasOwn(accounts, target.provider)) return target;
1001
+ const workerKey = target.provider === 'codex' ? 'codexAccount' : 'claudeAccount';
1002
+ const defaultId = configStore?.workers?.()?.[workerKey] || 'default';
1003
+ const choice = target.accountId || (target.kind === 'direct' ? brain.accountId || 'default' : (!Object.hasOwn(target, 'accountId') && fallback) || defaultId);
1004
+ const rows = accounts[target.provider] || [];
1005
+ const { row, reason } = select ? chooseUsageAccount(rows, choice, defaultId) : { row: rows.find(a => a.id === choice), reason: null };
1006
+ const accountId = row?.id || choice;
1007
+ const before = rows.find(a => a.id === choice);
1008
+ const usage = row?.usage ? { remainingPercent: row.usage.remainingPercent, limited: row.usage.limited, resetsAt: row.usage.resetsAt } : null;
1009
+ const why = reason === 'accounts_all_limited' ? 'All subscription accounts are limited; selected the soonest reset.' : reason === 'account_limited' ? `Account ${before?.label || choice} out of usage${before?.usage?.resetsAt ? ` (resets ${before.usage.resetsAt})` : ''} → ${row?.label || accountId}, ${usage?.remainingPercent == null ? 'usage unknown' : `${Math.round(usage.remainingPercent)}% left`}.` : reason === 'account_auto' ? `Auto · most usage left → ${row?.label || accountId}, ${usage?.remainingPercent == null ? 'usage unknown; using subscription default' : `${Math.round(usage.remainingPercent)}% left`}.` : null;
1010
+ const account = row ? { id: row.id, label: row.label || row.id, mode: row.mode || null, signedIn: row.signedIn ?? null, identity: row.identity || null, ...(usage ? { usage } : {}), ...(why ? { reason: why } : {}) } : null;
1011
+ const message = !row && choice === 'auto' ? 'Auto requires a signed-in subscription account. Sign in to a Claude or ChatGPT subscription, or pin an API-key account explicitly.' : !row
1012
+ ? `Account ${accountId} was removed. Open Model routes and save a new Account choice for this route.`
1013
+ : row.signedIn === false ? `Account ${row.label || row.id} (${row.id}) is signed out. Sign it in from the ${providerLabel(target.provider)} account menu, or save another Account in Model routes.` : '';
1014
+ const accountError = message ? { code: row ? 'ACCOUNT_SIGNED_OUT' : choice === 'auto' ? 'ACCOUNT_NO_SUBSCRIPTION' : 'ACCOUNT_NOT_FOUND', message } : null;
1015
+ // Provider histories are account-owned. Another account can do the task on
1016
+ // a worker; it cannot resume this account's conversation.
1017
+ const moved = target.kind === 'direct' && accountId !== (brain.accountId || 'default');
1018
+ const accountCorrection = why ? { reason, from: before?.label || choice, to: row?.label || accountId, usage, workerForAccount: moved, message: why + (moved ? ' Runs on a worker because this conversation belongs to another account.' : '') } : null;
1019
+ const enriched = { ...target, accountKind: target.accountKind || target.kind, ...(moved ? { kind: 'dispatch' } : {}), accountId, account, accountError, accountChoice: target.accountChoice || choice, accountCorrection };
1020
+ if (check && accountError) throw routeError(accountError.code, message, 409, { accountId, target: enriched });
1021
+ return enriched;
1022
+ }
1023
+ function accountCorrections(target, base = []) { return target.accountCorrection && !base.some(c => c.reason === target.accountCorrection.reason && c.from === target.accountCorrection.from && c.to === target.accountCorrection.to && c.message === target.accountCorrection.message) ? [...base, target.accountCorrection] : base; }
1024
+ function checkedTarget(target, brain, cat, route = null, fallback = null) {
1025
+ try { return accountTarget(target, brain, cat, { fallback }); }
1026
+ catch (error) {
1027
+ if (route) call('routeEvent', route.sessionId, 'declined', routeView({ ...route, target: error.target, status: 'declined', reason: error.message, reasonCode: error.code }, { catalog: cat, brain }));
1028
+ throw error;
1029
+ }
1030
+ }
755
1031
  function requireSession(sessionId) {
756
1032
  const session = sessionId ? getSession(sessionId) : null;
757
1033
  if (!session) throw routeError('SESSION_NOT_FOUND', `Unknown assistant session ${sessionId}`, 404);
@@ -773,6 +1049,8 @@ export function createAssistantRouter({
773
1049
  reasonCode: source.reasonCode || null,
774
1050
  // Design's no_capable_model: no model can see.
775
1051
  ...(source.needs ? { needs: source.needs } : {}),
1052
+ // budget_exhausted: the providers whose share is used up (the panel names them).
1053
+ ...(Array.isArray(source.dry) ? { dry: [...source.dry] } : {}),
776
1054
  };
777
1055
  }
778
1056
 
@@ -803,19 +1081,24 @@ export function createAssistantRouter({
803
1081
  return call('continueDirect', sessionId, { target, route: source, text: source.summary || '' }) !== false;
804
1082
  }
805
1083
 
806
- function resultFor(status, { routeId, mode, source = null, target = null, card = null, runIds = [], corrections = [], cat = null, brain = {}, brainInfo = null, remembered = false, continuation = false } = {}) {
1084
+ function resultFor(status, { routeId, mode, source = null, target = null, card = null, runIds = [], corrections = [], cat = null, brain = {}, brainInfo = null, remembered = false, continuation = false, sessionId = null } = {}) {
807
1085
  const described = target ? { ...describeTarget(target, { catalog: cat, brain, brainInfo }), continuation } : null;
808
1086
  // A saved route that replaced the brain's pick (never mode) says so first.
809
1087
  const binding = (corrections || []).find((c) => c?.reason === 'remembered_route');
810
1088
  const skipped = (corrections || []).find((c) => c?.reason === 'remembered_disabled');
811
1089
  // Image / video creation: a pick that cannot make the medium was moved onto one that can.
812
1090
  const maker = (corrections || []).find((c) => c?.reason === 'needs_output');
813
- const lead = binding ? `The user's saved route for this task class applies (${binding.to}), not your pick (${binding.from}). `
814
- : skipped ? `The saved route for this task class (${skipped.from}) is on a disabled model, so your pick stands. `
815
- : maker ? `Your pick (${maker.from}) cannot make what this task creates, so it runs on ${maker.to}. ` : '';
1091
+ // The session budget moved the task: the provider it was going to has no share left.
1092
+ const moved = (corrections || []).find((c) => c?.reason === 'budget_share');
1093
+ const lead = moved ? `Budget: ${moved.text.replace(/^budget: /, '')} (the session budget moved it; with a budget the saved routes yield). `
1094
+ : binding ? `The user's saved route for this task class applies (${binding.to}), not your pick (${binding.from}). `
1095
+ : skipped ? `The saved route for this task class (${skipped.from}) is on a disabled model, so your pick stands. `
1096
+ : maker ? `Your pick (${maker.from}) cannot make what this task creates, so it runs on ${maker.to}. ` : '';
1097
+ const shares = sessionId && budgeted() ? compactBudget(sessionId) : null;
816
1098
  return {
817
1099
  ok: true, status, routeId, mode, source, target: described, remembered, continuation,
818
1100
  corrections, runIds, expiresAt: card?.expiresAt ? new Date(card.expiresAt).toISOString() : null,
1101
+ ...(shares ? { budget: shares } : {}),
819
1102
  next: `${lead}${nextText(status, described, { routeId, label: described?.label })}`,
820
1103
  };
821
1104
  }
@@ -868,7 +1151,13 @@ export function createAssistantRouter({
868
1151
  const routeId = `route-${randomId()}`;
869
1152
  const requestId = routeId;
870
1153
  const r = routing();
871
- const { options, defaultOptionId } = buildRouteOptions({ request, evaluation, brain, brainInfo, catalog: cat, routing: r });
1154
+ const { options, defaultOptionId } = buildRouteOptions({ request, evaluation, brain, brainInfo, catalog: cat, routing: r, coverage: evaluation.coverage || null });
1155
+ for (const option of options) {
1156
+ const first = request.proposals[0];
1157
+ const fallback = origin === 'dispatch' && option.badge !== 'remembered' && option.provider === first?.provider ? first.accountId : null;
1158
+ Object.assign(option, accountTarget(option, brain, cat, { check: false, fallback }));
1159
+ if (option.accountError) { option.disabled = true; option.disabledReason = option.accountError.message; }
1160
+ }
872
1161
  const computer = computerConsent({ session, request, options, defaultOptionId, brain, origin });
873
1162
  const expiresAt = now() + r.cardTimeoutMinutes * 60_000;
874
1163
  const pref = r.preferences?.[request.taskClass];
@@ -1005,21 +1294,30 @@ export function createAssistantRouter({
1005
1294
  const request = normalizeRouteRequest(body, { brain });
1006
1295
  const mode = modeFor(sessionId);
1007
1296
  const r = routing();
1008
- const evaluation = evaluateRoute({ mode, routing: r, request, brain, brainInfo, catalog: cat });
1297
+ const evaluation = await evaluateWithBudget(sessionId, { mode, routing: r, request, brain, brainInfo, catalog: cat });
1298
+ if (evaluation.unavailable === 'budget_exhausted') return budgetDeclinedResult({ sessionId, request, mode, cat, brain, brainInfo, evaluation });
1009
1299
  if (evaluation.unavailable) return unavailableResult({ sessionId, request, mode, cat, brain, brainInfo });
1010
1300
  if (!evaluation.ask) {
1011
1301
  const routeId = `route-${randomId()}`;
1012
- const target = evaluation.auto;
1013
- const decidedBy = evaluation.source === 'remembered' ? 'remembered' : 'brain';
1302
+ let target = evaluation.auto;
1303
+ const checked = accountTarget(target, brain, cat, { check: false, select: request.taskClass !== 'chat' });
1304
+ if (checked.accountError) {
1305
+ const failed = { routeId, sessionId, taskClass: request.taskClass, summary: request.summary, target: checked, status: 'declined', reason: checked.accountError.message, reasonCode: checked.accountError.code, mode };
1306
+ call('routeEvent', sessionId, 'declined', routeView(failed, { catalog: cat, brain, brainInfo }));
1307
+ return { ok: false, status: 'declined', routeId, mode, code: checked.accountError.code, target: describeTarget(checked, { catalog: cat, brain }), next: checked.accountError.message };
1308
+ }
1309
+ target = checked;
1310
+ const decidedBy = evaluation.source === 'remembered' ? 'remembered' : evaluation.source === 'budget' ? 'budget' : 'brain';
1014
1311
  const route = {
1015
1312
  routeId, status: 'approved', sessionId, taskClass: request.taskClass, summary: request.summary, confidence: request.confidence,
1016
- target, decidedBy, remembered: evaluation.source === 'remembered', corrections: evaluation.corrections, mode,
1313
+ target, decidedBy, remembered: evaluation.source === 'remembered', corrections: accountCorrections(target, evaluation.corrections), mode,
1017
1314
  reason: request.proposals[0]?.reason || null, runIds: [], expiresAt: now() + DECIDED_TTL_MS,
1018
1315
  };
1019
1316
  decided.set(routeId, route);
1317
+ noteBudgetTask(sessionId, request, target);
1020
1318
  if (request.taskClass !== 'chat') call('routeEvent', sessionId, 'auto', routeView(route, { catalog: cat, brain, brainInfo }));
1021
1319
  const continuation = origin === 'agent_route' ? maybeContinuation(sessionId, target, brain, route) : false;
1022
- return resultFor('approved', { routeId, mode, source: evaluation.source, target, corrections: evaluation.corrections, cat, brain, brainInfo, remembered: route.remembered, continuation });
1320
+ return resultFor('approved', { routeId, mode, source: evaluation.source, target, corrections: route.corrections, cat, brain, brainInfo, remembered: route.remembered, continuation, sessionId });
1023
1321
  }
1024
1322
  // The session as it is now (the catalog was awaited since): its computer-use answer decides what the card says.
1025
1323
  const card = createCard({ sessionId, request, evaluation, brain, brainInfo, cat, mode, origin, session: getSession(sessionId) || session });
@@ -1035,7 +1333,7 @@ export function createAssistantRouter({
1035
1333
  const outcome = card.settled || (wait > 0 ? await waitFor(card, wait, signal) : null);
1036
1334
  if (!outcome) {
1037
1335
  card.mailboxOnDecision = true;
1038
- return resultFor('pending', { routeId: card.routeId, mode, card, cat, brain, brainInfo });
1336
+ return resultFor('pending', { routeId: card.routeId, mode, card, cat, brain, brainInfo, sessionId });
1039
1337
  }
1040
1338
  if (outcome.result) return outcome.result;
1041
1339
  const closed = resultFor(outcome.status || 'cancelled', { routeId: card.routeId, mode, card, cat, brain, brainInfo });
@@ -1085,7 +1383,18 @@ export function createAssistantRouter({
1085
1383
  closeCard(card);
1086
1384
  return result;
1087
1385
  }
1088
- const { target, optionId, remember, corrections = [] } = parsed;
1386
+ const { optionId, remember } = parsed;
1387
+ const target = checkedTarget(parsed.target, brain, cat);
1388
+ const corrections = accountCorrections(target, parsed.corrections || []);
1389
+ // The session budget must be able to pay for the pick: a provider with no share left divides the
1390
+ // unspent budget again first; still none, and the card stays open with the reason.
1391
+ if (budgeted() && !budgetCovers(coverageOf(card.sessionId), cat, target)) {
1392
+ try { await budget.ensureCovered(card.sessionId, { provider: target.provider, forBrain: target.kind === 'direct', free: targetIsFree(cat, target) }); } catch { /* decided on the shares as they are */ }
1393
+ if (cards.get(routeId) !== card || card.status !== 'pending') openCard(routeId, { sessionId });
1394
+ if (!budgetCovers(coverageOf(card.sessionId), cat, target)) {
1395
+ throw routeError('BUDGET_SHARE_EXHAUSTED', `Budget: the ${providerLabel(target.provider)} share of this session's budget is used up. Pick another option (or a free model), or raise the budget.`, 409, { provider: target.provider });
1396
+ }
1397
+ }
1089
1398
  // Every held run must be able to start on the pick (the dispatcher's checkHeld: a design run's
1090
1399
  // sight and browser settings on that provider). A pick one would refuse is not approved, saved
1091
1400
  // or reported started: the card stays open with the reason.
@@ -1111,7 +1420,7 @@ export function createAssistantRouter({
1111
1420
  if (remember && card.taskClass !== 'chat' && configStore?.setPreference) {
1112
1421
  try {
1113
1422
  const described = describeTarget(target, { catalog: cat, brain, brainInfo });
1114
- configStore.setPreference(card.taskClass, { ...target, label: described?.label || null }, { by: 'user' });
1423
+ configStore.setPreference(card.taskClass, { ...target, kind: target.accountKind || target.kind, accountId: target.accountChoice || target.accountId, label: described?.label || null }, { by: 'user' });
1115
1424
  remembered = true;
1116
1425
  } catch (error) { log('router:remember-error', error?.message || String(error)); }
1117
1426
  }
@@ -1121,6 +1430,7 @@ export function createAssistantRouter({
1121
1430
  reason: card.options.find((o) => o.id === optionId)?.reason || null, alternatives: card.alternatives, expiresAt: now() + DECIDED_TTL_MS,
1122
1431
  };
1123
1432
  decided.set(routeId, route);
1433
+ noteBudgetTask(card.sessionId, { taskClass: card.taskClass, summary: card.summary }, target);
1124
1434
  const heldRuns = [...card.runIds];
1125
1435
  // A remote session: the held run this card was for is its one dispatch.
1126
1436
  if (heldRuns.length && session.remote === true) route.consumed = true;
@@ -1137,7 +1447,7 @@ export function createAssistantRouter({
1137
1447
  // `authority` is passed on untouched and never kept: the runtime alone can tell whose it is.
1138
1448
  call('computerApproved', card.sessionId, { routeId, origin, viaMailbox, authority });
1139
1449
  }
1140
- const result = resultFor('approved', { routeId, mode: card.mode, source: 'user', target, corrections, cat, brain, brainInfo, remembered, runIds: heldRuns, continuation });
1450
+ const result = resultFor('approved', { routeId, mode: card.mode, source: 'user', target, corrections, cat, brain, brainInfo, remembered, runIds: heldRuns, continuation, sessionId: card.sessionId });
1141
1451
  if (viaMailbox) {
1142
1452
  call('mailbox', card.sessionId, { kind: 'route_decided', route: view, runIds: heldRuns, text: heldRuns.length ? `The user picked ${view.target?.label || 'a model'}; the held run${heldRuns.length === 1 ? '' : 's'} started.` : `The user picked ${view.target?.label || 'a model'}. ${result.next}` });
1143
1453
  }
@@ -1173,10 +1483,27 @@ export function createAssistantRouter({
1173
1483
  // A remote session's route is claimed here, so a second dispatch cannot take it meanwhile.
1174
1484
  // `claimed` tells the dispatcher to give it back (releaseRoute) if it refuses the launch
1175
1485
  // before a run exists: only the run it starts uses the route up.
1486
+ const t = checkedTarget(approved.target, brain, cat, approved);
1487
+ approved.target = t;
1488
+ approved.corrections = accountCorrections(t, approved.corrections);
1489
+ // The approved provider may have run out of share since the approval: the unspent budget is
1490
+ // divided again first, then the dispatch moves the way agent_route would, or is refused.
1491
+ let runTarget = t;
1492
+ let budgetCorrection = null;
1493
+ if (budgeted() && !budgetCovers(coverageOf(sessionId), cat, t)) {
1494
+ try { await budget.ensureCovered(sessionId, { provider: t.provider, forBrain: false, free: targetIsFree(cat, t), task: { taskClass: approved.taskClass, summary: approved.summary || null, provider: t.provider, model: t.model || null } }); } catch { /* decided on the shares as they are */ }
1495
+ const request = normalizeRouteRequest({ task_class: approved.taskClass, summary: approved.summary || '', confidence: 1, needs_vision: !!spec.usesComputer, proposals: [{ kind: 'dispatch', provider: t.provider, model: t.model || null, effort: t.effort || null }] }, { brain });
1496
+ const moved = budgetMove({ target: { ...t, kind: 'dispatch' }, request, catalog: cat, brain, brainInfo, routing: r, coverage: coverageOf(sessionId) });
1497
+ if (moved.declined) {
1498
+ throw routeError('BUDGET_SHARE_EXHAUSTED', `Route ${routeId} was approved for ${t.provider}${t.model ? `/${t.model}` : ''}, but ${t.provider} has no share of this session's budget left, and no other provider can take this task now (out of share: ${(moved.dry || []).join(', ') || 'all'}). Do not dispatch it again on this route: call agent_route again for this task, or tell the user the budget is used up (Assistant → Budget).`, 409, { routeId, provider: t.provider, dry: moved.dry || [] });
1499
+ }
1500
+ if (moved.correction) { runTarget = moved.target; budgetCorrection = moved.correction; }
1501
+ }
1176
1502
  if (remote) approved.consumed = true;
1177
- const t = approved.target;
1178
- const applied = { ...spec, provider: t.provider, model: t.model || null, effort: t.effort || spec.effort || null };
1179
- return { action: 'start', spec: applied, route: { ...routeMeta(approved, cat, brain, brainInfo), requested: { provider: spec.provider || null, model: spec.model || null, effort: spec.effort || null } }, ...(remote ? { claimed: true } : {}) };
1503
+ const applied = { ...spec, provider: runTarget.provider, model: runTarget.model || null, effort: runTarget.effort || spec.effort || null, ...(runTarget.accountId ? { accountId: runTarget.accountId, codexAccountId: null, claudeAccountId: null } : runTarget !== t ? { accountId: null, codexAccountId: null, claudeAccountId: null } : {}) };
1504
+ const meta = routeMeta(approved, cat, brain, brainInfo);
1505
+ const route = budgetCorrection ? { ...meta, target: describeTarget(runTarget, { catalog: cat, brain, brainInfo }), corrections: [...(meta.corrections || []), budgetCorrection] } : meta;
1506
+ return { action: 'start', spec: applied, route: { ...route, requested: { provider: spec.provider || null, model: spec.model || null, effort: spec.effort || null } }, ...(remote ? { claimed: true } : {}) };
1180
1507
  }
1181
1508
  const pending = cards.get(routeId);
1182
1509
  if (pending && pending.sessionId === sessionId && pending.status === 'pending' && !(remote && pending.runIds.size)) {
@@ -1186,17 +1513,21 @@ export function createAssistantRouter({
1186
1513
  const request = normalizeRouteRequest({
1187
1514
  task_class: spec.taskClass || 'code', confidence: spec.confidence ?? null, summary: spec.title || clip(spec.task, 200),
1188
1515
  needs_vision: !!spec.usesComputer,
1189
- proposals: [{ kind: 'dispatch', provider: spec.provider || brain.provider, model: spec.model || null, effort: spec.effort || null }],
1516
+ proposals: [{ kind: 'dispatch', provider: spec.provider || brain.provider, model: spec.model || null, effort: spec.effort || null, accountId: spec.accountId || spec.codexAccountId || spec.claudeAccountId || null }],
1190
1517
  }, { brain });
1191
1518
  if (!spec.provider) request.proposals[0].provider = brain.provider || 'claude-code';
1192
- const evaluation = evaluateRoute({ mode, routing: r, request, brain, brainInfo, catalog: cat });
1519
+ const evaluation = await evaluateWithBudget(sessionId, { mode, routing: r, request, brain, brainInfo, catalog: cat });
1520
+ if (evaluation.unavailable === 'budget_exhausted') {
1521
+ throw routeError('BUDGET_SHARE_EXHAUSTED', budgetDeclinedText(sessionId, evaluation.dry || []), 409, { dry: evaluation.dry || [], taskClass: request.taskClass, reason: 'budget_exhausted' });
1522
+ }
1193
1523
  if (evaluation.unavailable) throw routeError('NO_CAPABLE_MODEL', noMakerText(request), 409, { requires: request.needsOutput, ...(request.needsOutput ? {} : { needs: 'vision' }), taskClass: request.taskClass });
1194
1524
  if (!evaluation.ask) {
1195
- const t = evaluation.auto;
1196
- const applied = { ...spec, provider: t.provider, model: t.model || null, effort: t.effort || spec.effort || null };
1525
+ const t = checkedTarget(evaluation.auto, brain, cat);
1526
+ noteBudgetTask(sessionId, request, t);
1527
+ const applied = { ...spec, provider: t.provider, model: t.model || null, effort: t.effort || spec.effort || null, ...(t.accountId ? { accountId: t.accountId, codexAccountId: null, claudeAccountId: null } : {}) };
1197
1528
  const route = {
1198
- routeId: `route-${randomId()}`, status: 'auto', decidedBy: evaluation.source === 'remembered' ? 'remembered' : 'brain', mode,
1199
- taskClass: request.taskClass, confidence: request.confidence, target: t, corrections: evaluation.corrections,
1529
+ routeId: `route-${randomId()}`, status: 'auto', decidedBy: evaluation.source === 'remembered' ? 'remembered' : evaluation.source === 'budget' ? 'budget' : 'brain', mode,
1530
+ taskClass: request.taskClass, confidence: request.confidence, target: t, corrections: accountCorrections(t, evaluation.corrections),
1200
1531
  remembered: evaluation.source === 'remembered',
1201
1532
  };
1202
1533
  return { action: 'start', spec: applied, route: { ...routeMeta(route, cat, brain, brainInfo), requested: { provider: spec.provider || null, model: spec.model || null, effort: spec.effort || null } } };
@@ -1298,7 +1629,7 @@ export function createAssistantRouter({
1298
1629
  const r = routing();
1299
1630
  const mode = modeFor(sessionId);
1300
1631
  const prefs = Object.entries(r.preferences || {}).filter(([, p]) => p?.provider)
1301
- .map(([cls, p]) => `${cls} → ${p.kind === 'direct' ? 'here' : p.provider}${p.model ? `/${p.model}` : ''}${p.effort ? ` (${p.effort})` : ''}`);
1632
+ .map(([cls, p]) => `${cls} → ${p.kind === 'direct' ? 'here' : p.provider}${p.model ? `/${p.model}` : ''}${p.effort ? ` (${p.effort})` : ''}${hasRouteAccounts(p.provider) ? ` as ${routeAccountText(p, { accounts: liveAccounts(catalog.peek?.()), brain: getSession(sessionId)?.brain, workers: { codex: configStore?.workers?.()?.codexAccount, 'claude-code': configStore?.workers?.()?.claudeAccount } })}` : ''}`);
1302
1633
  const modeLine = mode === 'always-ask' ? 'always ask (call agent_route before every actionable task; the user picks the model)'
1303
1634
  : mode === 'never' ? 'never ask (agent_route approves at once; the saved routes below are binding and agent_route applies them over your pick, so classify the task correctly)'
1304
1635
  : `ask when unsure (agent_route asks the user below ${Math.round(r.askBelow * 100)}% confidence or for unknown/missing models)`;