@maudecode/talaria-web 1.18.0 → 1.19.0

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 (567) hide show
  1. package/_release.json +8 -8
  2. package/dist/api/auth-raw.js +16 -16
  3. package/dist/api/auth-raw.js.map +1 -1
  4. package/dist/api/automation-raw.d.ts +2 -2
  5. package/dist/api/automation-raw.js +32 -32
  6. package/dist/api/automation-raw.js.map +1 -1
  7. package/dist/api/automation-router.d.ts +102 -22
  8. package/dist/api/automation-router.js +15 -13
  9. package/dist/api/automation-router.js.map +1 -1
  10. package/dist/api/chat-router.d.ts +1205 -34
  11. package/dist/api/chat-router.js +158 -78
  12. package/dist/api/chat-router.js.map +1 -1
  13. package/dist/api/git-router.d.ts +2 -2
  14. package/dist/api/git-router.js +29 -29
  15. package/dist/api/git-router.js.map +1 -1
  16. package/dist/api/raw-routes.d.ts +3 -3
  17. package/dist/api/raw-routes.js +63 -61
  18. package/dist/api/raw-routes.js.map +1 -1
  19. package/dist/api/router.d.ts +6 -6
  20. package/dist/api/router.js +22 -22
  21. package/dist/api/router.js.map +1 -1
  22. package/dist/api/session-visibility.d.ts +4 -4
  23. package/dist/api/session-visibility.js +26 -16
  24. package/dist/api/session-visibility.js.map +1 -1
  25. package/dist/api/sessions-router.d.ts +1928 -124
  26. package/dist/api/sessions-router.js +177 -173
  27. package/dist/api/sessions-router.js.map +1 -1
  28. package/dist/api/settings-router.d.ts +427 -9
  29. package/dist/api/settings-router.js +48 -32
  30. package/dist/api/settings-router.js.map +1 -1
  31. package/dist/api/sse-routes.d.ts +2 -2
  32. package/dist/api/sse-routes.js +63 -63
  33. package/dist/api/sse-routes.js.map +1 -1
  34. package/dist/api/tools-raw.d.ts +5 -0
  35. package/dist/api/tools-raw.js +58 -33
  36. package/dist/api/tools-raw.js.map +1 -1
  37. package/dist/api/tools-router.d.ts +663 -91
  38. package/dist/api/tools-router.js +309 -52
  39. package/dist/api/tools-router.js.map +1 -1
  40. package/dist/api/transcribe-stream.d.ts +16 -0
  41. package/dist/api/transcribe-stream.js +172 -0
  42. package/dist/api/transcribe-stream.js.map +1 -0
  43. package/dist/app.d.ts +4 -1
  44. package/dist/app.js +66 -47
  45. package/dist/app.js.map +1 -1
  46. package/dist/auth/gate.js +10 -9
  47. package/dist/auth/gate.js.map +1 -1
  48. package/dist/auth/passkeys.d.ts +15 -14
  49. package/dist/auth/passkeys.js +89 -91
  50. package/dist/auth/passkeys.js.map +1 -1
  51. package/dist/auth/store.d.ts +17 -7
  52. package/dist/auth/store.js +93 -52
  53. package/dist/auth/store.js.map +1 -1
  54. package/dist/bin/talaria-web.js +12 -81
  55. package/dist/bin/talaria-web.js.map +1 -1
  56. package/dist/cli/ctl.js +2 -1
  57. package/dist/cli/ctl.js.map +1 -1
  58. package/dist/cli/dotenv.d.ts +5 -0
  59. package/dist/cli/dotenv.js +14 -2
  60. package/dist/cli/dotenv.js.map +1 -1
  61. package/dist/cli/launcher.js +5 -5
  62. package/dist/cli/launcher.js.map +1 -1
  63. package/dist/cli/main.d.ts +1 -0
  64. package/dist/cli/main.js +85 -0
  65. package/dist/cli/main.js.map +1 -0
  66. package/dist/cli/supervise.d.ts +2 -0
  67. package/dist/cli/supervise.js +67 -4
  68. package/dist/cli/supervise.js.map +1 -1
  69. package/dist/config/agent-config.d.ts +34 -11
  70. package/dist/config/agent-config.js +113 -58
  71. package/dist/config/agent-config.js.map +1 -1
  72. package/dist/config.d.ts +2 -0
  73. package/dist/config.js +14 -5
  74. package/dist/config.js.map +1 -1
  75. package/dist/fs/atomic.d.ts +1 -0
  76. package/dist/fs/atomic.js +4 -1
  77. package/dist/fs/atomic.js.map +1 -1
  78. package/dist/fs/stat.d.ts +1 -0
  79. package/dist/fs/stat.js +11 -0
  80. package/dist/fs/stat.js.map +1 -0
  81. package/dist/http/context.d.ts +14 -9
  82. package/dist/http/context.js +29 -20
  83. package/dist/http/context.js.map +1 -1
  84. package/dist/onboarding.d.ts +2 -2
  85. package/dist/onboarding.js +10 -8
  86. package/dist/onboarding.js.map +1 -1
  87. package/dist/profiles/profiles.d.ts +10 -4
  88. package/dist/profiles/profiles.js +77 -30
  89. package/dist/profiles/profiles.js.map +1 -1
  90. package/dist/projects.d.ts +15 -6
  91. package/dist/projects.js +55 -47
  92. package/dist/projects.js.map +1 -1
  93. package/dist/providers/catalog.d.ts +78 -12
  94. package/dist/providers/catalog.js +445 -141
  95. package/dist/providers/catalog.js.map +1 -1
  96. package/dist/providers/env-file.d.ts +10 -3
  97. package/dist/providers/env-file.js +24 -75
  98. package/dist/providers/env-file.js.map +1 -1
  99. package/dist/providers/runtime-env.d.ts +4 -2
  100. package/dist/providers/runtime-env.js +9 -6
  101. package/dist/providers/runtime-env.js.map +1 -1
  102. package/dist/providers/tables.d.ts +3 -1
  103. package/dist/providers/tables.js +4 -2
  104. package/dist/providers/tables.js.map +1 -1
  105. package/dist/redact.js +18 -9
  106. package/dist/redact.js.map +1 -1
  107. package/dist/release.d.ts +7 -0
  108. package/dist/release.js +14 -0
  109. package/dist/release.js.map +1 -1
  110. package/dist/runtime.d.ts +5 -1
  111. package/dist/runtime.js +233 -90
  112. package/dist/runtime.js.map +1 -1
  113. package/dist/server.js +10 -0
  114. package/dist/server.js.map +1 -1
  115. package/dist/sessions/anchor.d.ts +9 -3
  116. package/dist/sessions/anchor.js +28 -36
  117. package/dist/sessions/anchor.js.map +1 -1
  118. package/dist/sessions/background-events.d.ts +2 -0
  119. package/dist/sessions/background-events.js +10 -0
  120. package/dist/sessions/background-events.js.map +1 -0
  121. package/dist/sessions/background-tasks.d.ts +26 -20
  122. package/dist/sessions/background-tasks.js +88 -86
  123. package/dist/sessions/background-tasks.js.map +1 -1
  124. package/dist/sessions/background-updates.d.ts +15 -8
  125. package/dist/sessions/background-updates.js +101 -25
  126. package/dist/sessions/background-updates.js.map +1 -1
  127. package/dist/sessions/cli-sessions.d.ts +15 -5
  128. package/dist/sessions/cli-sessions.js +62 -43
  129. package/dist/sessions/cli-sessions.js.map +1 -1
  130. package/dist/sessions/completions.d.ts +30 -2
  131. package/dist/sessions/completions.js +294 -53
  132. package/dist/sessions/completions.js.map +1 -1
  133. package/dist/sessions/compress.d.ts +8 -0
  134. package/dist/sessions/compress.js +2 -2
  135. package/dist/sessions/compress.js.map +1 -1
  136. package/dist/sessions/drafts.d.ts +14 -10
  137. package/dist/sessions/drafts.js +38 -43
  138. package/dist/sessions/drafts.js.map +1 -1
  139. package/dist/sessions/gateway-sessions.d.ts +46 -0
  140. package/dist/sessions/gateway-sessions.js +378 -0
  141. package/dist/sessions/gateway-sessions.js.map +1 -0
  142. package/dist/sessions/goals.d.ts +31 -0
  143. package/dist/sessions/goals.js +75 -0
  144. package/dist/sessions/goals.js.map +1 -0
  145. package/dist/sessions/journal.d.ts +5 -79
  146. package/dist/sessions/journal.js +15 -606
  147. package/dist/sessions/journal.js.map +1 -1
  148. package/dist/sessions/list.d.ts +27 -18
  149. package/dist/sessions/list.js +133 -131
  150. package/dist/sessions/list.js.map +1 -1
  151. package/dist/sessions/merge.d.ts +68 -10
  152. package/dist/sessions/merge.js +259 -51
  153. package/dist/sessions/merge.js.map +1 -1
  154. package/dist/sessions/notices.d.ts +22 -0
  155. package/dist/sessions/notices.js +72 -0
  156. package/dist/sessions/notices.js.map +1 -0
  157. package/dist/sessions/pending.d.ts +10 -4
  158. package/dist/sessions/pending.js +83 -7
  159. package/dist/sessions/pending.js.map +1 -1
  160. package/dist/sessions/prefix-index.d.ts +82 -0
  161. package/dist/sessions/prefix-index.js +232 -0
  162. package/dist/sessions/prefix-index.js.map +1 -0
  163. package/dist/sessions/recovery.d.ts +4 -7
  164. package/dist/sessions/recovery.js +97 -281
  165. package/dist/sessions/recovery.js.map +1 -1
  166. package/dist/sessions/relay.d.ts +20 -20
  167. package/dist/sessions/relay.js +205 -127
  168. package/dist/sessions/relay.js.map +1 -1
  169. package/dist/sessions/service.d.ts +162 -59
  170. package/dist/sessions/service.js +1133 -521
  171. package/dist/sessions/service.js.map +1 -1
  172. package/dist/sessions/session.d.ts +18 -2
  173. package/dist/sessions/session.js +29 -5
  174. package/dist/sessions/session.js.map +1 -1
  175. package/dist/sessions/shares.d.ts +7 -6
  176. package/dist/sessions/shares.js +22 -45
  177. package/dist/sessions/shares.js.map +1 -1
  178. package/dist/sessions/source-kind.js +1 -0
  179. package/dist/sessions/source-kind.js.map +1 -1
  180. package/dist/sessions/state-db-identity.d.ts +18 -0
  181. package/dist/sessions/state-db-identity.js +67 -0
  182. package/dist/sessions/state-db-identity.js.map +1 -0
  183. package/dist/sessions/state-db-worker.js +3 -1
  184. package/dist/sessions/state-db-worker.js.map +1 -1
  185. package/dist/sessions/state-db.d.ts +53 -8
  186. package/dist/sessions/state-db.js +576 -50
  187. package/dist/sessions/state-db.js.map +1 -1
  188. package/dist/sessions/storage/backend.d.ts +162 -0
  189. package/dist/sessions/storage/backend.js +19 -0
  190. package/dist/sessions/storage/backend.js.map +1 -0
  191. package/dist/sessions/storage/conformance.d.ts +20 -0
  192. package/dist/sessions/storage/conformance.js +237 -0
  193. package/dist/sessions/storage/conformance.js.map +1 -0
  194. package/dist/sessions/storage/document.d.ts +16 -0
  195. package/dist/sessions/storage/document.js +44 -0
  196. package/dist/sessions/storage/document.js.map +1 -0
  197. package/dist/sessions/storage/json-documents.d.ts +23 -0
  198. package/dist/sessions/storage/json-documents.js +233 -0
  199. package/dist/sessions/storage/json-documents.js.map +1 -0
  200. package/dist/sessions/storage/json-files.d.ts +12 -0
  201. package/dist/sessions/storage/json-files.js +124 -0
  202. package/dist/sessions/storage/json-files.js.map +1 -0
  203. package/dist/sessions/storage/json-installation.d.ts +3 -0
  204. package/dist/sessions/storage/json-installation.js +31 -0
  205. package/dist/sessions/storage/json-installation.js.map +1 -0
  206. package/dist/sessions/storage/json-journal.d.ts +114 -0
  207. package/dist/sessions/storage/json-journal.js +686 -0
  208. package/dist/sessions/storage/json-journal.js.map +1 -0
  209. package/dist/sessions/storage/json-recovery.d.ts +4 -0
  210. package/dist/sessions/storage/json-recovery.js +195 -0
  211. package/dist/sessions/storage/json-recovery.js.map +1 -0
  212. package/dist/sessions/storage/json.d.ts +68 -0
  213. package/dist/sessions/storage/json.js +258 -0
  214. package/dist/sessions/storage/json.js.map +1 -0
  215. package/dist/sessions/storage/row.d.ts +6 -0
  216. package/dist/sessions/storage/row.js +49 -0
  217. package/dist/sessions/storage/row.js.map +1 -0
  218. package/dist/sessions/storage/search-text.d.ts +4 -0
  219. package/dist/sessions/storage/search-text.js +16 -0
  220. package/dist/sessions/storage/search-text.js.map +1 -0
  221. package/dist/sessions/store.d.ts +63 -73
  222. package/dist/sessions/store.js +305 -611
  223. package/dist/sessions/store.js.map +1 -1
  224. package/dist/sessions/streams.d.ts +4 -0
  225. package/dist/sessions/streams.js +9 -1
  226. package/dist/sessions/streams.js.map +1 -1
  227. package/dist/sessions/titles.js +1 -1
  228. package/dist/sessions/titles.js.map +1 -1
  229. package/dist/sessions/todo.d.ts +5 -0
  230. package/dist/sessions/todo.js +28 -21
  231. package/dist/sessions/todo.js.map +1 -1
  232. package/dist/sessions/tool-display.d.ts +5 -4
  233. package/dist/sessions/tool-display.js +22 -16
  234. package/dist/sessions/tool-display.js.map +1 -1
  235. package/dist/sessions/turn-context.d.ts +10 -1
  236. package/dist/sessions/turn-context.js +7 -4
  237. package/dist/sessions/turn-context.js.map +1 -1
  238. package/dist/sessions/turn.d.ts +87 -17
  239. package/dist/sessions/turn.js +795 -347
  240. package/dist/sessions/turn.js.map +1 -1
  241. package/dist/sessions/wakeup-pause.d.ts +8 -13
  242. package/dist/sessions/wakeup-pause.js +31 -49
  243. package/dist/sessions/wakeup-pause.js.map +1 -1
  244. package/dist/sessions/window.js +13 -12
  245. package/dist/sessions/window.js.map +1 -1
  246. package/dist/settings.d.ts +19 -7
  247. package/dist/settings.js +179 -183
  248. package/dist/settings.js.map +1 -1
  249. package/dist/sidecar/client.d.ts +17 -3
  250. package/dist/sidecar/client.js +78 -8
  251. package/dist/sidecar/client.js.map +1 -1
  252. package/dist/sidecar/discover.d.ts +2 -1
  253. package/dist/sidecar/discover.js +93 -10
  254. package/dist/sidecar/discover.js.map +1 -1
  255. package/dist/sidecar/env.d.ts +4 -0
  256. package/dist/sidecar/env.js +3 -0
  257. package/dist/sidecar/env.js.map +1 -0
  258. package/dist/sidecar/fake.js +19 -2
  259. package/dist/sidecar/fake.js.map +1 -1
  260. package/dist/test/env-file.d.ts +5 -0
  261. package/dist/test/env-file.js +88 -0
  262. package/dist/test/env-file.js.map +1 -0
  263. package/dist/test/harness.d.ts +1 -0
  264. package/dist/test/harness.js +1 -1
  265. package/dist/test/harness.js.map +1 -1
  266. package/dist/test/passkeys.d.ts +22 -0
  267. package/dist/test/passkeys.js +62 -0
  268. package/dist/test/passkeys.js.map +1 -0
  269. package/dist/test/storage.d.ts +15 -0
  270. package/dist/test/storage.js +26 -0
  271. package/dist/test/storage.js.map +1 -0
  272. package/dist/tools/commands.d.ts +6 -1
  273. package/dist/tools/commands.js +8 -2
  274. package/dist/tools/commands.js.map +1 -1
  275. package/dist/tools/connectors.d.ts +33 -0
  276. package/dist/tools/connectors.js +121 -0
  277. package/dist/tools/connectors.js.map +1 -0
  278. package/dist/tools/crons.d.ts +38 -1
  279. package/dist/tools/crons.js +82 -15
  280. package/dist/tools/crons.js.map +1 -1
  281. package/dist/tools/dashboard-plugins.d.ts +5 -5
  282. package/dist/tools/dashboard-plugins.js +14 -14
  283. package/dist/tools/dashboard-plugins.js.map +1 -1
  284. package/dist/tools/extensions.d.ts +5 -4
  285. package/dist/tools/extensions.js +213 -207
  286. package/dist/tools/extensions.js.map +1 -1
  287. package/dist/tools/gateway-restart.d.ts +62 -0
  288. package/dist/tools/gateway-restart.js +142 -0
  289. package/dist/tools/gateway-restart.js.map +1 -0
  290. package/dist/tools/health.d.ts +15 -3
  291. package/dist/tools/health.js +97 -34
  292. package/dist/tools/health.js.map +1 -1
  293. package/dist/tools/hygiene.d.ts +4 -3
  294. package/dist/tools/hygiene.js +23 -9
  295. package/dist/tools/hygiene.js.map +1 -1
  296. package/dist/tools/kanban.d.ts +4 -2
  297. package/dist/tools/kanban.js +28 -19
  298. package/dist/tools/kanban.js.map +1 -1
  299. package/dist/tools/memory-masks.d.ts +5 -0
  300. package/dist/tools/memory-masks.js +256 -0
  301. package/dist/tools/memory-masks.js.map +1 -0
  302. package/dist/tools/memory.d.ts +29 -3
  303. package/dist/tools/memory.js +69 -27
  304. package/dist/tools/memory.js.map +1 -1
  305. package/dist/tools/plugin-settings.d.ts +15 -0
  306. package/dist/tools/plugin-settings.js +90 -0
  307. package/dist/tools/plugin-settings.js.map +1 -0
  308. package/dist/tools/skills.d.ts +9 -4
  309. package/dist/tools/skills.js +44 -13
  310. package/dist/tools/skills.js.map +1 -1
  311. package/dist/tools/update-notifications.d.ts +34 -15
  312. package/dist/tools/update-notifications.js +141 -57
  313. package/dist/tools/update-notifications.js.map +1 -1
  314. package/dist/tools/update-rollback.d.ts +11 -0
  315. package/dist/tools/update-rollback.js +28 -0
  316. package/dist/tools/update-rollback.js.map +1 -0
  317. package/dist/tools/updates.d.ts +93 -33
  318. package/dist/tools/updates.js +1051 -345
  319. package/dist/tools/updates.js.map +1 -1
  320. package/dist/util.d.ts +2 -0
  321. package/dist/util.js +5 -0
  322. package/dist/util.js.map +1 -1
  323. package/dist/workspace/git.d.ts +16 -1
  324. package/dist/workspace/git.js +291 -11
  325. package/dist/workspace/git.js.map +1 -1
  326. package/dist/workspace/media.d.ts +12 -9
  327. package/dist/workspace/media.js +31 -20
  328. package/dist/workspace/media.js.map +1 -1
  329. package/dist/workspace/rollback.d.ts +2 -2
  330. package/dist/workspace/rollback.js +5 -5
  331. package/dist/workspace/rollback.js.map +1 -1
  332. package/dist/workspace/workspaces.d.ts +15 -19
  333. package/dist/workspace/workspaces.js +55 -127
  334. package/dist/workspace/workspaces.js.map +1 -1
  335. package/dist/workspace/worktrees.js +170 -32
  336. package/dist/workspace/worktrees.js.map +1 -1
  337. package/package.json +5 -3
  338. package/sidecar/agent_dependency.json +4 -4
  339. package/sidecar/agent_support.json +26 -0
  340. package/sidecar/scripts/record-fixtures.py +12 -3
  341. package/sidecar/scripts/replay_sidecar.py +8 -0
  342. package/sidecar/scripts/suite_runner.py +302 -0
  343. package/sidecar/scripts/test.sh +12 -4
  344. package/sidecar/talaria_sidecar/__init__.py +6 -1
  345. package/sidecar/talaria_sidecar/__main__.py +4 -2
  346. package/sidecar/talaria_sidecar/agent_mutation.py +975 -0
  347. package/sidecar/talaria_sidecar/agent_services.py +1053 -0
  348. package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
  349. package/sidecar/talaria_sidecar/home.py +43 -1
  350. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  351. package/sidecar/talaria_sidecar/methods/aux.py +139 -31
  352. package/sidecar/talaria_sidecar/methods/chat.py +847 -134
  353. package/sidecar/talaria_sidecar/methods/commands.py +213 -4
  354. package/sidecar/talaria_sidecar/methods/config.py +123 -4
  355. package/sidecar/talaria_sidecar/methods/connectors.py +343 -0
  356. package/sidecar/talaria_sidecar/methods/cron.py +366 -34
  357. package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
  358. package/sidecar/talaria_sidecar/methods/gateway.py +194 -54
  359. package/sidecar/talaria_sidecar/methods/gateway_sessions.py +234 -0
  360. package/sidecar/talaria_sidecar/methods/goals.py +81 -41
  361. package/sidecar/talaria_sidecar/methods/kanban.py +80 -27
  362. package/sidecar/talaria_sidecar/methods/memory.py +84 -0
  363. package/sidecar/talaria_sidecar/methods/oauth.py +5 -2
  364. package/sidecar/talaria_sidecar/methods/process.py +160 -11
  365. package/sidecar/talaria_sidecar/methods/profiles.py +44 -6
  366. package/sidecar/talaria_sidecar/methods/providers.py +196 -26
  367. package/sidecar/talaria_sidecar/methods/runtime.py +69 -1
  368. package/sidecar/talaria_sidecar/methods/skills.py +67 -4
  369. package/sidecar/talaria_sidecar/methods/state_db.py +277 -70
  370. package/sidecar/talaria_sidecar/methods/stt.py +159 -78
  371. package/sidecar/talaria_sidecar/methods/text.py +14 -1
  372. package/sidecar/talaria_sidecar/methods/toolsets.py +49 -0
  373. package/sidecar/talaria_sidecar/methods/usage.py +113 -14
  374. package/sidecar/talaria_sidecar/methods/worktree.py +26 -19
  375. package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
  376. package/sidecar/talaria_sidecar/rpc.py +45 -4
  377. package/sidecar/talaria_sidecar/runtime.py +81 -12
  378. package/sidecar/talaria_sidecar/speed.py +35 -0
  379. package/static/dist/FILES.txt +126 -127
  380. package/static/dist/assets/AppShell-cey33v9s.js +8 -0
  381. package/static/dist/assets/ChatPage-i8wnugr7.js +18 -0
  382. package/static/dist/assets/ConnectTargets-hg19553x.js +1 -0
  383. package/static/dist/assets/ExtensionRoute-cly4apm5.js +1 -0
  384. package/static/dist/assets/Field-j8uqoigz.js +9 -0
  385. package/static/dist/assets/HubRoute-e4bgz41j.js +2 -0
  386. package/static/dist/assets/Markdown-lahuichm.js +598 -0
  387. package/static/dist/assets/RightPanel-lbo4i0fn.js +1 -0
  388. package/static/dist/assets/Select-gqatm2e0.js +1 -0
  389. package/static/dist/assets/SessionListPanel-mfqayfjh.js +1 -0
  390. package/static/dist/assets/SettingsLayout-b8980u7k.js +1 -0
  391. package/static/dist/assets/States-e7dkgtxf.js +1 -0
  392. package/static/dist/assets/Toaster-bfqqy786.js +1 -0
  393. package/static/dist/assets/_app-n1n6pfx1.js +1 -0
  394. package/static/dist/assets/{_app.ext._extensionId-gfbzpoou.js → _app.ext._extensionId-buxjxav3.js} +1 -1
  395. package/static/dist/assets/_app.index-jt8zwiu2.js +1 -0
  396. package/static/dist/assets/_app.insights-cv76ml71.js +1 -0
  397. package/static/dist/assets/_app.kanban-h6ts53j7.js +1 -0
  398. package/static/dist/assets/_app.logs-dm873z90.js +1 -0
  399. package/static/dist/assets/_app.memory-glk5y6m8.js +1 -0
  400. package/static/dist/assets/_app.profiles-ka6zpstq.js +1 -0
  401. package/static/dist/assets/{_app.session._sessionId-fn1bhde6.js → _app.session._sessionId-g2vzghvc.js} +1 -1
  402. package/static/dist/assets/_app.settings-hgjdhzcd.js +1 -0
  403. package/static/dist/assets/_app.settings._section-jf2kmhae.js +3 -0
  404. package/static/dist/assets/_app.skills-cii852yq.js +1 -0
  405. package/static/dist/assets/_app.tasks-fp9q7atm.js +1 -0
  406. package/static/dist/assets/_app.todos-dw94qzue.js +1 -0
  407. package/static/dist/assets/_app.workspaces-inyygzze.js +1 -0
  408. package/static/dist/assets/{abnfDiagram-VCTEODGH-c8kdixke.js → abnfDiagram-VCTEODGH-gjayq3ra.js} +1 -1
  409. package/static/dist/assets/architecture-7GRP2DOG-lp56972r.js +1 -0
  410. package/static/dist/assets/{architectureDiagram-5GKGNRK7-cku06x1p.js → architectureDiagram-5GKGNRK7-nwb3xovn.js} +1 -1
  411. package/static/dist/assets/archive-b1j004vn.js +1 -0
  412. package/static/dist/assets/back-g74gkozu.js +1 -0
  413. package/static/dist/assets/{blockDiagram-I7D4REHJ-jzthp1wa.js → blockDiagram-I7D4REHJ-joedghi3.js} +1 -1
  414. package/static/dist/assets/{c4Diagram-7LVT6UL2-mwr67mt8.js → c4Diagram-7LVT6UL2-hyrjrohe.js} +1 -1
  415. package/static/dist/assets/channel-es95o4y8.js +1 -0
  416. package/static/dist/assets/{chunk-4HAMMTFA-gpq1y228.js → chunk-4HAMMTFA-koouqbut.js} +1 -1
  417. package/static/dist/assets/{chunk-75Z2AOVW-ktje58c4.js → chunk-75Z2AOVW-bl4bhdnt.js} +1 -1
  418. package/static/dist/assets/{chunk-DU6HZSFF-ofajdixh.js → chunk-DU6HZSFF-gpedttsp.js} +3 -3
  419. package/static/dist/assets/{chunk-F27PBJKO-cmev85ti.js → chunk-F27PBJKO-d8xxi0zi.js} +1 -1
  420. package/static/dist/assets/{chunk-GMAD6QVW-htjgt22y.js → chunk-GMAD6QVW-jro0hl73.js} +1 -1
  421. package/static/dist/assets/{chunk-GVQU2GXP-qndvoxl7.js → chunk-GVQU2GXP-djn6gbyu.js} +1 -1
  422. package/static/dist/assets/{chunk-IMKFNOWR-e5bkd5q6.js → chunk-IMKFNOWR-vw4p5p7o.js} +1 -1
  423. package/static/dist/assets/{chunk-L3NEJ4N5-mj1155r6.js → chunk-L3NEJ4N5-n1hj77q8.js} +1 -1
  424. package/static/dist/assets/{chunk-OSK3NFVY-e7gkikcw.js → chunk-OSK3NFVY-oy0gwn2n.js} +1 -1
  425. package/static/dist/assets/{chunk-P2QGCYS3-f7t54xfm.js → chunk-P2QGCYS3-onf1hz6c.js} +1 -1
  426. package/static/dist/assets/{chunk-POPQ4Y6H-i927v9oa.js → chunk-POPQ4Y6H-m8sqn7cs.js} +1 -1
  427. package/static/dist/assets/{chunk-PWAF6VOD-m84lmnyt.js → chunk-PWAF6VOD-ek4smc8x.js} +1 -1
  428. package/static/dist/assets/{chunk-SHT3W25Y-mi0tlcu9.js → chunk-SHT3W25Y-g9gfvefo.js} +3 -3
  429. package/static/dist/assets/{chunk-SVP7TREG-dnau702j.js → chunk-SVP7TREG-f9nfv2cu.js} +1 -1
  430. package/static/dist/assets/{chunk-TICWLB2K-edem1gkg.js → chunk-TICWLB2K-bnk3m1mq.js} +1 -1
  431. package/static/dist/assets/classDiagram-ZZMXUADV-kmihoqxh.js +1 -0
  432. package/static/dist/assets/classDiagram-v2-VYDZK3BY-kmihoqxh.js +1 -0
  433. package/static/dist/assets/copied-bli9tbcx.js +1 -0
  434. package/static/dist/assets/{copy-magtofyp.js → copy-jamtd1pf.js} +1 -1
  435. package/static/dist/assets/{cynefin-OW5HDTMX-siqlulkl.js → cynefin-OW5HDTMX-f8xfdd3f.js} +1 -1
  436. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-i648tah9.js → cynefinDiagram-5FMLGOSQ-n9lb99h9.js} +1 -1
  437. package/static/dist/assets/{dagre-GXQ25YYZ-mwn6uc17.js → dagre-GXQ25YYZ-dgailddi.js} +4 -4
  438. package/static/dist/assets/{diagram-S7CK7UJ4-l8abxaes.js → diagram-S7CK7UJ4-m6aeoe7c.js} +1 -1
  439. package/static/dist/assets/{diagram-UQ7AKVKN-mekq7zgq.js → diagram-UQ7AKVKN-jall3j0x.js} +1 -1
  440. package/static/dist/assets/{diagram-VSXAHHWV-fnky6lvc.js → diagram-VSXAHHWV-hbawi7hn.js} +1 -1
  441. package/static/dist/assets/{diagram-VX7I27RA-xe6r5qfw.js → diagram-VX7I27RA-hendl2tt.js} +1 -1
  442. package/static/dist/assets/{diagram-Z3DM3KII-nq07dg7k.js → diagram-Z3DM3KII-igwc35p3.js} +1 -1
  443. package/static/dist/assets/{ebnfDiagram-PWID7BFC-c62ipm3g.js → ebnfDiagram-PWID7BFC-c64knz08.js} +1 -1
  444. package/static/dist/assets/{erDiagram-RLTQ6QDP-h39kmnzy.js → erDiagram-RLTQ6QDP-brdu9lcm.js} +1 -1
  445. package/static/dist/assets/eventmodeling-NTZA5JFV-njk1j8wl.js +1 -0
  446. package/static/dist/assets/flowDiagram-HODETNUW-nma6f0ga.js +1 -0
  447. package/static/dist/assets/folder-plus-lvt9kudb.js +1 -0
  448. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ngbsyp37.js → ganttDiagram-EL5Y4UJY-ma6jxj5i.js} +1 -1
  449. package/static/dist/assets/{gitGraph-4MIJSDKK-fr2i7caa.js → gitGraph-4MIJSDKK-naa9w6xa.js} +1 -1
  450. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-eravdti4.js → gitGraphDiagram-WWUBYQGX-hzltvsod.js} +1 -1
  451. package/static/dist/assets/{highlighted-body-KPVGNVTW-jwpop9p8.js → highlighted-body-KPVGNVTW-ghoavj8d.js} +1 -1
  452. package/static/dist/assets/host-hzm8n85n.js +1 -0
  453. package/static/dist/assets/index-drdcaqeq.css +2 -0
  454. package/static/dist/assets/index-jzm844he.js +11 -0
  455. package/static/dist/assets/{info-A6RAGUB7-h11x0dxo.js → info-A6RAGUB7-mn2ut5ko.js} +1 -1
  456. package/static/dist/assets/{infoDiagram-27XIBGKW-dfh47dw9.js → infoDiagram-27XIBGKW-h6kuqvsb.js} +1 -1
  457. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-i6ypq351.js → ishikawaDiagram-5VMMS53U-e7bnwq7h.js} +1 -1
  458. package/static/dist/assets/{journeyDiagram-3NMN7TZE-lduf1z95.js → journeyDiagram-3NMN7TZE-etd1bi6p.js} +1 -1
  459. package/static/dist/assets/{kanban-definition-UXKFOSKX-e18bu4pg.js → kanban-definition-UXKFOSKX-emb1g9dz.js} +4 -4
  460. package/static/dist/assets/lazyRouteComponent-o816ytzl.js +1 -0
  461. package/static/dist/assets/{line-meizqsvn.js → line-h0ojzsvs.js} +1 -1
  462. package/static/dist/assets/link-k01ryaf9.js +1 -0
  463. package/static/dist/assets/login-bsae49ew.js +1 -0
  464. package/static/dist/assets/mermaid-HWGCJPDP-ihl34s74.js +1 -0
  465. package/static/dist/assets/{mermaid-parser.core-ntne45i9.js → mermaid-parser.core-bnre27u8.js} +3 -3
  466. package/static/dist/assets/{mindmap-definition-YA3MSWOX-27b26eok.js → mindmap-definition-YA3MSWOX-o1vathns.js} +1 -1
  467. package/static/dist/assets/onboarding-c65tysya.js +1 -0
  468. package/static/dist/assets/onboarding_model_label-gpn28uix.js +1 -0
  469. package/static/dist/assets/{packet-AYTQ26CC-l6jdka8p.js → packet-AYTQ26CC-hatq7m2e.js} +1 -1
  470. package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
  471. package/static/dist/assets/{pegDiagram-XKGWAZYB-g1er3bxj.js → pegDiagram-XKGWAZYB-gwisz8jv.js} +1 -1
  472. package/static/dist/assets/{pie-WAS4IAKB-i0ylcrt9.js → pie-WAS4IAKB-fpftf0mk.js} +1 -1
  473. package/static/dist/assets/{pieDiagram-E7YTZNPT-fbz1e55z.js → pieDiagram-E7YTZNPT-khm1o9a6.js} +1 -1
  474. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-hhue7d8o.js → quadrantDiagram-AXDQQJYC-owqc4uqn.js} +1 -1
  475. package/static/dist/assets/{radar-RG4KPBEZ-f9b70aa4.js → radar-RG4KPBEZ-cntfwjpq.js} +1 -1
  476. package/static/dist/assets/{railroad-74A4TZTK-lgsuoeda.js → railroad-74A4TZTK-a7t5eibp.js} +1 -1
  477. package/static/dist/assets/railroad-abnf-HS5TGJTU-ol8cdktf.js +1 -0
  478. package/static/dist/assets/railroad-ebnf-LZEXJU2U-i8vzd429.js +1 -0
  479. package/static/dist/assets/railroad-peg-WCYAUIDC-nw3ulewz.js +1 -0
  480. package/static/dist/assets/{railroadDiagram-O6MQD6OU-eqpy9fnz.js → railroadDiagram-O6MQD6OU-etuh0qa0.js} +1 -1
  481. package/static/dist/assets/{refresh-cw-hlwb02qv.js → refresh-cw-mqbkv2a3.js} +1 -1
  482. package/static/dist/assets/refresh-l29ljp76.js +1 -0
  483. package/static/dist/assets/{requirementDiagram-BXWQKSXE-ogf6puhi.js → requirementDiagram-BXWQKSXE-oq2ulk22.js} +1 -1
  484. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-fqb8xcjf.js → sankeyDiagram-P5KCCOFB-iqc02emv.js} +1 -1
  485. package/static/dist/assets/saved-ndd4t2xb.js +1 -0
  486. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-dlcz6qjz.js → sequenceDiagram-WJ2MYXX4-i0v116q0.js} +2 -2
  487. package/static/dist/assets/settings_section_extensions_title-jhfo6o8z.js +1 -0
  488. package/static/dist/assets/share._token-c1ri0hy1.js +2 -0
  489. package/static/dist/assets/sliders-horizontal-j3jjt9tv.js +1 -0
  490. package/static/dist/assets/{stateDiagram-D77RDMKH-fyoznpcs.js → stateDiagram-D77RDMKH-dnpt1ato.js} +1 -1
  491. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-bxjacpk6.js +1 -0
  492. package/static/dist/assets/{swimlanes-42K2YHIH-oefldhfy.js → swimlanes-42K2YHIH-loqbdazb.js} +1 -1
  493. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-id62p9hz.js +8 -0
  494. package/static/dist/assets/{timeline-definition-24CTP7MA-b1vl2zil.js → timeline-definition-24CTP7MA-ol2hnvma.js} +1 -1
  495. package/static/dist/assets/toast-i3h34v6o.js +64 -0
  496. package/static/dist/assets/{treeView-Q6P3EWNA-f7hcci5o.js → treeView-Q6P3EWNA-idrj6iox.js} +1 -1
  497. package/static/dist/assets/{treemap-WGGIJYW6-nl85071a.js → treemap-WGGIJYW6-lzr49vv5.js} +1 -1
  498. package/static/dist/assets/untitled-oaoya4qj.js +1 -0
  499. package/static/dist/assets/useForm-nfaxgmsk.js +1 -0
  500. package/static/dist/assets/{useParams-b4dq6n4q.js → useParams-gjc70qru.js} +1 -1
  501. package/static/dist/assets/{useSearch-ipfmv3he.js → useSearch-e6ae539u.js} +1 -1
  502. package/static/dist/assets/{vennDiagram-4TSXK5OY-h5hzbk0n.js → vennDiagram-4TSXK5OY-bvwr9o2z.js} +1 -1
  503. package/static/dist/assets/{wardley-WFR3VGLG-jftvpm3e.js → wardley-WFR3VGLG-31ui8tma.js} +1 -1
  504. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-hedh85ge.js → wardleyDiagram-VM6X3IG4-k5wqtx4b.js} +1 -1
  505. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-e49523ab.js → xychartDiagram-S5SC5T6Z-aqbgvujg.js} +1 -1
  506. package/static/dist/index.html +1 -1
  507. package/static/dist/sw.js +1 -1
  508. package/static/dist/assets/AppShell-g3bkgnvt.js +0 -1
  509. package/static/dist/assets/ChatPage-oo7bdan6.js +0 -25
  510. package/static/dist/assets/Dialog-g355qxhp.js +0 -1
  511. package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +0 -1
  512. package/static/dist/assets/HubRoute-lmkrbw47.js +0 -2
  513. package/static/dist/assets/Markdown-j4kut4j2.js +0 -598
  514. package/static/dist/assets/RightPanel-ohlsdzfl.js +0 -1
  515. package/static/dist/assets/Select-kddq75qc.js +0 -1
  516. package/static/dist/assets/SessionListPanel-lu3oy0hw.js +0 -1
  517. package/static/dist/assets/SettingsLayout-o9bv4mgb.js +0 -1
  518. package/static/dist/assets/States-eq6fe29m.js +0 -1
  519. package/static/dist/assets/Toaster-pc247u9f.js +0 -1
  520. package/static/dist/assets/_app-f8n1i0m1.js +0 -1
  521. package/static/dist/assets/_app.index-bzzv80hk.js +0 -1
  522. package/static/dist/assets/_app.insights-dzixbryi.js +0 -1
  523. package/static/dist/assets/_app.kanban-c6hep2us.js +0 -1
  524. package/static/dist/assets/_app.logs-i69qds98.js +0 -1
  525. package/static/dist/assets/_app.memory-kl4vf0gd.js +0 -1
  526. package/static/dist/assets/_app.profiles-b8zinmyp.js +0 -1
  527. package/static/dist/assets/_app.settings-cg8plwmf.js +0 -1
  528. package/static/dist/assets/_app.settings._section-bkwbi9zn.js +0 -3
  529. package/static/dist/assets/_app.skills-cs47sbvs.js +0 -1
  530. package/static/dist/assets/_app.tasks-dwvk64tc.js +0 -1
  531. package/static/dist/assets/_app.todos-c8xjm8p3.js +0 -1
  532. package/static/dist/assets/_app.workspaces-ehvd1gds.js +0 -1
  533. package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +0 -1
  534. package/static/dist/assets/back-h7aun7ew.js +0 -1
  535. package/static/dist/assets/cancel-fhi0vcnu.js +0 -1
  536. package/static/dist/assets/channel-b2zo73z1.js +0 -1
  537. package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +0 -1
  538. package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +0 -1
  539. package/static/dist/assets/copied-hcplv46p.js +0 -1
  540. package/static/dist/assets/endpoints-klbxod4t.js +0 -64
  541. package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +0 -1
  542. package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +0 -1
  543. package/static/dist/assets/host-i828xuo7.js +0 -1
  544. package/static/dist/assets/index-eh8zilnv.css +0 -2
  545. package/static/dist/assets/index-r648f4o1.js +0 -11
  546. package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +0 -1
  547. package/static/dist/assets/link-oot2yn73.js +0 -1
  548. package/static/dist/assets/login-gu28l9ou.js +0 -1
  549. package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +0 -1
  550. package/static/dist/assets/onboarding-lkphzu1s.js +0 -1
  551. package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +0 -1
  552. package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +0 -1
  553. package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +0 -1
  554. package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +0 -1
  555. package/static/dist/assets/refresh-abtu93ke.js +0 -1
  556. package/static/dist/assets/saved-girnabkf.js +0 -1
  557. package/static/dist/assets/settings_section_extensions_title-h14cwk6f.js +0 -1
  558. package/static/dist/assets/share._token-kl0r7ido.js +0 -2
  559. package/static/dist/assets/sliders-horizontal-n1zm34qq.js +0 -1
  560. package/static/dist/assets/sse-iwku0sjo.js +0 -1
  561. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +0 -1
  562. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +0 -8
  563. package/static/dist/assets/todoStore-jjr5yr78.js +0 -1
  564. package/static/dist/assets/units-kgxzxfhi.js +0 -1
  565. package/static/dist/assets/untitled-cikfjntn.js +0 -1
  566. package/static/dist/assets/useForm-kqoqg0k9.js +0 -1
  567. package/static/dist/assets/useLocale-o59tbiea.js +0 -1
