@maudecode/talaria-web 1.18.0 → 1.20.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 (774) 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 +93 -38
  6. package/dist/api/automation-raw.js.map +1 -1
  7. package/dist/api/automation-router.d.ts +99 -17
  8. package/dist/api/automation-router.js +22 -14
  9. package/dist/api/automation-router.js.map +1 -1
  10. package/dist/api/chat-router.d.ts +1433 -37
  11. package/dist/api/chat-router.js +205 -90
  12. package/dist/api/chat-router.js.map +1 -1
  13. package/dist/api/git-router.d.ts +6 -2
  14. package/dist/api/git-router.js +30 -30
  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 +76 -62
  18. package/dist/api/raw-routes.js.map +1 -1
  19. package/dist/api/router.d.ts +5 -5
  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 +2635 -151
  26. package/dist/api/sessions-router.js +193 -177
  27. package/dist/api/sessions-router.js.map +1 -1
  28. package/dist/api/settings-router.d.ts +450 -11
  29. package/dist/api/settings-router.js +102 -49
  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 +102 -80
  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 +963 -109
  38. package/dist/api/tools-router.js +426 -72
  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 +80 -57
  45. package/dist/app.js.map +1 -1
  46. package/dist/auth/crypto.d.ts +3 -0
  47. package/dist/auth/crypto.js +18 -0
  48. package/dist/auth/crypto.js.map +1 -1
  49. package/dist/auth/gate.js +14 -9
  50. package/dist/auth/gate.js.map +1 -1
  51. package/dist/auth/passkeys.d.ts +15 -14
  52. package/dist/auth/passkeys.js +89 -91
  53. package/dist/auth/passkeys.js.map +1 -1
  54. package/dist/auth/store.d.ts +17 -7
  55. package/dist/auth/store.js +93 -52
  56. package/dist/auth/store.js.map +1 -1
  57. package/dist/bin/talaria-web.js +12 -81
  58. package/dist/bin/talaria-web.js.map +1 -1
  59. package/dist/cli/ctl.js +8 -2
  60. package/dist/cli/ctl.js.map +1 -1
  61. package/dist/cli/dotenv.d.ts +5 -0
  62. package/dist/cli/dotenv.js +14 -2
  63. package/dist/cli/dotenv.js.map +1 -1
  64. package/dist/cli/launcher.js +5 -5
  65. package/dist/cli/launcher.js.map +1 -1
  66. package/dist/cli/main.d.ts +1 -0
  67. package/dist/cli/main.js +90 -0
  68. package/dist/cli/main.js.map +1 -0
  69. package/dist/cli/supervise.d.ts +24 -0
  70. package/dist/cli/supervise.js +147 -6
  71. package/dist/cli/supervise.js.map +1 -1
  72. package/dist/config/agent-config.d.ts +37 -11
  73. package/dist/config/agent-config.js +146 -64
  74. package/dist/config/agent-config.js.map +1 -1
  75. package/dist/config.d.ts +2 -0
  76. package/dist/config.js +14 -5
  77. package/dist/config.js.map +1 -1
  78. package/dist/fs/atomic.d.ts +4 -0
  79. package/dist/fs/atomic.js +19 -2
  80. package/dist/fs/atomic.js.map +1 -1
  81. package/dist/fs/stat.d.ts +7 -0
  82. package/dist/fs/stat.js +27 -0
  83. package/dist/fs/stat.js.map +1 -0
  84. package/dist/http/context.d.ts +28 -9
  85. package/dist/http/context.js +77 -25
  86. package/dist/http/context.js.map +1 -1
  87. package/dist/onboarding.d.ts +2 -2
  88. package/dist/onboarding.js +10 -8
  89. package/dist/onboarding.js.map +1 -1
  90. package/dist/profiles/profiles.d.ts +12 -4
  91. package/dist/profiles/profiles.js +78 -30
  92. package/dist/profiles/profiles.js.map +1 -1
  93. package/dist/projects.d.ts +15 -6
  94. package/dist/projects.js +55 -47
  95. package/dist/projects.js.map +1 -1
  96. package/dist/providers/catalog.d.ts +85 -14
  97. package/dist/providers/catalog.js +502 -146
  98. package/dist/providers/catalog.js.map +1 -1
  99. package/dist/providers/env-file.d.ts +10 -3
  100. package/dist/providers/env-file.js +24 -75
  101. package/dist/providers/env-file.js.map +1 -1
  102. package/dist/providers/runtime-env.d.ts +4 -2
  103. package/dist/providers/runtime-env.js +9 -6
  104. package/dist/providers/runtime-env.js.map +1 -1
  105. package/dist/providers/tables.d.ts +3 -1
  106. package/dist/providers/tables.js +4 -2
  107. package/dist/providers/tables.js.map +1 -1
  108. package/dist/redact.js +56 -23
  109. package/dist/redact.js.map +1 -1
  110. package/dist/release.d.ts +7 -0
  111. package/dist/release.js +19 -3
  112. package/dist/release.js.map +1 -1
  113. package/dist/runtime.d.ts +5 -1
  114. package/dist/runtime.js +303 -96
  115. package/dist/runtime.js.map +1 -1
  116. package/dist/server.js +31 -18
  117. package/dist/server.js.map +1 -1
  118. package/dist/sessions/anchor.d.ts +11 -4
  119. package/dist/sessions/anchor.js +33 -37
  120. package/dist/sessions/anchor.js.map +1 -1
  121. package/dist/sessions/background-events.d.ts +2 -0
  122. package/dist/sessions/background-events.js +10 -0
  123. package/dist/sessions/background-events.js.map +1 -0
  124. package/dist/sessions/background-tasks.d.ts +49 -23
  125. package/dist/sessions/background-tasks.js +161 -89
  126. package/dist/sessions/background-tasks.js.map +1 -1
  127. package/dist/sessions/background-updates.d.ts +15 -8
  128. package/dist/sessions/background-updates.js +101 -25
  129. package/dist/sessions/background-updates.js.map +1 -1
  130. package/dist/sessions/cli-sessions.d.ts +23 -6
  131. package/dist/sessions/cli-sessions.js +84 -84
  132. package/dist/sessions/cli-sessions.js.map +1 -1
  133. package/dist/sessions/completions.d.ts +38 -2
  134. package/dist/sessions/completions.js +348 -77
  135. package/dist/sessions/completions.js.map +1 -1
  136. package/dist/sessions/compress.d.ts +8 -0
  137. package/dist/sessions/compress.js +2 -2
  138. package/dist/sessions/compress.js.map +1 -1
  139. package/dist/sessions/drafts.d.ts +14 -10
  140. package/dist/sessions/drafts.js +38 -43
  141. package/dist/sessions/drafts.js.map +1 -1
  142. package/dist/sessions/export.d.ts +2 -0
  143. package/dist/sessions/export.js +6 -2
  144. package/dist/sessions/export.js.map +1 -1
  145. package/dist/sessions/gateway-sessions.d.ts +86 -0
  146. package/dist/sessions/gateway-sessions.js +914 -0
  147. package/dist/sessions/gateway-sessions.js.map +1 -0
  148. package/dist/sessions/gateway-watcher.d.ts +10 -11
  149. package/dist/sessions/gateway-watcher.js +25 -21
  150. package/dist/sessions/gateway-watcher.js.map +1 -1
  151. package/dist/sessions/goals.d.ts +31 -0
  152. package/dist/sessions/goals.js +75 -0
  153. package/dist/sessions/goals.js.map +1 -0
  154. package/dist/sessions/journal.d.ts +5 -79
  155. package/dist/sessions/journal.js +15 -606
  156. package/dist/sessions/journal.js.map +1 -1
  157. package/dist/sessions/list.d.ts +34 -18
  158. package/dist/sessions/list.js +161 -147
  159. package/dist/sessions/list.js.map +1 -1
  160. package/dist/sessions/merge.d.ts +77 -13
  161. package/dist/sessions/merge.js +315 -65
  162. package/dist/sessions/merge.js.map +1 -1
  163. package/dist/sessions/notices.d.ts +22 -0
  164. package/dist/sessions/notices.js +72 -0
  165. package/dist/sessions/notices.js.map +1 -0
  166. package/dist/sessions/pending.d.ts +10 -4
  167. package/dist/sessions/pending.js +83 -7
  168. package/dist/sessions/pending.js.map +1 -1
  169. package/dist/sessions/prefix-index.d.ts +85 -0
  170. package/dist/sessions/prefix-index.js +243 -0
  171. package/dist/sessions/prefix-index.js.map +1 -0
  172. package/dist/sessions/recovery.d.ts +4 -7
  173. package/dist/sessions/recovery.js +100 -283
  174. package/dist/sessions/recovery.js.map +1 -1
  175. package/dist/sessions/relay.d.ts +20 -20
  176. package/dist/sessions/relay.js +205 -127
  177. package/dist/sessions/relay.js.map +1 -1
  178. package/dist/sessions/service.d.ts +251 -78
  179. package/dist/sessions/service.js +1510 -564
  180. package/dist/sessions/service.js.map +1 -1
  181. package/dist/sessions/session.d.ts +20 -2
  182. package/dist/sessions/session.js +33 -5
  183. package/dist/sessions/session.js.map +1 -1
  184. package/dist/sessions/shares.d.ts +7 -6
  185. package/dist/sessions/shares.js +22 -45
  186. package/dist/sessions/shares.js.map +1 -1
  187. package/dist/sessions/source-kind.js +1 -0
  188. package/dist/sessions/source-kind.js.map +1 -1
  189. package/dist/sessions/state-db-identity.d.ts +18 -0
  190. package/dist/sessions/state-db-identity.js +67 -0
  191. package/dist/sessions/state-db-identity.js.map +1 -0
  192. package/dist/sessions/state-db-worker.js +24 -5
  193. package/dist/sessions/state-db-worker.js.map +1 -1
  194. package/dist/sessions/state-db.d.ts +157 -21
  195. package/dist/sessions/state-db.js +1145 -204
  196. package/dist/sessions/state-db.js.map +1 -1
  197. package/dist/sessions/state-saved.d.ts +1 -1
  198. package/dist/sessions/state-saved.js +35 -11
  199. package/dist/sessions/state-saved.js.map +1 -1
  200. package/dist/sessions/storage/backend.d.ts +171 -0
  201. package/dist/sessions/storage/backend.js +19 -0
  202. package/dist/sessions/storage/backend.js.map +1 -0
  203. package/dist/sessions/storage/conformance.d.ts +20 -0
  204. package/dist/sessions/storage/conformance.js +237 -0
  205. package/dist/sessions/storage/conformance.js.map +1 -0
  206. package/dist/sessions/storage/document.d.ts +16 -0
  207. package/dist/sessions/storage/document.js +44 -0
  208. package/dist/sessions/storage/document.js.map +1 -0
  209. package/dist/sessions/storage/json-documents.d.ts +25 -0
  210. package/dist/sessions/storage/json-documents.js +277 -0
  211. package/dist/sessions/storage/json-documents.js.map +1 -0
  212. package/dist/sessions/storage/json-files.d.ts +23 -0
  213. package/dist/sessions/storage/json-files.js +244 -0
  214. package/dist/sessions/storage/json-files.js.map +1 -0
  215. package/dist/sessions/storage/json-installation.d.ts +3 -0
  216. package/dist/sessions/storage/json-installation.js +31 -0
  217. package/dist/sessions/storage/json-installation.js.map +1 -0
  218. package/dist/sessions/storage/json-journal.d.ts +113 -0
  219. package/dist/sessions/storage/json-journal.js +756 -0
  220. package/dist/sessions/storage/json-journal.js.map +1 -0
  221. package/dist/sessions/storage/json-recovery.d.ts +4 -0
  222. package/dist/sessions/storage/json-recovery.js +195 -0
  223. package/dist/sessions/storage/json-recovery.js.map +1 -0
  224. package/dist/sessions/storage/json.d.ts +82 -0
  225. package/dist/sessions/storage/json.js +350 -0
  226. package/dist/sessions/storage/json.js.map +1 -0
  227. package/dist/sessions/storage/row.d.ts +6 -0
  228. package/dist/sessions/storage/row.js +49 -0
  229. package/dist/sessions/storage/row.js.map +1 -0
  230. package/dist/sessions/storage/search-text.d.ts +4 -0
  231. package/dist/sessions/storage/search-text.js +16 -0
  232. package/dist/sessions/storage/search-text.js.map +1 -0
  233. package/dist/sessions/store.d.ts +67 -73
  234. package/dist/sessions/store.js +341 -607
  235. package/dist/sessions/store.js.map +1 -1
  236. package/dist/sessions/streams.d.ts +12 -7
  237. package/dist/sessions/streams.js +58 -15
  238. package/dist/sessions/streams.js.map +1 -1
  239. package/dist/sessions/titles.js +1 -1
  240. package/dist/sessions/titles.js.map +1 -1
  241. package/dist/sessions/todo.d.ts +5 -0
  242. package/dist/sessions/todo.js +28 -21
  243. package/dist/sessions/todo.js.map +1 -1
  244. package/dist/sessions/tool-display.d.ts +8 -5
  245. package/dist/sessions/tool-display.js +28 -16
  246. package/dist/sessions/tool-display.js.map +1 -1
  247. package/dist/sessions/turn-context.d.ts +10 -1
  248. package/dist/sessions/turn-context.js +13 -6
  249. package/dist/sessions/turn-context.js.map +1 -1
  250. package/dist/sessions/turn.d.ts +110 -22
  251. package/dist/sessions/turn.js +918 -380
  252. package/dist/sessions/turn.js.map +1 -1
  253. package/dist/sessions/wakeup-pause.d.ts +8 -13
  254. package/dist/sessions/wakeup-pause.js +31 -49
  255. package/dist/sessions/wakeup-pause.js.map +1 -1
  256. package/dist/sessions/window.js +13 -12
  257. package/dist/sessions/window.js.map +1 -1
  258. package/dist/settings.d.ts +19 -7
  259. package/dist/settings.js +179 -183
  260. package/dist/settings.js.map +1 -1
  261. package/dist/sidecar/client.d.ts +41 -4
  262. package/dist/sidecar/client.js +192 -42
  263. package/dist/sidecar/client.js.map +1 -1
  264. package/dist/sidecar/discover.d.ts +2 -1
  265. package/dist/sidecar/discover.js +93 -10
  266. package/dist/sidecar/discover.js.map +1 -1
  267. package/dist/sidecar/env.d.ts +4 -0
  268. package/dist/sidecar/env.js +3 -0
  269. package/dist/sidecar/env.js.map +1 -0
  270. package/dist/sidecar/fake.d.ts +6 -2
  271. package/dist/sidecar/fake.js +34 -4
  272. package/dist/sidecar/fake.js.map +1 -1
  273. package/dist/spa.d.ts +9 -4
  274. package/dist/spa.js +36 -12
  275. package/dist/spa.js.map +1 -1
  276. package/dist/startup-timing.d.ts +66 -0
  277. package/dist/startup-timing.js +129 -0
  278. package/dist/startup-timing.js.map +1 -0
  279. package/dist/test/env-file.d.ts +5 -0
  280. package/dist/test/env-file.js +88 -0
  281. package/dist/test/env-file.js.map +1 -0
  282. package/dist/test/fs-handles.d.ts +11 -0
  283. package/dist/test/fs-handles.js +31 -0
  284. package/dist/test/fs-handles.js.map +1 -0
  285. package/dist/test/harness.d.ts +1 -0
  286. package/dist/test/harness.js +1 -1
  287. package/dist/test/harness.js.map +1 -1
  288. package/dist/test/passkeys.d.ts +22 -0
  289. package/dist/test/passkeys.js +62 -0
  290. package/dist/test/passkeys.js.map +1 -0
  291. package/dist/test/storage.d.ts +15 -0
  292. package/dist/test/storage.js +26 -0
  293. package/dist/test/storage.js.map +1 -0
  294. package/dist/tools/commands.d.ts +6 -1
  295. package/dist/tools/commands.js +8 -2
  296. package/dist/tools/commands.js.map +1 -1
  297. package/dist/tools/connectors.d.ts +33 -0
  298. package/dist/tools/connectors.js +121 -0
  299. package/dist/tools/connectors.js.map +1 -0
  300. package/dist/tools/crons.d.ts +46 -5
  301. package/dist/tools/crons.js +100 -26
  302. package/dist/tools/crons.js.map +1 -1
  303. package/dist/tools/dashboard-plugins.d.ts +34 -5
  304. package/dist/tools/dashboard-plugins.js +146 -14
  305. package/dist/tools/dashboard-plugins.js.map +1 -1
  306. package/dist/tools/extensions.d.ts +5 -4
  307. package/dist/tools/extensions.js +213 -207
  308. package/dist/tools/extensions.js.map +1 -1
  309. package/dist/tools/gateway-restart.d.ts +82 -0
  310. package/dist/tools/gateway-restart.js +222 -0
  311. package/dist/tools/gateway-restart.js.map +1 -0
  312. package/dist/tools/health.d.ts +28 -4
  313. package/dist/tools/health.js +171 -50
  314. package/dist/tools/health.js.map +1 -1
  315. package/dist/tools/hygiene.d.ts +4 -3
  316. package/dist/tools/hygiene.js +23 -9
  317. package/dist/tools/hygiene.js.map +1 -1
  318. package/dist/tools/insights.d.ts +1 -1
  319. package/dist/tools/insights.js +2 -2
  320. package/dist/tools/insights.js.map +1 -1
  321. package/dist/tools/kanban.d.ts +4 -2
  322. package/dist/tools/kanban.js +28 -19
  323. package/dist/tools/kanban.js.map +1 -1
  324. package/dist/tools/mcp-health.js +74 -24
  325. package/dist/tools/mcp-health.js.map +1 -1
  326. package/dist/tools/mcp.js +19 -1
  327. package/dist/tools/mcp.js.map +1 -1
  328. package/dist/tools/memory-masks.d.ts +5 -0
  329. package/dist/tools/memory-masks.js +256 -0
  330. package/dist/tools/memory-masks.js.map +1 -0
  331. package/dist/tools/memory.d.ts +29 -3
  332. package/dist/tools/memory.js +69 -27
  333. package/dist/tools/memory.js.map +1 -1
  334. package/dist/tools/plugin-oauth.d.ts +14 -0
  335. package/dist/tools/plugin-oauth.js +53 -0
  336. package/dist/tools/plugin-oauth.js.map +1 -0
  337. package/dist/tools/plugin-settings.d.ts +15 -0
  338. package/dist/tools/plugin-settings.js +90 -0
  339. package/dist/tools/plugin-settings.js.map +1 -0
  340. package/dist/tools/prune-advice.d.ts +50 -0
  341. package/dist/tools/prune-advice.js +114 -0
  342. package/dist/tools/prune-advice.js.map +1 -0
  343. package/dist/tools/skills.d.ts +13 -5
  344. package/dist/tools/skills.js +53 -19
  345. package/dist/tools/skills.js.map +1 -1
  346. package/dist/tools/update-notifications.d.ts +102 -17
  347. package/dist/tools/update-notifications.js +400 -82
  348. package/dist/tools/update-notifications.js.map +1 -1
  349. package/dist/tools/update-rollback.d.ts +11 -0
  350. package/dist/tools/update-rollback.js +28 -0
  351. package/dist/tools/update-rollback.js.map +1 -0
  352. package/dist/tools/updates.d.ts +100 -33
  353. package/dist/tools/updates.js +1085 -345
  354. package/dist/tools/updates.js.map +1 -1
  355. package/dist/util.d.ts +2 -0
  356. package/dist/util.js +5 -0
  357. package/dist/util.js.map +1 -1
  358. package/dist/workspace/git.d.ts +16 -1
  359. package/dist/workspace/git.js +291 -11
  360. package/dist/workspace/git.js.map +1 -1
  361. package/dist/workspace/media.d.ts +12 -9
  362. package/dist/workspace/media.js +98 -38
  363. package/dist/workspace/media.js.map +1 -1
  364. package/dist/workspace/preview.js +4 -14
  365. package/dist/workspace/preview.js.map +1 -1
  366. package/dist/workspace/rollback.d.ts +8 -4
  367. package/dist/workspace/rollback.js +22 -12
  368. package/dist/workspace/rollback.js.map +1 -1
  369. package/dist/workspace/workspaces.d.ts +15 -19
  370. package/dist/workspace/workspaces.js +55 -127
  371. package/dist/workspace/workspaces.js.map +1 -1
  372. package/dist/workspace/worktrees.js +170 -32
  373. package/dist/workspace/worktrees.js.map +1 -1
  374. package/package.json +6 -4
  375. package/sidecar/agent_dependency.json +4 -4
  376. package/sidecar/agent_support.json +29 -0
  377. package/sidecar/scripts/record-fixtures.py +12 -3
  378. package/sidecar/scripts/replay_sidecar.py +8 -0
  379. package/sidecar/scripts/suite_runner.py +458 -0
  380. package/sidecar/scripts/test.sh +11 -5
  381. package/sidecar/talaria_sidecar/__init__.py +6 -1
  382. package/sidecar/talaria_sidecar/__main__.py +13 -4
  383. package/sidecar/talaria_sidecar/agent_mutation.py +1111 -0
  384. package/sidecar/talaria_sidecar/agent_services.py +1116 -0
  385. package/sidecar/talaria_sidecar/context_detail.py +59 -0
  386. package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
  387. package/sidecar/talaria_sidecar/home.py +133 -14
  388. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  389. package/sidecar/talaria_sidecar/methods/aux.py +211 -41
  390. package/sidecar/talaria_sidecar/methods/chat.py +1378 -200
  391. package/sidecar/talaria_sidecar/methods/checkpoints.py +29 -4
  392. package/sidecar/talaria_sidecar/methods/commands.py +483 -13
  393. package/sidecar/talaria_sidecar/methods/config.py +195 -6
  394. package/sidecar/talaria_sidecar/methods/connectors.py +375 -0
  395. package/sidecar/talaria_sidecar/methods/cron.py +374 -36
  396. package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
  397. package/sidecar/talaria_sidecar/methods/dashboard_plugins.py +301 -0
  398. package/sidecar/talaria_sidecar/methods/gateway.py +215 -55
  399. package/sidecar/talaria_sidecar/methods/gateway_sessions.py +418 -0
  400. package/sidecar/talaria_sidecar/methods/goals.py +81 -41
  401. package/sidecar/talaria_sidecar/methods/kanban.py +82 -30
  402. package/sidecar/talaria_sidecar/methods/memory.py +84 -0
  403. package/sidecar/talaria_sidecar/methods/oauth.py +144 -13
  404. package/sidecar/talaria_sidecar/methods/plugin_catalog.py +198 -0
  405. package/sidecar/talaria_sidecar/methods/plugin_oauth.py +234 -0
  406. package/sidecar/talaria_sidecar/methods/process.py +229 -16
  407. package/sidecar/talaria_sidecar/methods/profiles.py +48 -9
  408. package/sidecar/talaria_sidecar/methods/providers.py +196 -26
  409. package/sidecar/talaria_sidecar/methods/runtime.py +71 -1
  410. package/sidecar/talaria_sidecar/methods/skills.py +83 -5
  411. package/sidecar/talaria_sidecar/methods/state_db.py +405 -112
  412. package/sidecar/talaria_sidecar/methods/stt.py +159 -78
  413. package/sidecar/talaria_sidecar/methods/text.py +14 -1
  414. package/sidecar/talaria_sidecar/methods/toolsets.py +51 -0
  415. package/sidecar/talaria_sidecar/methods/usage.py +113 -14
  416. package/sidecar/talaria_sidecar/methods/worktree.py +145 -19
  417. package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
  418. package/sidecar/talaria_sidecar/rpc.py +70 -7
  419. package/sidecar/talaria_sidecar/runtime.py +122 -14
  420. package/sidecar/talaria_sidecar/speed.py +47 -0
  421. package/sidecar/talaria_sidecar/startup_timing.py +119 -0
  422. package/static/dist/FILES.txt +242 -215
  423. package/static/dist/THIRD_PARTY_LICENSES.md +6 -22
  424. package/static/dist/assets/AppShell-3isgvoro.js +8 -0
  425. package/static/dist/assets/Button-f4n6dapf.js +1 -0
  426. package/static/dist/assets/ChatPage-k7b6xfax.js +4 -0
  427. package/static/dist/assets/ConnectTargets-juqvt5lm.js +1 -0
  428. package/static/dist/assets/Dialog-hta5utr3.js +1 -0
  429. package/static/dist/assets/ExtensionRoute-zrhjn0wm.js +1 -0
  430. package/static/dist/assets/Field-b6ez48oz.js +1 -0
  431. package/static/dist/assets/HubRoute-iwvso2tz.js +2 -0
  432. package/static/dist/assets/LinkCheckContext-ix6zqqkk.js +1 -0
  433. package/static/dist/assets/Markdown-jdeth7zq.js +2 -0
  434. package/static/dist/assets/MarkdownRenderer-fbswsgld.js +302 -0
  435. package/static/dist/assets/MarkdownRenderer-i9o6nr38.css +1 -0
  436. package/static/dist/assets/OAuthSignIn-bpd8hom3.js +1 -0
  437. package/static/dist/assets/ResourceSwitch-odi6dy2o.js +1 -0
  438. package/static/dist/assets/RightPanel-eu6enlo6.js +1 -0
  439. package/static/dist/assets/Select-kr07928w.js +1 -0
  440. package/static/dist/assets/SessionListPanel-etb2i54l.js +1 -0
  441. package/static/dist/assets/SettingsLayout-eu3xxp23.js +1 -0
  442. package/static/dist/assets/States-icab19yg.js +1 -0
  443. package/static/dist/assets/TerminalPanel-hngmsr8b.js +16 -0
  444. package/static/dist/assets/Toaster-nb2uzjw1.js +1 -0
  445. package/static/dist/assets/_app-ga1moszl.js +1 -0
  446. package/static/dist/assets/_app.ext._extensionId-na62087t.js +1 -0
  447. package/static/dist/assets/_app.index-jypgdqoi.js +1 -0
  448. package/static/dist/assets/_app.insights-kabejdtm.js +1 -0
  449. package/static/dist/assets/_app.kanban-m19kfcmx.js +1 -0
  450. package/static/dist/assets/_app.logs-iw4m96sz.js +1 -0
  451. package/static/dist/assets/_app.memory-jq5frwcg.js +1 -0
  452. package/static/dist/assets/_app.profiles-kwa3m7rp.js +1 -0
  453. package/static/dist/assets/_app.session._sessionId-o5lyeipj.js +1 -0
  454. package/static/dist/assets/_app.settings-ckl8hdll.js +1 -0
  455. package/static/dist/assets/_app.settings._section-dtdjk98a.js +3 -0
  456. package/static/dist/assets/_app.skills-n016rihd.js +1 -0
  457. package/static/dist/assets/_app.tasks-n8zuqsqe.js +1 -0
  458. package/static/dist/assets/_app.todos-btew5v2s.js +1 -0
  459. package/static/dist/assets/_app.workspaces-lnfk95ch.js +1 -0
  460. package/static/dist/assets/abnfDiagram-VCTEODGH-dttheegb.js +1 -0
  461. package/static/dist/assets/{angular-html-nu6l31w3.js → angular-html-l28bd5fo.js} +1 -1
  462. package/static/dist/assets/{angular-ts-fbx4f8mm.js → angular-ts-ffzx1vj0.js} +1 -1
  463. package/static/dist/assets/{apl-bx2xr7dh.js → apl-b7h7mmy4.js} +1 -1
  464. package/static/dist/assets/{arc-7popfhbk.js → arc-fk1b21lm.js} +1 -1
  465. package/static/dist/assets/architecture-7GRP2DOG-ic435919.js +1 -0
  466. package/static/dist/assets/architectureDiagram-5GKGNRK7-8r3glgiq.js +36 -0
  467. package/static/dist/assets/archive-il7c6053.js +1 -0
  468. package/static/dist/assets/{astro-g77i97cb.js → astro-k117fy0t.js} +1 -1
  469. package/static/dist/assets/back-gu3nu279.js +1 -0
  470. package/static/dist/assets/{blade-boo0u92y.js → blade-npefsmn8.js} +1 -1
  471. package/static/dist/assets/{blockDiagram-I7D4REHJ-jzthp1wa.js → blockDiagram-I7D4REHJ-jenyos6c.js} +4 -4
  472. package/static/dist/assets/{c-cat0w3sc.js → c-2umg6kbn.js} +1 -1
  473. package/static/dist/assets/{c4Diagram-7LVT6UL2-mwr67mt8.js → c4Diagram-7LVT6UL2-nzlx7yc4.js} +1 -1
  474. package/static/dist/assets/cancel-bsnfo36u.js +1 -0
  475. package/static/dist/assets/channel-dsbtk01y.js +1 -0
  476. package/static/dist/assets/{chunk-4HAMMTFA-gpq1y228.js → chunk-4HAMMTFA-g0cru63b.js} +20 -20
  477. package/static/dist/assets/{chunk-75Z2AOVW-ktje58c4.js → chunk-75Z2AOVW-7rkswkjw.js} +1 -1
  478. package/static/dist/assets/chunk-DU6HZSFF-fn73jzdo.js +128 -0
  479. package/static/dist/assets/{chunk-F27PBJKO-cmev85ti.js → chunk-F27PBJKO-oey8e72m.js} +1 -1
  480. package/static/dist/assets/{chunk-FOHPRMQF-opuflp73.js → chunk-FOHPRMQF-ipewusgj.js} +8 -8
  481. package/static/dist/assets/{chunk-GMAD6QVW-htjgt22y.js → chunk-GMAD6QVW-l2a32wiy.js} +6 -6
  482. package/static/dist/assets/{chunk-GVQU2GXP-qndvoxl7.js → chunk-GVQU2GXP-o9dwzh8h.js} +1 -1
  483. package/static/dist/assets/{chunk-IMKFNOWR-e5bkd5q6.js → chunk-IMKFNOWR-eblwj20b.js} +5 -5
  484. package/static/dist/assets/{chunk-L3NEJ4N5-mj1155r6.js → chunk-L3NEJ4N5-o2mufspb.js} +1 -1
  485. package/static/dist/assets/chunk-OSK3NFVY-ijp9pw29.js +10 -0
  486. package/static/dist/assets/{chunk-P2QGCYS3-f7t54xfm.js → chunk-P2QGCYS3-hs9hqwl4.js} +1 -1
  487. package/static/dist/assets/{chunk-POPQ4Y6H-i927v9oa.js → chunk-POPQ4Y6H-bp9rsqez.js} +1 -1
  488. package/static/dist/assets/{chunk-PWAF6VOD-m84lmnyt.js → chunk-PWAF6VOD-brkprov6.js} +1 -1
  489. package/static/dist/assets/{chunk-SHT3W25Y-mi0tlcu9.js → chunk-SHT3W25Y-gmpv0pxz.js} +13 -13
  490. package/static/dist/assets/{chunk-SVP7TREG-dnau702j.js → chunk-SVP7TREG-oldm08fu.js} +4 -4
  491. package/static/dist/assets/{chunk-TICWLB2K-edem1gkg.js → chunk-TICWLB2K-ooqilfv7.js} +6 -6
  492. package/static/dist/assets/{chunk-XXDRQBXY-n40pf9vy.js → chunk-XXDRQBXY-jkmnqu65.js} +1 -1
  493. package/static/dist/assets/classDiagram-ZZMXUADV-fvgcv24g.js +1 -0
  494. package/static/dist/assets/classDiagram-v2-VYDZK3BY-fvgcv24g.js +1 -0
  495. package/static/dist/assets/{cobol-go622aep.js → cobol-ssf9hkh7.js} +1 -1
  496. package/static/dist/assets/{coffee-jwo1xjhq.js → coffee-lsi9fwae.js} +1 -1
  497. package/static/dist/assets/copied-bz3gkazh.js +1 -0
  498. package/static/dist/assets/copy-jo006coq.js +1 -0
  499. package/static/dist/assets/cose-bilkent-JH36ORCC-layy2r27.js +1 -0
  500. package/static/dist/assets/{cpp-nre7j0kq.js → cpp-lfrh4y55.js} +1 -1
  501. package/static/dist/assets/{crystal-lviso2je.js → crystal-efh4pte7.js} +1 -1
  502. package/static/dist/assets/{css-ywz9j4hb.js → css-i82qdo89.js} +1 -1
  503. package/static/dist/assets/cynefin-OW5HDTMX-d47op7qs.js +1 -0
  504. package/static/dist/assets/cynefinDiagram-5FMLGOSQ-doa7594y.js +62 -0
  505. package/static/dist/assets/cytoscape.esm-fjn2algp.js +321 -0
  506. package/static/dist/assets/dagre-GXQ25YYZ-os768dp4.js +4 -0
  507. package/static/dist/assets/dagre-o5jmjz46.js +1 -0
  508. package/static/dist/assets/{diagram-S7CK7UJ4-l8abxaes.js → diagram-S7CK7UJ4-fyi81nxd.js} +4 -4
  509. package/static/dist/assets/diagram-UQ7AKVKN-j3exggaq.js +41 -0
  510. package/static/dist/assets/{diagram-VSXAHHWV-fnky6lvc.js → diagram-VSXAHHWV-omo1en2l.js} +1 -1
  511. package/static/dist/assets/diagram-VX7I27RA-oro2cyu0.js +24 -0
  512. package/static/dist/assets/diagram-Z3DM3KII-byt9vdb7.js +24 -0
  513. package/static/dist/assets/dist-crc46di9.js +7 -0
  514. package/static/dist/assets/dist-etpoyscq.js +44 -0
  515. package/static/dist/assets/{dist-c4c2m3kv.js → dist-nb670eu6.js} +1 -1
  516. package/static/dist/assets/ebnfDiagram-PWID7BFC-ee3haugh.js +1 -0
  517. package/static/dist/assets/{edge-dvv35byi.js → edge-lrob635n.js} +1 -1
  518. package/static/dist/assets/{elixir-i2f1ki1g.js → elixir-kdpxeq93.js} +1 -1
  519. package/static/dist/assets/{elm-cpepyjm4.js → elm-c8irj9uh.js} +1 -1
  520. package/static/dist/assets/erDiagram-RLTQ6QDP-d1e93yff.js +99 -0
  521. package/static/dist/assets/{erb-idlyfheg.js → erb-flwsv146.js} +1 -1
  522. package/static/dist/assets/eventmodeling-NTZA5JFV-i1ihb40c.js +1 -0
  523. package/static/dist/assets/flowDiagram-HODETNUW-jahfpiod.js +1 -0
  524. package/static/dist/assets/folder-plus-ivoglq98.js +1 -0
  525. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ngbsyp37.js → ganttDiagram-EL5Y4UJY-m7g0jud4.js} +1 -1
  526. package/static/dist/assets/{git-rebase-d9lpfpbn.js → git-rebase-bli19aoq.js} +1 -1
  527. package/static/dist/assets/gitGraph-4MIJSDKK-hzdj07yy.js +1 -0
  528. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-eravdti4.js → gitGraphDiagram-WWUBYQGX-dm9agbna.js} +1 -1
  529. package/static/dist/assets/{glimmer-js-39i0urfb.js → glimmer-js-d4icg38p.js} +1 -1
  530. package/static/dist/assets/{glimmer-ts-gdtd6n73.js → glimmer-ts-bk75772u.js} +1 -1
  531. package/static/dist/assets/{glsl-o1djm5pp.js → glsl-lqormp2b.js} +1 -1
  532. package/static/dist/assets/{graphql-kj6rz55t.js → graphql-hw6s5qzd.js} +1 -1
  533. package/static/dist/assets/{hack-pe1qqr2i.js → hack-ozrv6l4d.js} +1 -1
  534. package/static/dist/assets/{haml-cknnbcqs.js → haml-gytfosdt.js} +1 -1
  535. package/static/dist/assets/{handlebars-hwq64orh.js → handlebars-b6g7jffd.js} +1 -1
  536. package/static/dist/assets/highlighted-body-KPVGNVTW-mmgz2czu.js +1 -0
  537. package/static/dist/assets/host-fr6lni1p.js +1 -0
  538. package/static/dist/assets/{html-derivative-h2i2pyjc.js → html-derivative-f1tqcroo.js} +1 -1
  539. package/static/dist/assets/{html-ha05xtvt.js → html-on6oj6yj.js} +1 -1
  540. package/static/dist/assets/{http-cf94zu27.js → http-odhufsl7.js} +1 -1
  541. package/static/dist/assets/{hurl-lslax568.js → hurl-mu680336.js} +1 -1
  542. package/static/dist/assets/index-73649w7o.js +10 -0
  543. package/static/dist/assets/index-lhf1fsdf.css +2 -0
  544. package/static/dist/assets/info-A6RAGUB7-lll1dqmm.js +1 -0
  545. package/static/dist/assets/infoDiagram-27XIBGKW-k7esc639.js +2 -0
  546. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-i6ypq351.js → ishikawaDiagram-5VMMS53U-lgr6k9wo.js} +6 -6
  547. package/static/dist/assets/{java-ndjf2o89.js → java-nxbe8uju.js} +1 -1
  548. package/static/dist/assets/{javascript-coftlwy3.js → javascript-lz3kxcyz.js} +1 -1
  549. package/static/dist/assets/{jinja-etei9wy7.js → jinja-l5pk84gt.js} +1 -1
  550. package/static/dist/assets/{jison-hyekgft1.js → jison-jdutdwyy.js} +1 -1
  551. package/static/dist/assets/{journeyDiagram-3NMN7TZE-lduf1z95.js → journeyDiagram-3NMN7TZE-ndmbxuqk.js} +1 -1
  552. package/static/dist/assets/{json-lok0yaho.js → json-dnge4ssf.js} +1 -1
  553. package/static/dist/assets/{jsx-b64w0nbx.js → jsx-c3lwhkep.js} +1 -1
  554. package/static/dist/assets/{jsx-runtime-eu92a5pu.js → jsx-runtime-o40ukmjl.js} +1 -1
  555. package/static/dist/assets/{julia-jxxzz2yn.js → julia-hwd4rrcb.js} +1 -1
  556. package/static/dist/assets/{just-mc29iw9t.js → just-nka3oxdx.js} +1 -1
  557. package/static/dist/assets/{kanban-definition-UXKFOSKX-e18bu4pg.js → kanban-definition-UXKFOSKX-mjxym0jp.js} +28 -28
  558. package/static/dist/assets/katex-cbdd1ohw.css +1 -0
  559. package/static/dist/assets/katex-pbij2oo6.js +257 -0
  560. package/static/dist/assets/{latex-pafa03ft.js → latex-iqmr7e40.js} +1 -1
  561. package/static/dist/assets/lazyRouteComponent-cwb6l7xh.js +1 -0
  562. package/static/dist/assets/{line-meizqsvn.js → line-mtr5k6il.js} +1 -1
  563. package/static/dist/assets/linear-jlwrvvy6.js +1 -0
  564. package/static/dist/assets/link-iadw4ghw.js +2 -0
  565. package/static/dist/assets/{liquid-hz5c2pov.js → liquid-okmk9tbp.js} +1 -1
  566. package/static/dist/assets/locale-cs-n41hptse.js +5 -0
  567. package/static/dist/assets/locale-de-k7ht47ew.js +5 -0
  568. package/static/dist/assets/locale-es-ih078lco.js +5 -0
  569. package/static/dist/assets/locale-fr-ojabnf41.js +5 -0
  570. package/static/dist/assets/locale-it-kb96jufy.js +5 -0
  571. package/static/dist/assets/locale-ja-jpsp3xs1.js +7 -0
  572. package/static/dist/assets/locale-ko-cpzuy0n7.js +5 -0
  573. package/static/dist/assets/locale-pl-dbap4leu.js +5 -0
  574. package/static/dist/assets/locale-pt-nci7w72x.js +5 -0
  575. package/static/dist/assets/locale-ru-soghe4cs.js +7 -0
  576. package/static/dist/assets/locale-tr-ks00mu68.js +5 -0
  577. package/static/dist/assets/locale-vi-ij7twfm6.js +5 -0
  578. package/static/dist/assets/locale-zh-Hant-feakwr7a.js +5 -0
  579. package/static/dist/assets/locale-zh-ren0vjyv.js +7 -0
  580. package/static/dist/assets/login-lkhtzfcl.js +1 -0
  581. package/static/dist/assets/{lua-leyf8mlb.js → lua-krx4xj5h.js} +1 -1
  582. package/static/dist/assets/{marko-cn1fzpdv.js → marko-gbvpfehz.js} +1 -1
  583. package/static/dist/assets/{mdc-huhy88df.js → mdc-c21zlyi5.js} +1 -1
  584. package/static/dist/assets/mermaid-HWGCJPDP-jx0q5dm2.js +1 -0
  585. package/static/dist/assets/mermaid-parser.core-k91vdnvi.js +7 -0
  586. package/static/dist/assets/micromark-factory-space-fnmnpi6s.js +1 -0
  587. package/static/dist/assets/{mindmap-definition-YA3MSWOX-27b26eok.js → mindmap-definition-YA3MSWOX-ehztfea4.js} +28 -28
  588. package/static/dist/assets/{nginx-e7lvhsmn.js → nginx-f3d68ed4.js} +1 -1
  589. package/static/dist/assets/{nim-dun5d8mk.js → nim-dw0jcy70.js} +1 -1
  590. package/static/dist/assets/onboarding-f67z0a2r.js +1 -0
  591. package/static/dist/assets/packet-AYTQ26CC-ok2xrblz.js +1 -0
  592. package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
  593. package/static/dist/assets/pegDiagram-XKGWAZYB-ec3rpj0m.js +1 -0
  594. package/static/dist/assets/{perl-bj3oqtet.js → perl-3c0r45c8.js} +1 -1
  595. package/static/dist/assets/{php-buzp12r0.js → php-rznhidvp.js} +1 -1
  596. package/static/dist/assets/pie-WAS4IAKB-iwkir23u.js +1 -0
  597. package/static/dist/assets/pieDiagram-E7YTZNPT-fcpqpekh.js +39 -0
  598. package/static/dist/assets/{pug-mjad7he9.js → pug-f2le4f3y.js} +1 -1
  599. package/static/dist/assets/{qml-d1gdp10s.js → qml-e2wbq9cr.js} +1 -1
  600. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-hhue7d8o.js → quadrantDiagram-AXDQQJYC-lqsync43.js} +1 -1
  601. package/static/dist/assets/queryKeys-ed4n2egc.js +9 -0
  602. package/static/dist/assets/{r-o3r9c7ao.js → r-epfzuw6q.js} +1 -1
  603. package/static/dist/assets/radar-RG4KPBEZ-hje4jbk6.js +1 -0
  604. package/static/dist/assets/railroad-74A4TZTK-gll9n6qi.js +1 -0
  605. package/static/dist/assets/railroad-abnf-HS5TGJTU-o6h6lpn4.js +1 -0
  606. package/static/dist/assets/railroad-ebnf-LZEXJU2U-juaefj6o.js +1 -0
  607. package/static/dist/assets/railroad-peg-WCYAUIDC-cgfhvpah.js +1 -0
  608. package/static/dist/assets/railroadDiagram-O6MQD6OU-lp3w7awf.js +1 -0
  609. package/static/dist/assets/{razor-ilc585yr.js → razor-pea1u1d0.js} +1 -1
  610. package/static/dist/assets/refresh-cw-iccm7a4e.js +1 -0
  611. package/static/dist/assets/refresh-i9ya9dvl.js +1 -0
  612. package/static/dist/assets/{regexp-mpjtcvwm.js → regexp-hzte0a5x.js} +1 -1
  613. package/static/dist/assets/registry-g7w3353u.js +1 -0
  614. package/static/dist/assets/requirementDiagram-BXWQKSXE-ha8eibcf.js +84 -0
  615. package/static/dist/assets/rolldown-runtime-bnnw9q4a.js +1 -0
  616. package/static/dist/assets/{rst-jrkwjdyi.js → rst-m6tugj5o.js} +1 -1
  617. package/static/dist/assets/{ruby-n4tjcbpz.js → ruby-l19vq3w1.js} +1 -1
  618. package/static/dist/assets/runtime-nlxpnegr.js +1 -0
  619. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-fqb8xcjf.js → sankeyDiagram-P5KCCOFB-oxf31pr9.js} +1 -1
  620. package/static/dist/assets/{sas-hjo6y93k.js → sas-ktdxo397.js} +1 -1
  621. package/static/dist/assets/{scss-ncidjv48.js → scss-kn3oxfwf.js} +1 -1
  622. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-dlcz6qjz.js → sequenceDiagram-WJ2MYXX4-jiq8lhyo.js} +11 -11
  623. package/static/dist/assets/settings_section_extensions_title-fadc7o65.js +1 -0
  624. package/static/dist/assets/share._token-o76ki0ly.js +2 -0
  625. package/static/dist/assets/{shellscript-m8whjpqd.js → shellscript-l100jtrg.js} +1 -1
  626. package/static/dist/assets/{shellsession-i5bj6syz.js → shellsession-m5qukwow.js} +1 -1
  627. package/static/dist/assets/sizeCapture-INFHLROL-dk1io13w.js +1 -0
  628. package/static/dist/assets/sliders-horizontal-ddcs88av.js +1 -0
  629. package/static/dist/assets/{soy-hhh36ysq.js → soy-mexlscur.js} +1 -1
  630. package/static/dist/assets/{sql-j21bzcl6.js → sql-o0ho5suh.js} +1 -1
  631. package/static/dist/assets/src-h89s0hs4.js +1 -0
  632. package/static/dist/assets/{stata-bbu2onh1.js → stata-be9ck1q3.js} +1 -1
  633. package/static/dist/assets/stateDiagram-D77RDMKH-f3fslnon.js +1 -0
  634. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hlt6yphw.js +1 -0
  635. package/static/dist/assets/{surrealql-l5vczuco.js → surrealql-gk1xkkyy.js} +1 -1
  636. package/static/dist/assets/{svelte-idn422g6.js → svelte-k6ecjilf.js} +1 -1
  637. package/static/dist/assets/swimlanes-42K2YHIH-k1hmk7jf.js +1 -0
  638. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-hd5t9tnt.js +8 -0
  639. package/static/dist/assets/{templ-e22qou3c.js → templ-gf3bftk6.js} +1 -1
  640. package/static/dist/assets/{tex-deqpc5ee.js → tex-d21fqv1z.js} +1 -1
  641. package/static/dist/assets/{timeline-definition-24CTP7MA-b1vl2zil.js → timeline-definition-24CTP7MA-n3ncfy9r.js} +8 -8
  642. package/static/dist/assets/toast-liparz2j.js +56 -0
  643. package/static/dist/assets/treeView-Q6P3EWNA-l3y2pejt.js +1 -0
  644. package/static/dist/assets/treemap-WGGIJYW6-jown9axh.js +1 -0
  645. package/static/dist/assets/{ts-tags-ctnibc27.js → ts-tags-n0g61gr6.js} +1 -1
  646. package/static/dist/assets/{tsx-i163n32g.js → tsx-cvv7wv6q.js} +1 -1
  647. package/static/dist/assets/{twig-x0fi7wm8.js → twig-n1f67204.js} +1 -1
  648. package/static/dist/assets/{typescript-g18c34xz.js → typescript-og7o55cx.js} +1 -1
  649. package/static/dist/assets/untitled-mj86r4gz.js +1 -0
  650. package/static/dist/assets/useDebounced-g8r0ggve.js +1 -0
  651. package/static/dist/assets/useForm-o4uu74mx.js +1 -0
  652. package/static/dist/assets/{useParams-b4dq6n4q.js → useParams-ftsveu84.js} +1 -1
  653. package/static/dist/assets/{useSearch-ipfmv3he.js → useSearch-crqueqga.js} +1 -1
  654. package/static/dist/assets/vennDiagram-4TSXK5OY-dh59uq1c.js +34 -0
  655. package/static/dist/assets/{vue-mwu5l1bb.js → vue-ej4xpwqw.js} +1 -1
  656. package/static/dist/assets/{vue-html-nskj3du0.js → vue-html-d48xsjuo.js} +1 -1
  657. package/static/dist/assets/{vue-vine-cqe77smq.js → vue-vine-pev88ip6.js} +1 -1
  658. package/static/dist/assets/wardley-WFR3VGLG-e6d74b0j.js +1 -0
  659. package/static/dist/assets/wardleyDiagram-VM6X3IG4-hnofh78f.js +78 -0
  660. package/static/dist/assets/with-selector-nu1b5z0v.js +1 -0
  661. package/static/dist/assets/{xml-gau7gvrv.js → xml-ifxp6gqb.js} +1 -1
  662. package/static/dist/assets/{xsl-ew09zq8b.js → xsl-fug2cckc.js} +1 -1
  663. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-e49523ab.js → xychartDiagram-S5SC5T6Z-bpymjelb.js} +6 -6
  664. package/static/dist/assets/{yaml-ofnrxtvf.js → yaml-fjmafnlo.js} +1 -1
  665. package/static/dist/extension-sdk.js +1 -1
  666. package/static/dist/index.html +1 -1
  667. package/static/dist/sw.js +180 -136
  668. package/static/dist/assets/AppShell-g3bkgnvt.js +0 -1
  669. package/static/dist/assets/ChatPage-oo7bdan6.js +0 -25
  670. package/static/dist/assets/Dialog-g355qxhp.js +0 -1
  671. package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +0 -1
  672. package/static/dist/assets/HubRoute-lmkrbw47.js +0 -2
  673. package/static/dist/assets/Markdown-j4kut4j2.js +0 -598
  674. package/static/dist/assets/Markdown-lwviulmg.css +0 -1
  675. package/static/dist/assets/RightPanel-ohlsdzfl.js +0 -1
  676. package/static/dist/assets/Select-kddq75qc.js +0 -1
  677. package/static/dist/assets/SessionListPanel-lu3oy0hw.js +0 -1
  678. package/static/dist/assets/SettingsLayout-o9bv4mgb.js +0 -1
  679. package/static/dist/assets/States-eq6fe29m.js +0 -1
  680. package/static/dist/assets/Toaster-pc247u9f.js +0 -1
  681. package/static/dist/assets/_app-f8n1i0m1.js +0 -1
  682. package/static/dist/assets/_app.ext._extensionId-gfbzpoou.js +0 -1
  683. package/static/dist/assets/_app.index-bzzv80hk.js +0 -1
  684. package/static/dist/assets/_app.insights-dzixbryi.js +0 -1
  685. package/static/dist/assets/_app.kanban-c6hep2us.js +0 -1
  686. package/static/dist/assets/_app.logs-i69qds98.js +0 -1
  687. package/static/dist/assets/_app.memory-kl4vf0gd.js +0 -1
  688. package/static/dist/assets/_app.profiles-b8zinmyp.js +0 -1
  689. package/static/dist/assets/_app.session._sessionId-fn1bhde6.js +0 -1
  690. package/static/dist/assets/_app.settings-cg8plwmf.js +0 -1
  691. package/static/dist/assets/_app.settings._section-bkwbi9zn.js +0 -3
  692. package/static/dist/assets/_app.skills-cs47sbvs.js +0 -1
  693. package/static/dist/assets/_app.tasks-dwvk64tc.js +0 -1
  694. package/static/dist/assets/_app.todos-c8xjm8p3.js +0 -1
  695. package/static/dist/assets/_app.workspaces-ehvd1gds.js +0 -1
  696. package/static/dist/assets/abnfDiagram-VCTEODGH-c8kdixke.js +0 -1
  697. package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +0 -1
  698. package/static/dist/assets/architectureDiagram-5GKGNRK7-cku06x1p.js +0 -36
  699. package/static/dist/assets/back-h7aun7ew.js +0 -1
  700. package/static/dist/assets/cancel-fhi0vcnu.js +0 -1
  701. package/static/dist/assets/channel-b2zo73z1.js +0 -1
  702. package/static/dist/assets/chunk-DU6HZSFF-ofajdixh.js +0 -128
  703. package/static/dist/assets/chunk-OSK3NFVY-e7gkikcw.js +0 -10
  704. package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +0 -1
  705. package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +0 -1
  706. package/static/dist/assets/copied-hcplv46p.js +0 -1
  707. package/static/dist/assets/copy-magtofyp.js +0 -1
  708. package/static/dist/assets/cose-bilkent-JH36ORCC-eclcy6ja.js +0 -1
  709. package/static/dist/assets/cynefin-OW5HDTMX-siqlulkl.js +0 -1
  710. package/static/dist/assets/cynefinDiagram-5FMLGOSQ-i648tah9.js +0 -62
  711. package/static/dist/assets/cytoscape.esm-bqu53u12.js +0 -321
  712. package/static/dist/assets/dagre-GXQ25YYZ-mwn6uc17.js +0 -4
  713. package/static/dist/assets/dagre-lkoknbjn.js +0 -1
  714. package/static/dist/assets/diagram-UQ7AKVKN-mekq7zgq.js +0 -41
  715. package/static/dist/assets/diagram-VX7I27RA-xe6r5qfw.js +0 -24
  716. package/static/dist/assets/diagram-Z3DM3KII-nq07dg7k.js +0 -24
  717. package/static/dist/assets/ebnfDiagram-PWID7BFC-c62ipm3g.js +0 -1
  718. package/static/dist/assets/endpoints-klbxod4t.js +0 -64
  719. package/static/dist/assets/erDiagram-RLTQ6QDP-h39kmnzy.js +0 -99
  720. package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +0 -1
  721. package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +0 -1
  722. package/static/dist/assets/gitGraph-4MIJSDKK-fr2i7caa.js +0 -1
  723. package/static/dist/assets/highlighted-body-KPVGNVTW-jwpop9p8.js +0 -1
  724. package/static/dist/assets/host-i828xuo7.js +0 -1
  725. package/static/dist/assets/index-eh8zilnv.css +0 -2
  726. package/static/dist/assets/index-r648f4o1.js +0 -11
  727. package/static/dist/assets/info-A6RAGUB7-h11x0dxo.js +0 -1
  728. package/static/dist/assets/infoDiagram-27XIBGKW-dfh47dw9.js +0 -2
  729. package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +0 -1
  730. package/static/dist/assets/linear-becwlnhk.js +0 -1
  731. package/static/dist/assets/link-oot2yn73.js +0 -1
  732. package/static/dist/assets/login-gu28l9ou.js +0 -1
  733. package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +0 -1
  734. package/static/dist/assets/mermaid-parser.core-ntne45i9.js +0 -7
  735. package/static/dist/assets/not-found-ccnnac39.js +0 -1
  736. package/static/dist/assets/onboarding-lkphzu1s.js +0 -1
  737. package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +0 -1
  738. package/static/dist/assets/packet-AYTQ26CC-l6jdka8p.js +0 -1
  739. package/static/dist/assets/pegDiagram-XKGWAZYB-g1er3bxj.js +0 -1
  740. package/static/dist/assets/pie-WAS4IAKB-i0ylcrt9.js +0 -1
  741. package/static/dist/assets/pieDiagram-E7YTZNPT-fbz1e55z.js +0 -39
  742. package/static/dist/assets/radar-RG4KPBEZ-f9b70aa4.js +0 -1
  743. package/static/dist/assets/railroad-74A4TZTK-lgsuoeda.js +0 -1
  744. package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +0 -1
  745. package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +0 -1
  746. package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +0 -1
  747. package/static/dist/assets/railroadDiagram-O6MQD6OU-eqpy9fnz.js +0 -1
  748. package/static/dist/assets/refresh-abtu93ke.js +0 -1
  749. package/static/dist/assets/refresh-cw-hlwb02qv.js +0 -1
  750. package/static/dist/assets/requirementDiagram-BXWQKSXE-ogf6puhi.js +0 -84
  751. package/static/dist/assets/rolldown-runtime-b9miacsj.js +0 -1
  752. package/static/dist/assets/root-mbklpuv4.js +0 -1
  753. package/static/dist/assets/saved-girnabkf.js +0 -1
  754. package/static/dist/assets/settings_section_extensions_title-h14cwk6f.js +0 -1
  755. package/static/dist/assets/share._token-kl0r7ido.js +0 -2
  756. package/static/dist/assets/sizeCapture-INFHLROL-g612l48j.js +0 -1
  757. package/static/dist/assets/sliders-horizontal-n1zm34qq.js +0 -1
  758. package/static/dist/assets/src-hjyib4mu.js +0 -1
  759. package/static/dist/assets/sse-iwku0sjo.js +0 -1
  760. package/static/dist/assets/stateDiagram-D77RDMKH-fyoznpcs.js +0 -1
  761. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +0 -1
  762. package/static/dist/assets/swimlanes-42K2YHIH-oefldhfy.js +0 -1
  763. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +0 -8
  764. package/static/dist/assets/todoStore-jjr5yr78.js +0 -1
  765. package/static/dist/assets/treeView-Q6P3EWNA-f7hcci5o.js +0 -1
  766. package/static/dist/assets/treemap-WGGIJYW6-nl85071a.js +0 -1
  767. package/static/dist/assets/units-kgxzxfhi.js +0 -1
  768. package/static/dist/assets/untitled-cikfjntn.js +0 -1
  769. package/static/dist/assets/useForm-kqoqg0k9.js +0 -1
  770. package/static/dist/assets/useLocale-o59tbiea.js +0 -1
  771. package/static/dist/assets/vennDiagram-4TSXK5OY-h5hzbk0n.js +0 -34
  772. package/static/dist/assets/wardley-WFR3VGLG-jftvpm3e.js +0 -1
  773. package/static/dist/assets/wardleyDiagram-VM6X3IG4-hedh85ge.js +0 -78
  774. /package/static/dist/assets/{ChatPage-gmu5c7lf.css → TerminalPanel-gmu5c7lf.css} +0 -0
