@maudecode/talaria-web 1.16.1 → 1.17.2

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 (328) hide show
  1. package/_release.json +4 -4
  2. package/dist/api/automation-raw.js +6 -3
  3. package/dist/api/automation-raw.js.map +1 -1
  4. package/dist/api/automation-router.d.ts +137 -9
  5. package/dist/api/automation-router.js +9 -6
  6. package/dist/api/automation-router.js.map +1 -1
  7. package/dist/api/chat-router.d.ts +51 -0
  8. package/dist/api/chat-router.js +29 -12
  9. package/dist/api/chat-router.js.map +1 -1
  10. package/dist/api/git-router.d.ts +12 -1
  11. package/dist/api/git-router.js +17 -7
  12. package/dist/api/git-router.js.map +1 -1
  13. package/dist/api/raw-routes.d.ts +6 -0
  14. package/dist/api/raw-routes.js +30 -0
  15. package/dist/api/raw-routes.js.map +1 -1
  16. package/dist/api/sessions-router.d.ts +260 -0
  17. package/dist/api/sessions-router.js +30 -2
  18. package/dist/api/sessions-router.js.map +1 -1
  19. package/dist/api/settings-router.d.ts +101 -2
  20. package/dist/api/settings-router.js +7 -2
  21. package/dist/api/settings-router.js.map +1 -1
  22. package/dist/api/sse-routes.d.ts +11 -3
  23. package/dist/api/sse-routes.js +144 -64
  24. package/dist/api/sse-routes.js.map +1 -1
  25. package/dist/api/tools-raw.js +6 -2
  26. package/dist/api/tools-raw.js.map +1 -1
  27. package/dist/api/tools-router.d.ts +73 -8
  28. package/dist/api/tools-router.js +9 -7
  29. package/dist/api/tools-router.js.map +1 -1
  30. package/dist/app.d.ts +0 -9377
  31. package/dist/app.js +7 -2
  32. package/dist/app.js.map +1 -1
  33. package/dist/auth/gate.js +3 -0
  34. package/dist/auth/gate.js.map +1 -1
  35. package/dist/config/agent-config.d.ts +2 -0
  36. package/dist/config/agent-config.js +9 -0
  37. package/dist/config/agent-config.js.map +1 -1
  38. package/dist/http/context.d.ts +3 -0
  39. package/dist/http/context.js +10 -4
  40. package/dist/http/context.js.map +1 -1
  41. package/dist/onboarding.js +4 -0
  42. package/dist/onboarding.js.map +1 -1
  43. package/dist/providers/catalog.d.ts +96 -7
  44. package/dist/providers/catalog.js +434 -113
  45. package/dist/providers/catalog.js.map +1 -1
  46. package/dist/redact.d.ts +1 -0
  47. package/dist/redact.js +27 -4
  48. package/dist/redact.js.map +1 -1
  49. package/dist/runtime.js +27 -9
  50. package/dist/runtime.js.map +1 -1
  51. package/dist/sessions/anchor.d.ts +2 -0
  52. package/dist/sessions/anchor.js +8 -3
  53. package/dist/sessions/anchor.js.map +1 -1
  54. package/dist/sessions/claude-code.d.ts +19 -0
  55. package/dist/sessions/claude-code.js +250 -0
  56. package/dist/sessions/claude-code.js.map +1 -0
  57. package/dist/sessions/completions.d.ts +13 -1
  58. package/dist/sessions/completions.js +63 -11
  59. package/dist/sessions/completions.js.map +1 -1
  60. package/dist/sessions/compress.d.ts +14 -1
  61. package/dist/sessions/compress.js +52 -11
  62. package/dist/sessions/compress.js.map +1 -1
  63. package/dist/sessions/journal.d.ts +8 -0
  64. package/dist/sessions/journal.js +44 -10
  65. package/dist/sessions/journal.js.map +1 -1
  66. package/dist/sessions/list.d.ts +6 -2
  67. package/dist/sessions/list.js +33 -10
  68. package/dist/sessions/list.js.map +1 -1
  69. package/dist/sessions/merge.d.ts +52 -2
  70. package/dist/sessions/merge.js +213 -10
  71. package/dist/sessions/merge.js.map +1 -1
  72. package/dist/sessions/service.d.ts +44 -2
  73. package/dist/sessions/service.js +251 -57
  74. package/dist/sessions/service.js.map +1 -1
  75. package/dist/sessions/session.d.ts +11 -1
  76. package/dist/sessions/session.js +23 -2
  77. package/dist/sessions/session.js.map +1 -1
  78. package/dist/sessions/state-db.d.ts +26 -4
  79. package/dist/sessions/state-db.js +126 -8
  80. package/dist/sessions/state-db.js.map +1 -1
  81. package/dist/sessions/store.js +5 -0
  82. package/dist/sessions/store.js.map +1 -1
  83. package/dist/sessions/streams.d.ts +10 -0
  84. package/dist/sessions/streams.js +14 -3
  85. package/dist/sessions/streams.js.map +1 -1
  86. package/dist/sessions/todo.d.ts +6 -1
  87. package/dist/sessions/todo.js +11 -4
  88. package/dist/sessions/todo.js.map +1 -1
  89. package/dist/sessions/tool-display.d.ts +12 -1
  90. package/dist/sessions/tool-display.js +76 -1
  91. package/dist/sessions/tool-display.js.map +1 -1
  92. package/dist/sessions/turn.d.ts +30 -9
  93. package/dist/sessions/turn.js +247 -78
  94. package/dist/sessions/turn.js.map +1 -1
  95. package/dist/settings.d.ts +20 -2
  96. package/dist/settings.js +40 -2
  97. package/dist/settings.js.map +1 -1
  98. package/dist/sidecar/fake.d.ts +2 -2
  99. package/dist/sidecar/fake.js +5 -2
  100. package/dist/sidecar/fake.js.map +1 -1
  101. package/dist/spa.js +2 -1
  102. package/dist/spa.js.map +1 -1
  103. package/dist/test/harness.js +2 -0
  104. package/dist/test/harness.js.map +1 -1
  105. package/dist/tools/insights.d.ts +8 -2
  106. package/dist/tools/insights.js +18 -2
  107. package/dist/tools/insights.js.map +1 -1
  108. package/dist/tools/kanban.d.ts +41 -1
  109. package/dist/tools/kanban.js +113 -2
  110. package/dist/tools/kanban.js.map +1 -1
  111. package/dist/tools/mcp.d.ts +2 -1
  112. package/dist/tools/mcp.js +11 -5
  113. package/dist/tools/mcp.js.map +1 -1
  114. package/dist/tools/terminal.d.ts +1 -0
  115. package/dist/tools/terminal.js +1 -1
  116. package/dist/tools/terminal.js.map +1 -1
  117. package/dist/tools/updates.d.ts +7 -0
  118. package/dist/tools/updates.js +74 -7
  119. package/dist/tools/updates.js.map +1 -1
  120. package/dist/workspace/fs.d.ts +7 -3
  121. package/dist/workspace/fs.js +5 -1
  122. package/dist/workspace/fs.js.map +1 -1
  123. package/dist/workspace/media.js +2 -0
  124. package/dist/workspace/media.js.map +1 -1
  125. package/dist/workspace/preview.d.ts +28 -0
  126. package/dist/workspace/preview.js +110 -0
  127. package/dist/workspace/preview.js.map +1 -0
  128. package/dist/workspace/rollback.d.ts +5 -4
  129. package/dist/workspace/rollback.js +12 -9
  130. package/dist/workspace/rollback.js.map +1 -1
  131. package/dist/workspace/upload.d.ts +2 -0
  132. package/dist/workspace/upload.js +4 -1
  133. package/dist/workspace/upload.js.map +1 -1
  134. package/package.json +2 -2
  135. package/sidecar/scripts/replay_sidecar.py +1 -1
  136. package/sidecar/talaria_sidecar/__init__.py +1 -1
  137. package/sidecar/talaria_sidecar/__main__.py +7 -1
  138. package/sidecar/talaria_sidecar/home.py +39 -0
  139. package/sidecar/talaria_sidecar/methods/chat.py +422 -38
  140. package/sidecar/talaria_sidecar/methods/commands.py +15 -2
  141. package/sidecar/talaria_sidecar/methods/cron.py +10 -6
  142. package/sidecar/talaria_sidecar/methods/kanban.py +115 -15
  143. package/sidecar/talaria_sidecar/methods/process.py +60 -1
  144. package/sidecar/talaria_sidecar/methods/profiles.py +7 -1
  145. package/sidecar/talaria_sidecar/methods/runtime.py +3 -0
  146. package/sidecar/talaria_sidecar/methods/text.py +17 -1
  147. package/sidecar/talaria_sidecar/methods/usage.py +257 -4
  148. package/static/dist/FILES.txt +126 -125
  149. package/static/dist/assets/AppShell-fb2td9zi.js +1 -0
  150. package/static/dist/assets/ChatPage-cj96kiy9.js +25 -0
  151. package/static/dist/assets/Dialog-kxcg7ard.js +1 -0
  152. package/static/dist/assets/ExtensionRoute-jf7h0754.js +1 -0
  153. package/static/dist/assets/HubRoute-gmanpd5y.js +2 -0
  154. package/static/dist/assets/{Markdown-jqrx5es0.js → Markdown-mdwlvj3w.js} +29 -29
  155. package/static/dist/assets/RightPanel-ftvrjlcp.js +1 -0
  156. package/static/dist/assets/Select-bilaypej.js +1 -0
  157. package/static/dist/assets/SessionListPanel-og8n36qs.js +1 -0
  158. package/static/dist/assets/SettingsLayout-jonjefmj.js +1 -0
  159. package/static/dist/assets/States-nmejrwrm.js +1 -0
  160. package/static/dist/assets/Toaster-nnl8zkt9.js +1 -0
  161. package/static/dist/assets/_app-gdidzd94.js +1 -0
  162. package/static/dist/assets/_app.ext._extensionId-opyh9y0v.js +1 -0
  163. package/static/dist/assets/_app.index-hbarzbjx.js +1 -0
  164. package/static/dist/assets/_app.insights-luywlarz.js +1 -0
  165. package/static/dist/assets/_app.kanban-kincnbr8.js +1 -0
  166. package/static/dist/assets/_app.logs-oi2gr3gs.js +1 -0
  167. package/static/dist/assets/_app.memory-b6jfivt6.js +1 -0
  168. package/static/dist/assets/_app.profiles-lpvixmqp.js +1 -0
  169. package/static/dist/assets/{_app.session._sessionId-hh27wwu2.js → _app.session._sessionId-bfd4h3id.js} +1 -1
  170. package/static/dist/assets/_app.settings-vowx3iq0.js +1 -0
  171. package/static/dist/assets/_app.settings._section-cnntg1tu.js +3 -0
  172. package/static/dist/assets/_app.skills-fb57lpau.js +1 -0
  173. package/static/dist/assets/_app.tasks-eszpx1q7.js +2 -0
  174. package/static/dist/assets/_app.todos-nr05ac6w.js +1 -0
  175. package/static/dist/assets/_app.workspaces-d2luomu6.js +1 -0
  176. package/static/dist/assets/{abnfDiagram-VCTEODGH-oity9y4o.js → abnfDiagram-VCTEODGH-b2x1lqot.js} +1 -1
  177. package/static/dist/assets/architecture-7GRP2DOG-hd0o7bde.js +1 -0
  178. package/static/dist/assets/{architectureDiagram-5GKGNRK7-ceaepnyr.js → architectureDiagram-5GKGNRK7-h38o5l7b.js} +1 -1
  179. package/static/dist/assets/{back-eu64w89w.js → back-bemaer09.js} +1 -1
  180. package/static/dist/assets/{blockDiagram-I7D4REHJ-n79st4pj.js → blockDiagram-I7D4REHJ-bqquxv2u.js} +1 -1
  181. package/static/dist/assets/{c4Diagram-7LVT6UL2-mi73akzk.js → c4Diagram-7LVT6UL2-f3fb8w6z.js} +1 -1
  182. package/static/dist/assets/{cancel-mzvkdjg8.js → cancel-drd0g6d9.js} +1 -1
  183. package/static/dist/assets/channel-cpdgf867.js +1 -0
  184. package/static/dist/assets/{chunk-4HAMMTFA-laat0g1q.js → chunk-4HAMMTFA-bzfyyzp3.js} +1 -1
  185. package/static/dist/assets/{chunk-75Z2AOVW-0psy1tr2.js → chunk-75Z2AOVW-ly99rfx5.js} +1 -1
  186. package/static/dist/assets/{chunk-DU6HZSFF-lw8uox8x.js → chunk-DU6HZSFF-bg6ecfqf.js} +3 -3
  187. package/static/dist/assets/{chunk-F27PBJKO-erassib3.js → chunk-F27PBJKO-e8k7mmqw.js} +1 -1
  188. package/static/dist/assets/{chunk-GMAD6QVW-fz7rx619.js → chunk-GMAD6QVW-cmalnoaj.js} +1 -1
  189. package/static/dist/assets/{chunk-GVQU2GXP-gdb0epb2.js → chunk-GVQU2GXP-gmif5o01.js} +1 -1
  190. package/static/dist/assets/{chunk-IMKFNOWR-ea8t26qw.js → chunk-IMKFNOWR-f1nwwh6u.js} +1 -1
  191. package/static/dist/assets/{chunk-L3NEJ4N5-mb1gu9kz.js → chunk-L3NEJ4N5-oktfh5v7.js} +1 -1
  192. package/static/dist/assets/{chunk-OSK3NFVY-eovmccvy.js → chunk-OSK3NFVY-b1sdx96z.js} +1 -1
  193. package/static/dist/assets/{chunk-P2QGCYS3-bo55zjo5.js → chunk-P2QGCYS3-fhu4nzxt.js} +1 -1
  194. package/static/dist/assets/{chunk-POPQ4Y6H-ja7ohe4h.js → chunk-POPQ4Y6H-em31rnef.js} +1 -1
  195. package/static/dist/assets/{chunk-PWAF6VOD-e30g9nus.js → chunk-PWAF6VOD-bxhhyftw.js} +1 -1
  196. package/static/dist/assets/{chunk-SHT3W25Y-gbb798f7.js → chunk-SHT3W25Y-fmrvwcav.js} +1 -1
  197. package/static/dist/assets/{chunk-SVP7TREG-h2iw8urm.js → chunk-SVP7TREG-lymukl2j.js} +1 -1
  198. package/static/dist/assets/{chunk-TICWLB2K-fd2cottz.js → chunk-TICWLB2K-lmza6k23.js} +1 -1
  199. package/static/dist/assets/classDiagram-ZZMXUADV-ko09sguk.js +1 -0
  200. package/static/dist/assets/classDiagram-v2-VYDZK3BY-ko09sguk.js +1 -0
  201. package/static/dist/assets/{copied-h65fc3cx.js → copied-ima3pysb.js} +1 -1
  202. package/static/dist/assets/copy-fw5b1pc6.js +1 -0
  203. package/static/dist/assets/{cynefin-OW5HDTMX-lbltyl5j.js → cynefin-OW5HDTMX-czlhbyhs.js} +1 -1
  204. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-lxhsqq4a.js → cynefinDiagram-5FMLGOSQ-gq7mvswb.js} +1 -1
  205. package/static/dist/assets/{dagre-GXQ25YYZ-gn4nnaln.js → dagre-GXQ25YYZ-cqdbf7dr.js} +1 -1
  206. package/static/dist/assets/{diagram-S7CK7UJ4-pa3ij6q7.js → diagram-S7CK7UJ4-o6arxa58.js} +1 -1
  207. package/static/dist/assets/{diagram-UQ7AKVKN-zbipyz57.js → diagram-UQ7AKVKN-nhnd68tz.js} +1 -1
  208. package/static/dist/assets/{diagram-VSXAHHWV-d1kbpzgf.js → diagram-VSXAHHWV-dk2sclzn.js} +1 -1
  209. package/static/dist/assets/{diagram-VX7I27RA-i3ty14xl.js → diagram-VX7I27RA-n3k5nyx3.js} +1 -1
  210. package/static/dist/assets/{diagram-Z3DM3KII-m8xefqa7.js → diagram-Z3DM3KII-m6q9cwdh.js} +1 -1
  211. package/static/dist/assets/{ebnfDiagram-PWID7BFC-nz7wj05l.js → ebnfDiagram-PWID7BFC-lsa5641p.js} +1 -1
  212. package/static/dist/assets/endpoints-d33siokx.js +64 -0
  213. package/static/dist/assets/{erDiagram-RLTQ6QDP-fa41ib83.js → erDiagram-RLTQ6QDP-fo2ckbe6.js} +1 -1
  214. package/static/dist/assets/eventmodeling-NTZA5JFV-emctlo64.js +1 -0
  215. package/static/dist/assets/flowDiagram-HODETNUW-bfi3moqa.js +1 -0
  216. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-n7zq01qu.js → ganttDiagram-EL5Y4UJY-nzqz6x9g.js} +1 -1
  217. package/static/dist/assets/{gitGraph-4MIJSDKK-c89emb07.js → gitGraph-4MIJSDKK-gukvcxe6.js} +1 -1
  218. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-cz7cgr6w.js → gitGraphDiagram-WWUBYQGX-lqhdp8f0.js} +1 -1
  219. package/static/dist/assets/{highlighted-body-KPVGNVTW-d9bapmni.js → highlighted-body-KPVGNVTW-igw6cp39.js} +1 -1
  220. package/static/dist/assets/index-cxq58xb1.js +11 -0
  221. package/static/dist/assets/index-hgtx08n5.css +2 -0
  222. package/static/dist/assets/{info-A6RAGUB7-ndddhhhh.js → info-A6RAGUB7-gvu7a93l.js} +1 -1
  223. package/static/dist/assets/{infoDiagram-27XIBGKW-ccxigcpf.js → infoDiagram-27XIBGKW-m8t8g6w1.js} +1 -1
  224. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-le0z9vtc.js → ishikawaDiagram-5VMMS53U-l07vkbgj.js} +1 -1
  225. package/static/dist/assets/{journeyDiagram-3NMN7TZE-ivsmb26o.js → journeyDiagram-3NMN7TZE-hybkgj8l.js} +1 -1
  226. package/static/dist/assets/{kanban-definition-UXKFOSKX-h6n2awt0.js → kanban-definition-UXKFOSKX-by13uvtn.js} +1 -1
  227. package/static/dist/assets/lazyRouteComponent-af0lvgjx.js +1 -0
  228. package/static/dist/assets/{line-dmzlfe0u.js → line-osxq6fq7.js} +1 -1
  229. package/static/dist/assets/link-jyxicdhg.js +1 -0
  230. package/static/dist/assets/{login-kjhasvp8.js → login-ec21t39x.js} +1 -1
  231. package/static/dist/assets/mermaid-HWGCJPDP-gfl8z3ep.js +1 -0
  232. package/static/dist/assets/{mermaid-parser.core-gwyd5bnq.js → mermaid-parser.core-i91oxmou.js} +3 -3
  233. package/static/dist/assets/{mindmap-definition-YA3MSWOX-l68gg5h7.js → mindmap-definition-YA3MSWOX-m82mx8ww.js} +1 -1
  234. package/static/dist/assets/onboarding-btnltvyz.js +1 -0
  235. package/static/dist/assets/onboarding_model_label-dp7xffoh.js +1 -0
  236. package/static/dist/assets/{packet-AYTQ26CC-gpv06065.js → packet-AYTQ26CC-nbtgge2o.js} +1 -1
  237. package/static/dist/assets/{pegDiagram-XKGWAZYB-e52vs23u.js → pegDiagram-XKGWAZYB-f1zxzga1.js} +1 -1
  238. package/static/dist/assets/{pie-WAS4IAKB-l6m7nz5s.js → pie-WAS4IAKB-of8vuhxs.js} +1 -1
  239. package/static/dist/assets/{pieDiagram-E7YTZNPT-b4is36bq.js → pieDiagram-E7YTZNPT-o7y8ftio.js} +1 -1
  240. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-m8vt2788.js → quadrantDiagram-AXDQQJYC-iqiwia1t.js} +1 -1
  241. package/static/dist/assets/{radar-RG4KPBEZ-nxbrhiov.js → radar-RG4KPBEZ-delnsy2v.js} +1 -1
  242. package/static/dist/assets/{railroad-74A4TZTK-222mu9hi.js → railroad-74A4TZTK-nspnyq9u.js} +1 -1
  243. package/static/dist/assets/railroad-abnf-HS5TGJTU-nst1qxyn.js +1 -0
  244. package/static/dist/assets/railroad-ebnf-LZEXJU2U-ci37x8qy.js +1 -0
  245. package/static/dist/assets/railroad-peg-WCYAUIDC-aimkk07z.js +1 -0
  246. package/static/dist/assets/{railroadDiagram-O6MQD6OU-o8lldfzv.js → railroadDiagram-O6MQD6OU-z41tqx5z.js} +1 -1
  247. package/static/dist/assets/refresh-b4uams1i.js +1 -0
  248. package/static/dist/assets/refresh-cw-nt1dtsx4.js +1 -0
  249. package/static/dist/assets/{requirementDiagram-BXWQKSXE-ovyq38kv.js → requirementDiagram-BXWQKSXE-bdvif194.js} +1 -1
  250. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-ou7lznor.js → sankeyDiagram-P5KCCOFB-h73e0rol.js} +1 -1
  251. package/static/dist/assets/{saved-bazmcup9.js → saved-kg52csy7.js} +1 -1
  252. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-ovckdzf2.js → sequenceDiagram-WJ2MYXX4-iv95mb1z.js} +1 -1
  253. package/static/dist/assets/{settings_section_extensions_title-ertwwhlx.js → settings_section_extensions_title-ej0iapxe.js} +1 -1
  254. package/static/dist/assets/share._token-f97e94r0.js +2 -0
  255. package/static/dist/assets/sliders-horizontal-fj18it47.js +1 -0
  256. package/static/dist/assets/sse-fwpqtzbg.js +1 -0
  257. package/static/dist/assets/{stateDiagram-D77RDMKH-fcrexkkv.js → stateDiagram-D77RDMKH-k417ixmf.js} +1 -1
  258. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hwn4apct.js +1 -0
  259. package/static/dist/assets/{swimlanes-42K2YHIH-f7usu8fw.js → swimlanes-42K2YHIH-esola70w.js} +1 -1
  260. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d1h97oyg.js +8 -0
  261. package/static/dist/assets/{timeline-definition-24CTP7MA-gp1hvz9f.js → timeline-definition-24CTP7MA-getrz2y8.js} +1 -1
  262. package/static/dist/assets/todoStore-eginf9h4.js +1 -0
  263. package/static/dist/assets/{treeView-Q6P3EWNA-gb9gf11u.js → treeView-Q6P3EWNA-hc7ap1q7.js} +1 -1
  264. package/static/dist/assets/{treemap-WGGIJYW6-nuvfnvvi.js → treemap-WGGIJYW6-b7k1syy5.js} +1 -1
  265. package/static/dist/assets/tts-lxdxnmjd.js +1 -0
  266. package/static/dist/assets/{untitled-htelhc68.js → untitled-ovpfthg4.js} +1 -1
  267. package/static/dist/assets/{useForm-h2t0u14e.js → useForm-bcblv1rx.js} +1 -1
  268. package/static/dist/assets/useLocale-fi5jb8ma.js +1 -0
  269. package/static/dist/assets/{useParams-dqd64jv7.js → useParams-lhg566ew.js} +1 -1
  270. package/static/dist/assets/{useSearch-j30pvsy7.js → useSearch-lbrh6ejn.js} +1 -1
  271. package/static/dist/assets/{vennDiagram-4TSXK5OY-fzchbzaj.js → vennDiagram-4TSXK5OY-n6pcy5qb.js} +1 -1
  272. package/static/dist/assets/{wardley-WFR3VGLG-m47mhfbh.js → wardley-WFR3VGLG-lpa18vz1.js} +1 -1
  273. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-odg6ct1z.js → wardleyDiagram-VM6X3IG4-kpclwwg7.js} +1 -1
  274. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-mrhms5um.js → xychartDiagram-S5SC5T6Z-paa2n8y0.js} +1 -1
  275. package/static/dist/index.html +1 -1
  276. package/static/dist/sw.js +2 -2
  277. package/static/dist/assets/AppShell-ef3bhu32.js +0 -1
  278. package/static/dist/assets/Button-nx37f4u1.js +0 -1
  279. package/static/dist/assets/ChatPage-cdcpykcg.js +0 -25
  280. package/static/dist/assets/Dialog-mec42hvb.js +0 -1
  281. package/static/dist/assets/HubRoute-ecb65zpr.js +0 -2
  282. package/static/dist/assets/RightPanel-d1lgvk4s.js +0 -1
  283. package/static/dist/assets/Select-h4ivl9qf.js +0 -1
  284. package/static/dist/assets/SessionListPanel-ewib046b.js +0 -1
  285. package/static/dist/assets/SettingsLayout-lx654cup.js +0 -1
  286. package/static/dist/assets/States-ehsb6lzo.js +0 -1
  287. package/static/dist/assets/Toaster-mch1pa3t.js +0 -1
  288. package/static/dist/assets/_app-m8d961ef.js +0 -1
  289. package/static/dist/assets/_app.ext._extensionId-mp33hbzi.js +0 -1
  290. package/static/dist/assets/_app.index-dk6zp7e0.js +0 -1
  291. package/static/dist/assets/_app.insights-jt2fk4i1.js +0 -1
  292. package/static/dist/assets/_app.kanban-if4pubeu.js +0 -1
  293. package/static/dist/assets/_app.logs-mfaw9m6g.js +0 -1
  294. package/static/dist/assets/_app.memory-h3eaarb6.js +0 -1
  295. package/static/dist/assets/_app.profiles-ienktovs.js +0 -1
  296. package/static/dist/assets/_app.settings-dpoe0bzf.js +0 -1
  297. package/static/dist/assets/_app.settings._section-l468ynag.js +0 -3
  298. package/static/dist/assets/_app.skills-kcbncc8i.js +0 -1
  299. package/static/dist/assets/_app.tasks-occdrqhw.js +0 -2
  300. package/static/dist/assets/_app.todos-nnke2rcc.js +0 -1
  301. package/static/dist/assets/_app.workspaces-cdlatvwj.js +0 -1
  302. package/static/dist/assets/architecture-7GRP2DOG-hj11333z.js +0 -1
  303. package/static/dist/assets/channel-mbvmf2vv.js +0 -1
  304. package/static/dist/assets/classDiagram-ZZMXUADV-o8r05k0n.js +0 -1
  305. package/static/dist/assets/classDiagram-v2-VYDZK3BY-o8r05k0n.js +0 -1
  306. package/static/dist/assets/copy-e6977uwq.js +0 -1
  307. package/static/dist/assets/endpoints-k7yf25m0.js +0 -64
  308. package/static/dist/assets/eventmodeling-NTZA5JFV-kpbq09bx.js +0 -1
  309. package/static/dist/assets/flowDiagram-HODETNUW-lcz0kx14.js +0 -1
  310. package/static/dist/assets/index-h00usru2.js +0 -11
  311. package/static/dist/assets/index-n73ijkv2.css +0 -2
  312. package/static/dist/assets/lazyRouteComponent-m1rtyk4t.js +0 -1
  313. package/static/dist/assets/link-budeg5t9.js +0 -1
  314. package/static/dist/assets/matchContext-f8oy3lo7.js +0 -1
  315. package/static/dist/assets/mermaid-HWGCJPDP-jb5d211j.js +0 -1
  316. package/static/dist/assets/onboarding-cdfrk0pj.js +0 -1
  317. package/static/dist/assets/railroad-abnf-HS5TGJTU-kdvgl6x8.js +0 -1
  318. package/static/dist/assets/railroad-ebnf-LZEXJU2U-itiqslby.js +0 -1
  319. package/static/dist/assets/railroad-peg-WCYAUIDC-go7kov78.js +0 -1
  320. package/static/dist/assets/refresh-cw-kbcje1uy.js +0 -1
  321. package/static/dist/assets/search-jen74z4u.js +0 -1
  322. package/static/dist/assets/share._token-dan9hgvj.js +0 -2
  323. package/static/dist/assets/sliders-horizontal-f9cb3lh3.js +0 -1
  324. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-lcfraua2.js +0 -1
  325. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ogmanni0.js +0 -8
  326. package/static/dist/assets/todoStore-eqtid4r4.js +0 -1
  327. package/static/dist/assets/useLocale-hcsb84pj.js +0 -1
  328. package/static/dist/assets/useNavigate-nwy70ztj.js +0 -1