@@ -1,15 +1,6 @@
1
- /**
2
- * Provider cards, the model picker catalog, live model lists, quotas and cost
3
- * history (Python `api/providers.py`, the `/api/models` parts of
4
- * `api/config.py`, `_handle_live_models`).
5
- *
6
- * The Agent's own primitives (registry, auth status, live model ids, account
7
- * usage) come through the sidecar; the static tables and key detection live
8
- * here. ponytail: one live-id cache with a 24h TTL replaces the Python
9
- * publisher/provenance machinery; `refresh()` evicts it.
10
- */
1
+ import { updateDocument } from '../sessions/storage/document.js';
11
2
  import { homeDotenvKeys } from '../cli/dotenv.js';
12
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
3
+ import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
13
4
  import { createHash, randomUUID } from 'node:crypto';
14
5
  import { join } from 'node:path';
15
6
  import { atomicWriteText } from '../fs/atomic.js';
@@ -17,7 +8,7 @@ import { str } from '../util.js';
17
8
  import { QUOTA_THRESHOLD_DEFAULTS } from '../settings.js';
18
9
  import { loadEnvFile } from './env-file.js';
19
10
  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
- import { activeProviderFromConfig, canonicaliseProviderId, configuredModelIds, configuredModelOptions, customProviderEntries, customProviderSlug, dict, effectiveDefaultModel, isDict, isOpenAiFamilyProvider, mainModelSupportsServiceTier, modelSection, parseProviderQualifiedModel, providerIdentity, resolveProviderAlias, } from '../config/agent-config.js';
11
+ import { activeProviderFromConfig, canonicaliseProviderId, configuredModelIds, configuredModelOptions, customProviderEntries, customProviderSlug, dict, effectiveDefaultModel, isDict, modelSection, parseProviderQualifiedModel, providerIdentity, resolveProviderAlias, } from '../config/agent-config.js';
21
12
  /** Python `_model_matches_picker_selection`: same bare id, and the routing hints agree when the selection names one. */
