@north-light/cli 0.3.560 → 0.3.562

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 (573) hide show
  1. package/dist/api/client.d.ts +27 -4
  2. package/dist/api/client.js +3 -3
  3. package/dist/api/contract.d.ts +1 -1
  4. package/dist/api/contract.js +1 -1
  5. package/dist/api/dto/bash-jobs.d.ts +5 -1
  6. package/dist/api/dto/broker-ops.d.ts +25 -1
  7. package/dist/api/dto/charges.d.ts +9 -2
  8. package/dist/api/dto/delivery.d.ts +26 -2
  9. package/dist/api/dto/execution-start.d.ts +57 -0
  10. package/dist/api/dto/execution-start.js +0 -0
  11. package/dist/api/dto/modelauth.d.ts +5 -0
  12. package/dist/api/dto/node-records.d.ts +16 -0
  13. package/dist/api/dto/objects.d.ts +1 -1
  14. package/dist/api/dto/reports.d.ts +0 -3
  15. package/dist/api/dto/run-events.d.ts +7 -1
  16. package/dist/api/dto/settings.d.ts +55 -0
  17. package/dist/api/dto/settings.js +0 -0
  18. package/dist/api/index.d.ts +2 -0
  19. package/dist/api/index.js +1 -1
  20. package/dist/api/routes.d.ts +11 -0
  21. package/dist/api/routes.js +1 -1
  22. package/dist/builtin-memory/crouter-sdk/release-notes.md +4 -0
  23. package/dist/cli.js +7 -7
  24. package/dist/clients/attach/chrome/bash-jobs.d.ts +11 -1
  25. package/dist/clients/attach/chrome/bash-jobs.js +1 -1
  26. package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
  27. package/dist/clients/attach/chrome/roster.js +6 -4
  28. package/dist/clients/attach/overlays/auth.js +2 -2
  29. package/dist/clients/attach/render/chat-view.js +1 -1
  30. package/dist/clients/attach/viewer.js +550 -567
  31. package/dist/commands/canvas-config.js +2 -2
  32. package/dist/commands/canvas-use.js +1 -1
  33. package/dist/commands/human.js +2 -2
  34. package/dist/commands/node/bash.js +3 -3
  35. package/dist/commands/node/create.js +2 -2
  36. package/dist/commands/person-settings.d.ts +1 -0
  37. package/dist/commands/person-settings.js +1 -0
  38. package/dist/commands/pkg/market-manage.d.ts +2 -2
  39. package/dist/commands/pkg/market-manage.js +1 -1
  40. package/dist/commands/pkg/plugin-manage.js +13 -13
  41. package/dist/commands/pkg/shared.js +1 -1
  42. package/dist/commands/profile/default.js +1 -1
  43. package/dist/commands/push.js +2 -3
  44. package/dist/commands/sys/config.js +1 -1
  45. package/dist/commands/sys/connect.js +1 -1
  46. package/dist/commands/sys/context/sysprompt.js +2 -2
  47. package/dist/commands/sys/doctor.js +1 -1
  48. package/dist/commands/sys/panels/broker-limits-panel.js +1 -1
  49. package/dist/commands/sys/panels/keybindings-panel.js +1 -1
  50. package/dist/commands/sys/panels/models-panel.d.ts +8 -2
  51. package/dist/commands/sys/panels/models-panel.js +1 -1
  52. package/dist/commands/sys/panels/prefs-panel.d.ts +1 -0
  53. package/dist/commands/sys/panels/prefs-panel.js +1 -1
  54. package/dist/commands/sys/panels/profiles-panel.js +1 -1
  55. package/dist/commands/sys/panels/provider-panel.d.ts +4 -0
  56. package/dist/commands/sys/panels/provider-panel.js +1 -1
  57. package/dist/commands/sys/provider-login.js +5 -5
  58. package/dist/commands/sys/setup-core.js +3 -3
  59. package/dist/commands/sys/setup-settings-runner.js +1 -1
  60. package/dist/commands/sys/spend.js +3 -3
  61. package/dist/commands/sys/update.js +1 -1
  62. package/dist/core/auth-file.d.ts +11 -19
  63. package/dist/core/auth-file.js +1 -2
  64. package/dist/core/auto-update.d.ts +1 -1
  65. package/dist/core/auto-update.js +4 -4
  66. package/dist/core/bash-jobs.d.ts +29 -71
  67. package/dist/core/bash-jobs.js +9 -10
  68. package/dist/core/blobs/directory.d.ts +3 -0
  69. package/dist/core/blobs/directory.js +1 -0
  70. package/dist/core/blobs/fault.d.ts +3 -0
  71. package/dist/core/blobs/fault.js +1 -0
  72. package/dist/core/blobs/index.d.ts +21 -0
  73. package/dist/core/blobs/index.js +1 -0
  74. package/dist/core/blobs/r2.d.ts +9 -0
  75. package/dist/core/blobs/r2.js +4 -0
  76. package/dist/core/bootstrap.d.ts +1 -1
  77. package/dist/core/bootstrap.js +2 -2
  78. package/dist/core/canvas/canvas.d.ts +9 -7
  79. package/dist/core/canvas/canvas.js +8 -8
  80. package/dist/core/canvas/crons.d.ts +3 -2
  81. package/dist/core/canvas/crons.js +3 -3
  82. package/dist/core/canvas/daemon-owner.d.ts +5 -0
  83. package/dist/core/canvas/daemon-owner.js +1 -1
  84. package/dist/core/canvas/db.d.ts +10 -9
  85. package/dist/core/canvas/db.js +4 -4
  86. package/dist/core/canvas/executions.d.ts +100 -4
  87. package/dist/core/canvas/executions.js +1 -1
  88. package/dist/core/canvas/export-sqlite.js +1 -1
  89. package/dist/core/canvas/focuses.d.ts +4 -0
  90. package/dist/core/canvas/focuses.js +1 -1
  91. package/dist/core/canvas/history.js +9 -9
  92. package/dist/core/canvas/managed-worktree-sweep-candidates.d.ts +2 -1
  93. package/dist/core/canvas/managed-worktree-sweep-candidates.js +3 -2
  94. package/dist/core/canvas/node-tokens.d.ts +1 -4
  95. package/dist/core/canvas/node-tokens.js +3 -3
  96. package/dist/core/canvas/paths.d.ts +0 -1
  97. package/dist/core/canvas/paths.js +1 -1
  98. package/dist/core/canvas/person-state.d.ts +26 -0
  99. package/dist/core/canvas/person-state.js +1 -0
  100. package/dist/core/canvas/person.d.ts +3 -10
  101. package/dist/core/canvas/person.js +7 -7
  102. package/dist/core/canvas/reconciler-leases.d.ts +12 -0
  103. package/dist/core/canvas/reconciler-leases.js +8 -0
  104. package/dist/core/canvas/recovery.d.ts +4 -0
  105. package/dist/core/canvas/recovery.js +1 -1
  106. package/dist/core/canvas/schema-pg/003-fleet-removals.sql +1 -1
  107. package/dist/core/canvas/schema-pg/005-fleet-blobs.sql +13 -0
  108. package/dist/core/canvas/schema-pg/006-job-rows.sql +20 -0
  109. package/dist/core/canvas/schema-pg/007-ledger-check.sql +117 -0
  110. package/dist/core/canvas/schema-version.d.ts +1 -1
  111. package/dist/core/canvas/schema-version.js +1 -1
  112. package/dist/core/canvas/sessions.d.ts +8 -11
  113. package/dist/core/canvas/sessions.js +1 -4
  114. package/dist/core/canvas/tree.d.ts +4 -2
  115. package/dist/core/canvas/tree.js +6 -6
  116. package/dist/core/canvas/types.d.ts +7 -0
  117. package/dist/core/command-hooks/transport/exec-lifecycle.js +1 -1
  118. package/dist/core/command-plugins/bundle.d.ts +1 -1
  119. package/dist/core/command-plugins/bundle.js +1 -1
  120. package/dist/core/command-plugins/compose.d.ts +1 -1
  121. package/dist/core/command-plugins/discovery.d.ts +0 -1
  122. package/dist/core/command-plugins/discovery.js +1 -1
  123. package/dist/core/command-plugins/help-addenda.js +1 -1
  124. package/dist/core/command-plugins/presence.d.ts +2 -0
  125. package/dist/core/command-plugins/presence.js +1 -1
  126. package/dist/core/config.d.ts +16 -15
  127. package/dist/core/config.js +1 -1
  128. package/dist/core/conversation-store/client-worker.js +1 -1
  129. package/dist/core/conversation-store/client.js +2 -2
  130. package/dist/core/conversation-store/index.d.ts +9 -11
  131. package/dist/core/conversation-store/index.js +1 -1
  132. package/dist/core/conversation-store/patch.js +1 -1
  133. package/dist/core/conversation-store/row-store.d.ts +14 -0
  134. package/dist/core/conversation-store/row-store.js +13 -0
  135. package/dist/core/directory/join.js +1 -1
  136. package/dist/core/env-name.d.ts +1 -0
  137. package/dist/core/env-name.js +1 -1
  138. package/dist/core/environment-recipe.d.ts +15 -0
  139. package/dist/core/environment-recipe.js +1 -0
  140. package/dist/core/environments.js +1 -1
  141. package/dist/core/events/emit.js +2 -2
  142. package/dist/core/events/run-event-chunks.d.ts +25 -0
  143. package/dist/core/events/run-event-chunks.js +5 -0
  144. package/dist/core/feed/messages.js +5 -5
  145. package/dist/core/grants/mirror.d.ts +15 -0
  146. package/dist/core/grants/mirror.js +15 -3
  147. package/dist/core/grants/on-sync.d.ts +9 -5
  148. package/dist/core/grants/on-sync.js +1 -1
  149. package/dist/core/grants/remove.d.ts +1 -6
  150. package/dist/core/grants/remove.js +1 -1
  151. package/dist/core/grants/sync.js +1 -1
  152. package/dist/core/grants/watch.d.ts +15 -9
  153. package/dist/core/grants/watch.js +1 -1
  154. package/dist/core/graph/custom.js +8 -8
  155. package/dist/core/graph/jobs.d.ts +83 -5
  156. package/dist/core/graph/jobs.js +11 -2
  157. package/dist/core/graph/kinds/job.js +2 -2
  158. package/dist/core/graph/names.js +7 -7
  159. package/dist/core/graph/objects.d.ts +5 -1
  160. package/dist/core/graph/objects.js +11 -11
  161. package/dist/core/graph/reader.d.ts +3 -0
  162. package/dist/core/graph/reader.js +3 -3
  163. package/dist/core/graph/repo-sync/identity.d.ts +3 -1
  164. package/dist/core/graph/repo-sync/identity.js +1 -1
  165. package/dist/core/graph/repo-sync/repos.d.ts +1 -1
  166. package/dist/core/graph/repo-sync/repos.js +2 -2
  167. package/dist/core/graph/repo-sync/sync.js +2 -2
  168. package/dist/core/installed-plugins.js +1 -1
  169. package/dist/core/keybindings/persistence.d.ts +1 -1
  170. package/dist/core/keybindings/persistence.js +1 -1
  171. package/dist/core/layout-migrate/loose-keys.d.ts +1 -1
  172. package/dist/core/layout-migrate/loose-keys.js +1 -1
  173. package/dist/core/ledger/account.d.ts +12 -0
  174. package/dist/core/ledger/account.js +2 -0
  175. package/dist/core/ledger/charges.d.ts +2 -2
  176. package/dist/core/ledger/charges.js +1 -1
  177. package/dist/core/ledger/check.d.ts +5 -1
  178. package/dist/core/ledger/check.js +1 -7
  179. package/dist/core/ledger/chunks.d.ts +8 -0
  180. package/dist/core/ledger/chunks.js +2 -0
  181. package/dist/core/ledger/index.d.ts +2 -1
  182. package/dist/core/ledger/index.js +1 -1
  183. package/dist/core/ledger/infra-allowance.d.ts +9 -0
  184. package/dist/core/ledger/infra-allowance.js +1 -0
  185. package/dist/core/ledger/limits.js +3 -3
  186. package/dist/core/ledger/payer.d.ts +1 -1
  187. package/dist/core/ledger/payer.js +1 -1
  188. package/dist/core/ledger/read.d.ts +14 -4
  189. package/dist/core/ledger/read.js +2 -3
  190. package/dist/core/ledger/totals.d.ts +5 -16
  191. package/dist/core/ledger/totals.js +4 -4
  192. package/dist/core/ledger/types.d.ts +12 -3
  193. package/dist/core/llm/run.js +2 -2
  194. package/dist/core/managed-providers.d.ts +72 -0
  195. package/dist/core/managed-providers.js +1 -0
  196. package/dist/core/model-auth/api-credential-store.d.ts +10 -0
  197. package/dist/core/model-auth/api-credential-store.js +1 -0
  198. package/dist/core/model-auth/daemon-login.d.ts +1 -1
  199. package/dist/core/model-auth/daemon-login.js +1 -1
  200. package/dist/core/model-auth/flows.d.ts +5 -4
  201. package/dist/core/model-auth/flows.js +1 -1
  202. package/dist/core/model-auth/handoff-jti.d.ts +3 -4
  203. package/dist/core/model-auth/handoff-jti.js +1 -1
  204. package/dist/core/model-auth/readiness.js +1 -1
  205. package/dist/core/model-config.d.ts +1 -1
  206. package/dist/core/model-config.js +1 -1
  207. package/dist/core/model-routes-pool.d.ts +14 -0
  208. package/dist/core/model-routes-pool.js +1 -0
  209. package/dist/core/model-routes.d.ts +11 -16
  210. package/dist/core/model-routes.js +1 -1
  211. package/dist/core/peer/ingest.d.ts +2 -0
  212. package/dist/core/peer/ingest.js +1 -1
  213. package/dist/core/peer/stubs.d.ts +8 -0
  214. package/dist/core/peer/stubs.js +1 -1
  215. package/dist/core/permissions/evaluator.d.ts +7 -2
  216. package/dist/core/permissions/evaluator.js +2 -2
  217. package/dist/core/plugin-env.js +1 -1
  218. package/dist/core/pool-credential-store.d.ts +2 -5
  219. package/dist/core/pool-credential-store.js +1 -1
  220. package/dist/core/profiles/app-env-store.d.ts +5 -5
  221. package/dist/core/profiles/app-env-store.js +1 -2
  222. package/dist/core/profiles/default-binding.d.ts +5 -5
  223. package/dist/core/profiles/default-binding.js +1 -1
  224. package/dist/core/profiles/env-store.d.ts +10 -21
  225. package/dist/core/profiles/env-store.js +1 -2
  226. package/dist/core/profiles/manifest.d.ts +14 -18
  227. package/dist/core/profiles/manifest.js +1 -1
  228. package/dist/core/profiles/relocate.js +1 -1
  229. package/dist/core/profiles/select.js +21 -21
  230. package/dist/core/provider-credential.d.ts +1 -1
  231. package/dist/core/provider-credential.js +1 -1
  232. package/dist/core/provider-management.d.ts +3 -4
  233. package/dist/core/provider-management.js +1 -1
  234. package/dist/core/providers/bundles.js +2 -2
  235. package/dist/core/providers/call.d.ts +3 -34
  236. package/dist/core/providers/description.d.ts +34 -0
  237. package/dist/core/providers/description.js +0 -0
  238. package/dist/core/providers/token-cache.d.ts +14 -14
  239. package/dist/core/providers/token-cache.js +1 -1
  240. package/dist/core/review/companion.js +1 -1
  241. package/dist/core/runs/activity.d.ts +1 -1
  242. package/dist/core/runs/activity.js +1 -1
  243. package/dist/core/runs/events.d.ts +7 -5
  244. package/dist/core/runs/events.js +2 -6
  245. package/dist/core/runs/operations.d.ts +1 -1
  246. package/dist/core/runs/operations.js +6 -6
  247. package/dist/core/runtime/bearings.js +1 -1
  248. package/dist/core/runtime/broker/attachments-fetch.d.ts +5 -0
  249. package/dist/core/runtime/broker/attachments-fetch.js +1 -0
  250. package/dist/core/runtime/broker/bash-job-launch.d.ts +4 -2
  251. package/dist/core/runtime/broker/bash-job-launch.js +1 -1
  252. package/dist/core/runtime/broker/boot-state.d.ts +9 -0
  253. package/dist/core/runtime/broker/boot-state.js +1 -0
  254. package/dist/core/runtime/broker/cli-relay.d.ts +2 -1
  255. package/dist/core/runtime/broker/cli-relay.js +1 -1
  256. package/dist/core/runtime/broker/client-registry.d.ts +3 -3
  257. package/dist/core/runtime/broker/daemon-ops.d.ts +10 -1
  258. package/dist/core/runtime/broker/daemon-ops.js +1 -1
  259. package/dist/core/runtime/broker/fault-retry.d.ts +2 -0
  260. package/dist/core/runtime/broker/fault-retry.js +1 -1
  261. package/dist/core/runtime/broker/frame-engine-control.js +1 -1
  262. package/dist/core/runtime/broker/held-deferred-inbox.d.ts +3 -0
  263. package/dist/core/runtime/broker/held-deferred-inbox.js +1 -1
  264. package/dist/core/runtime/broker/job-reporter.d.ts +18 -0
  265. package/dist/core/runtime/broker/job-reporter.js +2 -0
  266. package/dist/core/runtime/broker/model-adapter.d.ts +3 -0
  267. package/dist/core/runtime/broker/model-adapter.js +3 -2
  268. package/dist/core/runtime/broker/signal-stream.js +1 -1
  269. package/dist/core/runtime/broker/spilled-body.d.ts +5 -0
  270. package/dist/core/runtime/broker/spilled-body.js +1 -0
  271. package/dist/core/runtime/broker/summaries-post.d.ts +5 -0
  272. package/dist/core/runtime/broker/summaries-post.js +1 -0
  273. package/dist/core/runtime/broker/tool-groups.d.ts +3 -0
  274. package/dist/core/runtime/broker/tool-groups.js +1 -1
  275. package/dist/core/runtime/broker/turn-replies.d.ts +1 -0
  276. package/dist/core/runtime/broker/turn-replies.js +1 -1
  277. package/dist/core/runtime/broker-persona-guidance.js +2 -2
  278. package/dist/core/runtime/broker.js +1 -1
  279. package/dist/core/runtime/deliver-live.js +1 -1
  280. package/dist/core/runtime/first-boot-rows.d.ts +8 -0
  281. package/dist/core/runtime/first-boot-rows.js +2 -0
  282. package/dist/core/runtime/fleet.d.ts +13 -12
  283. package/dist/core/runtime/front-door-login.js +1 -1
  284. package/dist/core/runtime/front-door.js +6 -6
  285. package/dist/core/runtime/headless-pi.js +1 -1
  286. package/dist/core/runtime/host.d.ts +14 -3
  287. package/dist/core/runtime/host.js +3 -3
  288. package/dist/core/runtime/invocation.d.ts +6 -0
  289. package/dist/core/runtime/kickoff.d.ts +2 -14
  290. package/dist/core/runtime/kickoff.js +17 -19
  291. package/dist/core/runtime/launch-config-worker.js +1 -1
  292. package/dist/core/runtime/launch.js +2 -2
  293. package/dist/core/runtime/model-registry.js +1 -1
  294. package/dist/core/runtime/node-read.d.ts +4 -6
  295. package/dist/core/runtime/node-read.js +3 -3
  296. package/dist/core/runtime/nodes.js +1 -1
  297. package/dist/core/runtime/outcome-document.d.ts +0 -1
  298. package/dist/core/runtime/outcome-document.js +1 -1
  299. package/dist/core/runtime/output-result.d.ts +15 -0
  300. package/dist/core/runtime/output-result.js +5 -0
  301. package/dist/core/runtime/placement.d.ts +17 -1
  302. package/dist/core/runtime/placement.js +1 -1
  303. package/dist/core/runtime/provider-credential-gate.d.ts +6 -2
  304. package/dist/core/runtime/provider-credential-gate.js +2 -2
  305. package/dist/core/runtime/recovery-hold.d.ts +21 -2
  306. package/dist/core/runtime/recovery-hold.js +2 -1
  307. package/dist/core/runtime/recycle.js +1 -1
  308. package/dist/core/runtime/reset.js +1 -1
  309. package/dist/core/runtime/revive.d.ts +2 -2
  310. package/dist/core/runtime/revive.js +2 -2
  311. package/dist/core/runtime/runner/blaxel/client.d.ts +81 -0
  312. package/dist/core/runtime/runner/blaxel/client.js +1 -0
  313. package/dist/core/runtime/runner/blaxel/image-build.d.ts +74 -0
  314. package/dist/core/runtime/runner/blaxel/image-build.js +5 -0
  315. package/dist/core/runtime/runner/blaxel/runner.d.ts +41 -0
  316. package/dist/core/runtime/runner/blaxel/runner.js +3 -0
  317. package/dist/core/runtime/runner/index.d.ts +8 -0
  318. package/dist/core/runtime/runner/index.js +1 -0
  319. package/dist/core/runtime/runner/process.d.ts +11 -0
  320. package/dist/core/runtime/runner/process.js +1 -0
  321. package/dist/core/runtime/runner/types.d.ts +95 -0
  322. package/dist/core/runtime/runner/types.js +0 -0
  323. package/dist/core/runtime/situational-context.d.ts +3 -6
  324. package/dist/core/runtime/situational-context.js +2 -2
  325. package/dist/core/runtime/situational-live.js +1 -1
  326. package/dist/core/runtime/spawn-env-rows.d.ts +5 -0
  327. package/dist/core/runtime/spawn-env-rows.js +1 -0
  328. package/dist/core/runtime/spawn-env.d.ts +4 -18
  329. package/dist/core/runtime/spawn-env.js +1 -1
  330. package/dist/core/runtime/spawn.js +2 -2
  331. package/dist/core/runtime/stop-guard.d.ts +1 -1
  332. package/dist/core/runtime/stop-guard.js +2 -8
  333. package/dist/core/runtime/structured-output.d.ts +0 -67
  334. package/dist/core/runtime/structured-output.js +3 -8
  335. package/dist/core/runtime/tmux-driver.d.ts +8 -0
  336. package/dist/core/runtime/tmux-driver.js +6 -6
  337. package/dist/core/runtime/view-socket.d.ts +20 -0
  338. package/dist/core/runtime/view-socket.js +3 -1
  339. package/dist/core/runtime/warm-pool.d.ts +13 -1
  340. package/dist/core/runtime/warm-pool.js +1 -1
  341. package/dist/core/scoped-state/legacy-profiles.d.ts +11 -0
  342. package/dist/core/scoped-state/legacy-profiles.js +1 -0
  343. package/dist/core/scoped-state/materialized-plugins.d.ts +18 -0
  344. package/dist/core/scoped-state/materialized-plugins.js +1 -0
  345. package/dist/core/scoped-state/migrate.d.ts +8 -1
  346. package/dist/core/scoped-state/migrate.js +3 -3
  347. package/dist/core/scoped-state/person-rows.d.ts +10 -0
  348. package/dist/core/scoped-state/person-rows.js +6 -0
  349. package/dist/core/scoped-state/person-store.d.ts +72 -0
  350. package/dist/core/scoped-state/person-store.js +1 -0
  351. package/dist/core/secrets/blaxel-launch.d.ts +24 -0
  352. package/dist/core/secrets/blaxel-launch.js +4 -0
  353. package/dist/core/secrets/blaxel-register.d.ts +2 -0
  354. package/dist/core/secrets/blaxel-register.js +1 -0
  355. package/dist/core/secrets/env-rows.d.ts +17 -0
  356. package/dist/core/secrets/env-rows.js +4 -0
  357. package/dist/core/secrets/environment-owner.d.ts +8 -0
  358. package/dist/core/secrets/environment-owner.js +1 -0
  359. package/dist/core/secrets/environment-secrets.d.ts +14 -0
  360. package/dist/core/secrets/environment-secrets.js +1 -0
  361. package/dist/core/secrets/index.d.ts +2 -0
  362. package/dist/core/secrets/index.js +1 -0
  363. package/dist/core/secrets/key.d.ts +3 -0
  364. package/dist/core/secrets/key.js +2 -0
  365. package/dist/core/secrets/seal.d.ts +18 -0
  366. package/dist/core/secrets/seal.js +1 -0
  367. package/dist/core/spaces/mounts.d.ts +1 -1
  368. package/dist/core/spaces/mounts.js +1 -1
  369. package/dist/core/spaces/process.d.ts +1 -1
  370. package/dist/core/spaces/stop.d.ts +7 -5
  371. package/dist/core/spaces/stop.js +2 -2
  372. package/dist/core/spaces/users.d.ts +2 -1
  373. package/dist/core/spaces/users.js +1 -1
  374. package/dist/core/storage/attachments.d.ts +11 -8
  375. package/dist/core/storage/attachments.js +1 -1
  376. package/dist/core/storage/tables.d.ts +4 -0
  377. package/dist/core/storage/tables.js +3 -3
  378. package/dist/core/subscription-state.d.ts +34 -72
  379. package/dist/core/subscription-state.js +1 -4
  380. package/dist/core/substrate/delivery/plan.js +1 -1
  381. package/dist/core/user-settings.js +1 -1
  382. package/dist/daemon/api/app-listener.js +10 -10
  383. package/dist/daemon/api/auth/person-bearer.d.ts +14 -0
  384. package/dist/daemon/api/auth/person-bearer.js +1 -0
  385. package/dist/daemon/api/bridge.js +4 -4
  386. package/dist/daemon/api/document-listener.js +3 -3
  387. package/dist/daemon/api/environment-of.d.ts +18 -0
  388. package/dist/daemon/api/environment-of.js +5 -0
  389. package/dist/daemon/api/handlers/analytics.js +1 -1
  390. package/dist/daemon/api/handlers/app-profiles.d.ts +2 -2
  391. package/dist/daemon/api/handlers/app-profiles.js +1 -1
  392. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  393. package/dist/daemon/api/handlers/bash.js +1 -1
  394. package/dist/daemon/api/handlers/broker-ops.d.ts +3 -0
  395. package/dist/daemon/api/handlers/broker-ops.js +1 -1
  396. package/dist/daemon/api/handlers/broker-recovery.js +1 -1
  397. package/dist/daemon/api/handlers/broker-signals.d.ts +0 -3
  398. package/dist/daemon/api/handlers/broker-signals.js +2 -2
  399. package/dist/daemon/api/handlers/canvas.js +4 -4
  400. package/dist/daemon/api/handlers/charges.d.ts +1 -1
  401. package/dist/daemon/api/handlers/charges.js +1 -1
  402. package/dist/daemon/api/handlers/conversations.d.ts +2 -0
  403. package/dist/daemon/api/handlers/conversations.js +3 -0
  404. package/dist/daemon/api/handlers/crons.js +1 -1
  405. package/dist/daemon/api/handlers/daemon.js +1 -1
  406. package/dist/daemon/api/handlers/delivery.d.ts +1 -1
  407. package/dist/daemon/api/handlers/delivery.js +1 -1
  408. package/dist/daemon/api/handlers/docs.js +1 -1
  409. package/dist/daemon/api/handlers/exec.js +1 -1
  410. package/dist/daemon/api/handlers/execution-start.d.ts +2 -0
  411. package/dist/daemon/api/handlers/execution-start.js +2 -0
  412. package/dist/daemon/api/handlers/files.js +1 -1
  413. package/dist/daemon/api/handlers/git-credential.d.ts +11 -0
  414. package/dist/daemon/api/handlers/git-credential.js +1 -0
  415. package/dist/daemon/api/handlers/hook-exec.d.ts +1 -1
  416. package/dist/daemon/api/handlers/hook-exec.js +1 -1
  417. package/dist/daemon/api/handlers/human-requests.js +1 -1
  418. package/dist/daemon/api/handlers/inbox.js +1 -1
  419. package/dist/daemon/api/handlers/messages.js +1 -1
  420. package/dist/daemon/api/handlers/model-config.js +1 -1
  421. package/dist/daemon/api/handlers/modelauth.d.ts +1 -1
  422. package/dist/daemon/api/handlers/modelauth.js +1 -1
  423. package/dist/daemon/api/handlers/node-events.d.ts +6 -0
  424. package/dist/daemon/api/handlers/node-events.js +3 -3
  425. package/dist/daemon/api/handlers/node-outcomes.js +1 -1
  426. package/dist/daemon/api/handlers/node-records.js +3 -2
  427. package/dist/daemon/api/handlers/nodes.js +1 -1
  428. package/dist/daemon/api/handlers/objects.js +3 -3
  429. package/dist/daemon/api/handlers/profiles.js +1 -1
  430. package/dist/daemon/api/handlers/providers.js +1 -1
  431. package/dist/daemon/api/handlers/reports.js +9 -13
  432. package/dist/daemon/api/handlers/reviews.js +1 -1
  433. package/dist/daemon/api/handlers/runs.js +1 -1
  434. package/dist/daemon/api/handlers/settings.d.ts +2 -0
  435. package/dist/daemon/api/handlers/settings.js +1 -0
  436. package/dist/daemon/api/handlers/structured-requests.js +1 -1
  437. package/dist/daemon/api/handlers/vendor-status.d.ts +1 -1
  438. package/dist/daemon/api/handlers/vendor-status.js +1 -1
  439. package/dist/daemon/api/handlers/worktree.js +1 -1
  440. package/dist/daemon/api/map.d.ts +2 -2
  441. package/dist/daemon/api/map.js +2 -2
  442. package/dist/daemon/api/operations.js +1 -1
  443. package/dist/daemon/api/reader.d.ts +8 -1
  444. package/dist/daemon/api/reader.js +1 -1
  445. package/dist/daemon/api/router.d.ts +4 -2
  446. package/dist/daemon/api/router.js +1 -1
  447. package/dist/daemon/api/server.d.ts +15 -3
  448. package/dist/daemon/api/server.js +1 -1
  449. package/dist/daemon/api/vendor-signin.d.ts +2 -1
  450. package/dist/daemon/api/vendor-signin.js +20 -20
  451. package/dist/daemon/api/viewer-presence.d.ts +20 -6
  452. package/dist/daemon/api/viewer-presence.js +2 -1
  453. package/dist/daemon/broker-signals/bus.d.ts +48 -11
  454. package/dist/daemon/broker-signals/bus.js +1 -1
  455. package/dist/daemon/cron/armed-context.js +1 -1
  456. package/dist/daemon/cron/lease-recovery.js +1 -1
  457. package/dist/daemon/cron/live-runs.d.ts +6 -0
  458. package/dist/daemon/cron/live-runs.js +1 -1
  459. package/dist/daemon/cron/run-socket.js +1 -1
  460. package/dist/daemon/cron-run.js +2 -2
  461. package/dist/daemon/crtrd-cli.js +1 -1
  462. package/dist/daemon/crtrd.d.ts +10 -0
  463. package/dist/daemon/crtrd.js +18 -17
  464. package/dist/daemon/delivery/run-delivery.d.ts +1 -1
  465. package/dist/daemon/delivery/run-delivery.js +1 -1
  466. package/dist/daemon/due-sources.d.ts +1 -1
  467. package/dist/daemon/due-sources.js +1 -1
  468. package/dist/daemon/due.js +7 -6
  469. package/dist/daemon/fleet.d.ts +6 -6
  470. package/dist/daemon/fleet.js +9 -9
  471. package/dist/daemon/park-activity.d.ts +2 -1
  472. package/dist/daemon/park-activity.js +1 -1
  473. package/dist/daemon/park-pending.d.ts +20 -13
  474. package/dist/daemon/park-pending.js +3 -1
  475. package/dist/daemon/profile-delete.js +1 -1
  476. package/dist/daemon/reconcilers/bash-deadline.d.ts +15 -10
  477. package/dist/daemon/reconcilers/bash-deadline.js +1 -3
  478. package/dist/daemon/reconcilers/broker-supervision.d.ts +13 -3
  479. package/dist/daemon/reconcilers/broker-supervision.js +4 -4
  480. package/dist/daemon/reconcilers/cron-lane.d.ts +0 -1
  481. package/dist/daemon/reconcilers/cron-lane.js +1 -1
  482. package/dist/daemon/reconcilers/fault-retry-backstop.d.ts +2 -9
  483. package/dist/daemon/reconcilers/fault-retry-backstop.js +1 -1
  484. package/dist/daemon/reconcilers/grant-effects.d.ts +8 -0
  485. package/dist/daemon/reconcilers/grant-effects.js +1 -0
  486. package/dist/daemon/reconcilers/grove-instance-sweep.d.ts +1 -3
  487. package/dist/daemon/reconcilers/grove-instance-sweep.js +1 -1
  488. package/dist/daemon/reconcilers/ledger-r2.d.ts +27 -0
  489. package/dist/daemon/reconcilers/ledger-r2.js +7 -0
  490. package/dist/daemon/reconcilers/ledger-sandbox.d.ts +55 -0
  491. package/dist/daemon/reconcilers/ledger-sandbox.js +9 -0
  492. package/dist/daemon/reconcilers/ledger-storage.d.ts +22 -0
  493. package/dist/daemon/reconcilers/ledger-storage.js +5 -0
  494. package/dist/daemon/reconcilers/live-obligation.d.ts +3 -2
  495. package/dist/daemon/reconcilers/live-obligation.js +1 -1
  496. package/dist/daemon/reconcilers/managed-worktree-sweep.d.ts +0 -5
  497. package/dist/daemon/reconcilers/managed-worktree-sweep.js +1 -1
  498. package/dist/daemon/reconcilers/node-deadline.d.ts +2 -1
  499. package/dist/daemon/reconcilers/node-deadline.js +1 -1
  500. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +1 -2
  501. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +1 -1
  502. package/dist/daemon/reconcilers/node-lifecycle/remote-wake.d.ts +31 -0
  503. package/dist/daemon/reconcilers/node-lifecycle/remote-wake.js +1 -0
  504. package/dist/daemon/reconcilers/node-lifecycle/requested-stops.d.ts +5 -0
  505. package/dist/daemon/reconcilers/node-lifecycle/requested-stops.js +2 -0
  506. package/dist/daemon/reconcilers/node-lifecycle/tick.js +1 -1
  507. package/dist/daemon/reconcilers/pending-review-submit.js +1 -1
  508. package/dist/daemon/reconcilers/recipe-image.d.ts +49 -0
  509. package/dist/daemon/reconcilers/recipe-image.js +7 -0
  510. package/dist/daemon/reconcilers/row-claim.d.ts +19 -0
  511. package/dist/daemon/reconcilers/row-claim.js +2 -0
  512. package/dist/daemon/reconcilers/storage-due-work.d.ts +18 -3
  513. package/dist/daemon/reconcilers/storage-due-work.js +2 -1
  514. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -13
  515. package/dist/daemon/reconcilers/storage-maintenance.js +1 -1
  516. package/dist/daemon/reconcilers/structured-request-lane.js +1 -1
  517. package/dist/daemon/reconcilers/throttled-lane.d.ts +11 -0
  518. package/dist/daemon/reconcilers/throttled-lane.js +1 -0
  519. package/dist/daemon/review/finish.js +1 -1
  520. package/dist/daemon/run-notices.d.ts +3 -0
  521. package/dist/daemon/run-notices.js +1 -1
  522. package/dist/daemon/runs/append-section.d.ts +4 -1
  523. package/dist/daemon/runs/append-section.js +2 -2
  524. package/dist/daemon/runs/observer.d.ts +3 -2
  525. package/dist/daemon/runs/observer.js +3 -2
  526. package/dist/daemon/runs/view-transport.d.ts +19 -0
  527. package/dist/daemon/runs/view-transport.js +1 -0
  528. package/dist/daemon/structured-requests/boot.d.ts +2 -1
  529. package/dist/daemon/structured-requests/boot.js +1 -1
  530. package/dist/daemon/structured-requests/effects.d.ts +4 -3
  531. package/dist/daemon/structured-requests/effects.js +1 -1
  532. package/dist/daemon/structured-requests/notifier.d.ts +2 -0
  533. package/dist/daemon/structured-requests/notifier.js +1 -1
  534. package/dist/daemon/structured-requests/sidecar.d.ts +4 -3
  535. package/dist/daemon/structured-requests/sidecar.js +1 -1
  536. package/dist/launcher/validate.js +1 -1
  537. package/dist/migrations/004-canvas-documents/index.js +2 -2
  538. package/dist/pi-extensions/broker-local.d.ts +3 -3
  539. package/dist/pi-extensions/broker-local.js +1 -2
  540. package/dist/pi-extensions/canvas-bash-valve.js +6 -6
  541. package/dist/pi-extensions/canvas-context-intro.js +2 -2
  542. package/dist/pi-extensions/canvas-elapsed.js +2 -2
  543. package/dist/pi-extensions/canvas-goal-capture.js +1 -1
  544. package/dist/pi-extensions/canvas-inbox-watcher.js +2 -2
  545. package/dist/pi-extensions/canvas-review-boundary.d.ts +11 -1
  546. package/dist/pi-extensions/canvas-review-boundary.js +3 -3
  547. package/dist/pi-extensions/canvas-stophook.js +1 -1
  548. package/dist/pi-extensions/canvas-structured-output.d.ts +5 -3
  549. package/dist/pi-extensions/canvas-structured-output.js +6 -11
  550. package/dist/pi-extensions/canvas-tool-guide.d.ts +9 -2
  551. package/dist/pi-extensions/canvas-tool-guide.js +6 -6
  552. package/dist/shared/inbox-entry-body.js +7 -7
  553. package/dist/types.d.ts +12 -0
  554. package/docs/sdk/release-notes.md +4 -0
  555. package/package.json +7 -4
  556. package/packages/crouter-identity/package.json +1 -1
  557. package/runtime.lock.json +13 -13
  558. package/scripts/git-credential-crtr +33 -0
  559. package/scripts/sandbox-bootstrap.sh +54 -0
  560. package/scripts/sandbox-start.mjs +320 -0
  561. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +0 -78
  562. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +0 -1179
  563. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +0 -1406
  564. package/dist/core/conversation-store/file-store.d.ts +0 -37
  565. package/dist/core/conversation-store/file-store.js +0 -9
  566. package/dist/core/conversation-store/listener.d.ts +0 -4
  567. package/dist/core/conversation-store/listener.js +0 -8
  568. package/dist/core/scoped-state/profiles.d.ts +0 -27
  569. package/dist/core/scoped-state/profiles.js +0 -1
  570. package/dist/core/scoped-state/providers.d.ts +0 -24
  571. package/dist/core/scoped-state/providers.js +0 -1
  572. package/dist/core/scoped-state/settings.d.ts +0 -28
  573. package/dist/core/scoped-state/settings.js +0 -1