@@ -8,13 +8,13 @@
8
8
  * here. ponytail: one live-id cache with a 24h TTL replaces the Python
9
9
  * publisher/provenance machinery; `refresh()` evicts it.
10
10
  */
11
- import { readCapped } from '../http/capped.js';
12
11
  import { homeDotenvKeys } from '../cli/dotenv.js';
13
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
12
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
14
13
  import { createHash, randomUUID } from 'node:crypto';
15
14
  import { join } from 'node:path';
16
15
  import { atomicWriteText } from '../fs/atomic.js';
17
16
  import { str } from '../util.js';
17
+ import { QUOTA_THRESHOLD_DEFAULTS } from '../settings.js';
18
18
  import { loadEnvFile } from './env-file.js';
19
19
  import { ACCOUNT_USAGE_PROVIDERS, FALLBACK_MODELS, OAUTH_PROVIDERS, PORTAL_PROVIDERS, PROVIDER_DISPLAY, PROVIDER_ENV_VAR, PROVIDER_ENV_VAR_ALIASES, PROVIDER_MODELS, SELF_HOSTED_PROVIDER_IDS, } from './tables.js';
20
20
  import { activeProviderFromConfig, canonicaliseProviderId, configuredModelIds, configuredModelOptions, customProviderEntries, customProviderSlug, dict, effectiveDefaultModel, isDict, isOpenAiFamilyProvider, mainModelSupportsServiceTier, modelSection, parseProviderQualifiedModel, providerIdentity, resolveProviderAlias, } from '../config/agent-config.js';