@@ -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,14 @@ export function uniqueQuotaSources(sources) {
458
467
  }
459
468
  export class ProviderCatalog {
460
469
  deps;
470
+ scopedKeyRevisions = new Map();
471
+ scopedKeyAuthority = new Map();
472
+ modelGenerations = new Map();
473
+ modelReads = new Map();
474
+ credentialReads = new Map();
461
475
  /** Failed live lookups by home, provider, and the `sourceFingerprint` they ran under (TAL-542). */
462
476
  liveFailed = new Set();
463
- /** `source`: the `sourceFingerprint` the ids were fetched under; another one makes them stale (TAL-542). */
477
+ /** `source`: the `sourceFingerprint` the ids were fetched under; another one makes them stale (TAL-542). `ttl`: seconds they stay fresh. */
464
478
  liveIds = new Map();
465
479
  liveInflight = new Map();
466
480
  /** `pooled`: the fill's `poolProviders` answer, null when unknown (TAL-638). */
@@ -482,15 +496,32 @@ export class ProviderCatalog {
482
496
  }
483
497
  return { groups: this.liveIds.size, models, age_seconds: oldest === null ? null : Math.max(0, Math.round(this.deps.now() - oldest)) };
484
498
  }
485
- invalidate(profileHome, provider) {
499
+ invalidate(profileHome, provider, opts = {}) {
500
+ // Explicit refresh retires builds without changing the credential identity of confirmed live ids.
501
+ if (profileHome)
502
+ this.modelGenerations.set(profileHome, (this.modelGenerations.get(profileHome) ?? 0) + 1);
503
+ else
504
+ for (const [home, generation] of this.modelGenerations)
505
+ this.modelGenerations.set(home, generation + 1);
486
506
  for (const key of [...this.liveIds.keys()]) {
487
507
  if (profileHome && !key.startsWith(`${profileHome}\0`))
488
508
  continue;
489
509
  if (provider && !key.endsWith(`\0${provider}`))
490
510
  continue;
491
- this.liveIds.delete(key);
511
+ if (opts.preserveConfirmed)
512
+ this.liveIds.get(key).ttl = 0;
513
+ else
514
+ this.liveIds.delete(key);
492
515
  }
493
516
  this.providersCache.clear();
517
+ for (const key of this.pickerLimitsCache.keys())
518
+ if (!profileHome || key === profileHome)
519
+ this.pickerLimitsCache.delete(key);
520
+ if (opts.preserveConfirmed)
521
+ return;
522
+ for (const key of [...this.lastQuotaSources.keys()])
523
+ if (!profileHome || key.startsWith(`${profileHome}\0`))
524
+ this.lastQuotaSources.delete(key);
494
525
  // Credentials changed: the last pool answers no longer stand in for a failed lookup.
495
526
  for (const key of [...this.lastPoolAnswers.keys()])
496
527
  if (!profileHome || key.startsWith(`${profileHome}\0`))
@@ -514,27 +545,44 @@ export class ProviderCatalog {
514
545
  }
515
546
  return catalogOptionId(catalog, model, provider);
516
547
  }
548
+ /** TAL-821: `providerLabel` against the last catalog built for this home; a cold home starts building one. */
549
+ providerLabel(profileHome, provider) {
550
+ const catalog = this.lastModels.get(profileHome);
551
+ if (!catalog)
552
+ void this.warmModelOptions(profileHome);
553
+ return providerLabel(catalog, provider);
554
+ }
517
555
  /**
518
556
  * TAL-542: the identity of the files a catalog and its live ids derive from: config.yaml, `.env`, the Agent's sign-in
519
557
  * store (`auth.json`, rewritten by an account switch), and the installed plugins directory, so a change outside Web
520
- * makes both stale.
558
+ * makes both stale. Opaque scoped-key revisions cover external secret sources without exposing their keys.
521
559
  */
522
- sourceFingerprint(profileHome) {
560
+ sourceFingerprint(profileHome, includeScoped = true) {
523
561
  const stamp = (name) => {
524
562
  try {
525
563
  const st = statSync(join(profileHome, name), { bigint: true });
526
- return `${String(st.mtimeNs)}:${String(st.size)}:${String(st.ino)}`;
564
+ return `${String(st.mtimeNs)}:${String(st.ctimeNs)}:${String(st.size)}:${String(st.ino)}`;
527
565
  }
528
566
  catch {
529
567
  return 'missing';
530
568
  }
531
569
  };
532
- return [this.deps.config.fingerprint(profileHome), ...['.env', 'auth.json', 'plugins'].map(stamp)].join('|');
570
+ const names = new Set([...Object.values(PROVIDER_ENV_VAR), ...Object.values(PROVIDER_ENV_VAR_ALIASES).flat()]);
571
+ const config = this.deps.config.peek(profileHome, { refresh: false }) ?? {};
572
+ for (const cp of [modelSection(config), ...Object.values(dict(config.providers)).filter(isDict), ...customProviderEntries(config)]) {
573
+ for (const name of [str(cp.key_env).trim(), /^\$\{([^}]+)\}$/.exec(str(cp.api_key).trim())?.[1]])
574
+ if (name)
575
+ names.add(name);
576
+ }
577
+ const credentials = [...names].sort().map((name) => [name, this.processEnv(name, profileHome)]);
578
+ const models = ['HERMES_MODEL', 'OPENAI_MODEL', 'LLM_MODEL'].map((name) => [name, this.deps.env[name]]);
579
+ const environment = createHash('sha256').update(JSON.stringify([credentials, models, includeScoped ? this.scopedKeyRevisions.get(profileHome) ?? '[]' : '[]'])).digest('hex');
580
+ return [profileHome, environment, this.deps.config.fingerprint(profileHome), ...['.env', 'auth.json', 'plugins'].map(stamp)].join('|');
533
581
  }
534
582
  /** The last catalog built for this home, younger than `maxAgeS`, while its source files are unchanged since (TAL-542). */
535
583
  currentModels(profileHome, maxAgeS = Infinity) {
536
584
  const meta = this.lastModelsMeta.get(profileHome);
537
- return meta?.source === this.sourceFingerprint(profileHome) && this.deps.now() - meta.at < maxAgeS ? this.lastModels.get(profileHome) : undefined;
585
+ return this.scopedKeyAuthority.get(profileHome) !== false && meta?.source === this.sourceFingerprint(profileHome) && this.deps.now() - meta.at < maxAgeS ? this.lastModels.get(profileHome) : undefined;
538
586
  }
539
587
  /**
540
588
  * TAL-542: `repairSessionModel` against this home's catalog. Agent-owned sign-in and plugin state has no file to
@@ -543,17 +591,19 @@ export class ProviderCatalog {
543
591
  */
544
592
  sessionModelRepair(profileHome, model, provider) {
545
593
  const catalog = this.currentModels(profileHome, PROVIDERS_TTL_S);
546
- if (!catalog) {
547
- void this.warmSessionModelRepair(profileHome);
594
+ const pid = canonicaliseProviderId(provider) || canonicaliseProviderId(catalog?.active_provider);
595
+ if (!catalog || this.lastModelsMeta.get(profileHome)?.unconfirmed.has(pid))
596
+ void this.models(profileHome).catch(() => undefined);
597
+ if (!catalog)
548
598
  return null;
549
- }
550
599
  return repairSessionModel(catalog, model, provider, this.lastModelsMeta.get(profileHome)?.unconfirmed);
551
600
  }
552
601
  /**
553
- * Builds the catalog `sessionModelRepair` reads unless a fresh enough one exists; call it after the request's last
554
- * await. A source that changed while the build awaited makes that build stale, so it is rebuilt once.
602
+ * Explicitly await discovery, or start it in the background from `sessionModelRepair`. Admission never awaits it.
603
+ * A source that changed while the build awaited makes that build stale, so it is rebuilt once.
555
604
  */
556
605
  async warmSessionModelRepair(profileHome) {
606
+ await this.poolProviders(profileHome);
557
607
  for (let attempt = 0; attempt < 2 && !this.currentModels(profileHome, PROVIDERS_TTL_S); attempt += 1) {
558
608
  try {
559
609
  await this.models(profileHome);
@@ -563,8 +613,14 @@ export class ProviderCatalog {
563
613
  }
564
614
  }
565
615
  }
616
+ /** TAL-1381: warms this home's catalog once, then answers `modelOptionFor` synchronously for every row of a batch. */
617
+ async modelOptionResolver(profileHome) {
618
+ await this.warmModelOptions(profileHome);
619
+ return (model, provider) => this.modelOptionFor(profileHome, model, provider);
620
+ }
566
621
  /** Builds the catalog `modelOptionFor` reads once per home, again after config.yaml or `.env` changes; an unavailable catalog leaves every option id null. */
567
622
  async warmModelOptions(profileHome) {
623
+ await this.poolProviders(profileHome);
568
624
  if (this.currentModels(profileHome))
569
625
  return;
570
626
  try {
@@ -613,18 +669,14 @@ export class ProviderCatalog {
613
669
  return false;
614
670
  }
615
671
  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
- }
672
+ return this.poolLookup(profileHome, `auth\0${pid}`, async (sidecar) => {
673
+ const { status } = await sidecar.call('providers.auth_status', { profile_home: profileHome, provider: pid });
674
+ return { logged_in: status.logged_in, key_source: str(status.key_source), error: str(status.error) };
675
+ });
625
676
  }
626
677
  /** `pooled`: the providers whose persisted credential pool holds an added account (TAL-638). */
627
- async probeKey(profileHome, pid, config, envValues, pooled) {
678
+ async probeKey(profileHome, pid, config, envValues, pooled, scopedKeys) {
679
+ let authoritative = true;
628
680
  let hasKey = this.providerHasKey(pid, config, envValues, profileHome);
629
681
  let isOauth = OAUTH_PROVIDERS.has(pid);
630
682
  let keySource = 'none';
@@ -632,7 +684,9 @@ export class ProviderCatalog {
632
684
  let removable = false;
633
685
  if (isOauth) {
634
686
  keySource = 'oauth';
635
- const status = await this.authStatus(profileHome, pid);
687
+ const lookup = await this.authStatus(profileHome, pid);
688
+ authoritative = lookup.authoritative;
689
+ const status = lookup.value;
636
690
  if (status?.logged_in) {
637
691
  hasKey = true;
638
692
  keySource = str(status.key_source) || 'oauth';
@@ -677,8 +731,16 @@ export class ProviderCatalog {
677
731
  hasKey = true;
678
732
  keySource = 'credential_pool';
679
733
  }
734
+ else if (isBalanceProvider(pid)) {
735
+ if (scopedKeys[pid]?.present) {
736
+ hasKey = true;
737
+ keySource = 'env';
738
+ }
739
+ }
680
740
  else if (!providerEnvVar(pid) && /^[a-z][a-z0-9_-]{0,63}$/.test(pid)) {
681
- const status = await this.authStatus(profileHome, pid);
741
+ const lookup = await this.authStatus(profileHome, pid);
742
+ authoritative = lookup.authoritative;
743
+ const status = lookup.value;
682
744
  if (status?.logged_in) {
683
745
  hasKey = true;
684
746
  const raw = str(status.key_source);
@@ -686,14 +748,21 @@ export class ProviderCatalog {
686
748
  isOauth = true;
687
749
  }
688
750
  }
689
- return { hasKey, keySource, authError, isOauth, removable };
751
+ return { authoritative, hasKey, keySource, authError, isOauth, removable };
690
752
  }
691
753
  /** Live model ids from the Agent for one provider, cached per profile home. */
692
754
  async liveModelIds(profileHome, pid, opts = {}) {
755
+ const inputSource = this.sourceFingerprint(profileHome, false);
756
+ if (!opts.source || opts.source !== this.sourceFingerprint(profileHome))
757
+ await this.poolProviders(profileHome);
693
758
  const key = `${profileHome}\0${pid}`;
694
- const source = this.sourceFingerprint(profileHome);
759
+ const source = inputSource === this.sourceFingerprint(profileHome, false) ? this.sourceFingerprint(profileHome) : inputSource;
695
760
  const hit = this.liveIds.get(key);
696
- if (hit?.source === source && !opts.force && this.deps.now() - hit.at < LIVE_TTL_S)
761
+ if (this.scopedKeyAuthority.get(profileHome) === false) {
762
+ this.liveFailed.add(`${key}\0${source}`);
763
+ return hit?.source === source ? hit.ids : [];
764
+ }
765
+ if (hit?.source === source && !opts.force && this.deps.now() - hit.at < hit.ttl)
697
766
  return hit.ids;
698
767
  // A lookup started under another source answers for the old endpoint or credential; this one starts its own.
699
768
  const flightKey = `${key}\0${source}`;
@@ -704,51 +773,75 @@ export class ProviderCatalog {
704
773
  // No sidecar is a failed lookup for this source: the ids it keeps showing never confirm a stale-model repair (TAL-542).
705
774
  if (!sidecar) {
706
775
  this.liveFailed.add(flightKey);
707
- return hit?.ids ?? [];
776
+ return hit?.source === source && source === this.sourceFingerprint(profileHome) ? hit.ids : [];
708
777
  }
709
778
  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 ?? []; })
779
+ .then((r) => {
780
+ if (source !== this.sourceFingerprint(profileHome)) {
781
+ this.liveFailed.add(flightKey);
782
+ return [];
783
+ }
784
+ if (r.fallback && hit?.source === source && hit.confirmed) {
785
+ this.liveFailed.add(flightKey);
786
+ return hit.ids;
787
+ }
788
+ 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 });
789
+ // TAL-799: the Agent's curated placeholder for an unreachable catalog is a failed lookup that still lists models.
790
+ if (r.fallback)
791
+ this.liveFailed.add(flightKey);
792
+ else
793
+ this.liveFailed.delete(flightKey);
794
+ return r.model_ids;
795
+ })
796
+ .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
797
  .finally(() => { this.liveInflight.delete(flightKey); });
713
798
  this.liveInflight.set(flightKey, run);
714
799
  return run;
715
800
  }
801
+ /** Whether the last live lookup for this provider under `source` failed or answered with the Agent's placeholder list. */
802
+ liveLookupFailed(profileHome, pid, source = this.sourceFingerprint(profileHome)) {
803
+ return this.liveFailed.has(`${profileHome}\0${pid}\0${source}`);
804
+ }
716
805
  /**
717
806
  * TAL-288: the model-provider plugins installed and enabled in this profile, as the sidecar reads them from the Agent.
718
807
  * A failed lookup lists none, so every other provider stays usable; an id that cannot be a provider routing hint is dropped.
719
808
  */
720
809
  async pluginProviders(profileHome) {
721
- const sidecar = this.deps.sidecar();
722
- if (!sidecar)
723
- return [];
724
- try {
810
+ return this.poolLookup(profileHome, 'plugins', async (sidecar) => {
725
811
  const { providers } = await sidecar.call('plugins.providers', { profile_home: profileHome }, { timeoutMs: PLUGIN_LIST_TIMEOUT_MS });
726
812
  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
- }
813
+ });
814
+ }
815
+ /**
816
+ * TAL-798: the GGUFs staged for the Agent's managed llama.cpp provider, selectable before its server runs (a turn boots
817
+ * it). Null when none are staged or the listing failed, so every other provider stays usable.
818
+ */
819
+ async localModels(profileHome) {
820
+ return this.poolLookup(profileHome, 'local-models', async (sidecar) => sidecar.call('providers.local_models', { profile_home: profileHome }, { timeoutMs: PLUGIN_LIST_TIMEOUT_MS }));
732
821
  }
733
822
  /** Python `get_providers`. */
734
823
  async providers(profileHome) {
735
824
  return structuredClone((await this.providerRows(profileHome)).payload);
736
825
  }
737
826
  /** The cached `providers` answer, shared with `quotas`; callers never mutate it. */
738
- async providerRows(profileHome) {
827
+ async providerRows(profileHome, pool) {
739
828
  const config = await this.deps.config.read(profileHome);
740
829
  const envValues = loadEnvFile(join(profileHome, '.env'));
741
- const cacheKey = JSON.stringify([config, envValues]);
830
+ pool ??= await this.poolProviders(profileHome);
831
+ const source = this.sourceFingerprint(profileHome);
832
+ const cacheKey = source;
742
833
  const hit = this.providersCache.get(profileHome);
743
- if (hit?.key === cacheKey && this.deps.now() - hit.at < PROVIDERS_TTL_S)
834
+ if (pool.authoritative && hit?.key === cacheKey && this.deps.now() - hit.at < PROVIDERS_TTL_S)
744
835
  return hit;
745
- const pooledIds = await this.poolProviders(profileHome);
836
+ const pooledIds = pool.value?.providers ?? null;
837
+ const pluginLookup = await this.pluginProviders(profileHome);
838
+ let authoritative = pool.authoritative && pluginLookup.authoritative;
746
839
  const pooled = new Set(pooledIds ?? []);
747
840
  const active = activeProviderFromConfig(config);
748
841
  const known = new Set([...Object.keys(PROVIDER_DISPLAY), ...Object.keys(PROVIDER_MODELS), ...OAUTH_PROVIDERS]);
749
842
  // 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
843
  // 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)));
844
+ const plugins = (pluginLookup.value ?? []).filter((p) => !known.has(p.name) && !known.has(providerIdentity(p.name)));
752
845
  const pluginIds = new Set(plugins.map((p) => p.name));
753
846
  const providersCfg = dict(config.providers);
754
847
  for (const key of Object.keys(providersCfg)) {
@@ -759,7 +852,8 @@ export class ProviderCatalog {
759
852
  }
760
853
  const rows = [];
761
854
  for (const pid of [...known].sort()) {
762
- const probe = await this.probeKey(profileHome, pid, config, envValues, pooled);
855
+ const probe = await this.probeKey(profileHome, pid, config, envValues, pooled, pool.value?.keys ?? {});
856
+ authoritative &&= probe.authoritative;
763
857
  if (pid === 'openai' && !probe.hasKey && looksLikeCodexOauthToken(str(envValues[providerEnvVar('openai') ?? ''] ?? this.deps.env.OPENAI_API_KEY)))
764
858
  continue;
765
859
  const configured = probe.hasKey || selfHostedEndpoint(pid, config);
@@ -768,7 +862,7 @@ export class ProviderCatalog {
768
862
  // Python: the card prefers the live catalog for every keyed provider (exactly like the picker), keeping the
769
863
  // static list as the cold/failed-probe fallback; Nous renders a featured subset with the full count.
770
864
  if (configured && (probe.isOauth || pid === 'lmstudio' || pid === 'nous' || !BESPOKE_CATALOG_PROVIDERS.has(pid))) {
771
- const live = await this.liveModelIds(profileHome, pid);
865
+ const live = await this.liveModelIds(profileHome, pid, { source });
772
866
  if (live.length) {
773
867
  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
868
  modelsTotal = live.length;
@@ -843,20 +937,47 @@ export class ProviderCatalog {
843
937
  row.key_source_kind = keySourceKind(row);
844
938
  row.models_total = Math.max(Number(row.models_total), row.models.length);
845
939
  }
846
- const entry = { at: this.deps.now(), key: cacheKey, payload: { providers: rows, active_provider: active }, pooled: pooledIds };
847
- this.providersCache.set(profileHome, entry);
940
+ authoritative &&= source === this.sourceFingerprint(profileHome);
941
+ const availability = this.availability(source, authoritative, Boolean(pool.value));
942
+ const entry = { at: this.deps.now(), key: cacheKey, payload: { providers: rows, active_provider: active, availability }, pooled: pooledIds };
943
+ if (authoritative)
944
+ this.providersCache.set(profileHome, entry);
848
945
  return entry;
849
946
  }
850
947
  /** Python `get_available_models` (static catalog + live ids for keyed providers). */
851
- async models(profileHome) {
948
+ async models(profileHome, attempt = 0) {
949
+ const generation = this.modelGenerations.get(profileHome) ?? 0;
950
+ this.modelGenerations.set(profileHome, generation);
951
+ const inputSource = this.sourceFingerprint(profileHome, false);
952
+ const pool = await this.poolProviders(profileHome);
953
+ if (generation !== this.modelGenerations.get(profileHome) || inputSource !== this.sourceFingerprint(profileHome, false)) {
954
+ if (attempt === 0)
955
+ return this.models(profileHome, 1);
956
+ throw new Error('Provider configuration changed during catalog lookup. Try again.');
957
+ }
852
958
  const source = this.sourceFingerprint(profileHome);
959
+ const key = `${source}\0${generation}`;
960
+ const pending = this.modelReads.get(key);
961
+ if (pending)
962
+ return structuredClone(await pending);
963
+ const read = this.buildModels(profileHome, pool, source, generation, attempt).finally(() => { this.modelReads.delete(key); });
964
+ this.modelReads.set(key, read);
965
+ return structuredClone(await read);
966
+ }
967
+ async buildModels(profileHome, pool, source, generation, attempt) {
968
+ const providerState = (await this.providerRows(profileHome, pool)).payload;
853
969
  const unconfirmed = new Set();
854
970
  const config = await this.deps.config.read(profileHome);
855
971
  const envValues = loadEnvFile(join(profileHome, '.env'));
856
- const active = activeProviderFromConfig(config);
857
972
  const model = modelSection(config);
858
973
  const defaultModel = effectiveDefaultModel(config, this.deps.env);
859
974
  const cfgBaseUrl = str(model.base_url).trim();
975
+ const localLookup = await this.localModels(profileHome);
976
+ const local = localLookup.value?.models.length ? localLookup.value : null;
977
+ const configured = activeProviderFromConfig(config);
978
+ // A `custom` main model at the running local server's endpoint is that server: its Local group owns the selection.
979
+ const atLocalServer = configured === 'custom' && Boolean(cfgBaseUrl) && cfgBaseUrl.replace(/\/+$/, '') === str(local?.current_endpoint).replace(/\/+$/, '');
980
+ const active = local && atLocalServer ? local.provider_id : configured;
860
981
  const detected = new Set();
861
982
  const configuredIds = new Map();
862
983
  const append = (pid, id) => {
@@ -870,6 +991,8 @@ export class ProviderCatalog {
870
991
  };
871
992
  if (active)
872
993
  detected.add(active);
994
+ if (local)
995
+ detected.add(local.provider_id);
873
996
  const providersCfg = dict(config.providers);
874
997
  const rawKeyFor = new Map();
875
998
  for (const [key, value] of Object.entries(providersCfg)) {
@@ -897,8 +1020,8 @@ export class ProviderCatalog {
897
1020
  // ready plugin providers (TAL-288), under their own name, and providers keyed only through the credential pool (TAL-638).
898
1021
  const signedIn = new Set();
899
1022
  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'))
1023
+ for (const row of providerState.providers) {
1024
+ if (row.has_key !== true || (row.is_oauth !== true && row.is_plugin_provider !== true && row.key_source !== 'credential_pool' && row.key_source !== 'env'))
902
1025
  continue;
903
1026
  signedIn.add(str(row.id));
904
1027
  if (row.is_plugin_provider === true)
@@ -936,8 +1059,15 @@ export class ProviderCatalog {
936
1059
  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
1060
  detected.add(named?.[0] ?? active ?? 'custom');
938
1061
  }
1062
+ // TAL-911: Agent pickers drop `model_catalog.excluded_providers` under any of their aliases, the active provider included.
1063
+ const excludedCfg = dict(config.model_catalog).excluded_providers;
1064
+ const excluded = new Set(Array.isArray(excludedCfg) ? excludedCfg.map(providerIdentity).filter(Boolean) : []);
1065
+ const isExcluded = (pid) => excluded.has(providerIdentity(pid));
1066
+ const pickerDefault = isExcluded(active) || isExcluded(parseProviderQualifiedModel(defaultModel)?.[1]) ? '' : defaultModel;
939
1067
  const groups = [];
940
1068
  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()) {
1069
+ if (isExcluded(pid))
1070
+ continue;
941
1071
  if (pid.startsWith('custom:')) {
942
1072
  const g = namedCustom.get(pid);
943
1073
  const models = [...(g?.models ?? [])];
@@ -945,6 +1075,11 @@ export class ProviderCatalog {
945
1075
  groups.push({ provider: g?.name ?? pid.replace('custom:', ''), provider_id: pid, models: applyProviderPrefix(models, pid, active) });
946
1076
  continue;
947
1077
  }
1078
+ if (pid === local?.provider_id) {
1079
+ // A GGUF's id is its file name, quant tag included (`Q4_K_M`); prettifying would mangle it.
1080
+ groups.push({ provider: 'Local', provider_id: pid, models: applyProviderPrefix(local.models.map((id) => ({ id, label: id })), pid, active) });
1081
+ continue;
1082
+ }
948
1083
  if (pid === 'custom') {
949
1084
  const models = [...customModels];
950
1085
  for (const id of configuredIds.get(pid) ?? [])
@@ -956,20 +1091,25 @@ export class ProviderCatalog {
956
1091
  }
957
1092
  const providerCfg = dict(providersCfg[rawKeyFor.get(pid) ?? pid]);
958
1093
  let raw = [];
1094
+ let allowFallback = false;
959
1095
  if ('models' in providerCfg && providerCfg.models_discovered !== true)
960
1096
  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}`))
1097
+ if (!raw.length) {
1098
+ // TAL-799: a provider without credentials lists the Agent's curated catalog too; the static tables only cover a missing sidecar.
1099
+ const keyed = this.providerHasKey(pid, config, envValues, profileHome) || selfHostedEndpoint(pid, config) || signedIn.has(pid);
1100
+ const live = await this.liveModelIds(profileHome, pid, { source });
1101
+ const failed = this.liveLookupFailed(profileHome, pid, source);
1102
+ // TAL-542: a failed lookup, or a curated list no account confirmed, leaves this group unconfirmed, so the stale-model repair never trusts it.
1103
+ allowFallback = failed || !keyed;
1104
+ if (allowFallback)
965
1105
  unconfirmed.add(pid);
966
1106
  if (live.length)
967
1107
  raw = live.map((id) => ({ id, label: pid === 'nous' ? `${formatOllamaLabel(id.includes('/') ? id.slice(id.indexOf('/') + 1) : id)} (via Nous)` : labelForModel(id, []) }));
968
1108
  // 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}`))
1109
+ else if (keyed && pid === 'nous' && !failed)
970
1110
  continue;
971
1111
  }
972
- if (!raw.length)
1112
+ if (!raw.length && allowFallback)
973
1113
  raw = pid === 'openrouter' ? FALLBACK_MODELS.map((m) => ({ id: m.id, label: m.label })) : [...(PROVIDER_MODELS[pid] ?? [])];
974
1114
  for (const id of configuredIds.get(pid) ?? [])
975
1115
  if (!raw.some((m) => m.id === id))
@@ -977,7 +1117,7 @@ export class ProviderCatalog {
977
1117
  if (raw.length)
978
1118
  groups.push({ provider: pluginNames.get(pid) ?? displayName(pid), provider_id: pid, models: applyProviderPrefix(raw, pid, active) });
979
1119
  }
980
- if (defaultModel) {
1120
+ if (pickerDefault) {
981
1121
  const all = new Set(groups.flatMap((g) => g.models.map((m) => m.id)));
982
1122
  if (!all.has(defaultModel) && !all.has(`@${active ?? ''}:${defaultModel}`)) {
983
1123
  const label = labelForModel(defaultModel, groups);
@@ -1010,11 +1150,6 @@ export class ProviderCatalog {
1010
1150
  }
1011
1151
  const rank = (g) => (g.provider_id === active ? 0 : g.provider_id.startsWith('custom:') ? 1 : withKeys.has(g.provider_id) ? 2 : 3);
1012
1152
  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
1153
  const aliases = {};
1019
1154
  const rawAliases = model.aliases;
1020
1155
  if (isDict(rawAliases))
@@ -1022,19 +1157,136 @@ export class ProviderCatalog {
1022
1157
  if (k && v)
1023
1158
  aliases[k.trim()] = str(v).trim();
1024
1159
  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) {
1160
+ if (source !== this.sourceFingerprint(profileHome)) {
1161
+ if (attempt === 0)
1162
+ return this.models(profileHome, 1);
1163
+ throw new Error('Provider configuration changed during catalog lookup. Try again.');
1164
+ }
1165
+ if (!providerState.availability.authoritative || !localLookup.authoritative)
1166
+ for (const g of kept)
1167
+ unconfirmed.add(g.provider_id);
1168
+ const previous = this.currentModels(profileHome);
1169
+ const availability = this.availability(source, providerState.availability.authoritative && localLookup.authoritative && !unconfirmed.size && source === this.sourceFingerprint(profileHome), previous?.availability?.status === 'available' || previous?.availability?.status === 'stale');
1170
+ for (const g of kept)
1171
+ 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')));
1172
+ const defaults = { availability, active_provider: active, default_model: defaultModel, default_provider_id: defaultProvider, default_bare_id: defaultBare };
1173
+ if (!kept.length && pickerDefault) {
1027
1174
  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: {} });
1175
+ return this.remember(profileHome, { source, generation, unconfirmed, attempt }, { ...defaults, groups: [{ provider: 'Default', provider_id: providerId, models: stampModelEntries([{ id: defaultModel, label: labelForModel(defaultModel, []) }], providerId) }], aliases: {}, configured_model_badges: {} });
1029
1176
  }
1030
1177
  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) });
1178
+ return this.remember(profileHome, { source, generation, unconfirmed, attempt }, { ...defaults, groups: stamped, aliases, configured_model_badges: this.badges(kept, active, defaultModel, fallbackCfg) });
1179
+ }
1180
+ pickerLimitsCache = new Map();
1181
+ /** Bounded, shared pool-limit reads; a warm picker serves its snapshot during refresh. */
1182
+ async pickerLimits(profileHome, providers) {
1183
+ const fingerprint = this.sourceFingerprint(profileHome);
1184
+ const source = JSON.stringify([fingerprint, providers]);
1185
+ let cached = this.pickerLimitsCache.get(profileHome);
1186
+ if (cached?.source !== source) {
1187
+ cached = { source, limits: {} };
1188
+ this.pickerLimitsCache.set(profileHome, cached);
1189
+ }
1190
+ const entry = cached;
1191
+ if ((entry.at === undefined || this.deps.now() - entry.at >= ACCOUNT_USAGE_CACHE_TTL_S) && !entry.read) {
1192
+ entry.read = (async () => {
1193
+ try {
1194
+ const sidecar = this.deps.sidecar();
1195
+ entry.limits = sidecar ? (await sidecar.call('usage.limits', { profile_home: profileHome, providers }, { timeoutMs: 3_000 })).limits : {};
1196
+ }
1197
+ catch (error) {
1198
+ entry.limits = {};
1199
+ this.deps.log(`[catalog] pool limits failed: ${str(error.message)}`);
1200
+ }
1201
+ entry.at = this.deps.now();
1202
+ })().finally(() => { delete entry.read; });
1203
+ }
1204
+ if (entry.at === undefined)
1205
+ await entry.read;
1206
+ return fingerprint === this.sourceFingerprint(profileHome) && this.pickerLimitsCache.get(profileHome) === entry ? entry.limits : {};
1207
+ }
1208
+ /** TAL-807: the picker adds pool limits and quota levels without storing them in the model catalog. */
1209
+ async pickerModels(profileHome, profile) {
1210
+ const catalog = await this.models(profileHome);
1211
+ const entries = (g) => [...g.models, ...(g.extra_models ?? [])];
1212
+ const providers = catalog.groups.map((g) => ({ slug: g.provider_id, models: entries(g).map((e) => e.bare_id ?? e.id) }));
1213
+ const limits = await this.pickerLimits(profileHome, providers);
1214
+ const active = (resetsAt) => Date.parse(resetsAt) / 1000 > this.deps.now();
1215
+ for (const g of catalog.groups) {
1216
+ const limit = limits[g.provider_id];
1217
+ if (limit?.scope === 'account' && active(limit.resets_at))
1218
+ g.limit = limit;
1219
+ if (limit?.scope === 'models') {
1220
+ // The sidecar answers by bare id; the picker names entries by id.
1221
+ const cooled = {};
1222
+ for (const e of entries(g)) {
1223
+ const resetsAt = limit.models[e.bare_id ?? e.id];
1224
+ if (resetsAt && active(resetsAt)) {
1225
+ e.limit = { scope: 'model', resets_at: resetsAt };
1226
+ cooled[e.id] = resetsAt;
1227
+ }
1228
+ }
1229
+ if (Object.keys(cooled).length)
1230
+ g.limit = { scope: 'models', models: cooled };
1231
+ }
1232
+ }
1233
+ const thresholds = (await this.deps.quotaThresholds?.(profile)) ?? QUOTA_THRESHOLD_DEFAULTS;
1234
+ // An account limit already says the login is out; the quota level would only repeat it.
1235
+ const levels = await Promise.all(catalog.groups.map((g) => (g.limit?.scope === 'account' ? Promise.resolve(null) : this.pickerQuotaLevel(profileHome, g.provider_id, thresholds))));
1236
+ catalog.groups.forEach((g, i) => { const level = levels[i]; if (level)
1237
+ g.quota_level = level; });
1238
+ return catalog;
1239
+ }
1240
+ /** TAL-807: account-usage reads the picker started, by `accountUsageCache` key. */
1241
+ pickerUsageReads = new Map();
1242
+ /**
1243
+ * TAL-807: the TAL-411 `remaining` level of the tightest account-scope window in the provider's cached account-usage
1244
+ * read; a model-family window (Opus or Sonnet weekly) never counts. A read past the cache TTL starts a new one, which
1245
+ * the picker waits for only when it has no read at all, and then for at most `PICKER_USAGE_WAIT_MS`; one past
1246
+ * `PICKER_USAGE_MAX_AGE_S` shows nothing.
1247
+ */
1248
+ async pickerQuotaLevel(profileHome, pid, t) {
1249
+ if (!ACCOUNT_USAGE_PROVIDERS.has(pid))
1250
+ return null;
1251
+ const key = `${profileHome}\0${pid}\0`;
1252
+ let cached = this.accountUsageCache.get(key);
1253
+ if (cached?.source !== this.sourceFingerprint(profileHome))
1254
+ cached = undefined;
1255
+ if (!cached || this.deps.now() - cached.at > ACCOUNT_USAGE_CACHE_TTL_S) {
1256
+ let read = this.pickerUsageReads.get(key);
1257
+ if (!read) {
1258
+ read = this.quota(profileHome, pid).then(() => undefined, () => undefined).finally(() => this.pickerUsageReads.delete(key));
1259
+ this.pickerUsageReads.set(key, read);
1260
+ }
1261
+ if (!cached) {
1262
+ let timer;
1263
+ await Promise.race([read, new Promise((resolve) => { timer = setTimeout(resolve, PICKER_USAGE_WAIT_MS); })]);
1264
+ clearTimeout(timer);
1265
+ cached = this.accountUsageCache.get(key);
1266
+ }
1267
+ }
1268
+ const limits = cached?.limits;
1269
+ if (cached?.source !== this.sourceFingerprint(profileHome) || !limits?.available || this.deps.now() - cached.at > PICKER_USAGE_MAX_AGE_S)
1270
+ return null;
1271
+ const raw = (Array.isArray(limits.windows) ? limits.windows : []).filter((w) => isDict(w) && w.scope !== 'model');
1272
+ const tightest = normalizeQuotaWindows(raw, this.deps.now()).windows
1273
+ .reduce((a, w) => (w.remaining_percent !== null && w.remaining_percent < (a?.remaining_percent ?? Infinity) ? w : a), undefined);
1274
+ return tightest ? quotaLevel(tightest, limits.stale ? 'stale' : 'available', 'remaining', t) : null;
1032
1275
  }
1033
1276
  /** Stamps the default's entry id and keeps the catalog for `modelOptionFor`. */
1034
- remember(profileHome, meta, catalog) {
1277
+ async remember(profileHome, meta, catalog) {
1278
+ if (catalog.availability)
1279
+ for (const group of catalog.groups)
1280
+ group.availability ??= catalog.availability;
1281
+ await this.stampSpeedTiers(profileHome, catalog.groups.flatMap((g) => [...g.models, ...(g.extra_models ?? [])]));
1282
+ if (meta.generation !== this.modelGenerations.get(profileHome) || meta.source !== this.sourceFingerprint(profileHome)) {
1283
+ if (meta.attempt === 0)
1284
+ return this.models(profileHome, 1);
1285
+ throw new Error('Provider configuration changed during catalog lookup. Try again.');
1286
+ }
1035
1287
  catalog.default_option_id = catalogOptionId(catalog, catalog.default_bare_id ?? null, catalog.default_provider_id ?? null);
1036
1288
  this.lastModels.set(profileHome, catalog);
1037
- this.lastModelsMeta.set(profileHome, { ...meta, at: this.deps.now() });
1289
+ this.lastModelsMeta.set(profileHome, { source: meta.source, unconfirmed: meta.unconfirmed, at: this.deps.now() });
1038
1290
  return structuredClone(catalog);
1039
1291
  }
1040
1292
  /** Python `_configured_model_badges_from_static_catalog`: which picker rows are the main/fallback selections. */
@@ -1066,14 +1318,17 @@ export class ProviderCatalog {
1066
1318
  return badges;
1067
1319
  }
1068
1320
  /** Python `_handle_live_models`. */
1069
- async liveModels(profileHome, providerRaw) {
1321
+ async liveModels(profileHome, providerRaw, attempt = 0) {
1322
+ const fingerprint = this.sourceFingerprint(profileHome);
1070
1323
  const config = await this.deps.config.read(profileHome);
1071
1324
  let provider = providerRaw.trim().toLowerCase() || str(modelSection(config).provider).trim().toLowerCase();
1072
1325
  if (!provider)
1073
1326
  return { error: 'no_provider', models: [] };
1074
1327
  provider = resolveProviderAlias(provider);
1075
1328
  let ids = await this.liveModelIds(profileHome, provider);
1076
- let source = 'live';
1329
+ if (fingerprint !== this.sourceFingerprint(profileHome))
1330
+ return attempt === 0 ? this.liveModels(profileHome, providerRaw, 1) : { availability: this.availability(this.sourceFingerprint(profileHome), false, false), provider, source: 'unavailable', models: [], count: 0 };
1331
+ let source = this.liveLookupFailed(profileHome, provider, fingerprint) ? 'fallback' : 'live';
1077
1332
  if (!ids.length) {
1078
1333
  const fromConfig = [];
1079
1334
  if (provider === 'custom' || provider.startsWith('custom:')) {
@@ -1083,16 +1338,33 @@ export class ProviderCatalog {
1083
1338
  fromConfig.push(...[str(cp.model).trim(), ...configuredModelIds(cp.models)].filter(Boolean));
1084
1339
  }
1085
1340
  }
1086
- ids = fromConfig.length ? [...new Set(fromConfig)] : (PROVIDER_MODELS[provider] ?? []).map((m) => m.id);
1087
- source = fromConfig.length ? 'config' : 'static';
1341
+ if (fromConfig.length) {
1342
+ ids = [...new Set(fromConfig)];
1343
+ source = 'config';
1344
+ }
1345
+ else if (source === 'fallback') {
1346
+ ids = (PROVIDER_MODELS[provider] ?? []).map((m) => m.id);
1347
+ source = 'static';
1348
+ }
1088
1349
  }
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
1350
  if (provider !== 'nous' && ids.length > MODEL_PICKER_OVERFLOW_THRESHOLD)
1092
1351
  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 };
1352
+ const models = stampModelEntries(ids.filter(Boolean).map((id) => ({ id, label: labelForModel(id, []) })), provider);
1353
+ await this.stampSpeedTiers(profileHome, models);
1354
+ if (fingerprint !== this.sourceFingerprint(profileHome))
1355
+ return attempt === 0 ? this.liveModels(profileHome, providerRaw, 1) : { availability: this.availability(this.sourceFingerprint(profileHome), false, false), provider, source: 'unavailable', models: [], count: 0 };
1356
+ return { availability: this.availability(fingerprint, source === 'live' || source === 'config', source === 'fallback' && models.length > 0), provider, source, models, count: models.length };
1357
+ }
1358
+ async stampSpeedTiers(profileHome, entries) {
1359
+ if (!entries.length)
1360
+ return;
1361
+ const config = await this.deps.config.read(profileHome);
1362
+ // An unavailable Agent offers no paid tier; writes still require a successful lookup.
1363
+ const tiers = await this.deps.config.speedTiers(profileHome, config, entries.map((e) => ({ model: e.bare_id ?? e.id, provider: e.provider_id ?? '' }))).catch(() => []);
1364
+ entries.forEach((entry, i) => {
1365
+ entry.supported_speed_tiers = tiers[i] ?? [];
1366
+ entry.supports_fast_tier = entry.supported_speed_tiers.some((t) => t.service_tier === 'priority');
1367
+ });
1096
1368
  }
1097
1369
  /**
1098
1370
  * Python `_resolve_model_context_length` inputs: the effective base URL, key, and any configured window for this
@@ -1155,27 +1427,35 @@ export class ProviderCatalog {
1155
1427
  if (envVar) {
1156
1428
  for (const name of [envVar, ...(PROVIDER_ENV_VAR_ALIASES[pid] ?? [])]) {
1157
1429
  const v = str(envValues[name] ?? this.processEnv(name, profileHome)).trim();
1430
+ // A reference owns this slot; only the Agent can resolve it before lower-priority keys.
1158
1431
  if (v)
1159
- return v;
1432
+ return v.startsWith('op://') ? null : v;
1160
1433
  }
1161
1434
  }
1162
1435
  // The same config sources `providerHasKey` counts, so a provider shown as keyed resolves its key.
1163
1436
  const model = modelSection(config);
1164
1437
  if (canonicaliseProviderId(model.provider) === canonicaliseProviderId(pid) && valueCountsAsApiKey(pid, model.api_key))
1165
- return str(model.api_key).trim();
1438
+ return str(model.api_key).trim().startsWith('op://') ? null : str(model.api_key).trim();
1166
1439
  for (const [key, value] of Object.entries(dict(config.providers))) {
1167
1440
  if (providerIdentity(key) === providerIdentity(pid) && isDict(value) && valueCountsAsApiKey(pid, value.api_key))
1168
- return str(value.api_key).trim();
1441
+ return str(value.api_key).trim().startsWith('op://') ? null : str(value.api_key).trim();
1169
1442
  }
1170
1443
  return null;
1171
1444
  }
1445
+ /** A raw key or the names the Agent resolves inside the profile's secret scope. */
1446
+ usageKey(pid, apiKey) {
1447
+ const envVar = providerEnvVar(pid);
1448
+ return apiKey ? { api_key: apiKey } : { env_vars: envVar ? [envVar, ...(PROVIDER_ENV_VAR_ALIASES[pid] ?? [])] : [] };
1449
+ }
1172
1450
  /** Account-usage snapshots by profile home, provider, and pool account. */
1173
1451
  accountUsageCache = new Map();
1452
+ quotaReadSequence = 0;
1174
1453
  /**
1175
1454
  * Python `get_provider_quota`. `credential` scopes the read to one credential-pool account (TAL-548): a benched account
1176
1455
  * answers with its local pool state, and every other read uses that account's key.
1177
1456
  */
1178
1457
  async quota(profileHome, providerRaw, opts = {}) {
1458
+ const sequence = opts.sequence ?? ++this.quotaReadSequence;
1179
1459
  const at = opts.at ?? this.deps.now();
1180
1460
  const computed_at = isoAt(at);
1181
1461
  const config = await this.deps.config.read(profileHome);
@@ -1193,7 +1473,9 @@ export class ProviderCatalog {
1193
1473
  let limits = null;
1194
1474
  // Python `_ACCOUNT_USAGE_CACHE_TTL_SECONDS`: a snapshot answers repeat polls for 45 s unless `refresh` is set.
1195
1475
  const cacheKey = `${profileHome}\0${provider}\0${credential?.credential_id ?? ''}`;
1196
- const cached = this.accountUsageCache.get(cacheKey);
1476
+ const hit = this.accountUsageCache.get(cacheKey);
1477
+ const source = this.sourceFingerprint(profileHome);
1478
+ const cached = hit?.source === source ? hit : undefined;
1197
1479
  if (cached && !opts.refresh && this.deps.now() - cached.at <= ACCOUNT_USAGE_CACHE_TTL_S)
1198
1480
  limits = cached.limits;
1199
1481
  else if (sidecar) {
@@ -1206,7 +1488,12 @@ export class ProviderCatalog {
1206
1488
  catch (error) {
1207
1489
  limits = { available: false, unavailable_reason: str(error.message), windows: [], details: [] };
1208
1490
  }
1209
- this.accountUsageCache.set(cacheKey, { at: this.deps.now(), limits });
1491
+ if (source === this.sourceFingerprint(profileHome)) {
1492
+ if ((this.accountUsageCache.get(cacheKey)?.sequence ?? -1) <= sequence)
1493
+ this.accountUsageCache.set(cacheKey, { at: this.deps.now(), limits, source, sequence });
1494
+ }
1495
+ else
1496
+ limits = null;
1210
1497
  }
1211
1498
  // The cache keeps the Agent's raw windows; pace is recomputed as of every response.
1212
1499
  if (limits)
@@ -1216,11 +1503,11 @@ export class ProviderCatalog {
1216
1503
  const reason = str(limits?.unavailable_reason).trim();
1217
1504
  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
1505
  }
1219
- if (provider === 'openrouter' || provider === 'deepseek' || provider === 'opencode-go') {
1506
+ if (isBalanceProvider(provider)) {
1220
1507
  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;
1508
+ const key = credential ? { credential_id: credential.credential_id } : this.usageKey(provider, apiKey);
1509
+ const read = await this.balance(profileHome, provider, key);
1510
+ const status = read?.status === 'no_key' ? 'no_key' : read?.status === 'ok' ? 'ok' : read?.status === 'http_error' ? read.http_status : null;
1224
1511
  if (provider === 'openrouter') {
1225
1512
  if (status === 'no_key')
1226
1513
  return answer(false, 'no_key', 'OpenRouter quota status needs an OPENROUTER_API_KEY configured on the server.');
@@ -1254,7 +1541,7 @@ export class ProviderCatalog {
1254
1541
  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
1542
  }
1256
1543
  // 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)) ?? [];
1544
+ const entries = credential ? [credential] : (await this.poolEntries(profileHome, provider, null)).value?.entries ?? [];
1258
1545
  if (entries.length) {
1259
1546
  const limits = credential ? poolEntryLimits(credential, at) : poolLimits(entries, at);
1260
1547
  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 +1553,7 @@ export class ProviderCatalog {
1266
1553
  * `apiKey`, the provider's configured key. Null when the pool is unknown (see `poolLookup`).
1267
1554
  */
1268
1555
  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);
1556
+ return this.poolLookup(profileHome, `pool\0${pid}`, (sidecar) => sidecar.call('usage.pool', { profile_home: profileHome, provider: pid, ...this.usageKey(pid, apiKey) }));
1270
1557
  }
1271
1558
  /** The last answer of each credential-pool lookup, by profile home and lookup. */
1272
1559
  lastPoolAnswers = new Map();
@@ -1275,23 +1562,48 @@ export class ProviderCatalog {
1275
1562
  * lookup, else null: an unknown pool, never an empty one, so no pool account is ever reported as removed.
1276
1563
  */
1277
1564
  async poolLookup(profileHome, lookup, call) {
1565
+ const source = this.sourceFingerprint(profileHome);
1278
1566
  const key = `${profileHome}\0${lookup}`;
1279
1567
  try {
1280
1568
  const sidecar = this.deps.sidecar();
1281
1569
  if (!sidecar)
1282
1570
  throw new Error('the sidecar is not running');
1283
1571
  const answer = await call(sidecar);
1284
- this.lastPoolAnswers.set(key, answer);
1285
- return answer;
1572
+ if (source !== this.sourceFingerprint(profileHome))
1573
+ return { value: null, authoritative: false };
1574
+ this.lastPoolAnswers.set(key, { source, value: answer });
1575
+ return { value: answer, authoritative: true };
1286
1576
  }
1287
1577
  catch (error) {
1288
1578
  this.deps.log(`[catalog] credential pool lookup ${lookup.replace('\0', ' ')} failed: ${str(error.message)}`);
1289
- return this.lastPoolAnswers.get(key) ?? null;
1579
+ const hit = this.lastPoolAnswers.get(key);
1580
+ return { value: hit?.source === source && source === this.sourceFingerprint(profileHome) ? hit.value : null, authoritative: false };
1290
1581
  }
1291
1582
  }
1292
- /** TAL-548: the providers whose persisted credential pool holds an added account; none when the sidecar cannot say. */
1583
+ /** One shared read of pool membership and scoped key revisions before reusing catalog data. */
1293
1584
  poolProviders(profileHome) {
1294
- return this.poolLookup(profileHome, 'providers', async (sidecar) => (await sidecar.call('usage.pool_providers', { profile_home: profileHome })).providers);
1585
+ const inputSource = this.sourceFingerprint(profileHome, false);
1586
+ const readKey = this.sourceFingerprint(profileHome);
1587
+ const pending = this.credentialReads.get(readKey);
1588
+ if (pending)
1589
+ return pending;
1590
+ const read = (async () => {
1591
+ const config = await this.deps.config.read(profileHome);
1592
+ const key_envs = Object.fromEntries(BALANCE_PROVIDERS.filter((pid) => this.apiKeyFor(pid, profileHome, config) === null).map((pid) => [pid, this.usageKey(pid, null).env_vars ?? []]));
1593
+ const result = await this.poolLookup(profileHome, 'providers', (sidecar) => sidecar.call('usage.pool_providers', { profile_home: profileHome, key_envs }));
1594
+ if (inputSource !== this.sourceFingerprint(profileHome, false))
1595
+ return { value: null, authoritative: false };
1596
+ this.scopedKeyAuthority.set(profileHome, result.authoritative);
1597
+ if (result.authoritative)
1598
+ this.scopedKeyRevisions.set(profileHome, JSON.stringify(Object.entries(result.value?.keys ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([pid, state]) => [pid, state.revision])));
1599
+ return result;
1600
+ })().catch(() => {
1601
+ if (inputSource === this.sourceFingerprint(profileHome, false))
1602
+ this.scopedKeyAuthority.set(profileHome, false);
1603
+ return { value: null, authoritative: false };
1604
+ }).finally(() => { this.credentialReads.delete(readKey); });
1605
+ this.credentialReads.set(readKey, read);
1606
+ return read;
1295
1607
  }
1296
1608
  /** TAL-548: one key-based balance read through the sidecar, which resolves a pool account's key itself; null when it fails. */
1297
1609
  async balance(profileHome, provider, key) {
@@ -1308,52 +1620,46 @@ export class ProviderCatalog {
1308
1620
  }
1309
1621
  quotaScopeCache = null;
1310
1622
  /** Python `_quota_server_scope_id`: a 32-hex id persisted at `<state>/.quota_scope_id` (created atomically once). */
1311
- quotaServerScopeId() {
1623
+ async quotaServerScopeId() {
1312
1624
  if (this.quotaScopeCache)
1313
1625
  return this.quotaScopeCache;
1314
- const dir = this.deps.stateDir;
1315
- if (!dir) {
1626
+ const documents = this.deps.documents;
1627
+ if (!documents) {
1316
1628
  this.quotaScopeCache = randomUUID().replaceAll('-', '');
1317
1629
  return this.quotaScopeCache;
1318
1630
  }
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
- }
1631
+ const stored = await updateDocument(documents, 'catalog', 'quota-scope', (value) => {
1632
+ const previous = typeof value === 'string' ? value.trim().toLowerCase() : '';
1633
+ if (/^[0-9a-f]{32}$/.test(previous))
1634
+ return { value, result: previous };
1635
+ const id = randomUUID().replaceAll('-', '');
1636
+ return { value: `${id}\n`, result: id };
1637
+ });
1334
1638
  this.quotaScopeCache = stored;
1335
1639
  return stored;
1336
1640
  }
1337
1641
  /** Python `_quota_profile_scope_id`. */
1338
- quotaProfileScopeId(profile) {
1339
- return `qscope_${createHash('sha256').update(`${this.quotaServerScopeId()}\0${profile}`).digest('hex').slice(0, 32)}`;
1642
+ async quotaProfileScopeId(profile) {
1643
+ return `qscope_${createHash('sha256').update(`${(await this.quotaServerScopeId())}\0${profile}`).digest('hex').slice(0, 32)}`;
1340
1644
  }
1341
1645
  /**
1342
1646
  * Python `get_provider_quotas`: the stable-identity envelope the iOS widget persists. A keyed provider is one source per
1343
1647
  * credential-pool account, else one for its own key (Python `_quota_source_descriptors`, TAL-548). Each source id appears
1344
1648
  * once (two custom providers can share a slug), ordered by provider, account label, then source id.
1345
1649
  */
1346
- async quotas(profileHome, profile, opts = {}) {
1650
+ async quotas(profileHome, profile, opts = {}, attempt = 0) {
1651
+ const sequence = ++this.quotaReadSequence;
1652
+ const source = this.sourceFingerprint(profileHome);
1347
1653
  const at = this.deps.now();
1348
1654
  const { payload: status, pooled: pooledIds } = await this.providerRows(profileHome);
1349
1655
  const active = status.active_provider;
1350
- const scopeId = this.quotaProfileScopeId(profile);
1656
+ const scopeId = (await this.quotaProfileScopeId(profile));
1351
1657
  // Python `_quota_source_id(profile, provider, credential_id)`: a pool account's id, or "provider" for the provider's own key.
1352
1658
  const sourceId = (pid, credential) => `qsrc_${createHash('sha256').update(`${scopeId}\0${pid}\0${credential}`).digest('hex').slice(0, 32)}`;
1353
1659
  // A provider configured only through `hermes auth add` has no key in .env or config.yaml, yet its pool accounts are
1354
1660
  // sources, including a signed-out OAuth provider's. An unknown pool (a failed lookup with no earlier answer) never
1355
1661
  // reports a requested source as missing.
1356
- let poolUnknown = pooledIds === null;
1662
+ let poolUnknown = !status.availability.authoritative;
1357
1663
  const pooled = new Set(pooledIds ?? []);
1358
1664
  const config = await this.deps.config.read(profileHome);
1359
1665
  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 +1667,80 @@ export class ProviderCatalog {
1361
1667
  const label = str(p.display_name) || pid;
1362
1668
  const apiKey = this.apiKeyFor(pid, profileHome, config);
1363
1669
  const found = await this.poolEntries(profileHome, pid, apiKey);
1364
- if (!found)
1670
+ if (!found.authoritative)
1365
1671
  poolUnknown = true;
1366
- const entries = found ?? [];
1672
+ const entries = found.value?.entries ?? [];
1367
1673
  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
1674
  // 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));
1675
+ 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
1676
  return ownKey ? [{ source_id: sourceId(pid, 'provider'), provider_id: pid, provider_label: label, account_label: label, credential: null }, ...accounts] : accounts;
1371
1677
  }));
1372
1678
  let descriptors = uniqueQuotaSources(perProvider.flat());
1679
+ const membership = new Set(descriptors.map((d) => d.source_id));
1373
1680
  const requested = str(opts.sourceId).trim() || null;
1374
1681
  if (requested)
1375
1682
  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 });
1683
+ const thresholds = (await this.deps.quotaThresholds?.(profile)) ?? QUOTA_THRESHOLD_DEFAULTS;
1684
+ let sources = await Promise.all(descriptors.map(async (d) => {
1685
+ const q = await this.quota(profileHome, d.provider_id, { refresh: opts.refresh ?? false, at, credential: d.credential, sequence });
1379
1686
  const limits = dict(q.account_limits);
1380
1687
  return classifyQuotaSource({
1381
- source_id: d.source_id, provider_id: d.provider_id, provider_label: d.provider_label, account_label: d.account_label,
1688
+ 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
1689
  is_active_provider: d.provider_id === active, supported: q.supported === true, status: str(limits.status) || str(q.status) || 'unavailable',
1383
1690
  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
1691
  unavailable_reason: limits.unavailable_reason ?? null, retry_after: limits.retry_after ?? null, fetched_at: limits.fetched_at ?? null, message: q.message ?? null,
1385
1692
  }, thresholds);
1386
1693
  }));
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 };
1694
+ if (source !== this.sourceFingerprint(profileHome) && attempt === 0)
1695
+ return this.quotas(profileHome, profile, opts, 1);
1696
+ poolUnknown ||= source !== this.sourceFingerprint(profileHome);
1697
+ const cacheKey = `${profileHome}\0${scopeId}`;
1698
+ const cached = source === this.sourceFingerprint(profileHome) ? this.lastQuotaSources.get(cacheKey) : undefined;
1699
+ const prior = cached?.source === source ? cached : undefined;
1700
+ const previous = prior?.sources ?? [];
1701
+ if (!poolUnknown) {
1702
+ sources = sources.map((s) => {
1703
+ const old = previous.find((p) => p.source_id === s.source_id);
1704
+ return s.status === 'unavailable' && old ? this.staleQuotaSource(old, str(s.unavailable_reason || s.message)) : s;
1705
+ });
1706
+ const latestMembership = prior && prior.sequence > sequence ? prior.membership : membership;
1707
+ const sourceSequences = new Map(prior?.sourceSequences);
1708
+ const merged = new Map(previous.filter((s) => latestMembership.has(str(s.source_id))).map((s) => [str(s.source_id), s]));
1709
+ for (const value of sources) {
1710
+ const id = str(value.source_id);
1711
+ if (!latestMembership.has(id) || (sourceSequences.get(id) ?? -1) > sequence)
1712
+ continue;
1713
+ merged.set(id, value);
1714
+ sourceSequences.set(id, sequence);
1715
+ }
1716
+ for (const id of sourceSequences.keys())
1717
+ if (!latestMembership.has(id))
1718
+ sourceSequences.delete(id);
1719
+ const snapshot = uniqueQuotaSources([...merged.values()]);
1720
+ this.lastQuotaSources.set(cacheKey, { source, sequence: Math.max(sequence, prior?.sequence ?? -1), membership: latestMembership, sources: snapshot, sourceSequences });
1721
+ // A delayed read answers with the newest confirmed projection, including confirmed removals.
1722
+ sources = snapshot.filter((s) => !requested || s.source_id === requested);
1723
+ }
1724
+ else {
1725
+ sources = previous.filter((s) => !requested || s.source_id === requested).map((s) => this.staleQuotaSource(s));
1726
+ }
1727
+ const currentSource = this.sourceFingerprint(profileHome);
1728
+ if (currentSource !== source) {
1729
+ poolUnknown = true;
1730
+ sources = [];
1731
+ }
1732
+ const latestMembership = this.lastQuotaSources.get(cacheKey)?.membership;
1733
+ const availability = this.availability(currentSource, !poolUnknown, sources.length > 0);
1734
+ 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 };
1735
+ }
1736
+ lastQuotaSources = new Map();
1737
+ staleQuotaSource(source, reason = 'Provider data is temporarily unavailable. Showing last-confirmed data.') {
1738
+ return { ...source, status: 'stale', unavailable_reason: reason, message: reason, urgency: { remaining: 'stale', pace: 'stale' } };
1739
+ }
1740
+ availability(source, authoritative, cached) {
1741
+ return { authoritative, status: authoritative ? 'available' : cached ? 'stale' : 'unavailable',
1742
+ unavailable_reason: authoritative ? null : cached ? 'Provider data is temporarily unavailable. Showing last-confirmed data.' : 'Provider data is temporarily unavailable. Try again.',
1743
+ generation: createHash('sha256').update(source).digest('hex') };
1388
1744
  }
1389
1745
  /** Python `get_provider_cost_history` (OpenRouter only; daily snapshots under `<home>/cost-snapshots`). */
1390
1746
  async costHistory(profileHome, providerRaw, days) {
@@ -1397,9 +1753,9 @@ export class ProviderCatalog {
1397
1753
  const budget = this.deps.costBudget();
1398
1754
  const config = await this.deps.config.read(profileHome);
1399
1755
  const apiKey = this.apiKeyFor('openrouter', profileHome, config);
1400
- if (!apiKey)
1756
+ const info = await this.balance(profileHome, 'openrouter', this.usageKey('openrouter', apiKey));
1757
+ if (info?.status === 'no_key')
1401
1758
  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
1759
  if (info?.status !== 'ok' || !info.quota) {
1404
1760
  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
1761
  }