22
13
  function modelMatchesPickerSelection(modelId, selected, providerId) {
23
14
  const candidate = modelId.trim();
@@ -133,16 +124,19 @@ export function stampAuxiliarySelections(aux, catalog) {
133
124
  const match = provider === 'auto' ? undefined : options.find((o) => o.provider === key && o.bare === model);
134
125
  if (match)
135
126
  return { value_label: match.label || match.bare, provider_label: match.group, option_id: match.id };
136
- const group = catalog.groups.find((g) => canonicaliseProviderId(g.provider_id) === key);
137
- return { value_label: model || null, provider_label: provider === 'auto' ? null : group?.provider ?? displayName(provider), option_id: null };
127
+ return { value_label: model || null, provider_label: provider === 'auto' ? null : providerLabel(catalog, provider), option_id: null };
138
128
  };
139
129
  // Auto falls back to the main chat model; name the effective one the catalog resolved (a legacy string `model` or a
140
130
  // `HERMES_MODEL`-style override never reaches `aux.main`).
141
131
  const mainParsed = parseProviderQualifiedModel(catalog.default_model);
142
132
  const main = describe(mainParsed?.[1] ?? catalog.active_provider ?? aux.main.provider, mainParsed?.[0] ?? (catalog.default_model || aux.main.model));
143
- const tasks = aux.tasks.map((t) => {
144
- if ((str(t.provider).trim() || 'auto') === 'auto' && !str(t.model).trim())
145
- return { ...t, is_auto: true, value_label: main.value_label, provider_label: main.provider_label, selected_option_id: null, in_catalog: true };
133
+ const isAuto = (route) => (str(route.provider).trim() || 'auto') === 'auto' && !str(route.model).trim();
134
+ const tasks = aux.tasks.map(({ effective, ...t }) => {
135
+ if (isAuto(t)) {
136
+ // TAL-800: an Auto task that inherits another slot runs on that slot's route, the main model only when it is Auto too.
137
+ const base = isDict(effective) && !isAuto(effective) ? describe(effective.provider, effective.model) : main;
138
+ return { ...t, is_auto: true, value_label: base.value_label, provider_label: base.provider_label, selected_option_id: null, in_catalog: true };
139
+ }
146
140
  const pinned = describe(t.provider, t.model);
147
141
  return { ...t, is_auto: false, value_label: pinned.value_label, provider_label: pinned.provider_label, selected_option_id: pinned.option_id, in_catalog: pinned.option_id !== null };
148
142
  });
@@ -167,7 +161,13 @@ const unqualifiedModelId = (id) => parseProviderQualifiedModel(id)?.[0] ?? id.tr
167
161
  const MODEL_PICKER_OVERFLOW_THRESHOLD = 25;
168
162
  const MODEL_PICKER_VISIBLE_TARGET = 15;
169
163
  const ACCOUNT_USAGE_CACHE_TTL_S = 45;
164
+ /** TAL-807: the oldest account-usage read the picker still shows a quota level from. */
165
+ const PICKER_USAGE_MAX_AGE_S = 600;
166
+ /** TAL-807: how long a picker read waits for a provider's first account-usage read. */
167
+ const PICKER_USAGE_WAIT_MS = 2_000;
170
168
  const LIVE_TTL_S = 86_400;
169
+ /** TAL-799: the Agent's `_PROVIDER_MODELS_FALLBACK_TTL`; its placeholder for an unreachable catalog is re-probed soon. */
170
+ const FALLBACK_TTL_S = 60;
171
171
  const PROVIDERS_TTL_S = 30;
172
172
  /** The sidecar reads a balance endpoint with a 15 s timeout of its own. */
173
173
  const BALANCE_TIMEOUT_MS = 20_000;
@@ -343,6 +343,11 @@ export function classifyQuotaSource(source, t) {
343
343
  urgency: { remaining: urgency(source.windows[0]).remaining, pace: urgency(source.windows[source.pace_window_index ?? 0]).pace },
344
344
  };
345
345
  }
346
+ /** A provider's name as the catalog lists it (a named custom endpoint's configured name, a plugin's display name), else its static display name. */
347
+ export function providerLabel(catalog, provider) {
348
+ const key = canonicaliseProviderId(provider);
349
+ return catalog?.groups.find((g) => canonicaliseProviderId(g.provider_id) === key)?.provider ?? displayName(provider);
350
+ }
346
351
  export function displayName(pid) {
347
352
  return PROVIDER_DISPLAY[pid] ?? pid.split('-').map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w)).join(' ');
348
353
  }