@@ -49,6 +49,58 @@ export function catalogOptionId(catalog, model, provider) {
49
49
  return e.id;
50
50
  return null;
51
51
  }
52
+ /** TAL-542: the vendor a first-party provider serves, or a model id names by prefix; '' when neither says. */
53
+ const PROVIDER_VENDOR = { openai: 'openai', 'openai-api': 'openai', 'openai-codex': 'openai', anthropic: 'anthropic', gemini: 'google', google: 'google' };
54
+ function modelVendor(model) {
55
+ const m = model.trim().toLowerCase();
56
+ if (m.includes('/'))
57
+ return { openai: 'openai', anthropic: 'anthropic', google: 'google', gemini: 'google' }[m.slice(0, m.indexOf('/'))] ?? '';
58
+ return m.startsWith('gpt') ? 'openai' : m.startsWith('claude') ? 'anthropic' : m.startsWith('gemini') ? 'google' : '';
59
+ }
60
+ /**
61
+ * TAL-542 (Python `_resolve_compatible_session_model_state` + `_repair_foreign_session_model_provider`): the pair a
62
+ * session's stale model starts on. A model its provider's catalog group does not list moves to the one other provider
63
+ * that lists it; else, when the model names another vendor than a first-party provider, to the profile default. No
64
+ * evidence (no group for the provider, a group built from a failed live lookup in `unlisted`, which never names an owner either) keeps the pair, and so does a stored
65
+ * provider that is the profile's own unless the model names another vendor. `null` keeps the pair.
66
+ */
67
+ export function repairSessionModel(catalog, model, provider, unlisted = new Set()) {
68
+ const active = canonicaliseProviderId(catalog.active_provider);
69
+ const pid = canonicaliseProviderId(provider) || active;
70
+ if (!model.trim() || !pid || unlisted.has(pid) || !catalog.groups.some((g) => canonicaliseProviderId(g.provider_id) === pid))
71
+ return null;
72
+ const loose = (id) => id.trim().toLowerCase().replaceAll('-', '.');
73
+ const want = loose(model);
74
+ const entries = catalog.groups.flatMap((g) => [...g.models, ...(g.extra_models ?? [])]);
75
+ const tail = (id) => loose(id.slice(id.indexOf('/') + 1));
76
+ const own = entries.filter((e) => canonicaliseProviderId(e.provider_id) === pid).map((e) => str(e.bare_id));
77
+ if (own.some((bare) => loose(bare) === want))
78
+ return null;
79
+ // Equivalence across ids only drops a recognised vendor prefix; another namespace is no evidence. A match starts on the
80
+ // matching entry's own id, so the turn runs on the id that provider advertises.
81
+ const vendorless = (id) => loose(id.replace(/^(openai|anthropic|google|gemini)\//i, ''));
82
+ const same = (bare) => vendorless(bare) === vendorless(model);
83
+ const here = own.find(same);
84
+ if (here)
85
+ return [here, pid];
86
+ // Python `_catalog_model_id_matches`: a model the provider lists under another namespace is kept, never moved.
87
+ if (own.some((bare) => tail(bare) === want))
88
+ return null;
89
+ if (!provider || pid !== active) {
90
+ const owners = new Map(entries.filter((e) => same(str(e.bare_id)) && !unlisted.has(canonicaliseProviderId(e.provider_id))).map((e) => [canonicaliseProviderId(e.provider_id), str(e.bare_id)]));
91
+ const [only] = owners.size === 1 ? [...owners] : [];
92
+ if (only)
93
+ return [only[1], only[0]];
94
+ }
95
+ const vendor = PROVIDER_VENDOR[pid] ?? '';
96
+ const named = modelVendor(model);
97
+ // Python: Codex takes bare ids, so an `openai/` id on it is stale too.
98
+ if (!vendor || !named || (named === vendor && !(pid === 'openai-codex' && model.includes('/'))))
99
+ return null;
100
+ const defaultModel = str(catalog.default_bare_id).trim();
101
+ const defaultProvider = canonicaliseProviderId(catalog.default_provider_id) || active;
102
+ return defaultModel && defaultProvider && (defaultModel !== model || defaultProvider !== pid) ? [defaultModel, defaultProvider] : null;
103
+ }
52
104
  /** Python `_split_picker_overflow_models`: past 25 rows the picker shows 15, keeping the selected model visible. */
53
105
  export function splitPickerOverflow(models, selected, providerId) {
54
106
  if (models.length <= MODEL_PICKER_OVERFLOW_THRESHOLD)
@@ -98,6 +150,14 @@ export function stampAuxiliarySelections(aux, catalog) {
98
150
  }
99
151
  /** Python `_BESPOKE_CATALOG_PROVIDERS`: cards whose catalog is resolved by their own rule, never the generic live probe. */
100
152
  const BESPOKE_CATALOG_PROVIDERS = new Set(['openai-codex', 'nous', 'xai-oauth', 'lmstudio', 'opencode-go']);
153
+ /** A provider's configured endpoint: its `providers.<id>.base_url` (alias-aware), else the active model's `base_url`. */
154
+ const configuredBaseUrl = (pid, config) => {
155
+ const providers = dict(config.providers);
156
+ const cfg = dict(providers[pid] ?? providers[Object.keys(providers).find((k) => providerIdentity(k) === providerIdentity(pid)) ?? '']);
157
+ return str(cfg.base_url).trim() || (activeProviderFromConfig(config) === pid ? str(modelSection(config).base_url).trim() : '');
158
+ };
159
+ /** TAL-570: Ollama and LM Studio run keyless, so a configured endpoint makes them ready the way a key does. */
160
+ const selfHostedEndpoint = (pid, config) => SELF_HOSTED_PROVIDER_IDS.has(pid) && Boolean(configuredBaseUrl(pid, config));
101
161
  /** Python `_unqualified_model_id`: strip a picker routing hint (`@provider:`). */
102
162
  const unqualifiedModelId = (id) => parseProviderQualifiedModel(id)?.[0] ?? id.trim();
103
163
  /** Python `_MODEL_PICKER_OVERFLOW_THRESHOLD` / `_MODEL_PICKER_VISIBLE_TARGET`. */
@@ -106,8 +166,8 @@ const MODEL_PICKER_VISIBLE_TARGET = 15;
106
166
  const ACCOUNT_USAGE_CACHE_TTL_S = 45;
107
167
  const LIVE_TTL_S = 86_400;
108
168
  const PROVIDERS_TTL_S = 30;
109
- const QUOTA_TIMEOUT_MS = 15_000;
110
- const OPENROUTER_KEY_URL = 'https://openrouter.ai/api/v1/key';
169
+ /** The sidecar reads a balance endpoint with a 15 s timeout of its own. */
170
+ const BALANCE_TIMEOUT_MS = 20_000;
111
171
  const PLUGIN_LIST_TIMEOUT_MS = 15_000;
112
172
  /** TAL-288: the card text for a plugin provider the Agent does not report ready. */
113
173
  const PLUGIN_SETUP_ERRORS = {
@@ -118,6 +178,34 @@ const PLUGIN_SETUP_ERRORS = {
118
178
  };
119
179
  const PLUGIN_NO_MODELS = 'This provider listed no models.';
120
180
  const COST_SNAPSHOT_MAX_DAYS = 365;
181
+ /**
182
+ * The last `days` daily snapshots with their spend deltas, bar heights, monthly pace, and budget standing (the legacy
183
+ * Web chart's formulas). A drop in `used` is a credit reset, so that day's delta is its `used`.
184
+ */
185
+ function costHistoryView(rows, days, budget) {
186
+ const window = rows.slice(-days);
187
+ const deltas = window.map((e, i) => {
188
+ const prev = window[i - 1];
189
+ if (i === 0 || e.used === null || prev?.used === null || prev?.used === undefined)
190
+ return null;
191
+ const delta = e.used - prev.used < 0 ? e.used : e.used - prev.used;
192
+ return Math.abs(delta) < 1e-9 ? 0 : Math.round(delta * 1e6) / 1e6;
193
+ });
194
+ const known = deltas.filter((d) => d !== null);
195
+ const maxDelta = Math.max(...deltas.map((d) => d ?? 0), 1e-9);
196
+ const pace = known.length ? (known.reduce((a, b) => a + b, 0) / known.length) * 30 : null;
197
+ const budgetPercent = pace !== null && pace > 0 && budget !== null && budget > 0 ? Math.round((pace / budget) * 100) : null;
198
+ return {
199
+ snapshots: window.map((e, i) => {
200
+ const delta = deltas[i] ?? null;
201
+ return { date: e.date, used: e.used, delta, bar_percent: delta === null ? 0 : Number(Math.max((delta / maxDelta) * 100, delta > 0 ? 2 : 0).toFixed(1)) };
202
+ }),
203
+ monthly_pace: pace === null ? null : Math.round(pace * 100) / 100,
204
+ has_enough_data: known.length > 0,
205
+ budget_percent: budgetPercent,
206
+ budget_level: budgetPercent === null ? null : budgetPercent >= 100 ? 'over' : budgetPercent >= 80 ? 'warn' : 'ok',
207
+ };
208
+ }
121
209
  const FIVE_HOUR_WINDOW_S = 18_000;
122
210
  const WEEK_WINDOW_S = 604_800;
123
211
  /** Swift `rounded()`: half away from zero. */
@@ -212,6 +300,46 @@ export function normalizeQuotaWindows(raw, nowSeconds) {
212
300
  weekly_window_index: index(labelHas('week')),
213
301
  };
214
302
  }
303
+ /** The burn-rate breakpoints apply once the window is old and used enough and the projection empties it before reset. */
304
+ function projectionEligible(pace, usedPercent, minimumElapsedHours) {
305
+ return pace.elapsed_minutes >= Math.max(0, minimumElapsedHours) * 60 && usedPercent >= 5 && pace.minutes_to_reset > 20
306
+ && (pace.projected_minutes_to_empty ?? Infinity) < pace.minutes_to_reset;
307
+ }
308
+ /** The App's former `ProviderQuotaUrgencyCalculator.urgency` for one colour basis; a pace basis without a pace falls back to remaining. */
309
+ function quotaLevel(window, status, basis, t) {
310
+ if (status === 'stale')
311
+ return 'stale';
312
+ const remaining = window?.remaining_percent ?? null;
313
+ if (status !== 'available' || remaining === null)
314
+ return status === 'available' ? 'healthy' : 'unavailable';
315
+ const pace = window?.pace ?? null;
316
+ if (basis === 'pace' && pace) {
317
+ const eligible = projectionEligible(pace, 100 - remaining, t.pace_minimum_elapsed_hours);
318
+ if (eligible && pace.burn_rate >= Math.max(0, t.pace_critical_burn_rate_percent) / 100)
319
+ return 'critical';
320
+ if (eligible && pace.burn_rate >= Math.max(0, t.pace_warning_burn_rate_percent) / 100)
321
+ return 'warning';
322
+ return pace.pace_delta_percent <= -Math.max(0, t.pace_tolerance_percent) ? 'warning' : 'healthy';
323
+ }
324
+ if (remaining <= Math.max(0, t.critical_remaining_percent))
325
+ return 'critical';
326
+ if (remaining <= Math.max(0, t.warning_remaining_percent))
327
+ return 'warning';
328
+ return 'healthy';
329
+ }
330
+ export function classifyQuotaSource(source, t) {
331
+ const urgency = (w) => ({ remaining: quotaLevel(w, source.status, 'remaining', t), pace: quotaLevel(w, source.status, 'pace', t) });
332
+ return {
333
+ ...source,
334
+ windows: source.windows.map((w) => ({
335
+ ...w,
336
+ pace: w.pace && { ...w.pace, status: w.pace.pace_delta_percent <= -Math.max(0, t.pace_tolerance_percent) ? 'over' : w.pace.pace_delta_percent > 1 ? 'under' : 'on' },
337
+ projection_eligible: w.pace !== null && w.remaining_percent !== null && projectionEligible(w.pace, 100 - w.remaining_percent, t.pace_minimum_elapsed_hours),
338
+ urgency: urgency(w),
339
+ })),
340
+ urgency: { remaining: urgency(source.windows[0]).remaining, pace: urgency(source.windows[source.pace_window_index ?? 0]).pace },
341
+ };
342
+ }
215
343
  export function displayName(pid) {
216
344
  return PROVIDER_DISPLAY[pid] ?? pid.split('-').map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w)).join(' ');
217
345
  }
@@ -300,6 +428,25 @@ export function deduplicateModelIds(groups) {
300
428
  m.id = `@${groups[gi].provider_id}:${m.id}`;
301
429
  }
302
430
  }