@@ -1,1179 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { createAssistantMessageEventStream, } from '@earendil-works/pi-ai';
3
- import { anthropicMessagesApi, openAICodexResponsesApi, openAIResponsesApi } from '@earendil-works/pi-ai/compat';
4
- import { ANTHROPIC_PROVIDER_ID, OPENAI_CHATGPT_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID, openAIDeviceId, MAX_RATE_LIMIT_COOLDOWN_MS, coolingUntilFor, formatCooldowns, formatStatusLine, getProviderLabel, isManagedProvider, isInvalidRefreshTokenError, addSubscription, enrichManagedLoginIdentity, markSubscriptionRateLimited, markSubscriptionSuccess, retryAfterMsFromHeaders, retryAfterMsFromError, promoteSubscription, readRotationConfig, readSubscriptionPool, refreshSubscriptionCredential, removeManagedAccount, resolveProviderCandidates, upsertSubscriptionWithUniqueAccount, subscriptionNeedsSuccessWrite, } from '../../../core/subscription-state.js';
5
- import { classifyRateLimit, describeRateLimitScope, rateLimitHeaders } from '../../../core/rate-limit-scope.js';
6
- import { readMergedLaunchConfig } from '../../../core/config.js';
7
- import { expandModelCandidates, modelRequestFromConfig, probeRouteAvailability } from '../../../core/model-routes.js';
8
- import { envModelExact, envNodeId } from '../../../shared/env.js';
9
- import { ManagedProviderCoolingError } from '../../../core/runtime/managed-provider-cooling.js';
10
- import { recordBrokerTurn } from '../../../core/runtime/broker/daemon-ops.js';
11
- import { recognizeProviderError } from '../../../core/provider-error-recognition.js';
12
- import { normalizeProviderFailure, providerFailureDiagnostic } from '../../../core/provider-failure.js';
13
- // A response without Retry-After proves only that THIS attempt was throttled; it
14
- // does not authorize crouter to invent a five-minute provider-wide outage. Keep
15
- // a short turn-local probe backoff to avoid an immediate retry loop, then let a
16
- // fresh turn prove whether the credential is usable again. Real
17
- // server-provided Retry-After deadlines are still honored exactly.
18
- const DEFAULT_RATE_LIMIT_BACKOFF_MS = 15 * 1000;
19
- // Short in-place retry budget for a transient network/5xx blip on the same credential.
20
- // Once this budget is exhausted, surface the error and rotate to another candidate.
21
- const TRANSIENT_RETRY_BACKOFFS_MS = [250, 750];
22
- // After the in-place transient retry budget is exhausted, rotate instead of failing: the
23
- // credential itself is probably healthy (this is a provider-side blip like "overloaded"),
24
- // so cool it down only briefly -- long enough to rotate to another credential / fallback
25
- // provider, short enough that it comes back quickly if it was the only option.
26
- const TRANSIENT_ROTATE_COOLDOWN_MS = 60 * 1000;
27
- // A 401/403 before any content means the provider refused this credential (a lapsed
28
- // plan, a revoked server-side key). That is not a transient blip, but it is not proof
29
- // the account is gone either: cool the account for 30 minutes and rotate, rather than
30
- // the 30-day invalid_grant backoff. Exact requests keep the auth error.
31
- const AUTH_REJECTED_COOLDOWN_MS = 30 * 60 * 1000;
32
- // A few short outer retries absorb a normal multi-broker collision; if the metadata file
33
- // remains busy, the provider call still proceeds. Subscription bookkeeping must never turn a
34
- // healthy model response into a fatal generation fault.
35
- const SUBSCRIPTION_LOCK_RETRY_BACKOFFS_MS = [25, 75, 150];
36
- const { streamSimple: streamAnthropicMessages } = anthropicMessagesApi();
37
- const { streamSimple: streamOpenAICodexResponses } = openAICodexResponsesApi();
38
- const { streamSimple: streamOpenAIResponses } = openAIResponsesApi();
39
- function createProviderRotationState(options) {
40
- return { delegatedProviderStreams: new Map(), transportOnlyExhaustedPasses: 0, ...(options === undefined ? {} : { options: { ...options } }) };
41
- }
42
- function configuredValue(state, key, envName) {
43
- return state.options === undefined ? process.env[envName] : state.options[key];
44
- }
45
- function isExactModel(state) {
46
- return state.options === undefined && envModelExact();
47
- }
48
- function classifyProviderSignal(signal, policy) {
49
- const hadRetryAfterHeader = signal.retryAfterMs !== undefined;
50
- if (signal.kind === 'rate-limit' && signal.matchedOn === 'status') {
51
- return { kind: 'rate_limit', status: signal.status, reason: 'http 429', hadRetryAfterHeader };
52
- }
53
- if (signal.kind === 'transient-http') {
54
- if (signal.status === 503 && hadRetryAfterHeader) {
55
- return { kind: 'rate_limit', status: signal.status, reason: 'http 503 with retry-after', hadRetryAfterHeader };
56
- }
57
- return { kind: 'transient', status: signal.status, reason: `http ${signal.status}`, hadRetryAfterHeader };
58
- }
59
- const textMatches = signal.textMatches ?? (signal.matchedOn === 'text' && signal.match ? [{ kind: signal.kind, match: signal.match }] : []);
60
- if (policy.rotateOnAuth && signal.kind === 'auth' && signal.matchedOn === 'status' && !textMatches.some(({ kind }) => kind === 'rate-limit')) {
61
- return { kind: 'rate_limit', status: signal.status, reason: `http ${signal.status}`, hadRetryAfterHeader, cooldownMs: AUTH_REJECTED_COOLDOWN_MS };
62
- }
63
- const transientMatch = textMatches.find(({ kind }) => kind === 'connection' || kind === 'socket' || kind === 'service-unavailable' || kind === 'overloaded');
64
- if (transientMatch) {
65
- return { kind: 'transient', status: signal.status, reason: transientMatch.match.toLowerCase(), hadRetryAfterHeader };
66
- }
67
- const rateLimitMatch = textMatches.find(({ kind }) => kind === 'rate-limit');
68
- if (rateLimitMatch) {
69
- return { kind: 'rate_limit', status: signal.status, reason: rateLimitMatch.match.toLowerCase(), hadRetryAfterHeader };
70
- }
71
- return { kind: 'fatal', status: signal.status, reason: 'unclassified', hadRetryAfterHeader };
72
- }
73
- function classifyProviderFailure(failure, retryAfterMs, policy) {
74
- const nested = failure.causes ?? [];
75
- const code = failure.code ?? nested.find((cause) => cause.code !== undefined)?.code;
76
- const errno = failure.errno ?? nested.find((cause) => cause.errno !== undefined)?.errno;
77
- const message = [failure.message, failure.body, ...nested.flatMap((cause) => [cause.message, cause.body])]
78
- .filter((part) => typeof part === 'string' && part.length > 0)
79
- .join(' ');
80
- return classifyProviderSignal(recognizeProviderError({
81
- status: failure.status,
82
- retryAfterMs,
83
- code,
84
- errno,
85
- message,
86
- }), policy);
87
- }
88
- /** Test seam for the policy mapping over the shared recognition signal. */
89
- export function __classifyProviderErrorForTest(error, observed = {}, policy = { rotateOnAuth: false }) {
90
- return classifyProviderFailure(normalizeProviderFailure(error, { status: observed.status }), observed.retryAfterMs, policy);
91
- }
92
- function providerFailureAssistantMessage(model, failure) {
93
- const message = toAssistantError(model, failure.message);
94
- message.diagnostics = [providerFailureDiagnostic(failure)];
95
- return message;
96
- }
97
- async function defaultSleep(ms) {
98
- await new Promise((resolve) => setTimeout(resolve, ms));
99
- }
100
- // Indirection so tests can inject deterministic waits without real timers.
101
- let activeSleep = defaultSleep;
102
- /** Test-only seam: override the sleep implementation used for in-place/wait-out retries. Pass undefined to restore the default. */
103
- export function __setSleepForTest(fn) {
104
- activeSleep = fn ?? defaultSleep;
105
- }
106
- // After this many CONSECUTIVE route-queue passes in which every failure was
107
- // transport-level (no HTTP status and no Retry-After header — the provider was
108
- // never reached at all), the broker process itself is presumed poisoned: a
109
- // corrupted large TLS upload can leave Node/undici's HTTP/2 session permanently
110
- // destroyed, after which EVERY provider request from this process fails while
111
- // curl and fresh processes on the same host succeed. There is no clean in-process
112
- // heal (the dispatcher cannot be safely re-instantiated), so the recovery is a
113
- // fresh process: self-terminate via SIGTERM and let the daemon's exit policy
114
- // respawn this broker against the same session. A genuine full network outage
115
- // trips this too, and a fresh process cannot heal that one — so the daemon's
116
- // respawn path probes network reachability before it counts a relaunch, and an
117
- // offline box parks the node until the network returns instead of spending its
118
- // respawn budget (daemon/fleet.ts, `deferRespawnWhileOffline`).
119
- export const POISONED_TRANSPORT_PASS_LIMIT = 2;
120
- async function defaultTerminatePoisonedProcess(tried) {
121
- // Only the broker process itself may self-terminate: runBroker stamps
122
- // CRTR_BROKER_PID with its own pid at boot. Any other host (tests, tooling)
123
- // keeps the ordinary terminal-error path.
124
- if (process.env["CRTR_BROKER_PID"] !== String(process.pid))
125
- return false;
126
- const routes = tried.split(', ').filter((route) => route !== '');
127
- const nodeId = envNodeId();
128
- if (nodeId === undefined || routes.length === 0) {
129
- throw new Error('poisoned broker is missing CRTR_NODE_ID or attempted routes');
130
- }
131
- await recordBrokerTurn(nodeId, {
132
- event: 'exit-diagnostic',
133
- exit_diagnostic: {
134
- schema: 'crtr.execution-exit-diagnostic/v1',
135
- transport_poison: { consecutive_passes: POISONED_TRANSPORT_PASS_LIMIT, routes },
136
- },
137
- });
138
- logRotation(`process transport poisoned: ${POISONED_TRANSPORT_PASS_LIMIT} consecutive route-queue passes failed with no HTTP response (tried ${tried}); exiting so the daemon respawns this broker fresh`);
139
- process.kill(process.pid, "SIGTERM");
140
- return true;
141
- }
142
- let activeTerminatePoisonedProcess = defaultTerminatePoisonedProcess;
143
- /** Test-only seam: override poisoned-process self-termination. Pass undefined to restore the default. */
144
- export function __setTerminatePoisonedProcessForTest(fn) {
145
- activeTerminatePoisonedProcess = fn ?? defaultTerminatePoisonedProcess;
146
- }
147
- function isSubscriptionLockContention(error) {
148
- const value = error;
149
- return value?.code === "ELOCKED" || (typeof value?.message === "string" && /lock file is already being held/i.test(value.message));
150
- }
151
- async function recordSubscriptionMutation(description, mutate) {
152
- for (let attempt = 0;; attempt++) {
153
- try {
154
- await mutate();
155
- return true;
156
- }
157
- catch (error) {
158
- if (!isSubscriptionLockContention(error))
159
- throw error;
160
- const backoffMs = SUBSCRIPTION_LOCK_RETRY_BACKOFFS_MS[attempt];
161
- if (backoffMs === undefined) {
162
- logRotation(`subscription metadata lock remained busy while ${description}; continuing without failing the provider turn`);
163
- return false;
164
- }
165
- await activeSleep(backoffMs);
166
- }
167
- }
168
- }
169
- /** Test-only seam for the bounded, non-fatal subscription bookkeeping path. */
170
- export async function __recordSubscriptionMutationForTest(description, mutate) {
171
- return recordSubscriptionMutation(description, mutate);
172
- }
173
- // A user/host abort (ESC, a critical-tier interrupt, a torn-down window) is NOT a provider
174
- // failure — it must never cool down a credential or rotate providers. The upstream pi-ai
175
- // providers set `reason: "aborted"` / `stopReason: "aborted"` (and throw "Request was aborted")
176
- // ONLY when the request's AbortSignal fired, so those markers alone are a reliable abort
177
- // signal; we do NOT require the signal to be threaded back through our own options (it may not
178
- // be), which is what made an ESC drain both provider pools.
179
- function isUserAbortEvent(event, signal) {
180
- if (signal?.aborted)
181
- return true;
182
- if (event.type !== "error")
183
- return false;
184
- return event.reason === "aborted" || event.error.stopReason === "aborted" || /request was aborted/i.test(event.error.errorMessage ?? "");
185
- }
186
- function isUserAbortError(error, signal) {
187
- if (signal?.aborted)
188
- return true;
189
- const value = error;
190
- const message = [value?.errorMessage, value?.message, error instanceof Error ? error.message : String(error ?? "")]
191
- .filter((part) => typeof part === "string" && part.length > 0)
192
- .join(" ");
193
- return (error instanceof Error && /abort/i.test(error.name)) || /request was aborted|operation was aborted|the user aborted/i.test(message);
194
- }
195
- function setStatus(state, text) {
196
- state.runtimeContext?.ui.setStatus("provider-rotation", text);
197
- }
198
- function logRotation(message) {
199
- process.stderr.write(`[provider-rotation] ${message}\n`);
200
- }
201
- function getCurrentModelRef(model) {
202
- return isManagedProvider(model?.provider) ? { providerId: model.provider, modelId: model.id } : undefined;
203
- }
204
- function toAssistantError(model, errorMessage) {
205
- return {
206
- role: "assistant",
207
- content: [],
208
- api: model.api,
209
- provider: model.provider,
210
- model: model.id,
211
- usage: {
212
- input: 0,
213
- output: 0,
214
- cacheRead: 0,
215
- cacheWrite: 0,
216
- totalTokens: 0,
217
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
218
- },
219
- stopReason: "error",
220
- errorMessage,
221
- timestamp: Date.now(),
222
- };
223
- }
224
- function loginClientId(cred) {
225
- return typeof cred.clientId === "string" && cred.clientId.trim() ? cred.clientId.trim() : undefined;
226
- }
227
- // Extract an account identity supplied directly by the OAuth credential.
228
- function loginAccountId(cred) {
229
- return typeof cred.accountId === "string" && cred.accountId.trim() ? cred.accountId.trim() : undefined;
230
- }
231
- // Short, display-only rendering of a Codex account id for `/provider-sub list`, so distinct
232
- // accounts are observable without dumping the full opaque id.
233
- function shortAccountId(accountId) {
234
- return accountId.length > 12 ? `${accountId.slice(0, 8)}\u2026${accountId.slice(-4)}` : accountId;
235
- }
236
- // The registered provider exposes its OAuth flow through `provider.auth.oauth`. Composed managed
237
- // providers retain that flow, so the session registry supplies the Anthropic and Codex flows.
238
- function oauthFlow(providerId, ctx) {
239
- const oauth = ctx?.modelRegistry.getProvider(providerId)?.auth.oauth;
240
- if (!oauth)
241
- throw new Error(`${getProviderLabel(providerId)} has no OAuth flow registered`);
242
- return oauth;
243
- }
244
- // `refresh` is a pure token exchange that reads only `credential.refresh` and persists
245
- // nothing; crouter records the rotated token in the selected pool row.
246
- async function defaultRefreshForProvider(state, providerId, refreshToken, parentSignal, clientId) {
247
- const localController = parentSignal === undefined ? new AbortController() : undefined;
248
- const signal = parentSignal ?? localController.signal;
249
- try {
250
- const refreshed = await oauthFlow(providerId, state.runtimeContext).refresh({ type: "oauth", refresh: refreshToken, access: "", expires: 0, ...(clientId ? { clientId } : {}) }, signal);
251
- const nextClientId = loginClientId(refreshed);
252
- return { refresh: refreshed.refresh, access: refreshed.access, expires: refreshed.expires, ...(nextClientId ? { clientId: nextClientId } : {}) };
253
- }
254
- finally {
255
- localController?.abort();
256
- }
257
- }
258
- // Indirection so tests can inject refresh failures without real network calls.
259
- let activeRefreshForProvider;
260
- /** Test-only seam: override the per-provider refresh function. Pass undefined to restore the default. */
261
- export function __setRefreshForProviderForTest(fn) {
262
- activeRefreshForProvider = fn;
263
- }
264
- function openBrowser(url) {
265
- const [cmd, args] = process.platform === "darwin"
266
- ? ["open", [url]]
267
- : process.platform === "win32"
268
- ? ["cmd", ["/c", "start", "", url]]
269
- : ["xdg-open", [url]];
270
- try {
271
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
272
- child.on("error", () => {
273
- /* best effort -- the manual-code dialog's title carries the URL too */
274
- });
275
- child.unref();
276
- }
277
- catch {
278
- /* best effort */
279
- }
280
- }
281
- // Every account refreshes the same way: the pool holds its value, so an expired token is a
282
- // token exchange serialized on the pool file's lock. A still-fresh access token takes the
283
- // side-effect-free fast path and never joins the lock. We do NOT route through pi's public
284
- // `getApiKey`, which wraps pi-ai's `getOAuthApiKey` and collapses a refresh failure into a
285
- // generic message; calling `activeRefreshForProvider` directly keeps the real error intact so
286
- // `isInvalidRefreshTokenError` can classify it (cooldown -> reauth -> cross-provider-fallback).
287
- async function refreshCredentialIfNeeded(state, providerId, credential, signal) {
288
- if (!credential.refresh || !credential.access || credential.expires === undefined) {
289
- throw new Error(`${getProviderLabel(providerId)} subscription "${credential.label}" is missing its credential value`);
290
- }
291
- if (credential.expires > Date.now())
292
- return credential;
293
- const refreshed = await refreshSubscriptionCredential(providerId, credential.label, (token, clientId) => activeRefreshForProvider?.(providerId, token, signal, clientId) ?? defaultRefreshForProvider(state, providerId, token, signal, clientId));
294
- return { ...credential, ...refreshed };
295
- }
296
- /** Test-only seam: exercises the real `refreshCredentialIfNeeded` (including its locked refresh
297
- * transaction) without duplicating its logic. */
298
- export async function __refreshCredentialIfNeededForTest(providerId, credential, signal) {
299
- return refreshCredentialIfNeeded(createProviderRotationState(), providerId, credential, signal);
300
- }
301
- async function defaultLoginForProvider(providerId, ctx) {
302
- return loginProvider(providerId, ctx);
303
- }
304
- let activeLoginForProvider = defaultLoginForProvider;
305
- /** Test-only seam: override the per-provider login function. Pass undefined to restore the default. */
306
- export function __setLoginForProviderForTest(fn) {
307
- activeLoginForProvider = fn ?? defaultLoginForProvider;
308
- }
309
- async function reauthenticateCredential(state, providerId, credential) {
310
- const ctx = state.runtimeContext;
311
- if (!ctx?.hasUI)
312
- return undefined;
313
- ctx.ui.notify(`${getProviderLabel(providerId)} subscription "${credential.label}" needs re-authentication. Opening OAuth login...`, "warning");
314
- const authenticated = await activeLoginForProvider(providerId, ctx);
315
- const identity = await enrichManagedLoginIdentity(providerId, authenticated);
316
- // A failed Anthropic profile lookup leaves identity unknown. Do not carry the old row's identity
317
- // onto newly issued tokens: it could describe a different account and bypass that account's
318
- // duplicate check. Codex supplies its identity directly, so it can retain its existing claim
319
- // only when that provider's login result omits one.
320
- const reauthedAccountId = identity.accountId ?? (providerId === ANTHROPIC_PROVIDER_ID ? undefined : loginAccountId(authenticated) ?? credential.accountId);
321
- const { authFailure: _authFailure, clientId: _oldClientId, ...healthyCredential } = credential;
322
- const reauthedClientId = loginClientId(authenticated);
323
- const next = {
324
- ...healthyCredential,
325
- refresh: authenticated.refresh,
326
- access: authenticated.access,
327
- expires: authenticated.expires,
328
- ...(reauthedAccountId ? { accountId: reauthedAccountId } : {}),
329
- ...(reauthedClientId ? { clientId: reauthedClientId } : {}),
330
- rateLimitedUntil: 0,
331
- lastAttemptAt: Date.now(),
332
- lastRateLimitedAt: credential.lastRateLimitedAt,
333
- };
334
- // Identity-checked upsert: reauthenticating this explicit slot into an account already held by
335
- // ANOTHER slot throws DuplicateSubscriptionError rather than creating a duplicate quota entry.
336
- return ((await upsertSubscriptionWithUniqueAccount(providerId, next)).find((entry) => entry.label === next.label) ?? next);
337
- }
338
- function defaultStreamForProvider(model, context, options) {
339
- if (model.provider === ANTHROPIC_PROVIDER_ID) {
340
- return streamAnthropicMessages(model, context, options);
341
- }
342
- if (model.provider === OPENAI_CHATGPT_PROVIDER_ID) {
343
- return streamOpenAIResponses(model, context, options);
344
- }
345
- return streamOpenAICodexResponses(model, context, options);
346
- }
347
- // Indirection so tests can inject a fake per-provider stream without real network calls.
348
- let activeStreamForProvider = defaultStreamForProvider;
349
- function streamForProvider(model, context, options) {
350
- return activeStreamForProvider(model, context, options);
351
- }
352
- const ANTHROPIC_SERVER_SIDE_FALLBACK_BETA = "server-side-fallback-2026-07-01";
353
- const ANTHROPIC_SERVER_SIDE_FALLBACK_PRIMARY_MODELS = new Set(["claude-fable-5", "claude-opus-5"]);
354
- function supportsAnthropicServerSideFallback(model) {
355
- return ANTHROPIC_SERVER_SIDE_FALLBACK_PRIMARY_MODELS.has(model.id);
356
- }
357
- function includesAnthropicBetaToken(value) {
358
- return value.split(",").some((token) => token.trim() === ANTHROPIC_SERVER_SIDE_FALLBACK_BETA);
359
- }
360
- function appendAnthropicFallbackBeta(headers) {
361
- if (headers instanceof Headers) {
362
- const next = new Headers(headers);
363
- const current = next.get("anthropic-beta");
364
- if (current === null || !includesAnthropicBetaToken(current)) {
365
- next.set("anthropic-beta", current === null ? ANTHROPIC_SERVER_SIDE_FALLBACK_BETA : `${current},${ANTHROPIC_SERVER_SIDE_FALLBACK_BETA}`);
366
- }
367
- return next;
368
- }
369
- if (Array.isArray(headers)) {
370
- const next = headers.map(([name, value]) => [name, value]);
371
- const index = next.findIndex(([name]) => name.toLowerCase() === "anthropic-beta");
372
- if (index === -1)
373
- next.push(["anthropic-beta", ANTHROPIC_SERVER_SIDE_FALLBACK_BETA]);
374
- else if (!includesAnthropicBetaToken(next[index][1]))
375
- next[index][1] = `${next[index][1]},${ANTHROPIC_SERVER_SIDE_FALLBACK_BETA}`;
376
- return next;
377
- }
378
- const next = { ...(headers ?? {}) };
379
- const headerName = Object.keys(next).find((name) => name.toLowerCase() === "anthropic-beta");
380
- if (headerName === undefined)
381
- next["anthropic-beta"] = ANTHROPIC_SERVER_SIDE_FALLBACK_BETA;
382
- else if (!includesAnthropicBetaToken(next[headerName]))
383
- next[headerName] = `${next[headerName]},${ANTHROPIC_SERVER_SIDE_FALLBACK_BETA}`;
384
- return next;
385
- }
386
- function withAnthropicFallbackBeta(fetch) {
387
- return (input, init) => {
388
- const headers = init?.headers ?? (input instanceof Request ? input.headers : undefined);
389
- return fetch(input, { ...init, headers: appendAnthropicFallbackBeta(headers) });
390
- };
391
- }
392
- /** Test-only seam: override the per-provider stream factory. Pass undefined to restore the default. */
393
- export function __setStreamForProviderForTest(fn) {
394
- activeStreamForProvider = fn ?? defaultStreamForProvider;
395
- }
396
- /** Test-only seam: exercise `fallbackRejectionHint` for one rejection discriminant
397
- * (with an optional resolved-but-rejected candidate) without staging a full turn. */
398
- // Drives one credential, retrying transient (non-rate-limit) failures in place on the
399
- // same credential and reporting a classified result for rotation logging.
400
- // A genuine rate limit ends the attempt immediately (the caller cools the credential down
401
- // and rotates); a transient blip gets a short in-place retry budget before giving up.
402
- async function streamProviderAttempt(state, providerId, model, context, options, credential, outer) {
403
- const policy = { rotateOnAuth: !isExactModel(state) };
404
- for (let attempt = 1;; attempt++) {
405
- const attemptAt = Date.now();
406
- let observedStatus;
407
- let requestedRetryAfterMs;
408
- let limitHeaders;
409
- const useAnthropicServerSideFallback = credential !== undefined
410
- && providerId === ANTHROPIC_PROVIDER_ID
411
- && supportsAnthropicServerSideFallback(model);
412
- const streamOptions = {
413
- ...options,
414
- ...(credential ? { apiKey: credential.access } : {}),
415
- ...(useAnthropicServerSideFallback
416
- ? {
417
- onPayload: async (payload, payloadModel) => {
418
- const callerPayload = await options?.onPayload?.(payload, payloadModel);
419
- return { ...(callerPayload === undefined ? payload : callerPayload), fallbacks: "default" };
420
- },
421
- // `headers` would overwrite pi-ai's OAuth beta list. Compose at fetch time so the
422
- // SDK's generated headers stay intact; replace this shim with pi-ai's `fallbacks`
423
- // option when that upstream option ships.
424
- fetch: withAnthropicFallbackBeta(options?.fetch ?? globalThis.fetch),
425
- }
426
- : {}),
427
- // Codex fast mode: the ChatGPT backend serves the turn at its priority
428
- // tier. The wire value is `priority` (`fast` is rejected 400), and the
429
- // response always echoes `service_tier: "default"` regardless — only
430
- // generation timing shows whether it took effect.
431
- ...(providerId === OPENAI_CODEX_PROVIDER_ID && state.providerOptions?.[OPENAI_CODEX_PROVIDER_ID]?.fastMode === true
432
- ? {
433
- onPayload: async (payload, payloadModel) => {
434
- const callerPayload = await options?.onPayload?.(payload, payloadModel);
435
- return { ...(callerPayload === undefined ? payload : callerPayload), service_tier: "priority" };
436
- },
437
- }
438
- : {}),
439
- onResponse: async (response, responseModel) => {
440
- observedStatus = response.status;
441
- if (response.status === 429 || response.status === 503) {
442
- requestedRetryAfterMs = retryAfterMsFromHeaders(response.headers);
443
- limitHeaders = rateLimitHeaders(response.headers);
444
- }
445
- await options?.onResponse?.(response, responseModel);
446
- },
447
- };
448
- const provider = state.runtimeContext?.modelRegistry.getProvider?.(model.provider);
449
- const inner = isManagedProvider(providerId)
450
- ? streamForProvider(model, context, streamOptions)
451
- : state.delegatedProviderStreams.get(providerId)?.(model, context, streamOptions)
452
- ?? provider?.streamSimple(model, context, streamOptions)
453
- ?? streamForProvider(model, context, streamOptions);
454
- const bufferedEvents = [];
455
- let sawContent = false;
456
- let committed = false;
457
- let classification;
458
- let failure;
459
- let failureMessage;
460
- // Events are buffered ONLY until this attempt commits to the provider -- i.e. until the
461
- // first real content event (thinking/text/toolcall) arrives, after which rotating away is
462
- // no longer possible. Before commit the buffer lets a clean pre-content rate-limit rotate
463
- // to another credential with nothing yet shown. Once committed we flush and forward every
464
- // subsequent event LIVE, so viewers stream token-by-token instead of receiving the whole
465
- // message at once when the turn finally completes.
466
- const emit = (event) => {
467
- if (committed)
468
- outer.push(event);
469
- else
470
- bufferedEvents.push(event);
471
- };
472
- const flush = () => {
473
- for (const event of bufferedEvents)
474
- outer.push(event);
475
- bufferedEvents.length = 0;
476
- committed = true;
477
- };
478
- try {
479
- for await (const event of inner) {
480
- if (event.type === "error") {
481
- if (isUserAbortEvent(event, options?.signal)) {
482
- emit(event);
483
- flush();
484
- return { result: "fatal", attemptAt };
485
- }
486
- failure = normalizeProviderFailure(event.error, { status: observedStatus });
487
- failureMessage = failure.message;
488
- classification = classifyProviderFailure(failure, requestedRetryAfterMs, policy);
489
- if (classification.kind === "fatal" || sawContent) {
490
- emit({ ...event, error: providerFailureAssistantMessage(model, failure) });
491
- flush();
492
- return { result: "fatal", attemptAt, classification, failure };
493
- }
494
- break;
495
- }
496
- if (event.type !== "start" && event.type !== "done")
497
- sawContent = true;
498
- emit(event);
499
- // First content event: commit to this provider -- flush the buffer and switch to live
500
- // forwarding so the rest of the turn streams incrementally.
501
- if (sawContent && !committed)
502
- flush();
503
- }
504
- }
505
- catch (error) {
506
- if (isUserAbortError(error, options?.signal)) {
507
- emit({
508
- type: "error",
509
- reason: "aborted",
510
- error: { ...toAssistantError(model, error instanceof Error ? error.message : String(error)), stopReason: "aborted" },
511
- });
512
- flush();
513
- return { result: "fatal", attemptAt };
514
- }
515
- requestedRetryAfterMs ??= retryAfterMsFromError(error);
516
- if (limitHeaders === undefined && error && typeof error === "object" && "headers" in error && error.headers && typeof error.headers === "object") {
517
- const fromError = rateLimitHeaders(error.headers);
518
- if (Object.keys(fromError).length)
519
- limitHeaders = fromError;
520
- }
521
- failure = normalizeProviderFailure(error, { status: observedStatus });
522
- failureMessage = failure.message;
523
- classification = classifyProviderFailure(failure, requestedRetryAfterMs, policy);
524
- if (classification.kind === "fatal" || sawContent) {
525
- emit({
526
- type: "error",
527
- reason: "error",
528
- error: providerFailureAssistantMessage(model, failure),
529
- });
530
- flush();
531
- return { result: "fatal", attemptAt, classification, failure };
532
- }
533
- }
534
- if (!classification && requestedRetryAfterMs !== undefined && !sawContent) {
535
- // Some providers signal a 429/503 purely via the response header without ever raising
536
- // a stream "error" event; treat that the same as an explicit rate-limit classification.
537
- classification = { kind: "rate_limit", status: observedStatus, reason: "retry-after header on success path", hadRetryAfterHeader: true };
538
- }
539
- if (!classification) {
540
- flush();
541
- return { result: "success", attemptAt };
542
- }
543
- if (classification.kind === "rate_limit") {
544
- // Only a genuine 429 is classified for scope; a 401/403 or 503 cooldown stays account-wide.
545
- const rateLimit = classification.status === 429 || (classification.status === undefined && classification.cooldownMs === undefined)
546
- ? classifyRateLimit(limitHeaders, model.id)
547
- : undefined;
548
- let retryAfterMs = classification.cooldownMs ?? requestedRetryAfterMs;
549
- // No Retry-After but a rejected window's reset: wait for the window, not the default backoff.
550
- if (retryAfterMs === undefined && rateLimit?.resetAt !== undefined)
551
- retryAfterMs = Math.max(0, rateLimit.resetAt - Date.now());
552
- if (rateLimit && Object.keys(rateLimit.headers).length) {
553
- logRotation(`${credential ? `${getProviderLabel(providerId)} subscription "${credential.label}"` : providerId} 429 on ${model.id}: scope=${describeRateLimitScope(rateLimit.scope)} headers=${JSON.stringify(rateLimit.headers)}`);
554
- }
555
- return { result: "rate_limited", retryAfterMs, attemptAt, classification, failureMessage, ...(rateLimit ? { rateLimit } : {}) };
556
- }
557
- // Transient: retry the SAME credential/provider in place first -- no cooldown for the
558
- // quick in-place budget.
559
- const backoffMs = TRANSIENT_RETRY_BACKOFFS_MS[attempt - 1];
560
- if (backoffMs === undefined) {
561
- // In-place retry budget exhausted. Rather than hard-failing the turn, hand back a
562
- // transient-exhausted result so the caller surfaces the error to the user AND rotates
563
- // to another credential / fallback provider. Nothing is emitted into the stream here:
564
- // the buffered (pre-content) events are discarded exactly like the rate-limit path, so
565
- // the rotation target streams a clean turn.
566
- return { result: "transient_exhausted", attemptAt, classification, attempts: attempt, failure, failureMessage };
567
- }
568
- logRotation(`${credential ? `${getProviderLabel(providerId)} subscription "${credential.label}"` : providerId} hit a transient error (${classification.reason}, status=${classification.status ?? "n/a"}); retrying same route in ${backoffMs}ms (attempt ${attempt + 1})`);
569
- await activeSleep(backoffMs);
570
- }
571
- }
572
- function emitTerminalError(stream, model, message, failure) {
573
- stream.push({ type: "error", reason: "error", error: failure === undefined ? toAssistantError(model, message) : providerFailureAssistantMessage(model, failure) });
574
- stream.end();
575
- }
576
- function enqueueFreshRouteCandidates(candidates, queuedRouteIds, knownRoutes, seenReadyLabels, cooldownAttempts, localCoolingDeadlines) {
577
- const now = Date.now();
578
- for (const [routeId, candidate] of knownRoutes) {
579
- if (queuedRouteIds.has(routeId) || (localCoolingDeadlines.get(routeId) ?? 0) > now || !isManagedProvider(candidate.providerId))
580
- continue;
581
- const seenLabels = seenReadyLabels.get(routeId) ?? new Set();
582
- const hasFreshCredential = readSubscriptionPool(candidate.providerId).some((credential) => {
583
- if (credential.authFailure !== undefined || coolingUntilFor(credential, candidate.model.id) > now)
584
- return false;
585
- const cooldownAttempt = cooldownAttempts.get(`${routeId}/${credential.label}`);
586
- return !seenLabels.has(credential.label) || (cooldownAttempt !== undefined && credential.lastAttemptAt > cooldownAttempt);
587
- });
588
- if (hasFreshCredential) {
589
- candidates.push(candidate);
590
- queuedRouteIds.add(routeId);
591
- }
592
- }
593
- }
594
- async function switchToRoute(pi, ctx, candidate) {
595
- if (ctx.model?.provider !== candidate.providerId || ctx.model.id !== candidate.model.id) {
596
- if (!await pi.setModel(candidate.model))
597
- return false;
598
- }
599
- pi.setThinkingLevel((candidate.thinkingLevel ?? 'off'));
600
- return ctx.model?.provider === candidate.providerId && ctx.model.id === candidate.model.id;
601
- }
602
- function logRouteQueueAdvance(from, to, result) {
603
- logRotation(`from=${from.providerId}/${from.model.id} from_route_id=${from.routeId} from_credential_source=${from.credentialSource} from_rank=${from.rank} to=${to ? `${to.providerId}/${to.model.id}` : "none"} to_route_id=${to?.routeId ?? "none"} to_credential_source=${to?.credentialSource ?? "none"} to_rank=${to?.rank ?? "none"} reason=${result.classification?.reason ?? result.result}; advancing route queue`);
604
- }
605
- // Drive the deterministic route queue from the live rank. Managed routes walk
606
- // their pool rows; every other declared route is one configured credential.
607
- async function runManagedProvider(state, model, context, options, stream) {
608
- const liveThinking = state.runtimeContext?.thinkingLevel === undefined || state.runtimeContext?.thinkingLevel === 'off'
609
- ? undefined
610
- : state.runtimeContext.thinkingLevel;
611
- const request = modelRequestFromConfig(`${model.provider}/${model.id}${liveThinking ? `:${liveThinking}` : ''}`, configuredValue(state, 'modelIntent', 'CRTR_MODEL_INTENT'), isExactModel(state));
612
- const expanded = request ? expandModelCandidates(request, configuredValue(state, 'nodeCwd', 'CRTR_NODE_CWD'), configuredValue(state, 'profileId', 'CRTR_PROFILE_ID')) : [];
613
- // The node's thinking level holds on its own model; a route to another
614
- // provider or model takes that cell's thinking.
615
- const onLiveModel = (candidate) => candidate.providerId === model.provider && candidate.modelId === model.id;
616
- const liveRank = (expanded.find((candidate) => onLiveModel(candidate) && candidate.thinkingLevel === liveThinking) ?? expanded.find(onLiveModel))?.rank ?? 0;
617
- const routeCandidates = expanded.slice(liveRank)
618
- .map((route) => {
619
- const registry = state.runtimeContext?.modelRegistry;
620
- const routeModel = registry?.find(route.providerId, route.modelId);
621
- if (!registry || !routeModel)
622
- return undefined;
623
- const availability = probeRouteAvailability(route, registry);
624
- if (!availability.registered || (!isManagedProvider(route.providerId) && !availability.authenticated))
625
- return undefined;
626
- return { routeId: route.routeId, credentialSource: route.credentialSource, rank: route.rank, providerId: route.providerId, model: routeModel, thinkingLevel: onLiveModel(route) ? liveThinking : route.thinkingLevel };
627
- })
628
- .filter((candidate) => candidate !== undefined)
629
- .filter((candidate, index, all) => all.findIndex((other) => other.routeId === candidate.routeId && other.providerId === candidate.providerId && other.model.id === candidate.model.id && other.thinkingLevel === candidate.thinkingLevel) === index);
630
- const isLiveRoute = (candidate) => candidate.providerId === model.provider && candidate.model.id === model.id && candidate.thinkingLevel === liveThinking;
631
- // A cooling or unauthenticated managed route contributes its diagnostic and may
632
- // later re-enter when a credential becomes ready, but it is never selected now.
633
- const candidates = routeCandidates.filter((candidate) => {
634
- if (isLiveRoute(candidate) || !isManagedProvider(candidate.providerId))
635
- return true;
636
- const pool = readSubscriptionPool(candidate.providerId);
637
- return pool.length === 0
638
- ? state.runtimeContext.modelRegistry.hasConfiguredAuth(candidate.model)
639
- : pool.some((entry) => entry.authFailure === undefined && coolingUntilFor(entry, candidate.model.id) <= Date.now());
640
- });
641
- if (!candidates.some(isLiveRoute)) {
642
- candidates.unshift({ routeId: `live:${model.provider}/${model.id}`, credentialSource: 'live', rank: liveRank, providerId: model.provider, model, thinkingLevel: liveThinking });
643
- }
644
- const queuedRouteIds = new Set(candidates.map((candidate) => candidate.routeId));
645
- const knownRoutes = new Map([...routeCandidates, ...candidates].map((candidate) => [candidate.routeId, candidate]));
646
- const attemptedRoutes = new Set();
647
- const authFailures = new Set();
648
- const localCoolingDeadlines = new Map();
649
- const seenReadyLabels = new Map();
650
- const cooldownAttempts = new Map();
651
- let terminalModel = model;
652
- let lastTransientFailure;
653
- // The last pre-content 401/403 that cooled a route; its text is the only
654
- // record of why the routes ran out when they end in a cooling error.
655
- let lastAuthRejection;
656
- // The last 429 whose response carried rate-limit headers: recorded on the
657
- // cooling fault so an incident traces to the account or to one model family.
658
- let lastRateLimit;
659
- // Any HTTP response observed this pass (a status, a Retry-After header, or a
660
- // rate-limit classification) proves this process can still reach a provider,
661
- // clearing the poisoned-transport suspicion.
662
- let sawHttpEvidence = false;
663
- for (let candidate = candidates.shift(); candidate !== undefined; candidate = candidates.shift()) {
664
- const { routeId, providerId, model: candidateModel } = candidate;
665
- queuedRouteIds.delete(routeId);
666
- knownRoutes.set(routeId, candidate);
667
- attemptedRoutes.add(routeId);
668
- terminalModel = candidateModel;
669
- if (!await switchToRoute(state.extensionAPI, state.runtimeContext, candidate)) {
670
- emitTerminalError(stream, candidateModel, `Provider route ${routeId} is unavailable: failed to select ${providerId}/${candidateModel.id}`);
671
- return;
672
- }
673
- const rawPool = isManagedProvider(providerId) ? readSubscriptionPool(providerId) : [];
674
- if (!isManagedProvider(providerId) || rawPool.length === 0) {
675
- const liveKey = isLiveRoute(candidate) ? options?.apiKey : undefined;
676
- if (isManagedProvider(providerId) && !liveKey && !state.runtimeContext.modelRegistry.hasConfiguredAuth(candidateModel))
677
- continue;
678
- const auth = isManagedProvider(providerId) && !liveKey
679
- ? await state.runtimeContext.modelRegistry.getApiKeyAndHeaders(candidateModel)
680
- : undefined;
681
- if (isManagedProvider(providerId) && !liveKey && (!auth?.ok || !auth.apiKey))
682
- continue;
683
- const headers = { ...options?.headers };
684
- for (const [name, value] of Object.entries(auth?.ok ? auth.headers ?? {} : {})) {
685
- for (const existing of Object.keys(headers))
686
- if (existing.toLowerCase() === name.toLowerCase())
687
- delete headers[existing];
688
- headers[name] = value;
689
- }
690
- const attemptOptions = auth?.ok ? { ...options, apiKey: auth.apiKey, headers } : options;
691
- const result = await streamProviderAttempt(state, providerId, candidateModel, context, attemptOptions, undefined, stream);
692
- if (result.result === 'success' || result.result === 'fatal') {
693
- state.transportOnlyExhaustedPasses = 0;
694
- stream.end();
695
- return;
696
- }
697
- if (result.result === 'rate_limited' || result.classification?.status !== undefined || result.classification?.hadRetryAfterHeader === true)
698
- sawHttpEvidence = true;
699
- const cooldownMs = Math.min(result.result === 'rate_limited' ? result.retryAfterMs ?? DEFAULT_RATE_LIMIT_BACKOFF_MS : TRANSIENT_ROTATE_COOLDOWN_MS, MAX_RATE_LIMIT_COOLDOWN_MS);
700
- if (result.result === 'transient_exhausted')
701
- lastTransientFailure = result;
702
- if (result.result === 'rate_limited' && (result.classification?.status === 401 || result.classification?.status === 403))
703
- lastAuthRejection = result;
704
- if (result.rateLimit && Object.keys(result.rateLimit.headers).length)
705
- lastRateLimit = { providerId, model: candidateModel, observation: result.rateLimit };
706
- localCoolingDeadlines.set(routeId, Math.max(localCoolingDeadlines.get(routeId) ?? 0, Date.now() + cooldownMs));
707
- logRouteQueueAdvance(candidate, candidates[0], result);
708
- continue;
709
- }
710
- for (const entry of rawPool)
711
- if (entry.authFailure === 'invalid_grant')
712
- authFailures.add(`${providerId}/${entry.label}`);
713
- const available = rawPool.filter((entry) => entry.authFailure === undefined && coolingUntilFor(entry, candidateModel.id) <= Date.now());
714
- const seenLabels = seenReadyLabels.get(routeId) ?? new Set();
715
- for (const credential of available)
716
- seenLabels.add(credential.label);
717
- seenReadyLabels.set(routeId, seenLabels);
718
- let advanceResult;
719
- for (const credential of available) {
720
- let freshCredential;
721
- try {
722
- freshCredential = await refreshCredentialIfNeeded(state, providerId, credential, options?.signal);
723
- }
724
- catch (error) {
725
- if (!isInvalidRefreshTokenError(error))
726
- throw error;
727
- let reauthenticated;
728
- try {
729
- reauthenticated = await reauthenticateCredential(state, providerId, credential);
730
- }
731
- catch (loginError) {
732
- state.runtimeContext?.ui.notify(`${getProviderLabel(providerId)} re-authentication failed: ${loginError instanceof Error ? loginError.message : String(loginError)}`, 'error');
733
- }
734
- if (!reauthenticated) {
735
- authFailures.add(`${providerId}/${credential.label}`);
736
- continue;
737
- }
738
- freshCredential = reauthenticated;
739
- }
740
- const result = await streamProviderAttempt(state, providerId, candidateModel, context, options, freshCredential, stream);
741
- if (result.result === 'success') {
742
- state.transportOnlyExhaustedPasses = 0;
743
- if (subscriptionNeedsSuccessWrite(freshCredential, candidateModel.id))
744
- await recordSubscriptionMutation(`recording ${getProviderLabel(providerId)} success`, () => markSubscriptionSuccess(providerId, freshCredential.label, result.attemptAt, candidateModel.id));
745
- stream.end();
746
- return;
747
- }
748
- if (result.result === 'fatal') {
749
- state.transportOnlyExhaustedPasses = 0;
750
- stream.end();
751
- return;
752
- }
753
- if (result.result === 'rate_limited' || result.classification?.status !== undefined || result.classification?.hadRetryAfterHeader === true)
754
- sawHttpEvidence = true;
755
- const cooldownMs = Math.min(result.result === 'rate_limited' ? result.retryAfterMs ?? DEFAULT_RATE_LIMIT_BACKOFF_MS : TRANSIENT_ROTATE_COOLDOWN_MS, MAX_RATE_LIMIT_COOLDOWN_MS);
756
- if (result.result === 'transient_exhausted')
757
- lastTransientFailure = result;
758
- if (result.result === 'rate_limited' && (result.classification?.status === 401 || result.classification?.status === 403))
759
- lastAuthRejection = result;
760
- if (result.rateLimit && Object.keys(result.rateLimit.headers).length)
761
- lastRateLimit = { providerId, model: candidateModel, account: freshCredential.label, observation: result.rateLimit };
762
- if (result.result === 'rate_limited' && result.retryAfterMs !== undefined) {
763
- const persisted = await recordSubscriptionMutation(`recording ${getProviderLabel(providerId)} server cooldown`, () => markSubscriptionRateLimited(providerId, freshCredential.label, cooldownMs, result.attemptAt, Date.now(), result.rateLimit?.scope ?? { kind: 'account' }));
764
- if (persisted)
765
- cooldownAttempts.set(`${routeId}/${freshCredential.label}`, result.attemptAt);
766
- else
767
- localCoolingDeadlines.set(routeId, Math.max(localCoolingDeadlines.get(routeId) ?? 0, Date.now() + cooldownMs));
768
- }
769
- else {
770
- localCoolingDeadlines.set(routeId, Math.max(localCoolingDeadlines.get(routeId) ?? 0, Date.now() + cooldownMs));
771
- }
772
- advanceResult = result;
773
- }
774
- enqueueFreshRouteCandidates(candidates, queuedRouteIds, knownRoutes, seenReadyLabels, cooldownAttempts, localCoolingDeadlines);
775
- if (advanceResult !== undefined)
776
- logRouteQueueAdvance(candidate, candidates[0], advanceResult);
777
- }
778
- const coolingDeadline = earliestCoolingDeadline([...knownRoutes.values()], localCoolingDeadlines);
779
- const tried = [...attemptedRoutes].join(', ');
780
- if (lastTransientFailure !== undefined) {
781
- state.transportOnlyExhaustedPasses = sawHttpEvidence ? 0 : state.transportOnlyExhaustedPasses + 1;
782
- if (state.transportOnlyExhaustedPasses >= POISONED_TRANSPORT_PASS_LIMIT && await activeTerminatePoisonedProcess(tried)) {
783
- // The stream is deliberately left OPEN: the SIGTERM handler sees a live
784
- // turn, so it either certifies a clean abort (→ the daemon respawns and
785
- // resumes with a continuation prompt) or marks the turn dirty (→ the
786
- // respawn re-cycles the turn). Ending the stream first could settle the
787
- // turn idle before the signal lands, leaving the respawned node parked
788
- // with a terminal error and nothing to re-drive it.
789
- return;
790
- }
791
- const detail = lastTransientFailure.failureMessage ?? lastTransientFailure.classification?.reason ?? 'unknown connection failure';
792
- emitTerminalError(stream, terminalModel, `Provider routes failed after transient retries: ${detail} (tried ${tried})`, lastTransientFailure.failure);
793
- return;
794
- }
795
- state.transportOnlyExhaustedPasses = 0;
796
- if (coolingDeadline !== undefined) {
797
- const cooling = `cooling down ~${Math.ceil((coolingDeadline - Date.now()) / 1000)}s`;
798
- const message = lastAuthRejection === undefined
799
- ? `All provider routes are rate limited (${cooling}; tried ${tried})`
800
- : `All provider routes are rate limited or rejected (${cooling}; last rejection ${lastAuthRejection.classification.reason}: ${lastAuthRejection.failureMessage ?? 'no error message'}; tried ${tried})`;
801
- emitCoolingError(stream, terminalModel, new ManagedProviderCoolingError(message, coolingDeadline), lastRateLimit);
802
- return;
803
- }
804
- const authFailure = [...authFailures][0];
805
- if (authFailure !== undefined) {
806
- emitTerminalError(stream, terminalModel, `Provider authentication failed (invalid_grant) for ${authFailure}; re-authenticate the credential and retry (tried ${tried})`);
807
- return;
808
- }
809
- emitTerminalError(stream, terminalModel, `All provider routes exhausted (tried ${tried})`);
810
- }
811
- // The earliest strictly-future deadline only when every attempted route is cooling.
812
- function earliestCoolingDeadline(attempted, localCoolingDeadlines) {
813
- const now = Date.now();
814
- let earliest;
815
- let sawRoute = false;
816
- for (const route of attempted) {
817
- const localUntil = localCoolingDeadlines.get(route.routeId) ?? 0;
818
- if (localUntil > now) {
819
- sawRoute = true;
820
- earliest = earliest === undefined ? localUntil : Math.min(earliest, localUntil);
821
- continue;
822
- }
823
- if (!isManagedProvider(route.providerId))
824
- return undefined;
825
- const pool = readSubscriptionPool(route.providerId).filter((entry) => entry.authFailure === undefined);
826
- // Routes with no usable credential were never runnable this turn, so they do
827
- // not defeat the cooling outcome of another route. An auth-failed-only pool
828
- // is likewise unauthenticated; a valid cooling row still contributes its
829
- // deadline even when a sibling row carries invalid_grant.
830
- if (pool.length === 0)
831
- continue;
832
- sawRoute = true;
833
- if (pool.some((entry) => coolingUntilFor(entry, route.model.id) <= now))
834
- return undefined;
835
- const routeEarliest = Math.min(...pool.map((entry) => coolingUntilFor(entry, route.model.id)));
836
- earliest = earliest === undefined ? routeEarliest : Math.min(earliest, routeEarliest);
837
- }
838
- return sawRoute ? earliest : undefined;
839
- }
840
- // Emit the typed cooling terminal error, attaching its diagnostic (the earliest
841
- // deadline) to the assistant error message so the deadline survives verbatim into
842
- // agent_end.messages for the stophook to read.
843
- function emitCoolingError(stream, model, error, rateLimit) {
844
- const message = toAssistantError(model, error.message);
845
- message.diagnostics = [
846
- error.toDiagnostic(),
847
- ...(rateLimit === undefined ? [] : [providerFailureDiagnostic({
848
- status: 429,
849
- ratelimit: {
850
- scope: describeRateLimitScope(rateLimit.observation.scope),
851
- // The rate-limited route may be a fallback, not the model the error is stamped with.
852
- model: `${rateLimit.providerId}/${rateLimit.model.id}`,
853
- ...(rateLimit.observation.claim !== undefined ? { claim: rateLimit.observation.claim } : {}),
854
- ...(rateLimit.account !== undefined ? { account: rateLimit.account } : {}),
855
- headers: rateLimit.observation.headers,
856
- },
857
- })]),
858
- ];
859
- stream.push({ type: "error", reason: "error", error: message });
860
- stream.end();
861
- }
862
- function streamManagedProvider(state, model, context, options) {
863
- const stream = createAssistantMessageEventStream();
864
- runManagedProvider(state, model, context, options, stream).catch((error) => {
865
- stream.push({
866
- type: "error",
867
- reason: "error",
868
- error: toAssistantError(model, error instanceof Error ? error.message : String(error)),
869
- });
870
- stream.end();
871
- });
872
- return stream;
873
- }
874
- function parseCommand(args) {
875
- return args
876
- .trim()
877
- .split(/\s+/)
878
- .map((part) => part.trim())
879
- .filter(Boolean);
880
- }
881
- function parseProvider(value) {
882
- return isManagedProvider(value) ? value : undefined;
883
- }
884
- /** Merged config for the NODE's scope (its own cwd/profile), not this
885
- * process's ambient one. */
886
- function readTargetLaunchConfig(state, ctx) {
887
- return readMergedLaunchConfig(ctx.cwd, configuredValue(state, 'profileId', 'CRTR_PROFILE_ID') || null);
888
- }
889
- function installDeclaredRouteProviders(state, pi, ctx, config) {
890
- const providerIds = new Set(Object.values({ ...(config.modelRoutes ?? {}), ...(config.legacyModelRoutes ?? {}) }).map((route) => route.providerId));
891
- for (const providerId of providerIds) {
892
- if (isManagedProvider(providerId) || state.delegatedProviderStreams.has(providerId))
893
- continue;
894
- const provider = ctx.modelRegistry.getProvider?.(providerId);
895
- const model = ctx.modelRegistry.getAll?.().find((entry) => entry.provider === providerId);
896
- if (!provider || !model)
897
- continue;
898
- state.delegatedProviderStreams.set(providerId, provider.streamSimple.bind(provider));
899
- // `api` is required by pi whenever a registration supplies streamSimple.
900
- // Re-registration merges this wrapper over the existing declaration, so the
901
- // provider's models, auth, and captured composed transport remain intact.
902
- pi.registerProvider(providerId, { api: model.api, streamSimple: (model, context, options) => streamManagedProvider(state, model, context, options) });
903
- }
904
- }
905
- function updateRotationStatus(state, ctx) {
906
- const current = getCurrentModelRef(ctx.model);
907
- if (current && isManagedProvider(current.providerId))
908
- setStatus(state, formatStatusLine(current.providerId, Date.now(), current.modelId));
909
- }
910
- /** Restore only when the deterministic route rank improves; no shared-home
911
- * preferred-model state is needed or consulted. */
912
- async function restoreHigherRankIfAvailable(state, pi, ctx) {
913
- if (!readRotationConfig().revertWhenAvailable)
914
- return false;
915
- const liveThinking = ctx.thinkingLevel === undefined || ctx.thinkingLevel === 'off' ? undefined : ctx.thinkingLevel;
916
- const request = modelRequestFromConfig(`${ctx.model?.provider ?? ''}/${ctx.model?.id ?? ''}${liveThinking ? `:${liveThinking}` : ''}`, configuredValue(state, 'modelIntent', 'CRTR_MODEL_INTENT'), isExactModel(state));
917
- if (!request || !ctx.model)
918
- return false;
919
- const candidates = expandModelCandidates(request, configuredValue(state, 'nodeCwd', 'CRTR_NODE_CWD'), configuredValue(state, 'profileId', 'CRTR_PROFILE_ID'));
920
- const onLiveModel = (candidate) => candidate.providerId === ctx.model?.provider && candidate.modelId === ctx.model?.id;
921
- const liveRank = (candidates.find((candidate) => onLiveModel(candidate) && candidate.thinkingLevel === liveThinking) ?? candidates.find(onLiveModel))?.rank;
922
- if (liveRank === undefined)
923
- return false;
924
- const preferred = candidates.find((candidate) => candidate.rank < liveRank && resolveProviderCandidates([{
925
- ...candidate,
926
- ...probeRouteAvailability(candidate, ctx.modelRegistry),
927
- automaticFallback: true,
928
- }]).candidates.length > 0);
929
- if (!preferred)
930
- return false;
931
- const model = ctx.modelRegistry.find(preferred.providerId, preferred.modelId);
932
- if (!model || !await pi.setModel(model))
933
- return false;
934
- pi.setThinkingLevel((preferred.thinkingLevel ?? 'off'));
935
- return true;
936
- }
937
- // Manual-code input is a human-in-the-loop OAuth round-trip (open link, log in on
938
- // whatever device, copy the localhost redirect URL back) that routinely exceeds
939
- // DEFAULT_DIALOG_TIMEOUT_MS -- give it a human-scale bound instead of the ordinary
940
- // confirm/select default.
941
- const OAUTH_MANUAL_INPUT_TIMEOUT_MS = 600_000;
942
- /**
943
- * pi-ai races the manual-code-paste dialog against its loopback callback server. When the
944
- * callback supplies a code, the manual-input promise can remain pending, so `run` receives:
945
- * - `readCode`, a `ctx.ui.input` wrapper with an AbortSignal. Aborting it removes the pending
946
- * broker entry and emits an `extension_ui_dismiss` frame for exactly that dialog.
947
- * - `dismissManual`, which aborts the signal at the post-code `onProgress` boundary before
948
- * the token exchange completes. `onProgress` is emitted only after a code is available, so
949
- * dismissing then cannot close a dialog the user still needs; `finally` covers paths without
950
- * progress, and repeated AbortController calls are inert.
951
- *
952
- * `undefined` from `readCode` means cancellation or timeout. It throws so OAuth ends directly
953
- * instead of opening a second paste-only dialog.
954
- */
955
- export async function loginWithManualInputCleanup(ctx, run) {
956
- const controller = new AbortController();
957
- const readCode = async (title, placeholder) => {
958
- const result = await ctx.ui.input(title, placeholder, { timeout: OAUTH_MANUAL_INPUT_TIMEOUT_MS, signal: controller.signal });
959
- if (result === undefined)
960
- throw new Error("OAuth login cancelled");
961
- return result;
962
- };
963
- try {
964
- return await run(readCode, () => controller.abort());
965
- }
966
- finally {
967
- controller.abort();
968
- }
969
- }
970
- // The manual-code dialog carries the authorize URL in its title (rendered as a tappable link
971
- // on web), so a headless remote box where `openBrowser` opens nothing still has a real path
972
- // through login. pi's own `manual_code` message is URL-less, hence these per-provider titles.
973
- const MANUAL_CODE_DIALOG = {
974
- [ANTHROPIC_PROVIDER_ID]: {
975
- title: (url) => `Open this URL to authenticate Claude:\n${url}\n\nPaste the redirect URL or code`,
976
- placeholder: "http://localhost:53692/callback?...",
977
- },
978
- [OPENAI_CODEX_PROVIDER_ID]: {
979
- title: (url) => `Open this URL to authenticate OpenAI Codex:\n${url}\n\nPaste the redirect URL or code`,
980
- placeholder: "http://localhost:1455/auth/callback?...",
981
- },
982
- [OPENAI_CHATGPT_PROVIDER_ID]: {
983
- title: (url) => `Open this URL to sign in with ChatGPT:\n${url}\n\nPaste the final redirect URL`,
984
- placeholder: "http://127.0.0.1:1455/auth/callback?...",
985
- },
986
- };
987
- // This extension adapts the provider OAuth interaction onto `ctx.ui`. The attach viewer and
988
- // `crtr sys setup` use LoginDialogComponent with `core/auth-interaction.ts`; the broker uses
989
- // the dialog primitives below.
990
- async function loginProvider(providerId, ctx) {
991
- const oauth = oauthFlow(providerId, ctx);
992
- const manual = MANUAL_CODE_DIALOG[providerId];
993
- const controller = new AbortController();
994
- let authorizeUrl;
995
- // ChatGPT sign-in identifies this install to OpenAI by a stable device UUID.
996
- const loginOptions = providerId === OPENAI_CHATGPT_PROVIDER_ID ? { getDeviceId: openAIDeviceId } : undefined;
997
- return loginWithManualInputCleanup(ctx, (readCode, dismissManual) => oauth.login({
998
- signal: controller.signal,
999
- notify: (event) => {
1000
- switch (event.type) {
1001
- case "auth_url":
1002
- authorizeUrl = event.url;
1003
- openBrowser(event.url);
1004
- if (event.instructions)
1005
- ctx.ui.notify(event.instructions, "info");
1006
- break;
1007
- case "device_code":
1008
- openBrowser(event.verificationUri);
1009
- ctx.ui.notify(`Enter code ${event.userCode} at ${event.verificationUri}`, "info");
1010
- break;
1011
- case "progress":
1012
- // Progress follows receipt of an OAuth code, so dismiss the now-unneeded manual
1013
- // dialog before token exchange. The browser path may emit no progress; `finally`
1014
- // closes that dialog when the OAuth attempt settles.
1015
- dismissManual();
1016
- ctx.ui.notify(event.message, "info");
1017
- break;
1018
- case "info":
1019
- ctx.ui.notify(event.message, "info");
1020
- break;
1021
- }
1022
- },
1023
- prompt: async (prompt) => {
1024
- if (prompt.type === "manual_code")
1025
- return readCode(manual.title(authorizeUrl), manual.placeholder);
1026
- if (prompt.type === "select") {
1027
- // Codex now opens its login with a browser-vs-device-code choice.
1028
- const labels = prompt.options.map((option) => option.label);
1029
- const chosen = await ctx.ui.select(prompt.message, labels);
1030
- const selected = chosen === undefined ? undefined : prompt.options[labels.indexOf(chosen)];
1031
- if (!selected)
1032
- throw new Error("OAuth login cancelled");
1033
- return selected.id;
1034
- }
1035
- return (await ctx.ui.input(prompt.message, prompt.placeholder ?? "")) ?? "";
1036
- },
1037
- }, loginOptions)).finally(() => controller.abort());
1038
- }
1039
- export function createProviderRotationExtension(options) {
1040
- const state = createProviderRotationState(options);
1041
- return async (pi) => {
1042
- state.extensionAPI = pi;
1043
- pi.registerProvider(ANTHROPIC_PROVIDER_ID, {
1044
- api: "anthropic-messages",
1045
- streamSimple: (model, context, streamOptions) => streamManagedProvider(state, model, context, streamOptions),
1046
- });
1047
- pi.registerProvider(OPENAI_CODEX_PROVIDER_ID, {
1048
- api: "openai-codex-responses",
1049
- streamSimple: (model, context, streamOptions) => streamManagedProvider(state, model, context, streamOptions),
1050
- });
1051
- // pi's built-in `openai` provider (Responses API). Its pool holds ChatGPT sign-ins; with an
1052
- // empty pool the route falls back to the provider's own auth (OPENAI_API_KEY / auth.json).
1053
- pi.registerProvider(OPENAI_CHATGPT_PROVIDER_ID, {
1054
- api: "openai-responses",
1055
- streamSimple: (model, context, streamOptions) => streamManagedProvider(state, model, context, streamOptions),
1056
- });
1057
- pi.on("session_start", async (_event, ctx) => {
1058
- state.runtimeContext = ctx;
1059
- const config = readTargetLaunchConfig(state, ctx);
1060
- state.providerOptions = config.providerOptions;
1061
- installDeclaredRouteProviders(state, pi, ctx, config);
1062
- updateRotationStatus(state, ctx);
1063
- });
1064
- pi.on("before_agent_start", async (_event, ctx) => {
1065
- state.runtimeContext = ctx;
1066
- // Re-read per turn: this is what makes a provider-options toggle apply to
1067
- // a broker that is already running, on its next turn.
1068
- state.providerOptions = readTargetLaunchConfig(state, ctx).providerOptions;
1069
- const restored = await restoreHigherRankIfAvailable(state, pi, ctx);
1070
- if (restored && ctx.model && isManagedProvider(ctx.model.provider)) {
1071
- logRotation(`restored higher-ranked route ${ctx.model.provider}/${ctx.model.id}`);
1072
- setStatus(state, formatStatusLine(ctx.model.provider, Date.now(), ctx.model.id));
1073
- }
1074
- });
1075
- pi.on("model_select", async (_event, ctx) => {
1076
- state.runtimeContext = ctx;
1077
- updateRotationStatus(state, ctx);
1078
- });
1079
- pi.registerCommand("provider-sub", {
1080
- description: "Manage subscription credential pools for provider rotation",
1081
- handler: async (args, ctx) => {
1082
- const [providerArg, verb, ...rest] = parseCommand(args);
1083
- const providerId = parseProvider(providerArg);
1084
- if (!providerId) {
1085
- ctx.ui.notify("Usage: /provider-sub <anthropic|openai-codex|openai> <list|add|select|rm>", "error");
1086
- return;
1087
- }
1088
- const action = verb === "rm" ? "remove" : verb || "list";
1089
- if (action === "status" || action === "list") {
1090
- const pool = readSubscriptionPool(providerId);
1091
- const lines = [formatStatusLine(providerId, Date.now(), ctx.model?.provider === providerId ? ctx.model.id : undefined), ""];
1092
- for (const [index, entry] of pool.entries()) {
1093
- const cooling = formatCooldowns(entry);
1094
- const cooldown = cooling ? ` ${cooling}` : "";
1095
- const account = entry.accountId ? ` account=${shortAccountId(entry.accountId)}` : "";
1096
- lines.push(`${index + 1}. ${entry.label}${account}${cooldown}`);
1097
- }
1098
- ctx.ui.notify(lines.join("\n"), "info");
1099
- return;
1100
- }
1101
- if (action === "add") {
1102
- if (!ctx.hasUI) {
1103
- ctx.ui.notify(`${getProviderLabel(providerId)} login requires an interactive UI.`, "error");
1104
- return;
1105
- }
1106
- const pool = readSubscriptionPool(providerId);
1107
- const label = (rest.join(" ") || (await ctx.ui.input(`${getProviderLabel(providerId)} subscription label`, "")) || "").trim();
1108
- if (!label)
1109
- return;
1110
- if (pool.some((entry) => entry.label.toLowerCase() === label.toLowerCase())) {
1111
- ctx.ui.notify(`${getProviderLabel(providerId)} subscription already exists: ${label}`, "error");
1112
- return;
1113
- }
1114
- try {
1115
- const cred = await activeLoginForProvider(providerId, ctx);
1116
- const identity = await enrichManagedLoginIdentity(providerId, cred);
1117
- // Explicit add always creates a new label, so a matching real account is rejected by
1118
- // the locked uniqueness gate rather than creating a second quota slot.
1119
- await addSubscription(providerId, {
1120
- label,
1121
- refresh: cred.refresh,
1122
- access: cred.access,
1123
- expires: cred.expires,
1124
- ...(identity.accountId ? { accountId: identity.accountId } : {}),
1125
- ...(loginClientId(cred) ? { clientId: loginClientId(cred) } : {}),
1126
- rateLimitedUntil: 0,
1127
- lastAttemptAt: 0,
1128
- lastRateLimitedAt: 0,
1129
- });
1130
- logRotation(`added ${getProviderLabel(providerId)} subscription "${label}"`);
1131
- ctx.ui.notify(`Added ${getProviderLabel(providerId)} subscription: ${label}`, "info");
1132
- updateRotationStatus(state, ctx);
1133
- }
1134
- catch (error) {
1135
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1136
- }
1137
- return;
1138
- }
1139
- if (action === "select") {
1140
- const ref = rest.join(" ");
1141
- if (!ref) {
1142
- ctx.ui.notify(`Usage: /provider-sub ${providerId} select <label|index>`, "error");
1143
- return;
1144
- }
1145
- try {
1146
- await promoteSubscription(providerId, ref);
1147
- logRotation(`selected ${getProviderLabel(providerId)} subscription "${ref}"`);
1148
- ctx.ui.notify(`Preferred ${getProviderLabel(providerId)} subscription: ${ref}`, "info");
1149
- updateRotationStatus(state, ctx);
1150
- }
1151
- catch (error) {
1152
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1153
- }
1154
- return;
1155
- }
1156
- if (action === "remove") {
1157
- const ref = rest.join(" ");
1158
- if (!ref) {
1159
- ctx.ui.notify(`Usage: /provider-sub ${providerId} rm <label|index>`, "error");
1160
- return;
1161
- }
1162
- try {
1163
- await removeManagedAccount(providerId, ref);
1164
- logRotation(`removed ${getProviderLabel(providerId)} subscription "${ref}"`);
1165
- ctx.ui.notify(`Removed ${getProviderLabel(providerId)} subscription: ${ref}`, "info");
1166
- updateRotationStatus(state, ctx);
1167
- }
1168
- catch (error) {
1169
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1170
- }
1171
- return;
1172
- }
1173
- ctx.ui.notify(`Unknown /provider-sub verb: ${verb}`, "error");
1174
- },
1175
- });
1176
- };
1177
- }
1178
- const defaultProviderRotationExtension = createProviderRotationExtension();
1179
- export default defaultProviderRotationExtension;