@@ -391,6 +396,10 @@ function looksLikeCodexOauthToken(value) {
391
396
  return false;
392
397
  }
393
398
  }
399
+ const BALANCE_PROVIDERS = ['openrouter', 'deepseek', 'opencode-go'];
400
+ function isBalanceProvider(pid) {
401
+ return BALANCE_PROVIDERS.some((provider) => provider === pid);
402
+ }
394
403
  function valueCountsAsApiKey(pid, value) {
395
404
  const text = str(value).trim();
396
405
  if (!text)
@@ -458,9 +467,12 @@ export function uniqueQuotaSources(sources) {
458
467
  }
459
468
  export class ProviderCatalog {
460
469
  deps;
470
+ scopedKeyRevisions = new Map();
471
+ scopedKeyAuthority = new Map();
472
+ credentialReads = new Map();
461
473
  /** Failed live lookups by home, provider, and the `sourceFingerprint` they ran under (TAL-542). */
462
474
  liveFailed = new Set();
463
- /** `source`: the `sourceFingerprint` the ids were fetched under; another one makes them stale (TAL-542). */
475
+ /** `source`: the `sourceFingerprint` the ids were fetched under; another one makes them stale (TAL-542). `ttl`: seconds they stay fresh. */
464
476
  liveIds = new Map();
465
477
  liveInflight = new Map();
466
478
  /** `pooled`: the fill's `poolProviders` answer, null when unknown (TAL-638). */
@@ -482,15 +494,23 @@ export class ProviderCatalog {
482
494
  }
483
495
  return { groups: this.liveIds.size, models, age_seconds: oldest === null ? null : Math.max(0, Math.round(this.deps.now() - oldest)) };
484
496
  }
485
- invalidate(profileHome, provider) {
497
+ invalidate(profileHome, provider, opts = {}) {
486
498
  for (const key of [...this.liveIds.keys()]) {
487
499
  if (profileHome && !key.startsWith(`${profileHome}\0`))
488
500
  continue;
489
501
  if (provider && !key.endsWith(`\0${provider}`))
490
502
  continue;
491
- this.liveIds.delete(key);
503
+ if (opts.preserveConfirmed)
504
+ this.liveIds.get(key).ttl = 0;
505
+ else
506
+ this.liveIds.delete(key);
492
507
  }
493
508
  this.providersCache.clear();
509
+ if (opts.preserveConfirmed)
510
+ return;
511
+ for (const key of [...this.lastQuotaSources.keys()])
512
+ if (!profileHome || key.startsWith(`${profileHome}\0`))
513
+ this.lastQuotaSources.delete(key);
494
514
  // Credentials changed: the last pool answers no longer stand in for a failed lookup.
495
515
  for (const key of [...this.lastPoolAnswers.keys()])
496
516
  if (!profileHome || key.startsWith(`${profileHome}\0`))
@@ -514,27 +534,44 @@ export class ProviderCatalog {
514
534
  }
515
535
  return catalogOptionId(catalog, model, provider);
516
536
  }
537
+ /** TAL-821: `providerLabel` against the last catalog built for this home; a cold home starts building one. */
538
+ providerLabel(profileHome, provider) {
539
+ const catalog = this.lastModels.get(profileHome);
540
+ if (!catalog)
541
+ void this.warmModelOptions(profileHome);
542
+ return providerLabel(catalog, provider);
543
+ }
517
544
  /**
518
545
  * TAL-542: the identity of the files a catalog and its live ids derive from: config.yaml, `.env`, the Agent's sign-in
519
546
  * store (`auth.json`, rewritten by an account switch), and the installed plugins directory, so a change outside Web
520
- * makes both stale.
547
+ * makes both stale. Opaque scoped-key revisions cover external secret sources without exposing their keys.
521
548
  */
522
- sourceFingerprint(profileHome) {
549
+ sourceFingerprint(profileHome, includeScoped = true) {
523
550
  const stamp = (name) => {
524
551
  try {
525
552
  const st = statSync(join(profileHome, name), { bigint: true });
526
- return `${String(st.mtimeNs)}:${String(st.size)}:${String(st.ino)}`;
553
+ return `${String(st.mtimeNs)}:${String(st.ctimeNs)}:${String(st.size)}:${String(st.ino)}`;
527
554
  }
528
555
  catch {
529
556
  return 'missing';
530
557
  }
531
558
  };
532
- return [this.deps.config.fingerprint(profileHome), ...['.env', 'auth.json', 'plugins'].map(stamp)].join('|');
559
+ const names = new Set([...Object.values(PROVIDER_ENV_VAR), ...Object.values(PROVIDER_ENV_VAR_ALIASES).flat()]);
560
+ const config = this.deps.config.peek(profileHome, { refresh: false }) ?? {};
561
+ for (const cp of [modelSection(config), ...Object.values(dict(config.providers)).filter(isDict), ...customProviderEntries(config)]) {
562
+ for (const name of [str(cp.key_env).trim(), /^\$\{([^}]+)\}$/.exec(str(cp.api_key).trim())?.[1]])
563
+ if (name)
564
+ names.add(name);
565
+ }
566
+ const credentials = [...names].sort().map((name) => [name, this.processEnv(name, profileHome)]);
567
+ const models = ['HERMES_MODEL', 'OPENAI_MODEL', 'LLM_MODEL'].map((name) => [name, this.deps.env[name]]);
568
+ const environment = createHash('sha256').update(JSON.stringify([credentials, models, includeScoped ? this.scopedKeyRevisions.get(profileHome) ?? '[]' : '[]'])).digest('hex');
569
+ return [profileHome, environment, this.deps.config.fingerprint(profileHome), ...['.env', 'auth.json', 'plugins'].map(stamp)].join('|');
533
570
  }
534
571
  /** The last catalog built for this home, younger than `maxAgeS`, while its source files are unchanged since (TAL-542). */
535
572
  currentModels(profileHome, maxAgeS = Infinity) {
536
573
  const meta = this.lastModelsMeta.get(profileHome);
537
- return meta?.source === this.sourceFingerprint(profileHome) && this.deps.now() - meta.at < maxAgeS ? this.lastModels.get(profileHome) : undefined;
574
+ return this.scopedKeyAuthority.get(profileHome) !== false && meta?.source === this.sourceFingerprint(profileHome) && this.deps.now() - meta.at < maxAgeS ? this.lastModels.get(profileHome) : undefined;
538
575
  }
539
576
  /**
540
577
  * TAL-542: `repairSessionModel` against this home's catalog. Agent-owned sign-in and plugin state has no file to
@@ -554,6 +591,7 @@ export class ProviderCatalog {
554
591
  * await. A source that changed while the build awaited makes that build stale, so it is rebuilt once.
555
592
  */
556
593
  async warmSessionModelRepair(profileHome) {
594
+ await this.poolProviders(profileHome);
557
595
  for (let attempt = 0; attempt < 2 && !this.currentModels(profileHome, PROVIDERS_TTL_S); attempt += 1) {
558
596
  try {
559
597
  await this.models(profileHome);
@@ -565,6 +603,7 @@ export class ProviderCatalog {
565
603
  }
566
604
  /** Builds the catalog `modelOptionFor` reads once per home, again after config.yaml or `.env` changes; an unavailable catalog leaves every option id null. */
567
605
  async warmModelOptions(profileHome) {
606
+ await this.poolProviders(profileHome);
568
607
  if (this.currentModels(profileHome))
569
608
  return;
570
609
  try {
@@ -613,18 +652,14 @@ export class ProviderCatalog {
613
652
  return false;
614
653
  }
615
654
  async authStatus(profileHome, pid) {
616
- const sidecar = this.deps.sidecar();
617
- if (!sidecar)
618
- return null;
619
- try {
620
- return (await sidecar.call('providers.auth_status', { profile_home: profileHome, provider: pid })).status;
621
- }
622
- catch {
623
- return null;
624
- }
655
+ return this.poolLookup(profileHome, `auth\0${pid}`, async (sidecar) => {
656
+ const { status } = await sidecar.call('providers.auth_status', { profile_home: profileHome, provider: pid });
657
+ return { logged_in: status.logged_in, key_source: str(status.key_source), error: str(status.error) };
658
+ });
625
659
  }
626
660
  /** `pooled`: the providers whose persisted credential pool holds an added account (TAL-638). */
627
- async probeKey(profileHome, pid, config, envValues, pooled) {
661
+ async probeKey(profileHome, pid, config, envValues, pooled, scopedKeys) {
662
+ let authoritative = true;
628
663
  let hasKey = this.providerHasKey(pid, config, envValues, profileHome);
629
664
  let isOauth = OAUTH_PROVIDERS.has(pid);
630
665
  let keySource = 'none';
@@ -632,7 +667,9 @@ export class ProviderCatalog {
632
667
  let removable = false;
633
668
  if (isOauth) {
634
669
  keySource = 'oauth';
635
- const status = await this.authStatus(profileHome, pid);
670
+ const lookup = await this.authStatus(profileHome, pid);
671
+ authoritative = lookup.authoritative;
672
+ const status = lookup.value;
636
673
  if (status?.logged_in) {
637
674
  hasKey = true;
638
675
  keySource = str(status.key_source) || 'oauth';
@@ -677,8 +714,16 @@ export class ProviderCatalog {
677
714
  hasKey = true;
678
715
  keySource = 'credential_pool';
679
716
  }
717
+ else if (isBalanceProvider(pid)) {
718
+ if (scopedKeys[pid]?.present) {
719
+ hasKey = true;
720
+ keySource = 'env';
721
+ }
722
+ }
680
723
  else if (!providerEnvVar(pid) && /^[a-z][a-z0-9_-]{0,63}$/.test(pid)) {
681
- const status = await this.authStatus(profileHome, pid);
724
+ const lookup = await this.authStatus(profileHome, pid);
725
+ authoritative = lookup.authoritative;
726
+ const status = lookup.value;
682
727
  if (status?.logged_in) {
683
728
  hasKey = true;
684
729
  const raw = str(status.key_source);
@@ -686,14 +731,21 @@ export class ProviderCatalog {
686
731
  isOauth = true;
687
732
  }
688
733
  }
689
- return { hasKey, keySource, authError, isOauth, removable };
734
+ return { authoritative, hasKey, keySource, authError, isOauth, removable };
690
735
  }
691
736
  /** Live model ids from the Agent for one provider, cached per profile home. */
692
737
  async liveModelIds(profileHome, pid, opts = {}) {
738
+ const inputSource = this.sourceFingerprint(profileHome, false);
739
+ if (!opts.source || opts.source !== this.sourceFingerprint(profileHome))
740
+ await this.poolProviders(profileHome);
693
741
  const key = `${profileHome}\0${pid}`;
694
- const source = this.sourceFingerprint(profileHome);
742
+ const source = inputSource === this.sourceFingerprint(profileHome, false) ? this.sourceFingerprint(profileHome) : inputSource;
695
743
  const hit = this.liveIds.get(key);
696
- if (hit?.source === source && !opts.force && this.deps.now() - hit.at < LIVE_TTL_S)
744
+ if (this.scopedKeyAuthority.get(profileHome) === false) {
745
+ this.liveFailed.add(`${key}\0${source}`);
746
+ return hit?.source === source ? hit.ids : [];
747
+ }
748
+ if (hit?.source === source && !opts.force && this.deps.now() - hit.at < hit.ttl)
697
749
  return hit.ids;
698
750
  // A lookup started under another source answers for the old endpoint or credential; this one starts its own.
699
751
  const flightKey = `${key}\0${source}`;
@@ -704,51 +756,75 @@ export class ProviderCatalog {
704
756
  // No sidecar is a failed lookup for this source: the ids it keeps showing never confirm a stale-model repair (TAL-542).
705
757
  if (!sidecar) {
706
758
  this.liveFailed.add(flightKey);
707
- return hit?.ids ?? [];
759
+ return hit?.source === source && source === this.sourceFingerprint(profileHome) ? hit.ids : [];
708
760
  }
709
761
  const run = sidecar.call('providers.model_ids', { profile_home: profileHome, provider: pid, ...(opts.force ? { force_refresh: true } : {}) }, { timeoutMs: 30_000 })
710
- .then((r) => { this.liveIds.set(key, { at: this.deps.now(), ids: r.model_ids, source }); this.liveFailed.delete(flightKey); return r.model_ids; })
711
- .catch((error) => { this.deps.log(`[catalog] live model ids for ${pid} failed: ${str(error.message)}`); this.liveFailed.add(flightKey); return hit?.ids ?? []; })
762
+ .then((r) => {
763
+ if (source !== this.sourceFingerprint(profileHome)) {
764
+ this.liveFailed.add(flightKey);
765
+ return [];
766
+ }
767
+ if (r.fallback && hit?.source === source && hit.confirmed) {
768
+ this.liveFailed.add(flightKey);
769
+ return hit.ids;
770
+ }
771
+ this.liveIds.set(key, { at: this.deps.now(), ids: r.model_ids, source, ttl: r.fallback ? FALLBACK_TTL_S : LIVE_TTL_S, confirmed: !r.fallback });
772
+ // TAL-799: the Agent's curated placeholder for an unreachable catalog is a failed lookup that still lists models.
773
+ if (r.fallback)
774
+ this.liveFailed.add(flightKey);
775
+ else
776
+ this.liveFailed.delete(flightKey);
777
+ return r.model_ids;
778
+ })
779
+ .catch((error) => { this.deps.log(`[catalog] live model ids for ${pid} failed: ${str(error.message)}`); this.liveFailed.add(flightKey); return hit?.source === source && source === this.sourceFingerprint(profileHome) ? hit.ids : []; })
712
780
  .finally(() => { this.liveInflight.delete(flightKey); });
713
781
  this.liveInflight.set(flightKey, run);
714
782
  return run;
715
783
  }
784
+ /** Whether the last live lookup for this provider under `source` failed or answered with the Agent's placeholder list. */
785
+ liveLookupFailed(profileHome, pid, source = this.sourceFingerprint(profileHome)) {
786
+ return this.liveFailed.has(`${profileHome}\0${pid}\0${source}`);
787
+ }
716
788
  /**
717
789
  * TAL-288: the model-provider plugins installed and enabled in this profile, as the sidecar reads them from the Agent.
718
790
  * A failed lookup lists none, so every other provider stays usable; an id that cannot be a provider routing hint is dropped.
719
791
  */
720
792
  async pluginProviders(profileHome) {
721
- const sidecar = this.deps.sidecar();
722
- if (!sidecar)
723
- return [];
724
- try {
793
+ return this.poolLookup(profileHome, 'plugins', async (sidecar) => {
725
794
  const { providers } = await sidecar.call('plugins.providers', { profile_home: profileHome }, { timeoutMs: PLUGIN_LIST_TIMEOUT_MS });
726
795
  return providers.filter((p, i, all) => /^[a-z0-9][a-z0-9._-]{0,63}$/.test(p.name) && all.findIndex((q) => q.name === p.name) === i);
727
- }
728
- catch (error) {
729
- this.deps.log(`[catalog] plugin providers failed: ${str(error.message)}`);
730
- return [];
731
- }
796
+ });
797
+ }
798
+ /**
799
+ * TAL-798: the GGUFs staged for the Agent's managed llama.cpp provider, selectable before its server runs (a turn boots
800
+ * it). Null when none are staged or the listing failed, so every other provider stays usable.
801
+ */
802
+ async localModels(profileHome) {
803
+ return this.poolLookup(profileHome, 'local-models', async (sidecar) => sidecar.call('providers.local_models', { profile_home: profileHome }, { timeoutMs: PLUGIN_LIST_TIMEOUT_MS }));
732
804
  }
733
805
  /** Python `get_providers`. */
734
806
  async providers(profileHome) {
735
807
  return structuredClone((await this.providerRows(profileHome)).payload);
736
808
  }
737
809
  /** The cached `providers` answer, shared with `quotas`; callers never mutate it. */
738
- async providerRows(profileHome) {
810
+ async providerRows(profileHome, pool) {
739
811
  const config = await this.deps.config.read(profileHome);
740
812
  const envValues = loadEnvFile(join(profileHome, '.env'));
741
- const cacheKey = JSON.stringify([config, envValues]);
813
+ pool ??= await this.poolProviders(profileHome);
814
+ const source = this.sourceFingerprint(profileHome);
815
+ const cacheKey = source;
742
816
  const hit = this.providersCache.get(profileHome);
743
- if (hit?.key === cacheKey && this.deps.now() - hit.at < PROVIDERS_TTL_S)
817
+ if (pool.authoritative && hit?.key === cacheKey && this.deps.now() - hit.at < PROVIDERS_TTL_S)
744
818
  return hit;
745
- const pooledIds = await this.poolProviders(profileHome);
819
+ const pooledIds = pool.value?.providers ?? null;
820
+ const pluginLookup = await this.pluginProviders(profileHome);
821
+ let authoritative = pool.authoritative && pluginLookup.authoritative;
746
822
  const pooled = new Set(pooledIds ?? []);
747
823
  const active = activeProviderFromConfig(config);
748
824
  const known = new Set([...Object.keys(PROVIDER_DISPLAY), ...Object.keys(PROVIDER_MODELS), ...OAUTH_PROVIDERS]);
749
825
  // A plugin never stands in for a built-in provider, by id or alias (that would route to the built-in's billing); it
750
826
  // does own a `providers.<id>` config entry.
751
- const plugins = (await this.pluginProviders(profileHome)).filter((p) => !known.has(p.name) && !known.has(providerIdentity(p.name)));
827
+ const plugins = (pluginLookup.value ?? []).filter((p) => !known.has(p.name) && !known.has(providerIdentity(p.name)));
752
828
  const pluginIds = new Set(plugins.map((p) => p.name));
753
829
  const providersCfg = dict(config.providers);
754
830
  for (const key of Object.keys(providersCfg)) {
@@ -759,7 +835,8 @@ export class ProviderCatalog {
759
835
  }
760
836
  const rows = [];
761
837
  for (const pid of [...known].sort()) {
762
- const probe = await this.probeKey(profileHome, pid, config, envValues, pooled);
838
+ const probe = await this.probeKey(profileHome, pid, config, envValues, pooled, pool.value?.keys ?? {});
839
+ authoritative &&= probe.authoritative;
763
840
  if (pid === 'openai' && !probe.hasKey && looksLikeCodexOauthToken(str(envValues[providerEnvVar('openai') ?? ''] ?? this.deps.env.OPENAI_API_KEY)))
764
841
  continue;
765
842
  const configured = probe.hasKey || selfHostedEndpoint(pid, config);
@@ -768,7 +845,7 @@ export class ProviderCatalog {
768
845
  // Python: the card prefers the live catalog for every keyed provider (exactly like the picker), keeping the
769
846
  // static list as the cold/failed-probe fallback; Nous renders a featured subset with the full count.
770
847
  if (configured && (probe.isOauth || pid === 'lmstudio' || pid === 'nous' || !BESPOKE_CATALOG_PROVIDERS.has(pid))) {
771
- const live = await this.liveModelIds(profileHome, pid);
848
+ const live = await this.liveModelIds(profileHome, pid, { source });
772
849
  if (live.length) {
773
850
  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, []) }));
774
851
  modelsTotal = live.length;
@@ -843,20 +920,30 @@ export class ProviderCatalog {
843
920
  row.key_source_kind = keySourceKind(row);
844
921
  row.models_total = Math.max(Number(row.models_total), row.models.length);
845
922
  }
846
- const entry = { at: this.deps.now(), key: cacheKey, payload: { providers: rows, active_provider: active }, pooled: pooledIds };
847
- this.providersCache.set(profileHome, entry);
923
+ authoritative &&= source === this.sourceFingerprint(profileHome);
924
+ const availability = this.availability(source, authoritative, Boolean(pool.value));
925
+ const entry = { at: this.deps.now(), key: cacheKey, payload: { providers: rows, active_provider: active, availability }, pooled: pooledIds };
926
+ if (authoritative)
927
+ this.providersCache.set(profileHome, entry);
848
928
  return entry;
849
929
  }
850
930
  /** Python `get_available_models` (static catalog + live ids for keyed providers). */
851
- async models(profileHome) {
931
+ async models(profileHome, attempt = 0) {
932
+ const pool = await this.poolProviders(profileHome);
852
933
  const source = this.sourceFingerprint(profileHome);
934
+ const providerState = (await this.providerRows(profileHome, pool)).payload;
853
935
  const unconfirmed = new Set();
854
936
  const config = await this.deps.config.read(profileHome);
855
937
  const envValues = loadEnvFile(join(profileHome, '.env'));
856
- const active = activeProviderFromConfig(config);
857
938
  const model = modelSection(config);
858
939
  const defaultModel = effectiveDefaultModel(config, this.deps.env);
859
940
  const cfgBaseUrl = str(model.base_url).trim();
941
+ const localLookup = await this.localModels(profileHome);
942
+ const local = localLookup.value?.models.length ? localLookup.value : null;
943
+ const configured = activeProviderFromConfig(config);
944
+ // A `custom` main model at the running local server's endpoint is that server: its Local group owns the selection.
945
+ const atLocalServer = configured === 'custom' && Boolean(cfgBaseUrl) && cfgBaseUrl.replace(/\/+$/, '') === str(local?.current_endpoint).replace(/\/+$/, '');
946
+ const active = local && atLocalServer ? local.provider_id : configured;
860
947
  const detected = new Set();
861
948
  const configuredIds = new Map();
862
949
  const append = (pid, id) => {
@@ -870,6 +957,8 @@ export class ProviderCatalog {
870
957
  };
871
958
  if (active)
872
959
  detected.add(active);
960
+ if (local)
961
+ detected.add(local.provider_id);
873
962
  const providersCfg = dict(config.providers);
874
963
  const rawKeyFor = new Map();
875
964
  for (const [key, value] of Object.entries(providersCfg)) {
@@ -897,8 +986,8 @@ export class ProviderCatalog {
897
986
  // ready plugin providers (TAL-288), under their own name, and providers keyed only through the credential pool (TAL-638).
898
987
  const signedIn = new Set();
899
988
  const pluginNames = new Map();
900
- for (const row of (await this.providerRows(profileHome)).payload.providers) {
901
- if (row.has_key !== true || (row.is_oauth !== true && row.is_plugin_provider !== true && row.key_source !== 'credential_pool'))
989
+ for (const row of providerState.providers) {
990
+ if (row.has_key !== true || (row.is_oauth !== true && row.is_plugin_provider !== true && row.key_source !== 'credential_pool' && row.key_source !== 'env'))
902
991
  continue;
903
992
  signedIn.add(str(row.id));
904
993
  if (row.is_plugin_provider === true)
@@ -936,8 +1025,15 @@ export class ProviderCatalog {
936
1025
  const named = [...namedCustom.entries()].find(([, v]) => customProviderEntries(config).some((e) => customProviderSlug(e.name) === customProviderSlug(v.name) && str(e.base_url).trim().replace(/\/+$/, '') === cfgBaseUrl.replace(/\/+$/, '')));
937
1026
  detected.add(named?.[0] ?? active ?? 'custom');
938
1027
  }
1028
+ // TAL-911: Agent pickers drop `model_catalog.excluded_providers` under any of their aliases, the active provider included.
1029
+ const excludedCfg = dict(config.model_catalog).excluded_providers;
1030
+ const excluded = new Set(Array.isArray(excludedCfg) ? excludedCfg.map(providerIdentity).filter(Boolean) : []);
1031
+ const isExcluded = (pid) => excluded.has(providerIdentity(pid));
1032
+ const pickerDefault = isExcluded(active) || isExcluded(parseProviderQualifiedModel(defaultModel)?.[1]) ? '' : defaultModel;
939
1033
  const groups = [];
940
1034
  for (const pid of [...detected].map((p) => (p.startsWith('custom') || pluginNames.has(p) ? p : canonicaliseProviderId(p) || p)).filter((v, i, a) => v && a.indexOf(v) === i).sort()) {
1035
+ if (isExcluded(pid))
1036
+ continue;
941
1037
  if (pid.startsWith('custom:')) {
942
1038
  const g = namedCustom.get(pid);
943
1039
  const models = [...(g?.models ?? [])];
@@ -945,6 +1041,11 @@ export class ProviderCatalog {
945
1041
  groups.push({ provider: g?.name ?? pid.replace('custom:', ''), provider_id: pid, models: applyProviderPrefix(models, pid, active) });
946
1042
  continue;
947
1043
  }
1044
+ if (pid === local?.provider_id) {
1045
+ // A GGUF's id is its file name, quant tag included (`Q4_K_M`); prettifying would mangle it.
1046
+ groups.push({ provider: 'Local', provider_id: pid, models: applyProviderPrefix(local.models.map((id) => ({ id, label: id })), pid, active) });
1047
+ continue;
1048
+ }
948
1049
  if (pid === 'custom') {
949
1050
  const models = [...customModels];
950
1051
  for (const id of configuredIds.get(pid) ?? [])
@@ -956,20 +1057,25 @@ export class ProviderCatalog {
956
1057
  }
957
1058
  const providerCfg = dict(providersCfg[rawKeyFor.get(pid) ?? pid]);
958
1059
  let raw = [];
1060
+ let allowFallback = false;
959
1061
  if ('models' in providerCfg && providerCfg.models_discovered !== true)
960
1062
  raw = configuredModelOptions(providerCfg.models);
961
- if (!raw.length && (this.providerHasKey(pid, config, envValues, profileHome) || selfHostedEndpoint(pid, config) || signedIn.has(pid))) {
962
- const live = await this.liveModelIds(profileHome, pid);
963
- // TAL-542: a failed lookup leaves this group's list unconfirmed, so the stale-model repair never trusts it.
964
- if (this.liveFailed.has(`${profileHome}\0${pid}\0${source}`))
1063
+ if (!raw.length) {
1064
+ // TAL-799: a provider without credentials lists the Agent's curated catalog too; the static tables only cover a missing sidecar.
1065
+ const keyed = this.providerHasKey(pid, config, envValues, profileHome) || selfHostedEndpoint(pid, config) || signedIn.has(pid);
1066
+ const live = await this.liveModelIds(profileHome, pid, { source });
1067
+ const failed = this.liveLookupFailed(profileHome, pid, source);
1068
+ // TAL-542: a failed lookup, or a curated list no account confirmed, leaves this group unconfirmed, so the stale-model repair never trusts it.
1069
+ allowFallback = failed || !keyed;
1070
+ if (allowFallback)
965
1071
  unconfirmed.add(pid);
966
1072
  if (live.length)
967
1073
  raw = live.map((id) => ({ id, label: pid === 'nous' ? `${formatOllamaLabel(id.includes('/') ? id.slice(id.indexOf('/') + 1) : id)} (via Nous)` : labelForModel(id, []) }));
968
1074
  // Python (#1567): an authenticated Nous account with an empty live catalog shows no group; only a failed lookup falls back to the curated list.
969
- else if (pid === 'nous' && !this.liveFailed.has(`${profileHome}\0${pid}\0${source}`))
1075
+ else if (keyed && pid === 'nous' && !failed)
970
1076
  continue;
971
1077
  }
972
- if (!raw.length)
1078
+ if (!raw.length && allowFallback)
973
1079
  raw = pid === 'openrouter' ? FALLBACK_MODELS.map((m) => ({ id: m.id, label: m.label })) : [...(PROVIDER_MODELS[pid] ?? [])];
974
1080
  for (const id of configuredIds.get(pid) ?? [])
975
1081
  if (!raw.some((m) => m.id === id))
@@ -977,7 +1083,7 @@ export class ProviderCatalog {
977
1083
  if (raw.length)
978
1084
  groups.push({ provider: pluginNames.get(pid) ?? displayName(pid), provider_id: pid, models: applyProviderPrefix(raw, pid, active) });
979
1085
  }
980
- if (defaultModel) {
1086
+ if (pickerDefault) {
981
1087
  const all = new Set(groups.flatMap((g) => g.models.map((m) => m.id)));
982
1088
  if (!all.has(defaultModel) && !all.has(`@${active ?? ''}:${defaultModel}`)) {
983
1089
  const label = labelForModel(defaultModel, groups);
@@ -1010,11 +1116,6 @@ export class ProviderCatalog {
1010
1116
  }
1011
1117
  const rank = (g) => (g.provider_id === active ? 0 : g.provider_id.startsWith('custom:') ? 1 : withKeys.has(g.provider_id) ? 2 : 3);
1012
1118
  kept.sort((a, b) => rank(a) - rank(b) || (a.provider_id < b.provider_id ? -1 : a.provider_id > b.provider_id ? 1 : 0));
1013
- for (const g of kept)
1014
- if (isOpenAiFamilyProvider(g.provider_id))
1015
- for (const m of g.models)
1016
- if (/^(@[^:]+:)?(openai\/)?(gpt-5|o[0-9])/.test(m.id))
1017
- m.supports_fast_tier = true;
1018
1119
  const aliases = {};
1019
1120
  const rawAliases = model.aliases;
1020
1121
  if (isDict(rawAliases))
@@ -1022,19 +1123,118 @@ export class ProviderCatalog {
1022
1123
  if (k && v)
1023
1124
  aliases[k.trim()] = str(v).trim();
1024
1125
  const [defaultBare, defaultProvider] = parseProviderQualifiedModel(defaultModel) ?? [defaultModel, active];
1025
- const defaults = { active_provider: active, default_model: defaultModel, default_provider_id: defaultProvider, default_bare_id: defaultBare };
1026
- if (!kept.length && defaultModel) {
1126
+ if (source !== this.sourceFingerprint(profileHome)) {
1127
+ if (attempt === 0)
1128
+ return this.models(profileHome, 1);
1129
+ throw new Error('Provider configuration changed during catalog lookup. Try again.');
1130
+ }
1131
+ if (!providerState.availability.authoritative || !localLookup.authoritative)
1132
+ for (const g of kept)
1133
+ unconfirmed.add(g.provider_id);
1134
+ const previous = this.currentModels(profileHome);
1135
+ const availability = this.availability(source, providerState.availability.authoritative && localLookup.authoritative && !unconfirmed.size && source === this.sourceFingerprint(profileHome), previous?.availability?.status === 'available' || previous?.availability?.status === 'stale');
1136
+ for (const g of kept)
1137
+ g.availability = this.availability(source, !unconfirmed.has(g.provider_id) && providerState.availability.authoritative, Boolean(previous?.groups.some((old) => old.provider_id === g.provider_id && old.availability?.status !== 'unavailable')));
1138
+ const defaults = { availability, active_provider: active, default_model: defaultModel, default_provider_id: defaultProvider, default_bare_id: defaultBare };
1139
+ if (!kept.length && pickerDefault) {
1027
1140
  const providerId = active ?? 'default';
1028
- 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: {} });
1141
+ return this.remember(profileHome, { source, unconfirmed, attempt }, { ...defaults, groups: [{ provider: 'Default', provider_id: providerId, models: stampModelEntries([{ id: defaultModel, label: labelForModel(defaultModel, []) }], providerId) }], aliases: {}, configured_model_badges: {} });
1029
1142
  }
1030
1143
  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) } : {}) }));
1031
- return this.remember(profileHome, { source, unconfirmed }, { ...defaults, groups: stamped, aliases, configured_model_badges: this.badges(kept, active, defaultModel, fallbackCfg) });
1144
+ return this.remember(profileHome, { source, unconfirmed, attempt }, { ...defaults, groups: stamped, aliases, configured_model_badges: this.badges(kept, active, defaultModel, fallbackCfg) });
1145
+ }
1146
+ /**
1147
+ * TAL-807: the picker catalog (`/api/models`) with each group's live pool rate limit, read per request so an expired one
1148
+ * is gone, and its quota level. The cached catalog holds neither; a failed read leaves them out.
1149
+ */
1150
+ async pickerModels(profileHome, profile) {
1151
+ const catalog = await this.models(profileHome);
1152
+ const entries = (g) => [...g.models, ...(g.extra_models ?? [])];
1153
+ let limits = {};
1154
+ try {
1155
+ const sidecar = this.deps.sidecar();
1156
+ const providers = catalog.groups.map((g) => ({ slug: g.provider_id, models: entries(g).map((e) => e.bare_id ?? e.id) }));
1157
+ if (sidecar)
1158
+ limits = (await sidecar.call('usage.limits', { profile_home: profileHome, providers })).limits;
1159
+ }
1160
+ catch (error) {
1161
+ this.deps.log(`[catalog] pool limits failed: ${str(error.message)}`);
1162
+ }
1163
+ for (const g of catalog.groups) {
1164
+ const limit = limits[g.provider_id];
1165
+ if (limit?.scope === 'account')
1166
+ g.limit = limit;
1167
+ if (limit?.scope === 'models') {
1168
+ // The sidecar answers by bare id; the picker names entries by id.
1169
+ const cooled = {};
1170
+ for (const e of entries(g)) {
1171
+ const resetsAt = limit.models[e.bare_id ?? e.id];
1172
+ if (resetsAt) {
1173
+ e.limit = { scope: 'model', resets_at: resetsAt };
1174
+ cooled[e.id] = resetsAt;
1175
+ }
1176
+ }
1177
+ if (Object.keys(cooled).length)
1178
+ g.limit = { scope: 'models', models: cooled };
1179
+ }
1180
+ }
1181
+ const thresholds = (await this.deps.quotaThresholds?.(profile)) ?? QUOTA_THRESHOLD_DEFAULTS;
1182
+ // An account limit already says the login is out; the quota level would only repeat it.
1183
+ const levels = await Promise.all(catalog.groups.map((g) => (g.limit?.scope === 'account' ? Promise.resolve(null) : this.pickerQuotaLevel(profileHome, g.provider_id, thresholds))));
1184
+ catalog.groups.forEach((g, i) => { const level = levels[i]; if (level)
1185
+ g.quota_level = level; });
1186
+ return catalog;
1187
+ }
1188
+ /** TAL-807: account-usage reads the picker started, by `accountUsageCache` key. */
1189
+ pickerUsageReads = new Map();
1190
+ /**
1191
+ * TAL-807: the TAL-411 `remaining` level of the tightest account-scope window in the provider's cached account-usage
1192
+ * read; a model-family window (Opus or Sonnet weekly) never counts. A read past the cache TTL starts a new one, which
1193
+ * the picker waits for only when it has no read at all, and then for at most `PICKER_USAGE_WAIT_MS`; one past
1194
+ * `PICKER_USAGE_MAX_AGE_S` shows nothing.
1195
+ */
1196
+ async pickerQuotaLevel(profileHome, pid, t) {
1197
+ if (!ACCOUNT_USAGE_PROVIDERS.has(pid))
1198
+ return null;
1199
+ const key = `${profileHome}\0${pid}\0`;
1200
+ let cached = this.accountUsageCache.get(key);
1201
+ if (cached?.source !== this.sourceFingerprint(profileHome))
1202
+ cached = undefined;
1203
+ if (!cached || this.deps.now() - cached.at > ACCOUNT_USAGE_CACHE_TTL_S) {
1204
+ let read = this.pickerUsageReads.get(key);
1205
+ if (!read) {
1206
+ read = this.quota(profileHome, pid).then(() => undefined, () => undefined).finally(() => this.pickerUsageReads.delete(key));
1207
+ this.pickerUsageReads.set(key, read);
1208
+ }
1209
+ if (!cached) {
1210
+ let timer;
1211
+ await Promise.race([read, new Promise((resolve) => { timer = setTimeout(resolve, PICKER_USAGE_WAIT_MS); })]);
1212
+ clearTimeout(timer);
1213
+ cached = this.accountUsageCache.get(key);
1214
+ }
1215
+ }
1216
+ const limits = cached?.limits;
1217
+ if (cached?.source !== this.sourceFingerprint(profileHome) || !limits?.available || this.deps.now() - cached.at > PICKER_USAGE_MAX_AGE_S)
1218
+ return null;
1219
+ const raw = (Array.isArray(limits.windows) ? limits.windows : []).filter((w) => isDict(w) && w.scope !== 'model');
1220
+ const tightest = normalizeQuotaWindows(raw, this.deps.now()).windows
1221
+ .reduce((a, w) => (w.remaining_percent !== null && w.remaining_percent < (a?.remaining_percent ?? Infinity) ? w : a), undefined);
1222
+ return tightest ? quotaLevel(tightest, limits.stale ? 'stale' : 'available', 'remaining', t) : null;
1032
1223
  }
1033
1224
  /** Stamps the default's entry id and keeps the catalog for `modelOptionFor`. */
1034
- remember(profileHome, meta, catalog) {
1225
+ async remember(profileHome, meta, catalog) {
1226
+ if (catalog.availability)
1227
+ for (const group of catalog.groups)
1228
+ group.availability ??= catalog.availability;
1229
+ await this.stampSpeedTiers(profileHome, catalog.groups.flatMap((g) => [...g.models, ...(g.extra_models ?? [])]));
1230
+ if (meta.source !== this.sourceFingerprint(profileHome)) {
1231
+ if (meta.attempt === 0)
1232
+ return this.models(profileHome, 1);
1233
+ throw new Error('Provider configuration changed during catalog lookup. Try again.');
1234
+ }
1035
1235
  catalog.default_option_id = catalogOptionId(catalog, catalog.default_bare_id ?? null, catalog.default_provider_id ?? null);
1036
1236
  this.lastModels.set(profileHome, catalog);
1037
- this.lastModelsMeta.set(profileHome, { ...meta, at: this.deps.now() });
1237
+ this.lastModelsMeta.set(profileHome, { source: meta.source, unconfirmed: meta.unconfirmed, at: this.deps.now() });
1038
1238
  return structuredClone(catalog);
1039
1239
  }
1040
1240
  /** Python `_configured_model_badges_from_static_catalog`: which picker rows are the main/fallback selections. */
@@ -1066,14 +1266,17 @@ export class ProviderCatalog {
1066
1266
  return badges;
1067
1267
  }
1068
1268
  /** Python `_handle_live_models`. */
1069
- async liveModels(profileHome, providerRaw) {
1269
+ async liveModels(profileHome, providerRaw, attempt = 0) {
1270
+ const fingerprint = this.sourceFingerprint(profileHome);
1070
1271
  const config = await this.deps.config.read(profileHome);
1071
1272
  let provider = providerRaw.trim().toLowerCase() || str(modelSection(config).provider).trim().toLowerCase();
1072
1273
  if (!provider)
1073
1274
  return { error: 'no_provider', models: [] };
1074
1275
  provider = resolveProviderAlias(provider);
1075
1276
  let ids = await this.liveModelIds(profileHome, provider);
1076
- let source = 'live';
1277
+ if (fingerprint !== this.sourceFingerprint(profileHome))
1278
+ return attempt === 0 ? this.liveModels(profileHome, providerRaw, 1) : { availability: this.availability(this.sourceFingerprint(profileHome), false, false), provider, source: 'unavailable', models: [], count: 0 };
1279
+ let source = this.liveLookupFailed(profileHome, provider, fingerprint) ? 'fallback' : 'live';
1077
1280
  if (!ids.length) {
1078
1281
  const fromConfig = [];
1079
1282
  if (provider === 'custom' || provider.startsWith('custom:')) {
@@ -1083,16 +1286,33 @@ export class ProviderCatalog {
1083
1286
  fromConfig.push(...[str(cp.model).trim(), ...configuredModelIds(cp.models)].filter(Boolean));
1084
1287
  }
1085
1288
  }
1086
- ids = fromConfig.length ? [...new Set(fromConfig)] : (PROVIDER_MODELS[provider] ?? []).map((m) => m.id);
1087
- source = fromConfig.length ? 'config' : 'static';
1289
+ if (fromConfig.length) {
1290
+ ids = [...new Set(fromConfig)];
1291
+ source = 'config';
1292
+ }
1293
+ else if (source === 'fallback') {
1294
+ ids = (PROVIDER_MODELS[provider] ?? []).map((m) => m.id);
1295
+ source = 'static';
1296
+ }
1088
1297
  }
1089
- // Python: the dropdown-enrichment surface keeps the picker's visibility budget (15 rows past 25) and annotates
1090
- // OpenAI-family rows with `supports_fast_tier`; the answer carries `count`.
1091
1298
  if (provider !== 'nous' && ids.length > MODEL_PICKER_OVERFLOW_THRESHOLD)
1092
1299
  ids = ids.slice(0, MODEL_PICKER_VISIBLE_TARGET);
1093
- const annotateFastTier = isOpenAiFamilyProvider(provider);
1094
- const models = stampModelEntries(ids.filter(Boolean).map((id) => ({ id, label: labelForModel(id, []), ...(annotateFastTier ? { supports_fast_tier: mainModelSupportsServiceTier(id, provider) } : {}) })), provider);
1095
- return { provider, source, models, count: models.length };
1300
+ const models = stampModelEntries(ids.filter(Boolean).map((id) => ({ id, label: labelForModel(id, []) })), provider);
1301
+ await this.stampSpeedTiers(profileHome, models);
1302
+ if (fingerprint !== this.sourceFingerprint(profileHome))
1303
+ return attempt === 0 ? this.liveModels(profileHome, providerRaw, 1) : { availability: this.availability(this.sourceFingerprint(profileHome), false, false), provider, source: 'unavailable', models: [], count: 0 };
1304
+ return { availability: this.availability(fingerprint, source === 'live' || source === 'config', source === 'fallback' && models.length > 0), provider, source, models, count: models.length };
1305
+ }
1306
+ async stampSpeedTiers(profileHome, entries) {
1307
+ if (!entries.length)
1308
+ return;
1309
+ const config = await this.deps.config.read(profileHome);
1310
+ // An unavailable Agent offers no paid tier; writes still require a successful lookup.
1311
+ const tiers = await this.deps.config.speedTiers(profileHome, config, entries.map((e) => ({ model: e.bare_id ?? e.id, provider: e.provider_id ?? '' }))).catch(() => []);
1312
+ entries.forEach((entry, i) => {
1313
+ entry.supported_speed_tiers = tiers[i] ?? [];
1314
+ entry.supports_fast_tier = entry.supported_speed_tiers.some((t) => t.service_tier === 'priority');
1315
+ });
1096
1316
  }
1097
1317
  /**
1098
1318
  * Python `_resolve_model_context_length` inputs: the effective base URL, key, and any configured window for this
@@ -1155,27 +1375,35 @@ export class ProviderCatalog {
1155
1375
  if (envVar) {
1156
1376
  for (const name of [envVar, ...(PROVIDER_ENV_VAR_ALIASES[pid] ?? [])]) {
1157
1377
  const v = str(envValues[name] ?? this.processEnv(name, profileHome)).trim();
1378
+ // A reference owns this slot; only the Agent can resolve it before lower-priority keys.
1158
1379
  if (v)
1159
- return v;
1380
+ return v.startsWith('op://') ? null : v;
1160
1381
  }
1161
1382
  }
1162
1383
  // The same config sources `providerHasKey` counts, so a provider shown as keyed resolves its key.
1163
1384
  const model = modelSection(config);
1164
1385
  if (canonicaliseProviderId(model.provider) === canonicaliseProviderId(pid) && valueCountsAsApiKey(pid, model.api_key))
1165
- return str(model.api_key).trim();
1386
+ return str(model.api_key).trim().startsWith('op://') ? null : str(model.api_key).trim();
1166
1387
  for (const [key, value] of Object.entries(dict(config.providers))) {
1167
1388
  if (providerIdentity(key) === providerIdentity(pid) && isDict(value) && valueCountsAsApiKey(pid, value.api_key))
1168
- return str(value.api_key).trim();
1389
+ return str(value.api_key).trim().startsWith('op://') ? null : str(value.api_key).trim();
1169
1390
  }
1170
1391
  return null;
1171
1392
  }
1393
+ /** A raw key or the names the Agent resolves inside the profile's secret scope. */
1394
+ usageKey(pid, apiKey) {
1395
+ const envVar = providerEnvVar(pid);
1396
+ return apiKey ? { api_key: apiKey } : { env_vars: envVar ? [envVar, ...(PROVIDER_ENV_VAR_ALIASES[pid] ?? [])] : [] };
1397
+ }
1172
1398
  /** Account-usage snapshots by profile home, provider, and pool account. */
1173
1399
  accountUsageCache = new Map();
1400
+ quotaReadSequence = 0;
1174
1401
  /**
1175
1402
  * Python `get_provider_quota`. `credential` scopes the read to one credential-pool account (TAL-548): a benched account
1176
1403
  * answers with its local pool state, and every other read uses that account's key.
1177
1404
  */
1178
1405
  async quota(profileHome, providerRaw, opts = {}) {
1406
+ const sequence = opts.sequence ?? ++this.quotaReadSequence;
1179
1407
  const at = opts.at ?? this.deps.now();
1180
1408
  const computed_at = isoAt(at);
1181
1409
  const config = await this.deps.config.read(profileHome);
@@ -1193,7 +1421,9 @@ export class ProviderCatalog {
1193
1421
  let limits = null;
1194
1422
  // Python `_ACCOUNT_USAGE_CACHE_TTL_SECONDS`: a snapshot answers repeat polls for 45 s unless `refresh` is set.
1195
1423
  const cacheKey = `${profileHome}\0${provider}\0${credential?.credential_id ?? ''}`;
1196
- const cached = this.accountUsageCache.get(cacheKey);
1424
+ const hit = this.accountUsageCache.get(cacheKey);
1425
+ const source = this.sourceFingerprint(profileHome);
1426
+ const cached = hit?.source === source ? hit : undefined;
1197
1427
  if (cached && !opts.refresh && this.deps.now() - cached.at <= ACCOUNT_USAGE_CACHE_TTL_S)
1198
1428
  limits = cached.limits;
1199
1429
  else if (sidecar) {
@@ -1206,7 +1436,12 @@ export class ProviderCatalog {
1206
1436
  catch (error) {
1207
1437
  limits = { available: false, unavailable_reason: str(error.message), windows: [], details: [] };
1208
1438
  }
1209
- this.accountUsageCache.set(cacheKey, { at: this.deps.now(), limits });
1439
+ if (source === this.sourceFingerprint(profileHome)) {
1440
+ if ((this.accountUsageCache.get(cacheKey)?.sequence ?? -1) <= sequence)
1441
+ this.accountUsageCache.set(cacheKey, { at: this.deps.now(), limits, source, sequence });
1442
+ }
1443
+ else
1444
+ limits = null;
1210
1445
  }
1211
1446
  // The cache keeps the Agent's raw windows; pace is recomputed as of every response.
1212
1447
  if (limits)
@@ -1216,11 +1451,11 @@ export class ProviderCatalog {
1216
1451
  const reason = str(limits?.unavailable_reason).trim();
1217
1452
  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 });
1218
1453
  }
1219
- if (provider === 'openrouter' || provider === 'deepseek' || provider === 'opencode-go') {
1454
+ if (isBalanceProvider(provider)) {
1220
1455
  const apiKey = this.apiKeyFor(provider, profileHome, config);
1221
- const key = credential ? { credential_id: credential.credential_id } : apiKey ? { api_key: apiKey } : null;
1222
- const read = key ? await this.balance(profileHome, provider, key) : null;
1223
- const status = !key || read?.status === 'no_key' ? 'no_key' : read?.status === 'ok' ? 'ok' : read?.status === 'http_error' ? read.http_status : null;
1456
+ const key = credential ? { credential_id: credential.credential_id } : this.usageKey(provider, apiKey);
1457
+ const read = await this.balance(profileHome, provider, key);
1458
+ const status = read?.status === 'no_key' ? 'no_key' : read?.status === 'ok' ? 'ok' : read?.status === 'http_error' ? read.http_status : null;
1224
1459
  if (provider === 'openrouter') {
1225
1460
  if (status === 'no_key')
1226
1461
  return answer(false, 'no_key', 'OpenRouter quota status needs an OPENROUTER_API_KEY configured on the server.');
@@ -1254,7 +1489,7 @@ export class ProviderCatalog {
1254
1489
  return answer(false, 'unavailable', status === 403 ? 'OpenCode Go account limits require an active subscription.' : 'OpenCode Go account limits are temporarily unavailable.', { account_limits: null });
1255
1490
  }
1256
1491
  // Python `_local_pool_snapshot`: any other provider with a credential pool shows its accounts' local state.
1257
- const entries = credential ? [credential] : (await this.poolEntries(profileHome, provider, null)) ?? [];
1492
+ const entries = credential ? [credential] : (await this.poolEntries(profileHome, provider, null)).value?.entries ?? [];
1258
1493
  if (entries.length) {
1259
1494
  const limits = credential ? poolEntryLimits(credential, at) : poolLimits(entries, at);
1260
1495
  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 });
@@ -1266,7 +1501,7 @@ export class ProviderCatalog {
1266
1501
  * `apiKey`, the provider's configured key. Null when the pool is unknown (see `poolLookup`).
1267
1502
  */
1268
1503
  poolEntries(profileHome, pid, apiKey) {
1269
- return this.poolLookup(profileHome, `pool\0${pid}`, async (sidecar) => (await sidecar.call('usage.pool', { profile_home: profileHome, provider: pid, ...(apiKey ? { api_key: apiKey } : {}) })).entries);
1504
+ return this.poolLookup(profileHome, `pool\0${pid}`, (sidecar) => sidecar.call('usage.pool', { profile_home: profileHome, provider: pid, ...this.usageKey(pid, apiKey) }));
1270
1505
  }
1271
1506
  /** The last answer of each credential-pool lookup, by profile home and lookup. */
1272
1507
  lastPoolAnswers = new Map();
@@ -1275,23 +1510,48 @@ export class ProviderCatalog {
1275
1510
  * lookup, else null: an unknown pool, never an empty one, so no pool account is ever reported as removed.
1276
1511
  */
1277
1512
  async poolLookup(profileHome, lookup, call) {
1513
+ const source = this.sourceFingerprint(profileHome);
1278
1514
  const key = `${profileHome}\0${lookup}`;
1279
1515
  try {
1280
1516
  const sidecar = this.deps.sidecar();
1281
1517
  if (!sidecar)
1282
1518
  throw new Error('the sidecar is not running');
1283
1519
  const answer = await call(sidecar);
1284
- this.lastPoolAnswers.set(key, answer);
1285
- return answer;
1520
+ if (source !== this.sourceFingerprint(profileHome))
1521
+ return { value: null, authoritative: false };
1522
+ this.lastPoolAnswers.set(key, { source, value: answer });
1523
+ return { value: answer, authoritative: true };
1286
1524
  }
1287
1525
  catch (error) {
1288
1526
  this.deps.log(`[catalog] credential pool lookup ${lookup.replace('\0', ' ')} failed: ${str(error.message)}`);
1289
- return this.lastPoolAnswers.get(key) ?? null;
1527
+ const hit = this.lastPoolAnswers.get(key);
1528
+ return { value: hit?.source === source && source === this.sourceFingerprint(profileHome) ? hit.value : null, authoritative: false };
1290
1529
  }
1291
1530
  }
1292
- /** TAL-548: the providers whose persisted credential pool holds an added account; none when the sidecar cannot say. */
1531
+ /** One shared read of pool membership and scoped key revisions before reusing catalog data. */
1293
1532
  poolProviders(profileHome) {
1294
- return this.poolLookup(profileHome, 'providers', async (sidecar) => (await sidecar.call('usage.pool_providers', { profile_home: profileHome })).providers);
1533
+ const inputSource = this.sourceFingerprint(profileHome, false);
1534
+ const readKey = this.sourceFingerprint(profileHome);
1535
+ const pending = this.credentialReads.get(readKey);
1536
+ if (pending)
1537
+ return pending;
1538
+ const read = (async () => {
1539
+ const config = await this.deps.config.read(profileHome);
1540
+ const key_envs = Object.fromEntries(BALANCE_PROVIDERS.filter((pid) => this.apiKeyFor(pid, profileHome, config) === null).map((pid) => [pid, this.usageKey(pid, null).env_vars ?? []]));
1541
+ const result = await this.poolLookup(profileHome, 'providers', (sidecar) => sidecar.call('usage.pool_providers', { profile_home: profileHome, key_envs }));
1542
+ if (inputSource !== this.sourceFingerprint(profileHome, false))
1543
+ return { value: null, authoritative: false };
1544
+ this.scopedKeyAuthority.set(profileHome, result.authoritative);
1545
+ if (result.authoritative)
1546
+ this.scopedKeyRevisions.set(profileHome, JSON.stringify(Object.entries(result.value?.keys ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([pid, state]) => [pid, state.revision])));
1547
+ return result;
1548
+ })().catch(() => {
1549
+ if (inputSource === this.sourceFingerprint(profileHome, false))
1550
+ this.scopedKeyAuthority.set(profileHome, false);
1551
+ return { value: null, authoritative: false };
1552
+ }).finally(() => { this.credentialReads.delete(readKey); });
1553
+ this.credentialReads.set(readKey, read);
1554
+ return read;
1295
1555
  }
1296
1556
  /** TAL-548: one key-based balance read through the sidecar, which resolves a pool account's key itself; null when it fails. */
1297
1557
  async balance(profileHome, provider, key) {
@@ -1308,52 +1568,46 @@ export class ProviderCatalog {
1308
1568
  }
1309
1569
  quotaScopeCache = null;
1310
1570
  /** Python `_quota_server_scope_id`: a 32-hex id persisted at `<state>/.quota_scope_id` (created atomically once). */
1311
- quotaServerScopeId() {
1571
+ async quotaServerScopeId() {
1312
1572
  if (this.quotaScopeCache)
1313
1573
  return this.quotaScopeCache;
1314
- const dir = this.deps.stateDir;
1315
- if (!dir) {
1574
+ const documents = this.deps.documents;
1575
+ if (!documents) {
1316
1576
  this.quotaScopeCache = randomUUID().replaceAll('-', '');
1317
1577
  return this.quotaScopeCache;
1318
1578
  }
1319
- const path = join(dir, '.quota_scope_id');
1320
- let stored = '';
1321
- try {
1322
- stored = readFileSync(path, 'utf8').trim().toLowerCase();
1323
- }
1324
- catch {
1325
- stored = '';
1326
- }
1327
- if (!/^[0-9a-f]{32}$/.test(stored)) {
1328
- stored = randomUUID().replaceAll('-', '');
1329
- mkdirSync(dir, { recursive: true });
1330
- const temporary = `${path}.tmp.${String(process.pid)}`;
1331
- writeFileSync(temporary, `${stored}\n`, 'utf8');
1332
- renameSync(temporary, path);
1333
- }
1579
+ const stored = await updateDocument(documents, 'catalog', 'quota-scope', (value) => {
1580
+ const previous = typeof value === 'string' ? value.trim().toLowerCase() : '';
1581
+ if (/^[0-9a-f]{32}$/.test(previous))
1582
+ return { value, result: previous };
1583
+ const id = randomUUID().replaceAll('-', '');
1584
+ return { value: `${id}\n`, result: id };
1585
+ });
1334
1586
  this.quotaScopeCache = stored;
1335
1587
  return stored;
1336
1588
  }
1337
1589
  /** Python `_quota_profile_scope_id`. */
1338
- quotaProfileScopeId(profile) {
1339
- return `qscope_${createHash('sha256').update(`${this.quotaServerScopeId()}\0${profile}`).digest('hex').slice(0, 32)}`;
1590
+ async quotaProfileScopeId(profile) {
1591
+ return `qscope_${createHash('sha256').update(`${(await this.quotaServerScopeId())}\0${profile}`).digest('hex').slice(0, 32)}`;
1340
1592
  }
1341
1593
  /**
1342
1594
  * Python `get_provider_quotas`: the stable-identity envelope the iOS widget persists. A keyed provider is one source per
1343
1595
  * credential-pool account, else one for its own key (Python `_quota_source_descriptors`, TAL-548). Each source id appears
1344
1596
  * once (two custom providers can share a slug), ordered by provider, account label, then source id.
1345
1597
  */
1346
- async quotas(profileHome, profile, opts = {}) {
1598
+ async quotas(profileHome, profile, opts = {}, attempt = 0) {
1599
+ const sequence = ++this.quotaReadSequence;
1600
+ const source = this.sourceFingerprint(profileHome);
1347
1601
  const at = this.deps.now();
1348
1602
  const { payload: status, pooled: pooledIds } = await this.providerRows(profileHome);
1349
1603
  const active = status.active_provider;
1350
- const scopeId = this.quotaProfileScopeId(profile);
1604
+ const scopeId = (await this.quotaProfileScopeId(profile));
1351
1605
  // Python `_quota_source_id(profile, provider, credential_id)`: a pool account's id, or "provider" for the provider's own key.
1352
1606
  const sourceId = (pid, credential) => `qsrc_${createHash('sha256').update(`${scopeId}\0${pid}\0${credential}`).digest('hex').slice(0, 32)}`;
1353
1607
  // A provider configured only through `hermes auth add` has no key in .env or config.yaml, yet its pool accounts are
1354
1608
  // sources, including a signed-out OAuth provider's. An unknown pool (a failed lookup with no earlier answer) never
1355
1609
  // reports a requested source as missing.
1356
- let poolUnknown = pooledIds === null;
1610
+ let poolUnknown = !status.availability.authoritative;
1357
1611
  const pooled = new Set(pooledIds ?? []);
1358
1612
  const config = await this.deps.config.read(profileHome);
1359
1613
  const perProvider = await Promise.all(status.providers.filter((p) => p.has_key || p.is_custom || pooled.has(str(p.id))).map(async (p) => {
@@ -1361,30 +1615,80 @@ export class ProviderCatalog {
1361
1615
  const label = str(p.display_name) || pid;
1362
1616
  const apiKey = this.apiKeyFor(pid, profileHome, config);
1363
1617
  const found = await this.poolEntries(profileHome, pid, apiKey);
1364
- if (!found)
1618
+ if (!found.authoritative)
1365
1619
  poolUnknown = true;
1366
- const entries = found ?? [];
1620
+ const entries = found.value?.entries ?? [];
1367
1621
  const accounts = entries.map((e) => ({ source_id: sourceId(pid, e.credential_id), provider_id: pid, provider_label: label, account_label: e.label, credential: e }));
1368
1622
  // The provider's own key is a source unless a pool account already holds it.
1369
- const ownKey = !entries.length || (apiKey !== null && !entries.some((e) => e.matches_api_key));
1623
+ const ownKey = found.authoritative && ((!entries.length && (p.has_key === true || p.is_custom === true) && !pooled.has(pid)) || ((apiKey !== null || found.value?.has_api_key === true) && !entries.some((e) => e.matches_api_key)));
1370
1624
  return ownKey ? [{ source_id: sourceId(pid, 'provider'), provider_id: pid, provider_label: label, account_label: label, credential: null }, ...accounts] : accounts;
1371
1625
  }));
1372
1626
  let descriptors = uniqueQuotaSources(perProvider.flat());
1627
+ const membership = new Set(descriptors.map((d) => d.source_id));
1373
1628
  const requested = str(opts.sourceId).trim() || null;
1374
1629
  if (requested)
1375
1630
  descriptors = descriptors.filter((d) => d.source_id === requested);
1376
- const thresholds = this.deps.quotaThresholds?.(profile) ?? QUOTA_THRESHOLD_DEFAULTS;
1377
- const sources = await Promise.all(descriptors.map(async (d) => {
1378
- const q = await this.quota(profileHome, d.provider_id, { refresh: opts.refresh ?? false, at, credential: d.credential });
1631
+ const thresholds = (await this.deps.quotaThresholds?.(profile)) ?? QUOTA_THRESHOLD_DEFAULTS;
1632
+ let sources = await Promise.all(descriptors.map(async (d) => {
1633
+ const q = await this.quota(profileHome, d.provider_id, { refresh: opts.refresh ?? false, at, credential: d.credential, sequence });
1379
1634
  const limits = dict(q.account_limits);
1380
1635
  return classifyQuotaSource({
1381
- source_id: d.source_id, provider_id: d.provider_id, provider_label: d.provider_label, account_label: d.account_label,
1636
+ computed_at: isoAt(at), source_id: d.source_id, provider_id: d.provider_id, provider_label: d.provider_label, account_label: d.account_label,
1382
1637
  is_active_provider: d.provider_id === active, supported: q.supported === true, status: str(limits.status) || str(q.status) || 'unavailable',
1383
1638
  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 ?? [],
1384
1639
  unavailable_reason: limits.unavailable_reason ?? null, retry_after: limits.retry_after ?? null, fetched_at: limits.fetched_at ?? null, message: q.message ?? null,
1385
1640
  }, thresholds);
1386
1641
  }));
1387
- 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 };
1642
+ if (source !== this.sourceFingerprint(profileHome) && attempt === 0)
1643
+ return this.quotas(profileHome, profile, opts, 1);
1644
+ poolUnknown ||= source !== this.sourceFingerprint(profileHome);
1645
+ const cacheKey = `${profileHome}\0${scopeId}`;
1646
+ const cached = source === this.sourceFingerprint(profileHome) ? this.lastQuotaSources.get(cacheKey) : undefined;
1647
+ const prior = cached?.source === source ? cached : undefined;
1648
+ const previous = prior?.sources ?? [];
1649
+ if (!poolUnknown) {
1650
+ sources = sources.map((s) => {
1651
+ const old = previous.find((p) => p.source_id === s.source_id);
1652
+ return s.status === 'unavailable' && old ? this.staleQuotaSource(old, str(s.unavailable_reason || s.message)) : s;
1653
+ });
1654
+ const latestMembership = prior && prior.sequence > sequence ? prior.membership : membership;
1655
+ const sourceSequences = new Map(prior?.sourceSequences);
1656
+ const merged = new Map(previous.filter((s) => latestMembership.has(str(s.source_id))).map((s) => [str(s.source_id), s]));
1657
+ for (const value of sources) {
1658
+ const id = str(value.source_id);
1659
+ if (!latestMembership.has(id) || (sourceSequences.get(id) ?? -1) > sequence)
1660
+ continue;
1661
+ merged.set(id, value);
1662
+ sourceSequences.set(id, sequence);
1663
+ }
1664
+ for (const id of sourceSequences.keys())
1665
+ if (!latestMembership.has(id))
1666
+ sourceSequences.delete(id);
1667
+ const snapshot = uniqueQuotaSources([...merged.values()]);
1668
+ this.lastQuotaSources.set(cacheKey, { source, sequence: Math.max(sequence, prior?.sequence ?? -1), membership: latestMembership, sources: snapshot, sourceSequences });
1669
+ // A delayed read answers with the newest confirmed projection, including confirmed removals.
1670
+ sources = snapshot.filter((s) => !requested || s.source_id === requested);
1671
+ }
1672
+ else {
1673
+ sources = previous.filter((s) => !requested || s.source_id === requested).map((s) => this.staleQuotaSource(s));
1674
+ }
1675
+ const currentSource = this.sourceFingerprint(profileHome);
1676
+ if (currentSource !== source) {
1677
+ poolUnknown = true;
1678
+ sources = [];
1679
+ }
1680
+ const latestMembership = this.lastQuotaSources.get(cacheKey)?.membership;
1681
+ const availability = this.availability(currentSource, !poolUnknown, sources.length > 0);
1682
+ return { availability, version: 1, computed_at: isoAt(at), scope_id: scopeId, profile_id: profile, active_provider: active, requested_source_id: requested, missing_source: Boolean(requested && !poolUnknown && !latestMembership?.has(requested)), sources };
1683
+ }
1684
+ lastQuotaSources = new Map();
1685
+ staleQuotaSource(source, reason = 'Provider data is temporarily unavailable. Showing last-confirmed data.') {
1686
+ return { ...source, status: 'stale', unavailable_reason: reason, message: reason, urgency: { remaining: 'stale', pace: 'stale' } };
1687
+ }
1688
+ availability(source, authoritative, cached) {
1689
+ return { authoritative, status: authoritative ? 'available' : cached ? 'stale' : 'unavailable',
1690
+ unavailable_reason: authoritative ? null : cached ? 'Provider data is temporarily unavailable. Showing last-confirmed data.' : 'Provider data is temporarily unavailable. Try again.',
1691
+ generation: createHash('sha256').update(source).digest('hex') };
1388
1692
  }
1389
1693
  /** Python `get_provider_cost_history` (OpenRouter only; daily snapshots under `<home>/cost-snapshots`). */
1390
1694
  async costHistory(profileHome, providerRaw, days) {
@@ -1397,9 +1701,9 @@ export class ProviderCatalog {
1397
1701
  const budget = this.deps.costBudget();
1398
1702
  const config = await this.deps.config.read(profileHome);
1399
1703
  const apiKey = this.apiKeyFor('openrouter', profileHome, config);
1400
- if (!apiKey)
1704
+ const info = await this.balance(profileHome, 'openrouter', this.usageKey('openrouter', apiKey));
1705
+ if (info?.status === 'no_key')
1401
1706
  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.' };
1402
- const info = await this.balance(profileHome, 'openrouter', { api_key: apiKey });
1403
1707
  if (info?.status !== 'ok' || !info.quota) {
1404
1708
  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.' };
1405
1709
  }