431
+ /** OpenCode Go's usage windows: their label and length (Python `_sanitize_opencode_go_account_limits`). */
432
+ const OPENCODE_GO_WINDOWS = { rolling: ['5-hour', FIVE_HOUR_WINDOW_S], weekly: ['Weekly', WEEK_WINDOW_S], monthly: ['Monthly', null] };
433
+ /** One pool account's local state as account limits; `status` is the account's, which its quota source shows. */
434
+ function poolEntryLimits(entry, at) {
435
+ return {
436
+ source: 'local_pool', title: 'Credential pool', status: entry.status, available: entry.status === 'available', plan: null, details: [],
437
+ unavailable_reason: entry.unavailable_reason, retry_after: entry.retry_after, fetched_at: null, ...normalizeQuotaWindows([], at),
438
+ };
439
+ }
440
+ /** Python `_local_pool_snapshot`: a provider's pool accounts summarised as account limits. */
441
+ function poolLimits(entries, at) {
442
+ const count = (status) => entries.filter((e) => e.status === status).length;
443
+ const [available, exhausted, dead] = [count('available'), count('exhausted'), count('dead')];
444
+ const details = [`${String(available)}/${String(entries.length)} credentials available`, ...(exhausted ? [`${String(exhausted)} exhausted`] : []), ...(dead ? [`${String(dead)} dead`] : [])];
445
+ return {
446
+ source: 'local_pool', title: 'Credential pool', available: available > 0, plan: null, details, unavailable_reason: available ? null : 'All pool credentials are unavailable.', fetched_at: null,
447
+ ...normalizeQuotaWindows([], at),
448
+ };
449
+ }
303
450
  /** Quota sources keep the first row per source id, ordered by provider id, account label, then source id (TAL-272). */
304
451
  export function uniqueQuotaSources(sources) {
305
452
  const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
@@ -308,12 +455,17 @@ export function uniqueQuotaSources(sources) {
308
455
  }
309
456
  export class ProviderCatalog {
310
457
  deps;
458
+ /** Failed live lookups by home, provider, and the `sourceFingerprint` they ran under (TAL-542). */
311
459
  liveFailed = new Set();
460
+ /** `source`: the `sourceFingerprint` the ids were fetched under; another one makes them stale (TAL-542). */
312
461
  liveIds = new Map();
313
462
  liveInflight = new Map();
463
+ /** `pooled`: the fill's `poolProviders` answer, null when unknown (TAL-638). */
314
464
  providersCache = new Map();
315
465
  /** TAL-301: the last catalog served per profile home, for the sync session payloads' `model_option_id`. */
316
466
  lastModels = new Map();
467
+ /** TAL-542: per `lastModels` catalog, the `sourceFingerprint` it was built from, when, and the providers whose group came from a failed live lookup. */
468
+ lastModelsMeta = new Map();
317
469
  constructor(deps) {
318
470
  this.deps = deps;
319
471
  }
@@ -336,11 +488,19 @@ export class ProviderCatalog {
336
488
  this.liveIds.delete(key);
337
489
  }
338
490
  this.providersCache.clear();
491
+ // Credentials changed: the last pool answers no longer stand in for a failed lookup.
492
+ for (const key of [...this.lastPoolAnswers.keys()])
493
+ if (!profileHome || key.startsWith(`${profileHome}\0`))
494
+ this.lastPoolAnswers.delete(key);
339
495
  // The picker catalog changed: option ids rebuild on the next read rather than pairing against the old one.
340
- if (profileHome)
496
+ if (profileHome) {
341
497
  this.lastModels.delete(profileHome);
342
- else
498
+ this.lastModelsMeta.delete(profileHome);
499
+ }
500
+ else {
343
501
  this.lastModels.clear();
502
+ this.lastModelsMeta.clear();
503
+ }
344
504
  }
345
505
  /** TAL-301: the entry id a stored `(model, provider)` pair selects in the last catalog built for this home; a cold home starts building one. */
346
506
  modelOptionFor(profileHome, model, provider) {
@@ -351,9 +511,58 @@ export class ProviderCatalog {
351
511
  }
352
512
  return catalogOptionId(catalog, model, provider);
353
513
  }
354
- /** Builds the catalog `modelOptionFor` reads once per home; an unavailable catalog leaves every option id null. */
514
+ /**
515
+ * TAL-542: the identity of the files a catalog and its live ids derive from: config.yaml, `.env`, the Agent's sign-in
516
+ * store (`auth.json`, rewritten by an account switch), and the installed plugins directory, so a change outside Web
517
+ * makes both stale.
518
+ */
519
+ sourceFingerprint(profileHome) {
520
+ const stamp = (name) => {
521
+ try {
522
+ const st = statSync(join(profileHome, name), { bigint: true });
523
+ return `${String(st.mtimeNs)}:${String(st.size)}:${String(st.ino)}`;
524
+ }
525
+ catch {
526
+ return 'missing';
527
+ }
528
+ };
529
+ return [this.deps.config.fingerprint(profileHome), ...['.env', 'auth.json', 'plugins'].map(stamp)].join('|');
530
+ }
531
+ /** The last catalog built for this home, younger than `maxAgeS`, while its source files are unchanged since (TAL-542). */
532
+ currentModels(profileHome, maxAgeS = Infinity) {
533
+ const meta = this.lastModelsMeta.get(profileHome);
534
+ return meta?.source === this.sourceFingerprint(profileHome) && this.deps.now() - meta.at < maxAgeS ? this.lastModels.get(profileHome) : undefined;
535
+ }
536
+ /**
537
+ * TAL-542: `repairSessionModel` against this home's catalog. Agent-owned sign-in and plugin state has no file to
538
+ * fingerprint, so the repair also needs a catalog no older than the providers listing it read (`PROVIDERS_TTL_S`);
539
+ * a cold or stale one keeps the pair and starts a rebuild.
540
+ */
541
+ sessionModelRepair(profileHome, model, provider) {
542
+ const catalog = this.currentModels(profileHome, PROVIDERS_TTL_S);
543
+ if (!catalog) {
544
+ void this.warmSessionModelRepair(profileHome);
545
+ return null;
546
+ }
547
+ return repairSessionModel(catalog, model, provider, this.lastModelsMeta.get(profileHome)?.unconfirmed);
548
+ }
549
+ /**
550
+ * Builds the catalog `sessionModelRepair` reads unless a fresh enough one exists; call it after the request's last
551
+ * await. A source that changed while the build awaited makes that build stale, so it is rebuilt once.
552
+ */
553
+ async warmSessionModelRepair(profileHome) {
554
+ for (let attempt = 0; attempt < 2 && !this.currentModels(profileHome, PROVIDERS_TTL_S); attempt += 1) {
555
+ try {
556
+ await this.models(profileHome);
557
+ }
558
+ catch {
559
+ return; /* fail closed: the pair is kept */
560
+ }
561
+ }
562
+ }
563
+ /** Builds the catalog `modelOptionFor` reads once per home, again after config.yaml or `.env` changes; an unavailable catalog leaves every option id null. */
355
564
  async warmModelOptions(profileHome) {
356
- if (this.lastModels.has(profileHome))
565
+ if (this.currentModels(profileHome))
357
566
  return;
358
567
  try {
359
568
  await this.models(profileHome);
@@ -369,7 +578,7 @@ export class ProviderCatalog {
369
578
  return undefined;
370
579
  return this.deps.env[name];
371
580
  }
372
- /** Python `_provider_has_key` minus the credential pool (the sidecar answers OAuth/pool state). */
581
+ /** Python `_provider_has_key` minus the credential pool, which `probeKey` reads from the sidecar. */
373
582
  providerHasKey(pid, config, envValues, profileHome) {
374
583
  const envVar = providerEnvVar(pid);
375
584
  if (envVar) {
@@ -407,7 +616,8 @@ export class ProviderCatalog {
407
616
  return null;
408
617
  }
409
618
  }
410
- async probeKey(profileHome, pid, config, envValues) {
619
+ /** `pooled`: the providers whose persisted credential pool holds an added account (TAL-638). */
620
+ async probeKey(profileHome, pid, config, envValues, pooled) {
411
621
  let hasKey = this.providerHasKey(pid, config, envValues, profileHome);
412
622
  let isOauth = OAUTH_PROVIDERS.has(pid);
413
623
  let keySource = 'none';
@@ -452,6 +662,11 @@ export class ProviderCatalog {
452
662
  else
453
663
  keySource = 'config_yaml';
454
664
  }
665
+ else if (pooled.has(pid)) {
666
+ // Python `_has_explicit_pool_credentials`: keys added with `hermes auth add` live only in the Agent's pool.
667
+ hasKey = true;
668
+ keySource = 'credential_pool';
669
+ }
455
670
  else if (!providerEnvVar(pid) && /^[a-z][a-z0-9_-]{0,63}$/.test(pid)) {
456
671
  const status = await this.authStatus(profileHome, pid);
457
672
  if (status?.logged_in) {
@@ -466,20 +681,26 @@ export class ProviderCatalog {
466
681
  /** Live model ids from the Agent for one provider, cached per profile home. */
467
682
  async liveModelIds(profileHome, pid, opts = {}) {
468
683
  const key = `${profileHome}\0${pid}`;
684
+ const source = this.sourceFingerprint(profileHome);
469
685
  const hit = this.liveIds.get(key);
470
- if (hit && !opts.force && this.deps.now() - hit.at < LIVE_TTL_S)
686
+ if (hit?.source === source && !opts.force && this.deps.now() - hit.at < LIVE_TTL_S)
471
687
  return hit.ids;
472
- const inflight = this.liveInflight.get(key);
688
+ // A lookup started under another source answers for the old endpoint or credential; this one starts its own.
689
+ const flightKey = `${key}\0${source}`;
690
+ const inflight = this.liveInflight.get(flightKey);
473
691
  if (inflight && !opts.force)
474
692
  return inflight;
475
693
  const sidecar = this.deps.sidecar();
476
- if (!sidecar)
694
+ // No sidecar is a failed lookup for this source: the ids it keeps showing never confirm a stale-model repair (TAL-542).
695
+ if (!sidecar) {
696
+ this.liveFailed.add(flightKey);
477
697
  return hit?.ids ?? [];
698
+ }
478
699
  const run = sidecar.call('providers.model_ids', { profile_home: profileHome, provider: pid, ...(opts.force ? { force_refresh: true } : {}) }, { timeoutMs: 30_000 })
479
- .then((r) => { this.liveIds.set(key, { at: this.deps.now(), ids: r.model_ids }); this.liveFailed.delete(key); return r.model_ids; })
480
- .catch((error) => { this.deps.log(`[catalog] live model ids for ${pid} failed: ${str(error.message)}`); this.liveFailed.add(key); return hit?.ids ?? []; })
481
- .finally(() => { this.liveInflight.delete(key); });
482
- this.liveInflight.set(key, run);
700
+ .then((r) => { this.liveIds.set(key, { at: this.deps.now(), ids: r.model_ids, source }); this.liveFailed.delete(flightKey); return r.model_ids; })
701
+ .catch((error) => { this.deps.log(`[catalog] live model ids for ${pid} failed: ${str(error.message)}`); this.liveFailed.add(flightKey); return hit?.ids ?? []; })
702
+ .finally(() => { this.liveInflight.delete(flightKey); });
703
+ this.liveInflight.set(flightKey, run);
483
704
  return run;
484
705
  }
485
706
  /**
@@ -501,12 +722,18 @@ export class ProviderCatalog {
501
722
  }
502
723
  /** Python `get_providers`. */
503
724
  async providers(profileHome) {
725
+ return structuredClone((await this.providerRows(profileHome)).payload);
726
+ }
727
+ /** The cached `providers` answer, shared with `quotas`; callers never mutate it. */
728
+ async providerRows(profileHome) {
504
729
  const config = await this.deps.config.read(profileHome);
505
730
  const envValues = loadEnvFile(join(profileHome, '.env'));
506
731
  const cacheKey = JSON.stringify([config, envValues]);
507
732
  const hit = this.providersCache.get(profileHome);
508
733
  if (hit?.key === cacheKey && this.deps.now() - hit.at < PROVIDERS_TTL_S)
509
- return structuredClone(hit.payload);
734
+ return hit;
735
+ const pooledIds = await this.poolProviders(profileHome);
736
+ const pooled = new Set(pooledIds ?? []);
510
737
  const active = activeProviderFromConfig(config);
511
738
  const known = new Set([...Object.keys(PROVIDER_DISPLAY), ...Object.keys(PROVIDER_MODELS), ...OAUTH_PROVIDERS]);
512
739
  // A plugin never stands in for a built-in provider, by id or alias (that would route to the built-in's billing); it
@@ -522,14 +749,15 @@ export class ProviderCatalog {
522
749
  }
523
750
  const rows = [];
524
751
  for (const pid of [...known].sort()) {
525
- const probe = await this.probeKey(profileHome, pid, config, envValues);
752
+ const probe = await this.probeKey(profileHome, pid, config, envValues, pooled);
526
753
  if (pid === 'openai' && !probe.hasKey && looksLikeCodexOauthToken(str(envValues[providerEnvVar('openai') ?? ''] ?? this.deps.env.OPENAI_API_KEY)))
527
754
  continue;
755
+ const configured = probe.hasKey || selfHostedEndpoint(pid, config);
528
756
  let models = pid === 'openrouter' ? FALLBACK_MODELS.map((m) => ({ id: m.id, label: m.label })) : [...(PROVIDER_MODELS[pid] ?? [])];
529
757
  let modelsTotal = models.length;
530
758
  // Python: the card prefers the live catalog for every keyed provider (exactly like the picker), keeping the
531
759
  // static list as the cold/failed-probe fallback; Nous renders a featured subset with the full count.
532
- if (probe.hasKey && (probe.isOauth || pid === 'lmstudio' || pid === 'nous' || !BESPOKE_CATALOG_PROVIDERS.has(pid))) {
760
+ if (configured && (probe.isOauth || pid === 'lmstudio' || pid === 'nous' || !BESPOKE_CATALOG_PROVIDERS.has(pid))) {
533
761
  const live = await this.liveModelIds(profileHome, pid);
534
762
  if (live.length) {
535
763
  models = pid === 'nous' ? live.slice(0, 25).map((id) => ({ id: `@nous:${id}`, label: `${formatOllamaLabel(id.includes('/') ? id.slice(id.indexOf('/') + 1) : id)} (via Nous)` })) : live.map((id) => ({ id, label: labelForModel(id, []) }));
@@ -545,11 +773,12 @@ export class ProviderCatalog {
545
773
  if (pid !== 'nous')
546
774
  modelsTotal += added.length;
547
775
  }
548
- const baseUrl = str(providerCfg.base_url).trim() || (active === pid ? str(modelSection(config).base_url).trim() : '') || null;
776
+ const baseUrl = configuredBaseUrl(pid, config) || null;
549
777
  rows.push({
550
778
  id: pid,
551
779
  display_name: displayName(pid),
552
780
  has_key: probe.hasKey,
781
+ configured,
553
782
  configurable: !probe.isOauth && Boolean(providerEnvVar(pid)),
554
783
  is_oauth: probe.isOauth,
555
784
  is_plugin_provider: false,
@@ -568,7 +797,7 @@ export class ProviderCatalog {
568
797
  const ready = plugin.setup === 'ready';
569
798
  const live = ready ? await this.liveModelIds(profileHome, plugin.name) : [];
570
799
  rows.push({
571
- id: plugin.name, display_name: plugin.display_name || plugin.name, has_key: ready,
800
+ id: plugin.name, display_name: plugin.display_name || plugin.name, has_key: ready, configured: ready,
572
801
  configurable: false, is_oauth: false, is_plugin_provider: true, is_self_hosted: false, is_custom: false, key_source: ready ? 'plugin' : 'none',
573
802
  base_url: null, auth_error: plugin.setup === 'ready' ? (live.length ? null : PLUGIN_NO_MODELS) : PLUGIN_SETUP_ERRORS[plugin.setup], env_var: null,
574
803
  models: live.map((id) => ({ id, label: labelForModel(id, []) })), models_total: live.length,
@@ -585,7 +814,7 @@ export class ProviderCatalog {
585
814
  const envRef = /^\$\{([^}]+)\}$/.exec(cpKey)?.[1] ?? '';
586
815
  const hasKey = envRef ? Boolean((this.processEnv(envRef, profileHome) ?? '').trim()) : valueCountsAsApiKey(slug, cp.api_key) || Boolean(str(cp.key_env).trim() && this.processEnv(str(cp.key_env).trim(), profileHome));
587
816
  rows.push({
588
- id: slug, display_name: name, has_key: hasKey,
817
+ id: slug, display_name: name, has_key: hasKey, configured: hasKey,
589
818
  configurable: false, is_oauth: false, is_plugin_provider: false, is_self_hosted: false, is_custom: true, key_source: str(cp.api_key).trim() ? 'config_yaml' : 'none',
590
819
  base_url: str(cp.base_url).trim() || null, auth_error: null, env_var: null, models: ids.map((id) => ({ id, label: labelForModel(id, []) })), models_total: ids.length,
591
820
  });
@@ -595,12 +824,14 @@ export class ProviderCatalog {
595
824
  rows.sort((a, b) => rank(a) - rank(b) || (str(a.id) < str(b.id) ? -1 : str(a.id) > str(b.id) ? 1 : 0));
596
825
  for (const row of rows)
597
826
  row.models = stampModelEntries(row.models, str(row.id));
598
- const payload = { providers: rows, active_provider: active };
599
- this.providersCache.set(profileHome, { at: this.deps.now(), key: cacheKey, payload });
600
- return structuredClone(payload);
827
+ const entry = { at: this.deps.now(), key: cacheKey, payload: { providers: rows, active_provider: active }, pooled: pooledIds };
828
+ this.providersCache.set(profileHome, entry);
829
+ return entry;
601
830
  }
602
831
  /** Python `get_available_models` (static catalog + live ids for keyed providers). */
603
832
  async models(profileHome) {
833
+ const source = this.sourceFingerprint(profileHome);
834
+ const unconfirmed = new Set();
604
835
  const config = await this.deps.config.read(profileHome);
605
836
  const envValues = loadEnvFile(join(profileHome, '.env'));
606
837
  const active = activeProviderFromConfig(config);
@@ -644,11 +875,11 @@ export class ProviderCatalog {
644
875
  detected.add(canonical);
645
876
  }
646
877
  // Python: OAuth providers the Agent reports as logged in join the picker with their live catalog (#1567, #2545); so do
647
- // ready plugin providers (TAL-288), under their own name.
878
+ // ready plugin providers (TAL-288), under their own name, and providers keyed only through the credential pool (TAL-638).
648
879
  const signedIn = new Set();
649
880
  const pluginNames = new Map();
650
- for (const row of (await this.providers(profileHome)).providers) {
651
- if (row.has_key !== true || (row.is_oauth !== true && row.is_plugin_provider !== true))
881
+ for (const row of (await this.providerRows(profileHome)).payload.providers) {
882
+ if (row.has_key !== true || (row.is_oauth !== true && row.is_plugin_provider !== true && row.key_source !== 'credential_pool'))
652
883
  continue;
653
884
  signedIn.add(str(row.id));
654
885
  if (row.is_plugin_provider === true)
@@ -708,12 +939,15 @@ export class ProviderCatalog {
708
939
  let raw = [];
709
940
  if ('models' in providerCfg && providerCfg.models_discovered !== true)
710
941
  raw = configuredModelOptions(providerCfg.models);
711
- if (!raw.length && (this.providerHasKey(pid, config, envValues, profileHome) || signedIn.has(pid))) {
942
+ if (!raw.length && (this.providerHasKey(pid, config, envValues, profileHome) || selfHostedEndpoint(pid, config) || signedIn.has(pid))) {
712
943
  const live = await this.liveModelIds(profileHome, pid);
944
+ // TAL-542: a failed lookup leaves this group's list unconfirmed, so the stale-model repair never trusts it.
945
+ if (this.liveFailed.has(`${profileHome}\0${pid}\0${source}`))
946
+ unconfirmed.add(pid);
713
947
  if (live.length)
714
948
  raw = live.map((id) => ({ id, label: pid === 'nous' ? `${formatOllamaLabel(id.includes('/') ? id.slice(id.indexOf('/') + 1) : id)} (via Nous)` : labelForModel(id, []) }));
715
949
  // Python (#1567): an authenticated Nous account with an empty live catalog shows no group; only a failed lookup falls back to the curated list.
716
- else if (pid === 'nous' && !this.liveFailed.has(`${profileHome}\0${pid}`))
950
+ else if (pid === 'nous' && !this.liveFailed.has(`${profileHome}\0${pid}\0${source}`))
717
951
  continue;
718
952
  }
719
953
  if (!raw.length)
@@ -772,15 +1006,16 @@ export class ProviderCatalog {
772
1006
  const defaults = { active_provider: active, default_model: defaultModel, default_provider_id: defaultProvider, default_bare_id: defaultBare };
773
1007
  if (!kept.length && defaultModel) {
774
1008
  const providerId = active ?? 'default';
775
- return this.remember(profileHome, { ...defaults, groups: [{ provider: 'Default', provider_id: providerId, models: stampModelEntries([{ id: defaultModel, label: labelForModel(defaultModel, []) }], providerId) }], aliases: {}, configured_model_badges: {} });
1009
+ return this.remember(profileHome, { source, unconfirmed }, { ...defaults, groups: [{ provider: 'Default', provider_id: providerId, models: stampModelEntries([{ id: defaultModel, label: labelForModel(defaultModel, []) }], providerId) }], aliases: {}, configured_model_badges: {} });
776
1010
  }
777
1011
  const stamped = kept.map((g) => ({ ...g, models: stampModelEntries(g.models, g.provider_id), ...(g.extra_models ? { extra_models: stampModelEntries(g.extra_models, g.provider_id) } : {}) }));
778
- return this.remember(profileHome, { ...defaults, groups: stamped, aliases, configured_model_badges: this.badges(kept, active, defaultModel, fallbackCfg) });
1012
+ return this.remember(profileHome, { source, unconfirmed }, { ...defaults, groups: stamped, aliases, configured_model_badges: this.badges(kept, active, defaultModel, fallbackCfg) });
779
1013
  }
780
1014
  /** Stamps the default's entry id and keeps the catalog for `modelOptionFor`. */
781
- remember(profileHome, catalog) {
1015
+ remember(profileHome, meta, catalog) {
782
1016
  catalog.default_option_id = catalogOptionId(catalog, catalog.default_bare_id ?? null, catalog.default_provider_id ?? null);
783
1017
  this.lastModels.set(profileHome, catalog);
1018
+ this.lastModelsMeta.set(profileHome, { ...meta, at: this.deps.now() });
784
1019
  return structuredClone(catalog);
785
1020
  }
786
1021
  /** Python `_configured_model_badges_from_static_catalog`: which picker rows are the main/fallback selections. */
@@ -905,11 +1140,22 @@ export class ProviderCatalog {
905
1140
  return v;
906
1141
  }
907
1142
  }
908
- const p = dict(dict(config.providers)[pid]);
909
- return str(p.api_key).trim() || null;
1143
+ // The same config sources `providerHasKey` counts, so a provider shown as keyed resolves its key.
1144
+ const model = modelSection(config);
1145
+ if (canonicaliseProviderId(model.provider) === canonicaliseProviderId(pid) && valueCountsAsApiKey(pid, model.api_key))
1146
+ return str(model.api_key).trim();
1147
+ for (const [key, value] of Object.entries(dict(config.providers))) {
1148
+ if (providerIdentity(key) === providerIdentity(pid) && isDict(value) && valueCountsAsApiKey(pid, value.api_key))
1149
+ return str(value.api_key).trim();
1150
+ }
1151
+ return null;
910
1152
  }
911
- /** Python `get_provider_quota`. */
1153
+ /** Account-usage snapshots by profile home, provider, and pool account. */
912
1154
  accountUsageCache = new Map();
1155
+ /**
1156
+ * Python `get_provider_quota`. `credential` scopes the read to one credential-pool account (TAL-548): a benched account
1157
+ * answers with its local pool state, and every other read uses that account's key.
1158
+ */
913
1159
  async quota(profileHome, providerRaw, opts = {}) {
914
1160
  const at = opts.at ?? this.deps.now();
915
1161
  const computed_at = isoAt(at);
@@ -918,17 +1164,23 @@ export class ProviderCatalog {
918
1164
  if (!provider)
919
1165
  return { computed_at, ok: false, provider: null, display_name: null, supported: false, status: 'unavailable', quota: null, message: 'No active provider is configured.' };
920
1166
  const name = displayName(provider);
1167
+ const credential = opts.credential ?? null;
1168
+ const answer = (ok, status, message, extra = {}) => ({ computed_at, ok, provider, display_name: name, supported: true, status, quota: null, ...extra, message });
1169
+ if (credential && credential.status !== 'available') {
1170
+ return answer(false, 'unavailable', `${name} credential is unavailable. ${str(credential.unavailable_reason)}`.trim(), { account_limits: poolEntryLimits(credential, at) });
1171
+ }
921
1172
  if (ACCOUNT_USAGE_PROVIDERS.has(provider)) {
922
1173
  const sidecar = this.deps.sidecar();
923
1174
  let limits = null;
924
1175
  // Python `_ACCOUNT_USAGE_CACHE_TTL_SECONDS`: a snapshot answers repeat polls for 45 s unless `refresh` is set.
925
- const cacheKey = `${profileHome}\0${provider}`;
1176
+ const cacheKey = `${profileHome}\0${provider}\0${credential?.credential_id ?? ''}`;
926
1177
  const cached = this.accountUsageCache.get(cacheKey);
927
1178
  if (cached && !opts.refresh && this.deps.now() - cached.at <= ACCOUNT_USAGE_CACHE_TTL_S)
928
1179
  limits = cached.limits;
929
1180
  else if (sidecar) {
930
1181
  try {
931
- const snapshot = (await sidecar.call('usage.account', { profile_home: profileHome, provider, ...(opts.refresh ? { refresh: true } : {}) }, { timeoutMs: 35_000 })).snapshot;
1182
+ const params = { profile_home: profileHome, provider, ...(opts.refresh ? { refresh: true } : {}), ...(credential ? { credential_id: credential.credential_id } : {}) };
1183
+ const snapshot = (await sidecar.call('usage.account', params, { timeoutMs: 35_000 })).snapshot;
932
1184
  if (snapshot)
933
1185
  limits = { ...snapshot, title: str(snapshot.title) || 'Account limits', available: snapshot.available && !str(snapshot.unavailable_reason) };
934
1186
  }
@@ -941,40 +1193,98 @@ export class ProviderCatalog {
941
1193
  if (limits)
942
1194
  limits = { ...limits, ...normalizeQuotaWindows(limits.windows, at), fetched_at: isoUtc(limits.fetched_at) };
943
1195
  if (limits?.available)
944
- return { computed_at, ok: true, provider, display_name: name, supported: true, status: limits.stale ? 'stale' : 'available', label: limits.title, quota: null, account_limits: limits, message: limits.stale ? `${name} refresh failed; showing last-known account limits.` : `${name} account limits loaded.` };
1196
+ return answer(true, limits.stale ? 'stale' : 'available', limits.stale ? `${name} refresh failed; showing last-known account limits.` : `${name} account limits loaded.`, { label: limits.title, account_limits: limits });
945
1197
  const reason = str(limits?.unavailable_reason).trim();
946
- return { computed_at, ok: false, provider, display_name: name, supported: true, status: 'unavailable', quota: null, account_limits: limits, message: reason ? `${name} account limits are unavailable. ${reason}` : `${name} account limits are unavailable. Confirm provider authentication and try again.` };
1198
+ return answer(false, 'unavailable', reason ? `${name} account limits are unavailable. ${reason}` : `${name} account limits are unavailable. Confirm provider authentication and try again.`, { account_limits: limits });
947
1199
  }
948
- if (provider === 'openrouter') {
949
- const apiKey = this.apiKeyFor('openrouter', profileHome, config);
950
- if (!apiKey)
951
- return { computed_at, ok: false, provider, display_name: name, supported: true, status: 'no_key', quota: null, message: 'OpenRouter quota status needs an OPENROUTER_API_KEY configured on the server.' };
952
- const info = await this.fetchOpenRouterKey(apiKey);
953
- if (info.kind === 'ok')
954
- return { computed_at, ok: true, provider, display_name: name, supported: true, status: 'available', label: 'OpenRouter credits', quota: info.quota, message: 'OpenRouter quota status loaded.' };
955
- const status = info.kind === 'invalid_key' ? 'invalid_key' : 'unavailable';
956
- return { computed_at, ok: false, provider, display_name: name, supported: true, status, quota: null, message: status === 'invalid_key' ? 'OpenRouter rejected the configured API key.' : 'OpenRouter quota status is temporarily unavailable.' };
1200
+ if (provider === 'openrouter' || provider === 'deepseek' || provider === 'opencode-go') {
1201
+ const apiKey = this.apiKeyFor(provider, profileHome, config);
1202
+ const key = credential ? { credential_id: credential.credential_id } : apiKey ? { api_key: apiKey } : null;
1203
+ const read = key ? await this.balance(profileHome, provider, key) : null;
1204
+ const status = !key || read?.status === 'no_key' ? 'no_key' : read?.status === 'ok' ? 'ok' : read?.status === 'http_error' ? read.http_status : null;
1205
+ if (provider === 'openrouter') {
1206
+ if (status === 'no_key')
1207
+ return answer(false, 'no_key', 'OpenRouter quota status needs an OPENROUTER_API_KEY configured on the server.');
1208
+ if (status === 'ok' && read?.quota) {
1209
+ // The cost chart follows the configured key, so another pool account's spend never lands in it.
1210
+ if (!credential || credential.matches_api_key)
1211
+ this.recordCostSnapshot(profileHome, read.quota, at);
1212
+ return answer(true, 'available', 'OpenRouter quota status loaded.', { label: 'OpenRouter credits', quota: read.quota });
1213
+ }
1214
+ return status === 401 || status === 403 ? answer(false, 'invalid_key', 'OpenRouter rejected the configured API key.') : answer(false, 'unavailable', 'OpenRouter quota status is temporarily unavailable.');
1215
+ }
1216
+ if (provider === 'deepseek') {
1217
+ if (status === 'no_key')
1218
+ return answer(false, 'no_key', 'DeepSeek balance needs a DEEPSEEK_API_KEY configured on the server.', { balances: [] });
1219
+ if (status === 'ok' && read) {
1220
+ const available = read.is_available === true;
1221
+ return answer(available, available ? 'available' : 'exhausted', available ? 'DeepSeek balance loaded.' : 'DeepSeek balance is exhausted.', { label: 'DeepSeek balance', balances: read.balances });
1222
+ }
1223
+ return status === 401 || status === 403 ? answer(false, 'invalid_key', 'DeepSeek rejected the configured API key.', { balances: [] }) : answer(false, 'unavailable', 'DeepSeek balance is temporarily unavailable.', { balances: [] });
1224
+ }
1225
+ if (status === 'no_key')
1226
+ return answer(false, 'no_key', 'OpenCode Go account limits need an API key configured on the server.', { account_limits: null });
1227
+ if (status === 'ok' && read) {
1228
+ // Python `_sanitize_opencode_go_account_limits`: the rolling, weekly and monthly windows of the Go plan.
1229
+ const windows = read.windows.map((w) => ({ label: OPENCODE_GO_WINDOWS[w.key][0], window_seconds: OPENCODE_GO_WINDOWS[w.key][1], used_percent: w.used_percent, reset_at: w.reset_at, detail: w.rate_limited ? 'Rate limited' : null }));
1230
+ const limits = { provider, source: 'usage_api', title: 'Account limits', plan: 'Go', details: [], available: true, unavailable_reason: null, fetched_at: isoAt(at), ...normalizeQuotaWindows(windows, at) };
1231
+ return answer(true, 'available', 'OpenCode Go account limits loaded.', { label: 'OpenCode Go usage', account_limits: limits });
1232
+ }
1233
+ if (status === 401)
1234
+ return answer(false, 'invalid_key', 'OpenCode Go rejected the configured API key.', { account_limits: null });
1235
+ return answer(false, 'unavailable', status === 403 ? 'OpenCode Go account limits require an active subscription.' : 'OpenCode Go account limits are temporarily unavailable.', { account_limits: null });
1236
+ }
1237
+ // Python `_local_pool_snapshot`: any other provider with a credential pool shows its accounts' local state.
1238
+ const entries = credential ? [credential] : (await this.poolEntries(profileHome, provider, null)) ?? [];
1239
+ if (entries.length) {
1240
+ const limits = credential ? poolEntryLimits(credential, at) : poolLimits(entries, at);
1241
+ return limits.available ? answer(true, 'available', `${name} credential pool status loaded.`, { label: 'Credential pool', account_limits: limits }) : answer(false, 'unavailable', `${name} credential pool: all credentials are unavailable.`, { account_limits: limits });
957
1242
  }
958
1243
  return { computed_at, ok: false, provider, display_name: name, supported: false, status: 'unsupported', quota: null, message: `No verified server-side quota or balance endpoint is available for ${name}.` };
959
1244
  }
960
- async fetchOpenRouterKey(apiKey) {
961
- const f = this.deps.fetch ?? fetch;
1245
+ /**
1246
+ * TAL-548: the provider's credential-pool accounts as the sidecar reads them locally. Each says whether it holds
1247
+ * `apiKey`, the provider's configured key. Null when the pool is unknown (see `poolLookup`).
1248
+ */
1249
+ poolEntries(profileHome, pid, apiKey) {
1250
+ return this.poolLookup(profileHome, `pool\0${pid}`, async (sidecar) => (await sidecar.call('usage.pool', { profile_home: profileHome, provider: pid, ...(apiKey ? { api_key: apiKey } : {}) })).entries);
1251
+ }
1252
+ /** The last answer of each credential-pool lookup, by profile home and lookup. */
1253
+ lastPoolAnswers = new Map();
1254
+ /**
1255
+ * TAL-548: one credential-pool lookup through the sidecar. A failed lookup answers with the last answer to the same
1256
+ * lookup, else null: an unknown pool, never an empty one, so no pool account is ever reported as removed.
1257
+ */
1258
+ async poolLookup(profileHome, lookup, call) {
1259
+ const key = `${profileHome}\0${lookup}`;
962
1260
  try {
963
- const res = await f(OPENROUTER_KEY_URL, { headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' }, signal: AbortSignal.timeout(QUOTA_TIMEOUT_MS) });
964
- if (!res.ok)
965
- return { kind: res.status === 401 || res.status === 403 ? 'invalid_key' : 'unavailable' };
966
- const raw = await readCapped(res, 256 * 1024);
967
- if (!raw)
968
- return { kind: 'unavailable' };
969
- let payload = JSON.parse(raw.toString('utf8'));
970
- if (isDict(payload) && isDict(payload.data))
971
- payload = payload.data;
972
- const d = dict(payload);
973
- const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
974
- return { kind: 'ok', quota: { limit_remaining: num(d.limit_remaining), usage: num(d.usage), limit: num(d.limit) }, label: str(d.label).trim() || null };
1261
+ const sidecar = this.deps.sidecar();
1262
+ if (!sidecar)
1263
+ throw new Error('the sidecar is not running');
1264
+ const answer = await call(sidecar);
1265
+ this.lastPoolAnswers.set(key, answer);
1266
+ return answer;
975
1267
  }
976
- catch {
977
- return { kind: 'unavailable' };
1268
+ catch (error) {
1269
+ this.deps.log(`[catalog] credential pool lookup ${lookup.replace('\0', ' ')} failed: ${str(error.message)}`);
1270
+ return this.lastPoolAnswers.get(key) ?? null;
1271
+ }
1272
+ }
1273
+ /** TAL-548: the providers whose persisted credential pool holds an added account; none when the sidecar cannot say. */
1274
+ poolProviders(profileHome) {
1275
+ return this.poolLookup(profileHome, 'providers', async (sidecar) => (await sidecar.call('usage.pool_providers', { profile_home: profileHome })).providers);
1276
+ }
1277
+ /** TAL-548: one key-based balance read through the sidecar, which resolves a pool account's key itself; null when it fails. */
1278
+ async balance(profileHome, provider, key) {
1279
+ const sidecar = this.deps.sidecar();
1280
+ if (!sidecar)
1281
+ return null;
1282
+ try {
1283
+ return await sidecar.call('usage.balance', { profile_home: profileHome, provider, ...key }, { timeoutMs: BALANCE_TIMEOUT_MS });
1284
+ }
1285
+ catch (error) {
1286
+ this.deps.log(`[catalog] ${provider} balance failed: ${str(error.message)}`);
1287
+ return null;
978
1288
  }
979
1289
  }
980
1290
  quotaScopeCache = null;
@@ -1010,31 +1320,52 @@ export class ProviderCatalog {
1010
1320
  return `qscope_${createHash('sha256').update(`${this.quotaServerScopeId()}\0${profile}`).digest('hex').slice(0, 32)}`;
1011
1321
  }
1012
1322
  /**
1013
- * Python `get_provider_quotas`: one source per keyed provider, in the stable-identity envelope the iOS widget persists.
1014
- * Each source id appears once (two custom providers can share a slug), ordered by provider, account label, then source id.
1323
+ * Python `get_provider_quotas`: the stable-identity envelope the iOS widget persists. A keyed provider is one source per
1324
+ * credential-pool account, else one for its own key (Python `_quota_source_descriptors`, TAL-548). Each source id appears
1325
+ * once (two custom providers can share a slug), ordered by provider, account label, then source id.
1015
1326
  */
1016
1327
  async quotas(profileHome, profile, opts = {}) {
1017
1328
  const at = this.deps.now();
1018
- const status = await this.providers(profileHome);
1329
+ const { payload: status, pooled: pooledIds } = await this.providerRows(profileHome);
1019
1330
  const active = status.active_provider;
1020
1331
  const scopeId = this.quotaProfileScopeId(profile);
1021
- // Python `_quota_source_id(profile, provider, "provider")`: the single-credential descriptor per provider.
1022
- const sourceId = (pid) => `qsrc_${createHash('sha256').update(`${scopeId}\0${pid}\0provider`).digest('hex').slice(0, 32)}`;
1023
- let descriptors = uniqueQuotaSources(status.providers.filter((p) => p.has_key || p.is_custom).map((p) => ({ source_id: sourceId(str(p.id)), provider_id: str(p.id), provider_label: str(p.display_name) || str(p.id), account_label: str(p.display_name) || str(p.id) })));
1332
+ // Python `_quota_source_id(profile, provider, credential_id)`: a pool account's id, or "provider" for the provider's own key.
1333
+ const sourceId = (pid, credential) => `qsrc_${createHash('sha256').update(`${scopeId}\0${pid}\0${credential}`).digest('hex').slice(0, 32)}`;
1334
+ // A provider configured only through `hermes auth add` has no key in .env or config.yaml, yet its pool accounts are
1335
+ // sources, including a signed-out OAuth provider's. An unknown pool (a failed lookup with no earlier answer) never
1336
+ // reports a requested source as missing.
1337
+ let poolUnknown = pooledIds === null;
1338
+ const pooled = new Set(pooledIds ?? []);
1339
+ const config = await this.deps.config.read(profileHome);
1340
+ const perProvider = await Promise.all(status.providers.filter((p) => p.has_key || p.is_custom || pooled.has(str(p.id))).map(async (p) => {
1341
+ const pid = str(p.id);
1342
+ const label = str(p.display_name) || pid;
1343
+ const apiKey = this.apiKeyFor(pid, profileHome, config);
1344
+ const found = await this.poolEntries(profileHome, pid, apiKey);
1345
+ if (!found)
1346
+ poolUnknown = true;
1347
+ const entries = found ?? [];
1348
+ const accounts = entries.map((e) => ({ source_id: sourceId(pid, e.credential_id), provider_id: pid, provider_label: label, account_label: e.label, credential: e }));
1349
+ // The provider's own key is a source unless a pool account already holds it.
1350
+ const ownKey = !entries.length || (apiKey !== null && !entries.some((e) => e.matches_api_key));
1351
+ return ownKey ? [{ source_id: sourceId(pid, 'provider'), provider_id: pid, provider_label: label, account_label: label, credential: null }, ...accounts] : accounts;
1352
+ }));
1353
+ let descriptors = uniqueQuotaSources(perProvider.flat());
1024
1354
  const requested = str(opts.sourceId).trim() || null;
1025
1355
  if (requested)
1026
1356
  descriptors = descriptors.filter((d) => d.source_id === requested);
1357
+ const thresholds = this.deps.quotaThresholds?.(profile) ?? QUOTA_THRESHOLD_DEFAULTS;
1027
1358
  const sources = await Promise.all(descriptors.map(async (d) => {
1028
- const q = await this.quota(profileHome, d.provider_id, { refresh: opts.refresh ?? false, at });
1359
+ const q = await this.quota(profileHome, d.provider_id, { refresh: opts.refresh ?? false, at, credential: d.credential });
1029
1360
  const limits = dict(q.account_limits);
1030
- return {
1361
+ return classifyQuotaSource({
1031
1362
  source_id: d.source_id, provider_id: d.provider_id, provider_label: d.provider_label, account_label: d.account_label,
1032
1363
  is_active_provider: d.provider_id === active, supported: q.supported === true, status: str(limits.status) || str(q.status) || 'unavailable',
1033
- plan: limits.plan ?? null, windows: limits.windows ?? [], pace_window_index: limits.pace_window_index ?? null, session_window_index: limits.session_window_index ?? null, weekly_window_index: limits.weekly_window_index ?? null, quota: q.quota ?? null, balances: q.balances ?? [], details: limits.details ?? [],
1364
+ plan: limits.plan ?? null, windows: (limits.windows ?? []), pace_window_index: (limits.pace_window_index ?? null), session_window_index: limits.session_window_index ?? null, weekly_window_index: limits.weekly_window_index ?? null, quota: q.quota ?? null, balances: q.balances ?? [], details: limits.details ?? [],
1034
1365
  unavailable_reason: limits.unavailable_reason ?? null, retry_after: limits.retry_after ?? null, fetched_at: limits.fetched_at ?? null, message: q.message ?? null,
1035
- };
1366
+ }, thresholds);
1036
1367
  }));
1037
- return { version: 1, computed_at: isoAt(at), scope_id: scopeId, profile_id: profile, active_provider: active, requested_source_id: requested, missing_source: Boolean(requested && !descriptors.length), sources };
1368
+ return { version: 1, computed_at: isoAt(at), scope_id: scopeId, profile_id: profile, active_provider: active, requested_source_id: requested, missing_source: Boolean(requested && !descriptors.length && !poolUnknown), sources };
1038
1369
  }
1039
1370
  /** Python `get_provider_cost_history` (OpenRouter only; daily snapshots under `<home>/cost-snapshots`). */
1040
1371
  async costHistory(profileHome, providerRaw, days) {
@@ -1049,56 +1380,46 @@ export class ProviderCatalog {
1049
1380
  const apiKey = this.apiKeyFor('openrouter', profileHome, config);
1050
1381
  if (!apiKey)
1051
1382
  return { ok: false, provider, display_name: name, supported: true, status: 'no_key', monthly_budget: budget, message: 'OpenRouter cost history needs an OPENROUTER_API_KEY configured on the server.' };
1052
- const file = join(profileHome, 'cost-snapshots', 'openrouter.json');
1053
- const read = () => {
1054
- try {
1055
- const data = JSON.parse(readFileSync(file, 'utf8'));
1056
- return (Array.isArray(data.snapshots) ? data.snapshots : []).filter(isDict).map((e) => ({ date: str(e.date).trim(), used: typeof e.used === 'number' ? e.used : null, limit: typeof e.limit === 'number' ? e.limit : null })).filter((e) => e.date).sort((a, b) => (a.date < b.date ? -1 : 1));
1057
- }
1058
- catch {
1059
- return [];
1060
- }
1061
- };
1062
- const deltas = (rows) => {
1063
- const window = rows.slice(-days);
1064
- return window.map((e, i) => {
1065
- let delta = null;
1066
- const prev = window[i - 1];
1067
- if (i > 0 && e.used !== null && prev?.used !== null && prev?.used !== undefined) {
1068
- delta = e.used - prev.used;
1069
- if (delta < 0)
1070
- delta = e.used;
1071
- delta = Math.abs(delta) < 1e-9 ? 0 : Math.round(delta * 1e6) / 1e6;
1072
- }
1073
- return { date: e.date, used: e.used, delta };
1074
- });
1075
- };
1076
- const info = await this.fetchOpenRouterKey(apiKey);
1077
- if (info.kind !== 'ok') {
1078
- return { ok: false, provider, display_name: name, supported: true, status: 'unavailable', window_days: days, snapshots: deltas(read()), limit: null, label: null, monthly_budget: budget, message: 'OpenRouter cost history is temporarily unavailable. Showing last known data.' };
1383
+ const info = await this.balance(profileHome, 'openrouter', { api_key: apiKey });
1384
+ if (info?.status !== 'ok' || !info.quota) {
1385
+ return { ok: false, provider, display_name: name, supported: true, status: 'unavailable', window_days: days, ...costHistoryView(this.readCostSnapshots(profileHome), days, budget), limit: null, label: null, monthly_budget: budget, message: 'OpenRouter cost history is temporarily unavailable. Showing last known data.' };
1079
1386
  }
1080
- let snapshots = read();
1387
+ const snapshots = this.recordCostSnapshot(profileHome, info.quota, this.deps.now());
1388
+ return { ok: true, provider, display_name: name, supported: true, status: 'available', window_days: days, ...costHistoryView(snapshots, days, budget), limit: info.quota.limit, label: info.label ?? 'OpenRouter credits', monthly_budget: budget, message: 'OpenRouter cost history loaded.' };
1389
+ }
1390
+ readCostSnapshots(profileHome) {
1391
+ try {
1392
+ const data = JSON.parse(readFileSync(join(profileHome, 'cost-snapshots', 'openrouter.json'), 'utf8'));
1393
+ return (Array.isArray(data.snapshots) ? data.snapshots : []).filter(isDict).map((e) => ({ date: str(e.date).trim(), used: typeof e.used === 'number' ? e.used : null, limit: typeof e.limit === 'number' ? e.limit : null })).filter((e) => e.date).sort((a, b) => (a.date < b.date ? -1 : 1));
1394
+ }
1395
+ catch {
1396
+ return [];
1397
+ }
1398
+ }
1399
+ /** Writes or updates the UTC day's OpenRouter usage snapshot; every successful key read (quota or cost history) records one. */
1400
+ recordCostSnapshot(profileHome, quota, at) {
1401
+ let snapshots = this.readCostSnapshots(profileHome);
1081
1402
  try {
1082
- const today = new Date().toISOString().slice(0, 10);
1083
- const usage = info.quota.usage;
1084
- const limit = info.quota.limit;
1403
+ const today = isoAt(at).slice(0, 10);
1404
+ const used = finite(quota.usage);
1405
+ const limit = finite(quota.limit);
1085
1406
  const existing = snapshots.find((e) => e.date === today);
1086
1407
  if (existing) {
1087
- existing.used = usage;
1408
+ existing.used = used;
1088
1409
  existing.limit = limit;
1089
1410
  }
1090
1411
  else
1091
- snapshots.push({ date: today, used: usage, limit });
1412
+ snapshots.push({ date: today, used, limit });
1092
1413
  snapshots.sort((a, b) => (a.date < b.date ? -1 : 1));
1093
1414
  if (snapshots.length > COST_SNAPSHOT_MAX_DAYS)
1094
1415
  snapshots = snapshots.slice(-COST_SNAPSHOT_MAX_DAYS);
1095
1416
  mkdirSync(join(profileHome, 'cost-snapshots'), { recursive: true });
1096
- atomicWriteText(file, JSON.stringify({ provider, snapshots }, null, 2));
1417
+ atomicWriteText(join(profileHome, 'cost-snapshots', 'openrouter.json'), JSON.stringify({ provider: 'openrouter', snapshots }, null, 2));
1418
+ return snapshots;
1097
1419
  }
1098
1420
  catch {
1099
- snapshots = read();
1421
+ return this.readCostSnapshots(profileHome);
1100
1422
  }
1101
- return { ok: true, provider, display_name: name, supported: true, status: 'available', window_days: days, snapshots: deltas(snapshots), limit: info.quota.limit, label: info.label ?? 'OpenRouter credits', monthly_budget: budget, message: 'OpenRouter cost history loaded.' };
1102
1423
  }
1103
1424
  }
1104
1425
  export const envFileExists = (profileHome) => existsSync(join(profileHome, '.env'));