@north-light/crouter 0.3.170 → 0.3.172

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 (670) hide show
  1. package/dist/api/client.d.ts +37 -11
  2. package/dist/api/client.js +84 -15
  3. package/dist/api/dto/human.d.ts +53 -39
  4. package/dist/api/dto/human.js +2 -3
  5. package/dist/api/dto/inbox.d.ts +0 -1
  6. package/dist/api/dto/nodes.d.ts +12 -0
  7. package/dist/api/dto/review-comments.d.ts +137 -0
  8. package/dist/api/dto/review-comments.js +5 -0
  9. package/dist/api/dto/reviews.d.ts +109 -0
  10. package/dist/api/dto/reviews.js +5 -0
  11. package/dist/api/index.d.ts +2 -0
  12. package/dist/api/index.js +2 -0
  13. package/dist/api/routes.d.ts +17 -4
  14. package/dist/api/routes.js +21 -5
  15. package/dist/build-root.d.ts +1 -16
  16. package/dist/build-root.js +1 -25
  17. package/dist/builtin-memory/00-runtime-base.md +7 -5
  18. package/dist/builtin-memory/04-base-worker.md +1 -1
  19. package/dist/builtin-memory/04-orchestration-kernel.md +7 -7
  20. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  21. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -1
  23. package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -1
  24. package/dist/builtin-memory/05-kinds/plan/00-base.md +1 -1
  25. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -1
  26. package/dist/builtin-memory/05-kinds/review/00-base.md +1 -1
  27. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +20 -0
  28. package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -7
  29. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +3 -3
  30. package/dist/builtin-memory/05-kinds/spec/requirements.md +3 -3
  31. package/dist/builtin-memory/design.md +2 -2
  32. package/dist/builtin-memory/insights/capture.md +50 -0
  33. package/dist/builtin-memory/insights/init.md +45 -0
  34. package/dist/builtin-memory/insights/listen.md +9 -0
  35. package/dist/builtin-memory/internal/agent-shaping.md +2 -2
  36. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  37. package/dist/builtin-memory/internal/plugins.md +12 -11
  38. package/dist/builtin-memory/internal/storage-tiers.md +2 -2
  39. package/dist/builtin-memory/{planning.md → plan/roadmap.md} +3 -3
  40. package/dist/builtin-memory/plan.md +19 -0
  41. package/dist/builtin-memory/spec/guide.md +38 -0
  42. package/dist/builtin-memory/spec/requirements.md +31 -0
  43. package/dist/builtin-memory/spec/roadmap.md +35 -0
  44. package/dist/builtin-memory/spec.md +10 -86
  45. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +3 -4
  46. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +49 -5
  47. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +56 -0
  48. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +920 -0
  49. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +221 -156
  50. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +4 -5
  51. package/dist/cli.js +2 -6
  52. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +7 -35
  53. package/dist/clients/attach/__tests__/attach-keybindings.test.js +32 -16
  54. package/dist/clients/attach/__tests__/context-message.test.js +142 -6
  55. package/dist/clients/attach/__tests__/crtr-output.test.js +29 -45
  56. package/dist/clients/attach/__tests__/diagram.test.js +1 -1
  57. package/dist/clients/attach/__tests__/file-review-focus.test.js +48 -0
  58. package/dist/clients/attach/__tests__/group-activity.test.js +266 -0
  59. package/dist/clients/attach/__tests__/input-controller-extension-command.test.js +32 -0
  60. package/dist/clients/attach/__tests__/session-identity-refresh.test.js +32 -0
  61. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -2
  62. package/dist/clients/attach/chrome/header.d.ts +10 -0
  63. package/dist/clients/attach/chrome/header.js +30 -0
  64. package/dist/clients/attach/chrome/recap.d.ts +7 -0
  65. package/dist/clients/attach/chrome/recap.js +19 -0
  66. package/dist/clients/attach/chrome/status-line.d.ts +3 -0
  67. package/dist/clients/attach/chrome/status-line.js +5 -0
  68. package/dist/clients/attach/chrome/widgets.js +11 -21
  69. package/dist/clients/attach/command.js +11 -7
  70. package/dist/clients/attach/config.d.ts +4 -0
  71. package/dist/clients/attach/config.js +3 -0
  72. package/dist/clients/attach/input/controller.d.ts +19 -0
  73. package/dist/clients/attach/input/controller.js +62 -28
  74. package/dist/clients/attach/input/titled-editor.d.ts +0 -4
  75. package/dist/clients/attach/input/titled-editor.js +2 -10
  76. package/dist/clients/attach/overlays/file-review.d.ts +9 -5
  77. package/dist/clients/attach/overlays/file-review.js +9 -70
  78. package/dist/clients/attach/overlays/help.d.ts +33 -0
  79. package/dist/clients/attach/overlays/help.js +204 -0
  80. package/dist/clients/attach/render/activity-slot.d.ts +4 -0
  81. package/dist/clients/attach/render/activity-slot.js +13 -0
  82. package/dist/clients/attach/render/chat-view.d.ts +117 -12
  83. package/dist/clients/attach/render/chat-view.js +604 -135
  84. package/dist/clients/attach/render/context-message.d.ts +2 -1
  85. package/dist/clients/attach/render/context-message.js +17 -13
  86. package/dist/clients/attach/render/crtr-output.js +1 -3
  87. package/dist/clients/attach/render/diagram.js +2 -2
  88. package/dist/clients/attach/render/edit-diff.d.ts +4 -1
  89. package/dist/clients/attach/render/edit-diff.js +7 -11
  90. package/dist/clients/attach/render/group-activity.d.ts +32 -0
  91. package/dist/clients/attach/render/group-activity.js +206 -0
  92. package/dist/clients/attach/render/group-recap.d.ts +10 -0
  93. package/dist/clients/attach/render/group-recap.js +93 -0
  94. package/dist/clients/attach/render/html-markdown.d.ts +17 -0
  95. package/dist/clients/attach/render/html-markdown.js +228 -0
  96. package/dist/clients/attach/render/measured-container.d.ts +96 -0
  97. package/dist/clients/attach/render/measured-container.js +322 -0
  98. package/dist/clients/attach/render/scroll-animation.d.ts +8 -0
  99. package/dist/clients/attach/render/scroll-animation.js +80 -0
  100. package/dist/clients/attach/render/tool-calls.js +6 -6
  101. package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
  102. package/dist/clients/attach/render/transcript-copy.js +81 -0
  103. package/dist/clients/attach/render/viewport.d.ts +29 -0
  104. package/dist/clients/attach/render/viewport.js +159 -0
  105. package/dist/clients/attach/session/bindings.d.ts +10 -7
  106. package/dist/clients/attach/session/bindings.js +23 -8
  107. package/dist/clients/attach/session/connection.js +4 -5
  108. package/dist/clients/attach/session/context.d.ts +6 -4
  109. package/dist/clients/attach/session/frame.d.ts +34 -0
  110. package/dist/clients/attach/session/frame.js +176 -0
  111. package/dist/clients/attach/session/frames.d.ts +4 -1
  112. package/dist/clients/attach/session/frames.js +14 -1
  113. package/dist/clients/attach/session/identity.js +4 -3
  114. package/dist/clients/attach/session/input-wiring.d.ts +14 -4
  115. package/dist/clients/attach/session/input-wiring.js +15 -13
  116. package/dist/clients/attach/session/invariant.d.ts +9 -0
  117. package/dist/clients/attach/session/invariant.js +15 -0
  118. package/dist/clients/attach/session/keys.d.ts +10 -2
  119. package/dist/clients/attach/session/keys.js +13 -5
  120. package/dist/clients/attach/session/layout.d.ts +10 -8
  121. package/dist/clients/attach/session/layout.js +33 -34
  122. package/dist/clients/attach/session/mouse.d.ts +18 -0
  123. package/dist/clients/attach/session/mouse.js +64 -0
  124. package/dist/clients/attach/session/pane-tag.d.ts +3 -0
  125. package/dist/clients/attach/session/pane-tag.js +51 -8
  126. package/dist/clients/attach/session/reconnect.d.ts +3 -0
  127. package/dist/clients/attach/session/reconnect.js +42 -5
  128. package/dist/clients/attach/session/surface-frame.d.ts +20 -0
  129. package/dist/clients/attach/session/surface-frame.js +73 -0
  130. package/dist/clients/attach/session/surface-host.d.ts +64 -0
  131. package/dist/clients/attach/session/surface-host.js +255 -0
  132. package/dist/clients/attach/session/whip.d.ts +17 -0
  133. package/dist/clients/attach/session/whip.js +334 -0
  134. package/dist/clients/attach/slash/dispatch.d.ts +13 -1
  135. package/dist/clients/attach/slash/dispatch.js +51 -29
  136. package/dist/clients/attach/viewer.js +907 -819
  137. package/dist/clients/conversation/conversation.d.ts +65 -0
  138. package/dist/clients/conversation/conversation.js +318 -0
  139. package/dist/clients/conversation/frames.d.ts +30 -0
  140. package/dist/clients/conversation/frames.js +52 -0
  141. package/dist/clients/conversation/region.d.ts +51 -0
  142. package/dist/clients/conversation/region.js +265 -0
  143. package/dist/clients/conversation/submit.d.ts +38 -0
  144. package/dist/clients/conversation/submit.js +51 -0
  145. package/dist/clients/inbox/__tests__/inbox-controller.test.js +247 -0
  146. package/dist/clients/inbox/__tests__/mount-panel.test.js +776 -0
  147. package/dist/clients/inbox/controller.d.ts +79 -0
  148. package/dist/clients/inbox/controller.js +448 -0
  149. package/dist/clients/inbox/deck-adapter.d.ts +29 -0
  150. package/dist/clients/inbox/deck-adapter.js +62 -0
  151. package/dist/clients/inbox/layout.d.ts +8 -0
  152. package/dist/clients/inbox/layout.js +9 -0
  153. package/dist/clients/inbox/resolve.d.ts +10 -0
  154. package/dist/clients/inbox/resolve.js +34 -0
  155. package/dist/clients/inbox/review/__tests__/editor-roundtrip.test.js +29 -0
  156. package/dist/clients/inbox/review/__tests__/remap.test.js +196 -0
  157. package/dist/clients/inbox/review/__tests__/review-core.test.js +341 -0
  158. package/dist/clients/inbox/review/anchors.d.ts +41 -0
  159. package/dist/clients/inbox/review/anchors.js +89 -0
  160. package/dist/clients/inbox/review/columns.d.ts +22 -0
  161. package/dist/clients/inbox/review/columns.js +40 -0
  162. package/dist/clients/inbox/review/comments-client.d.ts +66 -0
  163. package/dist/clients/inbox/review/comments-client.js +98 -0
  164. package/dist/clients/inbox/review/composition.d.ts +60 -0
  165. package/dist/clients/inbox/review/composition.js +278 -0
  166. package/dist/clients/inbox/review/document-region.d.ts +120 -0
  167. package/dist/clients/inbox/review/document-region.js +902 -0
  168. package/dist/clients/inbox/review/frame.d.ts +45 -0
  169. package/dist/clients/inbox/review/frame.js +215 -0
  170. package/dist/clients/inbox/review/keys.d.ts +67 -0
  171. package/dist/clients/inbox/review/keys.js +130 -0
  172. package/dist/clients/inbox/review/launch.d.ts +10 -0
  173. package/dist/clients/inbox/review/launch.js +129 -0
  174. package/dist/clients/inbox/review/live-source.d.ts +24 -0
  175. package/dist/clients/inbox/review/live-source.js +160 -0
  176. package/dist/clients/inbox/review/nvim.d.ts +13 -0
  177. package/dist/clients/inbox/review/nvim.js +42 -0
  178. package/dist/clients/inbox/review/remap.d.ts +51 -0
  179. package/dist/clients/inbox/review/remap.js +157 -0
  180. package/dist/clients/inbox/review/review-client.d.ts +30 -0
  181. package/dist/clients/inbox/review/review-client.js +107 -0
  182. package/dist/clients/inbox/review/roundtrip.d.ts +5 -0
  183. package/dist/clients/inbox/review/roundtrip.js +36 -0
  184. package/dist/clients/inbox/review/state.d.ts +143 -0
  185. package/dist/clients/inbox/review/state.js +282 -0
  186. package/dist/clients/inbox/review-adapter.d.ts +29 -0
  187. package/dist/clients/inbox/review-adapter.js +92 -0
  188. package/dist/clients/inbox/review-visibility.d.ts +5 -0
  189. package/dist/clients/inbox/review-visibility.js +19 -0
  190. package/dist/clients/inbox/surface.d.ts +19 -0
  191. package/dist/clients/inbox/surface.js +128 -0
  192. package/dist/clients/inbox/tui/ansi.d.ts +66 -0
  193. package/dist/clients/inbox/tui/ansi.js +317 -0
  194. package/dist/clients/inbox/tui/clipboard.d.ts +2 -0
  195. package/dist/clients/inbox/tui/clipboard.js +26 -0
  196. package/dist/clients/inbox/tui/input.d.ts +7 -0
  197. package/dist/clients/inbox/tui/input.js +653 -0
  198. package/dist/clients/inbox/tui/keys.d.ts +28 -0
  199. package/dist/clients/inbox/tui/keys.js +111 -0
  200. package/dist/clients/inbox/tui/panel.d.ts +2 -0
  201. package/dist/clients/inbox/tui/panel.js +217 -0
  202. package/dist/clients/inbox/tui/render.d.ts +37 -0
  203. package/dist/clients/inbox/tui/render.js +476 -0
  204. package/dist/clients/inbox/tui/types.d.ts +57 -0
  205. package/dist/clients/inbox/tui/types.js +1 -0
  206. package/dist/clients/inbox/tui.d.ts +6 -0
  207. package/dist/clients/inbox/tui.js +74 -0
  208. package/dist/clients/surfaces/host.d.ts +66 -0
  209. package/dist/clients/surfaces/host.js +1 -0
  210. package/dist/clients/surfaces/region.d.ts +16 -0
  211. package/dist/clients/surfaces/region.js +1 -0
  212. package/dist/clients/web/__tests__/push-engine.test.js +14 -158
  213. package/dist/clients/web/dev-server.d.ts +3 -3
  214. package/dist/clients/web/dev-server.js +9 -23
  215. package/dist/clients/web/events.js +4 -6
  216. package/dist/clients/web/push-engine.d.ts +2 -66
  217. package/dist/clients/web/push-engine.js +30 -161
  218. package/dist/clients/web/push-registry.d.ts +0 -4
  219. package/dist/clients/web/push-registry.js +2 -7
  220. package/dist/clients/web/server.js +3 -35
  221. package/dist/clients/web/web-client/shared/protocol.d.ts +0 -108
  222. package/dist/clients/web/web-cmd.js +2 -2
  223. package/dist/commands/__tests__/human.test.js +27 -15
  224. package/dist/commands/api-client.js +3 -3
  225. package/dist/commands/canvas-browse.js +1 -1
  226. package/dist/commands/canvas-issue.js +1 -1
  227. package/dist/commands/canvas-use.js +1 -1
  228. package/dist/commands/human/doc.d.ts +2 -0
  229. package/dist/commands/human/doc.js +98 -0
  230. package/dist/commands/human/inbox.d.ts +2 -0
  231. package/dist/commands/human/inbox.js +101 -0
  232. package/dist/commands/human/prompts.d.ts +2 -3
  233. package/dist/commands/human/prompts.js +27 -180
  234. package/dist/commands/human/queue.d.ts +1 -5
  235. package/dist/commands/human/queue.js +54 -216
  236. package/dist/commands/human/review.d.ts +2 -0
  237. package/dist/commands/human/review.js +298 -0
  238. package/dist/commands/human/shared.d.ts +2 -69
  239. package/dist/commands/human/shared.js +1 -181
  240. package/dist/commands/human.js +15 -20
  241. package/dist/commands/memory/__tests__/lint.test.d.ts +1 -0
  242. package/dist/commands/memory/__tests__/lint.test.js +15 -0
  243. package/dist/commands/memory/delete.js +2 -2
  244. package/dist/commands/memory/lint.d.ts +1 -0
  245. package/dist/commands/memory/lint.js +21 -3
  246. package/dist/commands/memory/read.js +4 -4
  247. package/dist/commands/node.js +8 -7
  248. package/dist/commands/pkg/plugin-manage.js +143 -68
  249. package/dist/commands/surface-inspect.js +2 -2
  250. package/dist/commands/surface.js +6 -12
  251. package/dist/commands/sys/__tests__/setup-core.test.js +9 -5
  252. package/dist/commands/sys/__tests__/setup-front-door.test.js +7 -42
  253. package/dist/commands/sys/config.js +29 -3
  254. package/dist/commands/sys/feedback.js +1 -1
  255. package/dist/commands/sys/setup-core.d.ts +12 -11
  256. package/dist/commands/sys/setup-core.js +55 -46
  257. package/dist/commands/sys/setup-wizard.d.ts +4 -1
  258. package/dist/commands/sys/setup-wizard.js +50 -9
  259. package/dist/commands/sys/sync-deps.js +1 -1
  260. package/dist/commands/sys.js +4 -6
  261. package/dist/core/__tests__/broker-sdk-wiring.test.js +6 -1
  262. package/dist/core/__tests__/broker-snapshot-history.test.js +9 -1
  263. package/dist/core/__tests__/dead-node-policy-table.test.js +3 -4
  264. package/dist/core/__tests__/fixtures/fake-engine.d.ts +8 -5
  265. package/dist/core/__tests__/fixtures/fake-engine.js +28 -2
  266. package/dist/core/__tests__/full/broker-provider-retry.test.js +87 -2
  267. package/dist/core/__tests__/full/broker-whip-interrupt.test.d.ts +1 -0
  268. package/dist/core/__tests__/full/broker-whip-interrupt.test.js +39 -0
  269. package/dist/core/__tests__/full/detach-focus.test.js +1 -1
  270. package/dist/core/__tests__/human-deliver-e2e.test.js +64 -64
  271. package/dist/core/__tests__/human-deliver.test.js +71 -345
  272. package/dist/core/__tests__/inline-memory-refs.test.js +2 -12
  273. package/dist/core/__tests__/memory-resolver-precedence.test.js +12 -15
  274. package/dist/core/__tests__/migration.test.js +10 -0
  275. package/dist/core/__tests__/model-pin-durability.test.js +14 -0
  276. package/dist/core/__tests__/phase4-review-migration.test.d.ts +1 -0
  277. package/dist/core/__tests__/phase4-review-migration.test.js +100 -0
  278. package/dist/core/__tests__/phase4-review-store.test.d.ts +1 -0
  279. package/dist/core/__tests__/phase4-review-store.test.js +143 -0
  280. package/dist/core/__tests__/remote-canvas-target.test.js +2 -2
  281. package/dist/core/__tests__/revive.test.js +22 -53
  282. package/dist/core/__tests__/session-cycles.test.js +48 -18
  283. package/dist/core/__tests__/spawn-no-kickoff.test.js +8 -12
  284. package/dist/core/__tests__/stop-guard.test.js +16 -1
  285. package/dist/core/__tests__/tmux-surface.test.js +21 -25
  286. package/dist/core/asset-root.d.ts +7 -0
  287. package/dist/core/asset-root.js +18 -0
  288. package/dist/core/broker-client/client.d.ts +0 -7
  289. package/dist/core/broker-client/client.js +4 -10
  290. package/dist/core/broker-client/transport-relay.d.ts +3 -4
  291. package/dist/core/broker-client/transport-relay.js +3 -4
  292. package/dist/core/canvas/__tests__/attention.test.js +1 -1
  293. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +1 -1
  294. package/dist/core/canvas/__tests__/remote-transport.test.d.ts +1 -0
  295. package/dist/core/{view/__tests__/transport-remote.test.js → canvas/__tests__/remote-transport.test.js} +2 -2
  296. package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
  297. package/dist/core/canvas/attention.js +4 -4
  298. package/dist/core/canvas/canvas.d.ts +6 -11
  299. package/dist/core/canvas/canvas.js +11 -33
  300. package/dist/core/canvas/db.js +207 -38
  301. package/dist/core/canvas/extensions.d.ts +1 -1
  302. package/dist/core/canvas/extensions.js +8 -1
  303. package/dist/core/canvas/index.d.ts +0 -1
  304. package/dist/core/canvas/index.js +0 -1
  305. package/dist/core/canvas/nav-model.d.ts +1 -1
  306. package/dist/core/canvas/nav-model.js +1 -1
  307. package/dist/core/canvas/paths.d.ts +5 -0
  308. package/dist/core/canvas/paths.js +16 -0
  309. package/dist/core/canvas/remote-canvas-source.js +2 -2
  310. package/dist/core/canvas/remote-transport.d.ts +7 -0
  311. package/dist/core/{view/transport-remote.js → canvas/remote-transport.js} +3 -4
  312. package/dist/core/canvas/source.js +1 -1
  313. package/dist/core/canvas/status-glyph.js +4 -12
  314. package/dist/core/canvas/types.d.ts +10 -35
  315. package/dist/core/clipboard-text.d.ts +8 -0
  316. package/dist/core/clipboard-text.js +18 -0
  317. package/dist/core/command-manifests/registry.d.ts +1 -3
  318. package/dist/core/command-plugins/bundle.d.ts +29 -0
  319. package/dist/core/command-plugins/bundle.js +198 -0
  320. package/dist/core/command-plugins/discovery.d.ts +0 -8
  321. package/dist/core/command-plugins/discovery.js +2 -39
  322. package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
  323. package/dist/core/command-plugins/transport/http-fetch.js +17 -95
  324. package/dist/core/command.d.ts +1 -13
  325. package/dist/core/command.js +1 -16
  326. package/dist/core/config.d.ts +4 -0
  327. package/dist/core/config.js +28 -3
  328. package/dist/core/document-lines.d.ts +4 -0
  329. package/dist/core/document-lines.js +6 -0
  330. package/dist/core/feed/feed.d.ts +2 -0
  331. package/dist/core/feed/feed.js +11 -7
  332. package/dist/core/human/__tests__/inbox-cancel-worker.d.ts +1 -0
  333. package/dist/core/human/__tests__/inbox-cancel-worker.js +7 -0
  334. package/dist/core/human/__tests__/inbox-claim-worker.d.ts +1 -0
  335. package/dist/core/human/__tests__/inbox-claim-worker.js +17 -0
  336. package/dist/core/human/__tests__/inbox-core.test.d.ts +1 -0
  337. package/dist/core/human/__tests__/inbox-core.test.js +144 -0
  338. package/dist/core/human/__tests__/inbox-stale-claim-worker.d.ts +1 -0
  339. package/dist/core/human/__tests__/inbox-stale-claim-worker.js +8 -0
  340. package/dist/core/human/__tests__/visible-paths.test.d.ts +1 -0
  341. package/dist/{clients/attach → core/human}/__tests__/visible-paths.test.js +9 -0
  342. package/dist/core/human/claim.d.ts +23 -0
  343. package/dist/core/human/claim.js +67 -0
  344. package/dist/core/human/convention.d.ts +30 -0
  345. package/dist/core/human/convention.js +202 -0
  346. package/dist/core/human/deck-factories.d.ts +8 -0
  347. package/dist/core/human/deck-factories.js +15 -0
  348. package/dist/core/human/deck-schema.d.ts +70 -0
  349. package/dist/core/human/deck-schema.js +92 -0
  350. package/dist/core/human/root.d.ts +7 -0
  351. package/dist/core/human/root.js +37 -0
  352. package/dist/core/human/scan.d.ts +3 -0
  353. package/dist/core/human/scan.js +76 -0
  354. package/dist/core/human/summary.d.ts +8 -0
  355. package/dist/core/human/summary.js +44 -0
  356. package/dist/core/human/tickets.d.ts +49 -0
  357. package/dist/core/human/tickets.js +190 -0
  358. package/dist/core/human/types.d.ts +136 -0
  359. package/dist/core/human/types.js +5 -0
  360. package/dist/{clients/attach → core/human}/visible-paths.d.ts +1 -1
  361. package/dist/{clients/attach → core/human}/visible-paths.js +3 -2
  362. package/dist/core/inspector/chrome.d.ts +6 -0
  363. package/dist/core/inspector/chrome.js +14 -0
  364. package/dist/core/inspector/contract.d.ts +105 -0
  365. package/dist/core/inspector/contract.js +1 -0
  366. package/dist/core/inspector/core.d.ts +4 -5
  367. package/dist/core/inspector/core.js +3 -5
  368. package/dist/core/inspector/host.d.ts +46 -0
  369. package/dist/core/{tui → inspector}/host.js +42 -208
  370. package/dist/core/inspector/text.d.ts +2 -2
  371. package/dist/core/inspector/transport.d.ts +6 -0
  372. package/dist/core/inspector/transport.js +36 -0
  373. package/dist/core/inspector/tui.d.ts +2 -2
  374. package/dist/core/inspector/tui.js +15 -15
  375. package/dist/core/keybindings/__tests__/inbox-affordance.test.js +11 -11
  376. package/dist/core/keybindings/__tests__/resolve.test.js +10 -19
  377. package/dist/core/keybindings/attach-control.d.ts +9 -3
  378. package/dist/core/keybindings/attach-control.js +3 -1
  379. package/dist/core/keybindings/catalog.d.ts +6 -5
  380. package/dist/core/keybindings/catalog.js +61 -133
  381. package/dist/core/keybindings/inbox.d.ts +5 -6
  382. package/dist/core/keybindings/inbox.js +9 -10
  383. package/dist/core/keybindings/index.d.ts +3 -3
  384. package/dist/core/keybindings/index.js +3 -3
  385. package/dist/core/keybindings/persistence.d.ts +5 -1
  386. package/dist/core/keybindings/persistence.js +24 -6
  387. package/dist/core/keybindings/resolve.d.ts +6 -0
  388. package/dist/core/keybindings/resolve.js +10 -3
  389. package/dist/core/memory/inline-ref-grammar.js +2 -3
  390. package/dist/core/memory-resolver.d.ts +27 -29
  391. package/dist/core/memory-resolver.js +45 -39
  392. package/dist/core/model-routes.d.ts +33 -0
  393. package/dist/core/model-routes.js +42 -3
  394. package/dist/core/preview-registry.js +17 -55
  395. package/dist/core/profiles/select.js +3 -3
  396. package/dist/core/{view/remote-canvas-target.d.ts → remote-canvas-target.d.ts} +2 -2
  397. package/dist/core/{view/remote-canvas-target.js → remote-canvas-target.js} +4 -4
  398. package/dist/core/review/__tests__/capture-origin.test.d.ts +1 -0
  399. package/dist/core/review/__tests__/capture-origin.test.js +139 -0
  400. package/dist/core/review/__tests__/comments.test.d.ts +1 -0
  401. package/dist/core/review/__tests__/comments.test.js +98 -0
  402. package/dist/core/review/__tests__/project.test.d.ts +1 -0
  403. package/dist/core/review/__tests__/project.test.js +71 -0
  404. package/dist/core/review/__tests__/remap.test.d.ts +1 -0
  405. package/dist/core/review/__tests__/remap.test.js +159 -0
  406. package/dist/core/review/__tests__/stage-identity.test.d.ts +1 -0
  407. package/dist/core/review/__tests__/stage-identity.test.js +140 -0
  408. package/dist/core/review/birth.d.ts +7 -0
  409. package/dist/core/review/birth.js +23 -0
  410. package/dist/core/review/comments.d.ts +69 -0
  411. package/dist/core/review/comments.js +295 -0
  412. package/dist/core/review/companion.d.ts +35 -0
  413. package/dist/core/review/companion.js +205 -0
  414. package/dist/core/review/document.d.ts +27 -0
  415. package/dist/core/review/document.js +138 -0
  416. package/dist/core/review/project.d.ts +13 -0
  417. package/dist/core/review/project.js +58 -0
  418. package/dist/core/review/realize.d.ts +19 -0
  419. package/dist/core/review/realize.js +217 -0
  420. package/dist/core/review/remap.d.ts +24 -0
  421. package/dist/core/review/remap.js +156 -0
  422. package/dist/core/review/signal.d.ts +5 -0
  423. package/dist/core/review/signal.js +31 -0
  424. package/dist/core/review/stage.d.ts +26 -0
  425. package/dist/core/review/stage.js +184 -0
  426. package/dist/core/review/store.d.ts +46 -0
  427. package/dist/core/review/store.js +163 -0
  428. package/dist/core/review/ticket-filter.d.ts +17 -0
  429. package/dist/core/review/ticket-filter.js +32 -0
  430. package/dist/core/review/types.d.ts +130 -0
  431. package/dist/core/review/types.js +23 -0
  432. package/dist/core/runtime/__tests__/session-visibility.test.d.ts +1 -0
  433. package/dist/core/runtime/__tests__/session-visibility.test.js +119 -0
  434. package/dist/core/runtime/bearings.d.ts +5 -17
  435. package/dist/core/runtime/bearings.js +88 -71
  436. package/dist/core/runtime/broker-protocol.d.ts +31 -8
  437. package/dist/core/runtime/broker.d.ts +15 -1
  438. package/dist/core/runtime/broker.js +573 -141
  439. package/dist/core/runtime/canvas-extensions.d.ts +4 -5
  440. package/dist/core/runtime/canvas-extensions.js +5 -5
  441. package/dist/core/runtime/front-door.js +4 -4
  442. package/dist/core/runtime/headless-pi.d.ts +24 -0
  443. package/dist/core/runtime/headless-pi.js +152 -0
  444. package/dist/core/runtime/kickoff.d.ts +1 -5
  445. package/dist/core/runtime/kickoff.js +1 -5
  446. package/dist/core/runtime/model-registry.d.ts +3 -0
  447. package/dist/core/runtime/model-registry.js +13 -0
  448. package/dist/core/runtime/naming-persist.d.ts +4 -0
  449. package/dist/core/runtime/naming-persist.js +27 -1
  450. package/dist/core/runtime/naming.d.ts +17 -12
  451. package/dist/core/runtime/naming.js +91 -140
  452. package/dist/core/runtime/node-read.js +19 -10
  453. package/dist/core/runtime/nodes.d.ts +9 -14
  454. package/dist/core/runtime/nodes.js +3 -53
  455. package/dist/core/runtime/package-health.d.ts +6 -21
  456. package/dist/core/runtime/package-health.js +12 -123
  457. package/dist/core/runtime/placement-tmux.d.ts +2 -1
  458. package/dist/core/runtime/placement-tmux.js +6 -4
  459. package/dist/core/runtime/placement.d.ts +1 -0
  460. package/dist/core/runtime/placement.js +4 -8
  461. package/dist/core/runtime/promote.js +3 -0
  462. package/dist/core/runtime/recap.d.ts +2 -7
  463. package/dist/core/runtime/recap.js +35 -69
  464. package/dist/core/runtime/revive.js +34 -77
  465. package/dist/core/runtime/session-cycles.d.ts +4 -0
  466. package/dist/core/runtime/session-cycles.js +14 -1
  467. package/dist/core/runtime/session-visibility.d.ts +37 -0
  468. package/dist/core/runtime/session-visibility.js +83 -0
  469. package/dist/core/runtime/spawn.js +10 -22
  470. package/dist/core/runtime/stop-guard.d.ts +1 -1
  471. package/dist/core/runtime/stop-guard.js +18 -13
  472. package/dist/core/runtime/surface-bg.js +1 -1
  473. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  474. package/dist/core/runtime/tmux-chrome.js +1 -1
  475. package/dist/core/runtime/tmux.d.ts +35 -7
  476. package/dist/core/runtime/tmux.js +439 -62
  477. package/dist/core/runtime/tool-group-summary.d.ts +19 -0
  478. package/dist/core/runtime/tool-group-summary.js +88 -0
  479. package/dist/core/scope.d.ts +2 -7
  480. package/dist/core/scope.js +9 -35
  481. package/dist/core/substrate/schema.d.ts +2 -2
  482. package/dist/core/termrender/code-doc.d.ts +12 -0
  483. package/dist/core/termrender/code-doc.js +92 -0
  484. package/dist/core/termrender/display.d.ts +12 -0
  485. package/dist/core/termrender/display.js +19 -0
  486. package/dist/core/termrender/termrender.d.ts +73 -0
  487. package/dist/core/termrender/termrender.js +795 -0
  488. package/dist/core/termrender/version.d.ts +1 -0
  489. package/dist/core/termrender/version.js +1 -0
  490. package/dist/core/tui/terminal.js +1 -2
  491. package/dist/core/user-settings.d.ts +71 -3
  492. package/dist/core/user-settings.js +83 -1
  493. package/dist/daemon/api/__tests__/inbox.test.js +44 -133
  494. package/dist/daemon/api/handlers/human.d.ts +0 -10
  495. package/dist/daemon/api/handlers/human.js +104 -702
  496. package/dist/daemon/api/handlers/inbox.js +113 -142
  497. package/dist/daemon/api/handlers/review-comments.d.ts +2 -0
  498. package/dist/daemon/api/handlers/review-comments.js +500 -0
  499. package/dist/daemon/api/handlers/reviews.d.ts +2 -0
  500. package/dist/daemon/api/handlers/reviews.js +287 -0
  501. package/dist/daemon/api/map.d.ts +22 -7
  502. package/dist/daemon/api/map.js +159 -11
  503. package/dist/daemon/api/server.js +4 -0
  504. package/dist/daemon/cron-run.js +6 -9
  505. package/dist/daemon/crtrd.js +17 -33
  506. package/dist/daemon/fleet.d.ts +1 -8
  507. package/dist/daemon/fleet.js +4 -30
  508. package/dist/daemon/human/finish.d.ts +14 -0
  509. package/dist/daemon/human/finish.js +115 -0
  510. package/dist/daemon/human/sweep.d.ts +6 -0
  511. package/dist/daemon/human/sweep.js +49 -0
  512. package/dist/daemon/messaging/node-message.d.ts +18 -0
  513. package/dist/daemon/messaging/node-message.js +70 -0
  514. package/dist/daemon/review/comment-notify.d.ts +26 -0
  515. package/dist/daemon/review/comment-notify.js +107 -0
  516. package/dist/daemon/review/deliver.d.ts +7 -0
  517. package/dist/daemon/review/deliver.js +56 -0
  518. package/dist/daemon/review/finish.d.ts +22 -0
  519. package/dist/daemon/review/finish.js +254 -0
  520. package/dist/daemon/review/sweep.d.ts +11 -0
  521. package/dist/daemon/review/sweep.js +120 -0
  522. package/dist/index.d.ts +2 -0
  523. package/dist/index.js +2 -2
  524. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +42 -64
  525. package/dist/pi-extensions/__tests__/canvas-recap.test.d.ts +1 -0
  526. package/dist/pi-extensions/__tests__/canvas-recap.test.js +159 -0
  527. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +16 -38
  528. package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
  529. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
  530. package/dist/pi-extensions/canvas-context-intro.d.ts +8 -11
  531. package/dist/pi-extensions/canvas-context-intro.js +50 -104
  532. package/dist/pi-extensions/canvas-recap.d.ts +15 -2
  533. package/dist/pi-extensions/canvas-recap.js +192 -69
  534. package/dist/pi-extensions/canvas-review-boundary.d.ts +59 -0
  535. package/dist/pi-extensions/canvas-review-boundary.js +81 -0
  536. package/dist/pi-extensions/canvas-stophook.d.ts +3 -3
  537. package/dist/pi-extensions/canvas-stophook.js +78 -128
  538. package/dist/pi-extensions/canvas-structured-output.js +52 -8
  539. package/dist/shared/generated-context.d.ts +29 -4
  540. package/dist/shared/generated-context.js +122 -7
  541. package/dist/shared/tool-groups.d.ts +23 -0
  542. package/dist/shared/tool-groups.js +60 -0
  543. package/dist/shared/working-activity.d.ts +8 -0
  544. package/dist/shared/working-activity.js +26 -0
  545. package/dist/types.d.ts +36 -2
  546. package/dist/types.js +51 -0
  547. package/dist/web-client/assets/index-CCPaLeW7.css +2 -0
  548. package/dist/web-client/assets/index-kyOWsnZK.js +86 -0
  549. package/dist/web-client/index.html +2 -2
  550. package/dist/web-client/sw.js +1 -1
  551. package/docs/compat/hearth-crtr-v4.md +2 -2
  552. package/docs/compat/hearth-crtr-v5.md +4 -2
  553. package/docs/public-api.md +3 -3
  554. package/package.json +21 -14
  555. package/runtime.lock.json +4661 -1453
  556. package/scripts/postinstall.mjs +15 -2
  557. package/dist/builtin-pi-packages/pi-mode-switch/README.md +0 -36
  558. package/dist/builtin-pi-packages/pi-mode-switch/bin/mode +0 -36
  559. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +0 -449
  560. package/dist/builtin-pi-packages/pi-mode-switch/package.json +0 -14
  561. package/dist/builtin-pi-packages/pi-mode-switch/tsconfig.json +0 -9
  562. package/dist/builtin-views/_lib/states.mjs +0 -161
  563. package/dist/builtin-views/canvas/core.mjs +0 -657
  564. package/dist/builtin-views/canvas/text.mjs +0 -58
  565. package/dist/builtin-views/canvas/tui.mjs +0 -168
  566. package/dist/builtin-views/canvas/web.jsx +0 -121
  567. package/dist/builtin-views/chat/core.mjs +0 -684
  568. package/dist/builtin-views/chat/text.mjs +0 -101
  569. package/dist/builtin-views/chat/tui.mjs +0 -360
  570. package/dist/builtin-views/chat/web.jsx +0 -362
  571. package/dist/builtin-views/git-pr/core.mjs +0 -673
  572. package/dist/builtin-views/git-pr/text.mjs +0 -84
  573. package/dist/builtin-views/git-pr/tui.mjs +0 -301
  574. package/dist/builtin-views/git-pr/web.jsx +0 -216
  575. package/dist/builtin-views/inbox/_lib/render.mjs +0 -175
  576. package/dist/builtin-views/inbox/core.mjs +0 -1273
  577. package/dist/builtin-views/inbox/text.mjs +0 -73
  578. package/dist/builtin-views/inbox/tui.mjs +0 -314
  579. package/dist/builtin-views/inbox/web.jsx +0 -188
  580. package/dist/builtin-views/linkedin/core.mjs +0 -906
  581. package/dist/builtin-views/linkedin/text.mjs +0 -69
  582. package/dist/builtin-views/linkedin/tui.mjs +0 -413
  583. package/dist/builtin-views/linkedin/web.jsx +0 -206
  584. package/dist/builtin-views/prompt-review/core.mjs +0 -735
  585. package/dist/builtin-views/prompt-review/text.mjs +0 -15
  586. package/dist/builtin-views/prompt-review/tui.mjs +0 -196
  587. package/dist/builtin-views/prompt-review/web.jsx +0 -484
  588. package/dist/builtin-views/workspace-sidebar/__tests__/core.test.js +0 -50
  589. package/dist/builtin-views/workspace-sidebar/__tests__/core.test.ts +0 -53
  590. package/dist/builtin-views/workspace-sidebar/core.mjs +0 -670
  591. package/dist/builtin-views/workspace-sidebar/text.mjs +0 -53
  592. package/dist/builtin-views/workspace-sidebar/tui.mjs +0 -141
  593. package/dist/builtin-views/workspace-sidebar/web.jsx +0 -109
  594. package/dist/clients/attach/session/mode.d.ts +0 -25
  595. package/dist/clients/attach/session/mode.js +0 -97
  596. package/dist/commands/sys/promptstudio.d.ts +0 -2
  597. package/dist/commands/sys/promptstudio.js +0 -65
  598. package/dist/commands/view-cycle.d.ts +0 -2
  599. package/dist/commands/view-cycle.js +0 -130
  600. package/dist/commands/view-list.d.ts +0 -2
  601. package/dist/commands/view-list.js +0 -66
  602. package/dist/commands/view-new.d.ts +0 -2
  603. package/dist/commands/view-new.js +0 -74
  604. package/dist/commands/view-pick.d.ts +0 -6
  605. package/dist/commands/view-pick.js +0 -129
  606. package/dist/commands/view-run.d.ts +0 -2
  607. package/dist/commands/view-run.js +0 -214
  608. package/dist/commands/view.d.ts +0 -2
  609. package/dist/commands/view.js +0 -31
  610. package/dist/commands/workspace.d.ts +0 -2
  611. package/dist/commands/workspace.js +0 -165
  612. package/dist/core/__tests__/chat-view-reconnect.test.js +0 -105
  613. package/dist/core/__tests__/full/consult-startup-failure.test.js +0 -124
  614. package/dist/core/canvas/__tests__/human-work-outbox.test.js +0 -123
  615. package/dist/core/canvas/human-work-outbox.d.ts +0 -73
  616. package/dist/core/canvas/human-work-outbox.js +0 -261
  617. package/dist/core/command-plugins/store.d.ts +0 -16
  618. package/dist/core/command-plugins/store.js +0 -64
  619. package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +0 -40
  620. package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
  621. package/dist/core/runtime/fault-recovery-nudge.js +0 -3
  622. package/dist/core/tui/__tests__/host-keybindings.test.js +0 -113
  623. package/dist/core/tui/host.d.ts +0 -66
  624. package/dist/core/view/__tests__/transport-cache.test.js +0 -62
  625. package/dist/core/view/bridge.d.ts +0 -10
  626. package/dist/core/view/bridge.js +0 -31
  627. package/dist/core/view/chrome.d.ts +0 -9
  628. package/dist/core/view/chrome.js +0 -22
  629. package/dist/core/view/contract.d.ts +0 -205
  630. package/dist/core/view/contract.js +0 -23
  631. package/dist/core/view/loader.d.ts +0 -31
  632. package/dist/core/view/loader.js +0 -188
  633. package/dist/core/view/stream-local.d.ts +0 -3
  634. package/dist/core/view/stream-local.js +0 -231
  635. package/dist/core/view/transport-cache.d.ts +0 -8
  636. package/dist/core/view/transport-cache.js +0 -38
  637. package/dist/core/view/transport-local.d.ts +0 -7
  638. package/dist/core/view/transport-local.js +0 -83
  639. package/dist/core/view/transport-remote.d.ts +0 -2
  640. package/dist/core/view/transport.d.ts +0 -10
  641. package/dist/core/view/transport.js +0 -15
  642. package/dist/pi-extensions/naming-tool.d.ts +0 -24
  643. package/dist/pi-extensions/naming-tool.js +0 -67
  644. package/dist/prompts/view.d.ts +0 -7
  645. package/dist/prompts/view.js +0 -172
  646. package/dist/web/ViewChrome.d.ts +0 -7
  647. package/dist/web/ViewChrome.js +0 -28
  648. package/dist/web/ViewPane.d.ts +0 -39
  649. package/dist/web/ViewPane.js +0 -48
  650. package/dist/web/index.d.ts +0 -8
  651. package/dist/web/index.js +0 -22
  652. package/dist/web/runtime.d.ts +0 -39
  653. package/dist/web/runtime.js +0 -232
  654. package/dist/web/states.d.ts +0 -24
  655. package/dist/web/states.js +0 -24
  656. package/dist/web/transport-http.d.ts +0 -5
  657. package/dist/web/transport-http.js +0 -34
  658. package/dist/web/transport-stream.d.ts +0 -3
  659. package/dist/web/transport-stream.js +0 -204
  660. package/dist/web-client/assets/index-U_NZ66VE.js +0 -79
  661. package/dist/web-client/assets/index-aaeu4adv.css +0 -2
  662. /package/dist/{builtin-views/workspace-sidebar/__tests__/core.test.d.ts → clients/attach/__tests__/file-review-focus.test.d.ts} +0 -0
  663. /package/dist/clients/attach/__tests__/{visible-paths.test.d.ts → group-activity.test.d.ts} +0 -0
  664. /package/dist/{core/__tests__/chat-view-reconnect.test.d.ts → clients/attach/__tests__/input-controller-extension-command.test.d.ts} +0 -0
  665. /package/dist/{core/__tests__/full/consult-startup-failure.test.d.ts → clients/attach/__tests__/session-identity-refresh.test.d.ts} +0 -0
  666. /package/dist/{core/canvas/__tests__/human-work-outbox.test.d.ts → clients/inbox/__tests__/inbox-controller.test.d.ts} +0 -0
  667. /package/dist/{core/keybindings/__tests__/bespoke-consumers.test.d.ts → clients/inbox/__tests__/mount-panel.test.d.ts} +0 -0
  668. /package/dist/{core/tui/__tests__/host-keybindings.test.d.ts → clients/inbox/review/__tests__/editor-roundtrip.test.d.ts} +0 -0
  669. /package/dist/{core/view/__tests__/transport-cache.test.d.ts → clients/inbox/review/__tests__/remap.test.d.ts} +0 -0
  670. /package/dist/{core/view/__tests__/transport-remote.test.d.ts → clients/inbox/review/__tests__/review-core.test.d.ts} +0 -0
@@ -0,0 +1,920 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createAssistantMessageEventStream, } from "@earendil-works/pi-ai";
3
+ import { streamSimpleAnthropic, streamSimpleOpenAICodexResponses } from "@earendil-works/pi-ai/compat";
4
+ import { ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID, INVALID_REFRESH_TOKEN_BACKOFF_MS, formatStatusLine, getProviderLabel, isManagedProvider, isInvalidRefreshTokenError, addSubscription, enrichManagedLoginIdentity, markSubscriptionAuthenticationFailed, markSubscriptionRateLimited, markSubscriptionSuccess, parseRetryAfterHeader, promoteSubscription, readRotationConfig, readSubscriptionPool, refreshSubscriptionCredential, removeManagedAccount, resolveProviderCandidates, upsertSubscriptionWithUniqueAccount, } from "../../../core/subscription-state.js";
5
+ import { readMergedLaunchConfig } from "../../../core/config.js";
6
+ import { expandModelCandidates, modelRequestFromConfig, probeRouteAvailability } from "../../../core/model-routes.js";
7
+ import { ManagedProviderCoolingError } from "../../../core/runtime/managed-provider-cooling.js";
8
+ // A response without Retry-After proves only that THIS attempt was throttled; it
9
+ // does not authorize crouter to invent a five-minute provider-wide outage. Keep
10
+ // a short turn-local probe backoff to avoid an immediate retry loop, then let a
11
+ // fresh turn prove whether the credential is usable again. Real
12
+ // server-provided Retry-After deadlines are still honored exactly.
13
+ const DEFAULT_RATE_LIMIT_BACKOFF_MS = 15 * 1000;
14
+ // Short in-place retry budget for a transient network/5xx blip on the same credential.
15
+ // Once this budget is exhausted, surface the error and rotate to another candidate.
16
+ const TRANSIENT_RETRY_BACKOFFS_MS = [250, 750];
17
+ // After the in-place transient retry budget is exhausted, rotate instead of failing: the
18
+ // credential itself is probably healthy (this is a provider-side blip like "overloaded"),
19
+ // so cool it down only briefly -- long enough to rotate to another credential / fallback
20
+ // provider, short enough that it comes back quickly if it was the only option.
21
+ const TRANSIENT_ROTATE_COOLDOWN_MS = 60 * 1000;
22
+ // A few short outer retries absorb a normal multi-broker collision; if the metadata file
23
+ // remains busy, the provider call still proceeds. Subscription bookkeeping must never turn a
24
+ // healthy model response into a fatal generation fault.
25
+ const SUBSCRIPTION_LOCK_RETRY_BACKOFFS_MS = [25, 75, 150];
26
+ function createProviderRotationState(options) {
27
+ return { delegatedProviderStreams: new Map(), ...(options === undefined ? {} : { options: { ...options } }) };
28
+ }
29
+ function configuredValue(state, key, envName) {
30
+ return state.options === undefined ? process.env[envName] : state.options[key];
31
+ }
32
+ const TRANSIENT_MESSAGE_PATTERN = /connection|network|fetch failed|socket|econnreset|econnrefused|enotfound|etimedout|timed? out|timeout|overloaded|service.?unavailable|temporarily unavailable/i;
33
+ const RATE_LIMIT_MESSAGE_PATTERN = /\b429\b|rate.?limit|too many requests/i;
34
+ // Precise terminal usage-limit signals (as opposed to the bare `usage`/`quota` substrings
35
+ // dropped above): pi-ai's Codex adapter surfaces `usage_limit_reached` / `usage_not_included`
36
+ // as the friendly "You have hit your ChatGPT usage limit..." message, often on a non-429
37
+ // status, so the status-code check above misses it. These are specific enough not to false-
38
+ // positive on ordinary assistant text mentioning "usage".
39
+ const USAGE_LIMIT_MESSAGE_PATTERN = /usage[ _]limit|usage_not_included|usagelimiterror|insufficient_quota|quota exceeded/i;
40
+ function classifyProviderError(error, observed) {
41
+ const value = error;
42
+ const errorStatus = typeof value?.status === "number" ? value.status : undefined;
43
+ const status = observed.status ?? errorStatus;
44
+ const message = [value?.errorMessage, value?.message, error instanceof Error ? error.message : ""]
45
+ .filter((part) => typeof part === "string" && part.length > 0)
46
+ .join(" ");
47
+ const hadRetryAfterHeader = observed.retryAfterMs !== undefined;
48
+ if (status === 429)
49
+ return { kind: "rate_limit", status, reason: "http 429", hadRetryAfterHeader };
50
+ if (status === 503 && hadRetryAfterHeader)
51
+ return { kind: "rate_limit", status, reason: "http 503 with retry-after", hadRetryAfterHeader };
52
+ if (status === 500 || status === 502 || status === 503 || status === 504) {
53
+ return { kind: "transient", status, reason: `http ${status}`, hadRetryAfterHeader };
54
+ }
55
+ const transientMatch = message.match(TRANSIENT_MESSAGE_PATTERN);
56
+ if (transientMatch)
57
+ return { kind: "transient", status, reason: transientMatch[0].toLowerCase(), hadRetryAfterHeader };
58
+ const rateLimitMatch = message.match(RATE_LIMIT_MESSAGE_PATTERN);
59
+ if (rateLimitMatch)
60
+ return { kind: "rate_limit", status, reason: rateLimitMatch[0].toLowerCase(), hadRetryAfterHeader };
61
+ const usageLimitMatch = message.match(USAGE_LIMIT_MESSAGE_PATTERN);
62
+ if (usageLimitMatch)
63
+ return { kind: "rate_limit", status, reason: usageLimitMatch[0].toLowerCase(), hadRetryAfterHeader };
64
+ return { kind: "fatal", status, reason: "unclassified", hadRetryAfterHeader };
65
+ }
66
+ function providerErrorMessage(error) {
67
+ const value = error;
68
+ const message = typeof value?.errorMessage === "string" && value.errorMessage.trim()
69
+ ? value.errorMessage.trim()
70
+ : typeof value?.message === "string" && value.message.trim()
71
+ ? value.message.trim()
72
+ : error instanceof Error && error.message.trim()
73
+ ? error.message.trim()
74
+ : undefined;
75
+ return message;
76
+ }
77
+ async function defaultSleep(ms) {
78
+ await new Promise((resolve) => setTimeout(resolve, ms));
79
+ }
80
+ // Indirection so tests can inject deterministic waits without real timers.
81
+ let activeSleep = defaultSleep;
82
+ /** Test-only seam: override the sleep implementation used for in-place/wait-out retries. Pass undefined to restore the default. */
83
+ export function __setSleepForTest(fn) {
84
+ activeSleep = fn ?? defaultSleep;
85
+ }
86
+ function isSubscriptionLockContention(error) {
87
+ const value = error;
88
+ return value?.code === "ELOCKED" || (typeof value?.message === "string" && /lock file is already being held/i.test(value.message));
89
+ }
90
+ async function recordSubscriptionMutation(description, mutate) {
91
+ for (let attempt = 0;; attempt++) {
92
+ try {
93
+ await mutate();
94
+ return true;
95
+ }
96
+ catch (error) {
97
+ if (!isSubscriptionLockContention(error))
98
+ throw error;
99
+ const backoffMs = SUBSCRIPTION_LOCK_RETRY_BACKOFFS_MS[attempt];
100
+ if (backoffMs === undefined) {
101
+ logRotation(`subscription metadata lock remained busy while ${description}; continuing without failing the provider turn`);
102
+ return false;
103
+ }
104
+ await activeSleep(backoffMs);
105
+ }
106
+ }
107
+ }
108
+ /** Test-only seam for the bounded, non-fatal subscription bookkeeping path. */
109
+ export async function __recordSubscriptionMutationForTest(description, mutate) {
110
+ return recordSubscriptionMutation(description, mutate);
111
+ }
112
+ // A user/host abort (ESC, a critical-tier interrupt, a torn-down window) is NOT a provider
113
+ // failure — it must never cool down a credential or rotate providers. The upstream pi-ai
114
+ // providers set `reason: "aborted"` / `stopReason: "aborted"` (and throw "Request was aborted")
115
+ // ONLY when the request's AbortSignal fired, so those markers alone are a reliable abort
116
+ // signal; we do NOT require the signal to be threaded back through our own options (it may not
117
+ // be), which is what made an ESC drain both provider pools.
118
+ function isUserAbortEvent(event, signal) {
119
+ if (signal?.aborted)
120
+ return true;
121
+ if (event.type !== "error")
122
+ return false;
123
+ return event.reason === "aborted" || event.error.stopReason === "aborted" || /request was aborted/i.test(event.error.errorMessage ?? "");
124
+ }
125
+ function isUserAbortError(error, signal) {
126
+ if (signal?.aborted)
127
+ return true;
128
+ const value = error;
129
+ const message = [value?.errorMessage, value?.message, error instanceof Error ? error.message : String(error ?? "")]
130
+ .filter((part) => typeof part === "string" && part.length > 0)
131
+ .join(" ");
132
+ return (error instanceof Error && /abort/i.test(error.name)) || /request was aborted|operation was aborted|the user aborted/i.test(message);
133
+ }
134
+ function setStatus(state, text) {
135
+ state.runtimeContext?.ui.setStatus("provider-rotation", text);
136
+ }
137
+ function logRotation(message) {
138
+ process.stderr.write(`[provider-rotation] ${message}\n`);
139
+ }
140
+ function getCurrentModelRef(model) {
141
+ return isManagedProvider(model?.provider) ? { providerId: model.provider, modelId: model.id } : undefined;
142
+ }
143
+ function toAssistantError(model, errorMessage) {
144
+ return {
145
+ role: "assistant",
146
+ content: [],
147
+ api: model.api,
148
+ provider: model.provider,
149
+ model: model.id,
150
+ usage: {
151
+ input: 0,
152
+ output: 0,
153
+ cacheRead: 0,
154
+ cacheWrite: 0,
155
+ totalTokens: 0,
156
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
157
+ },
158
+ stopReason: "error",
159
+ errorMessage,
160
+ timestamp: Date.now(),
161
+ };
162
+ }
163
+ function retryAfterMs(headers) {
164
+ const retryAfterMsHeader = headers["retry-after-ms"];
165
+ if (retryAfterMsHeader) {
166
+ const millis = Number(retryAfterMsHeader);
167
+ if (Number.isFinite(millis))
168
+ return Math.max(0, millis);
169
+ }
170
+ return parseRetryAfterHeader(headers["retry-after"]);
171
+ }
172
+ // Extract an account identity supplied directly by the OAuth credential.
173
+ function loginAccountId(cred) {
174
+ return typeof cred.accountId === "string" && cred.accountId.trim() ? cred.accountId.trim() : undefined;
175
+ }
176
+ // Short, display-only rendering of a Codex account id for `/provider-sub list`, so distinct
177
+ // accounts are observable without dumping the full opaque id.
178
+ function shortAccountId(accountId) {
179
+ return accountId.length > 12 ? `${accountId.slice(0, 8)}\u2026${accountId.slice(-4)}` : accountId;
180
+ }
181
+ // The registered provider exposes its OAuth flow through `provider.auth.oauth`. Composed managed
182
+ // providers retain that flow, so the session registry supplies the Anthropic and Codex flows.
183
+ function oauthFlow(providerId, ctx) {
184
+ const oauth = ctx?.modelRegistry.getProvider(providerId)?.auth.oauth;
185
+ if (!oauth)
186
+ throw new Error(`${getProviderLabel(providerId)} has no OAuth flow registered`);
187
+ return oauth;
188
+ }
189
+ // `refresh` is a pure token exchange that reads only `credential.refresh` and persists
190
+ // nothing; crouter records the rotated token in the selected pool row.
191
+ async function defaultRefreshForProvider(state, providerId, refreshToken) {
192
+ return oauthFlow(providerId, state.runtimeContext).refresh({ type: "oauth", refresh: refreshToken, access: "", expires: 0 });
193
+ }
194
+ // Indirection so tests can inject refresh failures without real network calls.
195
+ let activeRefreshForProvider;
196
+ /** Test-only seam: override the per-provider refresh function. Pass undefined to restore the default. */
197
+ export function __setRefreshForProviderForTest(fn) {
198
+ activeRefreshForProvider = fn;
199
+ }
200
+ function openBrowser(url) {
201
+ const [cmd, args] = process.platform === "darwin"
202
+ ? ["open", [url]]
203
+ : process.platform === "win32"
204
+ ? ["cmd", ["/c", "start", "", url]]
205
+ : ["xdg-open", [url]];
206
+ try {
207
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
208
+ child.on("error", () => {
209
+ /* best effort -- the manual-code dialog's title carries the URL too */
210
+ });
211
+ child.unref();
212
+ }
213
+ catch {
214
+ /* best effort */
215
+ }
216
+ }
217
+ // Every account refreshes the same way: the pool holds its value, so an expired token is a
218
+ // token exchange serialized on the pool file's lock. A still-fresh access token takes the
219
+ // side-effect-free fast path and never joins the lock. We do NOT route through pi's public
220
+ // `getApiKey`, which wraps pi-ai's `getOAuthApiKey` and collapses a refresh failure into a
221
+ // generic message; calling `activeRefreshForProvider` directly keeps the real error intact so
222
+ // `isInvalidRefreshTokenError` can classify it (cooldown -> reauth -> cross-provider-fallback).
223
+ async function refreshCredentialIfNeeded(state, providerId, credential) {
224
+ if (!credential.refresh || !credential.access || credential.expires === undefined) {
225
+ throw new Error(`${getProviderLabel(providerId)} subscription "${credential.label}" is missing its credential value`);
226
+ }
227
+ if (credential.expires > Date.now())
228
+ return credential;
229
+ const refreshed = await refreshSubscriptionCredential(providerId, credential.label, (token) => activeRefreshForProvider?.(providerId, token) ?? defaultRefreshForProvider(state, providerId, token));
230
+ return { ...credential, ...refreshed };
231
+ }
232
+ /** Test-only seam: exercises the real `refreshCredentialIfNeeded` (including its locked refresh
233
+ * transaction) without duplicating its logic. */
234
+ export async function __refreshCredentialIfNeededForTest(providerId, credential) {
235
+ return refreshCredentialIfNeeded(createProviderRotationState(), providerId, credential);
236
+ }
237
+ async function defaultLoginForProvider(providerId, ctx) {
238
+ return loginProvider(providerId, ctx);
239
+ }
240
+ let activeLoginForProvider = defaultLoginForProvider;
241
+ /** Test-only seam: override the per-provider login function. Pass undefined to restore the default. */
242
+ export function __setLoginForProviderForTest(fn) {
243
+ activeLoginForProvider = fn ?? defaultLoginForProvider;
244
+ }
245
+ async function reauthenticateCredential(state, providerId, credential) {
246
+ const ctx = state.runtimeContext;
247
+ if (!ctx?.hasUI)
248
+ return undefined;
249
+ ctx.ui.notify(`${getProviderLabel(providerId)} subscription "${credential.label}" needs re-authentication. Opening OAuth login...`, "warning");
250
+ const authenticated = await activeLoginForProvider(providerId, ctx);
251
+ const identity = await enrichManagedLoginIdentity(providerId, authenticated);
252
+ // A failed Anthropic profile lookup leaves identity unknown. Do not carry the old row's identity
253
+ // onto newly issued tokens: it could describe a different account and bypass that account's
254
+ // duplicate check. Codex supplies its identity directly, so it can retain its existing claim
255
+ // only when that provider's login result omits one.
256
+ const reauthedAccountId = identity.accountId ?? (providerId === ANTHROPIC_PROVIDER_ID ? undefined : loginAccountId(authenticated) ?? credential.accountId);
257
+ const { authFailure: _authFailure, ...healthyCredential } = credential;
258
+ const next = {
259
+ ...healthyCredential,
260
+ refresh: authenticated.refresh,
261
+ access: authenticated.access,
262
+ expires: authenticated.expires,
263
+ ...(reauthedAccountId ? { accountId: reauthedAccountId } : {}),
264
+ rateLimitedUntil: 0,
265
+ lastAttemptAt: Date.now(),
266
+ lastRateLimitedAt: credential.lastRateLimitedAt,
267
+ };
268
+ // Identity-checked upsert: reauthenticating this explicit slot into an account already held by
269
+ // ANOTHER slot throws DuplicateSubscriptionError rather than creating a duplicate quota entry.
270
+ return ((await upsertSubscriptionWithUniqueAccount(providerId, next)).find((entry) => entry.label === next.label) ?? next);
271
+ }
272
+ function defaultStreamForProvider(model, context, options) {
273
+ if (model.provider === ANTHROPIC_PROVIDER_ID) {
274
+ return streamSimpleAnthropic(model, context, options);
275
+ }
276
+ return streamSimpleOpenAICodexResponses(model, context, options);
277
+ }
278
+ // Indirection so tests can inject a fake per-provider stream without real network calls.
279
+ let activeStreamForProvider = defaultStreamForProvider;
280
+ function streamForProvider(model, context, options) {
281
+ return activeStreamForProvider(model, context, options);
282
+ }
283
+ /** Test-only seam: override the per-provider stream factory. Pass undefined to restore the default. */
284
+ export function __setStreamForProviderForTest(fn) {
285
+ activeStreamForProvider = fn ?? defaultStreamForProvider;
286
+ }
287
+ /** Test-only seam: exercise `fallbackRejectionHint` for one rejection discriminant
288
+ * (with an optional resolved-but-rejected candidate) without staging a full turn. */
289
+ // Drives one credential, retrying transient (non-rate-limit) failures in place on the
290
+ // same credential and reporting a classified result for rotation logging.
291
+ // A genuine rate limit ends the attempt immediately (the caller cools the credential down
292
+ // and rotates); a transient blip gets a short in-place retry budget before giving up.
293
+ async function streamProviderAttempt(state, providerId, model, context, options, credential, outer) {
294
+ for (let attempt = 1;; attempt++) {
295
+ const attemptAt = Date.now();
296
+ let observedStatus;
297
+ let requestedRetryAfterMs;
298
+ const streamOptions = {
299
+ ...options,
300
+ ...(credential ? { apiKey: credential.access } : {}),
301
+ onResponse: async (response, responseModel) => {
302
+ observedStatus = response.status;
303
+ if (response.status === 429 || response.status === 503) {
304
+ requestedRetryAfterMs = retryAfterMs(response.headers);
305
+ }
306
+ await options?.onResponse?.(response, responseModel);
307
+ },
308
+ };
309
+ const provider = state.runtimeContext?.modelRegistry.getProvider?.(model.provider);
310
+ const inner = credential
311
+ ? streamForProvider(model, context, streamOptions)
312
+ : state.delegatedProviderStreams.get(providerId)?.(model, context, streamOptions)
313
+ ?? provider?.streamSimple(model, context, streamOptions)
314
+ ?? streamForProvider(model, context, streamOptions);
315
+ const bufferedEvents = [];
316
+ let sawContent = false;
317
+ let committed = false;
318
+ let classification;
319
+ let failureMessage;
320
+ // Events are buffered ONLY until this attempt commits to the provider -- i.e. until the
321
+ // first real content event (thinking/text/toolcall) arrives, after which rotating away is
322
+ // no longer possible. Before commit the buffer lets a clean pre-content rate-limit rotate
323
+ // to another credential with nothing yet shown. Once committed we flush and forward every
324
+ // subsequent event LIVE, so viewers stream token-by-token instead of receiving the whole
325
+ // message at once when the turn finally completes.
326
+ const emit = (event) => {
327
+ if (committed)
328
+ outer.push(event);
329
+ else
330
+ bufferedEvents.push(event);
331
+ };
332
+ const flush = () => {
333
+ for (const event of bufferedEvents)
334
+ outer.push(event);
335
+ bufferedEvents.length = 0;
336
+ committed = true;
337
+ };
338
+ try {
339
+ for await (const event of inner) {
340
+ if (event.type === "error") {
341
+ if (isUserAbortEvent(event, options?.signal)) {
342
+ emit(event);
343
+ flush();
344
+ return { result: "fatal", attemptAt };
345
+ }
346
+ failureMessage = providerErrorMessage(event.error);
347
+ classification = classifyProviderError(event.error, { status: observedStatus, retryAfterMs: requestedRetryAfterMs });
348
+ if (classification.kind === "fatal" || sawContent) {
349
+ emit(event);
350
+ flush();
351
+ return { result: "fatal", attemptAt, classification };
352
+ }
353
+ break;
354
+ }
355
+ if (event.type !== "start" && event.type !== "done")
356
+ sawContent = true;
357
+ emit(event);
358
+ // First content event: commit to this provider -- flush the buffer and switch to live
359
+ // forwarding so the rest of the turn streams incrementally.
360
+ if (sawContent && !committed)
361
+ flush();
362
+ }
363
+ }
364
+ catch (error) {
365
+ if (isUserAbortError(error, options?.signal)) {
366
+ emit({
367
+ type: "error",
368
+ reason: "aborted",
369
+ error: { ...toAssistantError(model, error instanceof Error ? error.message : String(error)), stopReason: "aborted" },
370
+ });
371
+ flush();
372
+ return { result: "fatal", attemptAt };
373
+ }
374
+ failureMessage = providerErrorMessage(error);
375
+ classification = classifyProviderError(error, { status: observedStatus, retryAfterMs: requestedRetryAfterMs });
376
+ if (classification.kind === "fatal" || sawContent) {
377
+ emit({
378
+ type: "error",
379
+ reason: "error",
380
+ error: toAssistantError(model, error instanceof Error ? error.message : String(error)),
381
+ });
382
+ flush();
383
+ return { result: "fatal", attemptAt, classification };
384
+ }
385
+ }
386
+ if (!classification && requestedRetryAfterMs !== undefined && !sawContent) {
387
+ // Some providers signal a 429/503 purely via the response header without ever raising
388
+ // a stream "error" event; treat that the same as an explicit rate-limit classification.
389
+ classification = { kind: "rate_limit", status: observedStatus, reason: "retry-after header on success path", hadRetryAfterHeader: true };
390
+ }
391
+ if (!classification) {
392
+ flush();
393
+ return { result: "success", attemptAt };
394
+ }
395
+ if (classification.kind === "rate_limit") {
396
+ return { result: "rate_limited", retryAfterMs: requestedRetryAfterMs, attemptAt, classification };
397
+ }
398
+ // Transient: retry the SAME credential/provider in place first -- no cooldown for the
399
+ // quick in-place budget.
400
+ const backoffMs = TRANSIENT_RETRY_BACKOFFS_MS[attempt - 1];
401
+ if (backoffMs === undefined) {
402
+ // In-place retry budget exhausted. Rather than hard-failing the turn, hand back a
403
+ // transient-exhausted result so the caller surfaces the error to the user AND rotates
404
+ // to another credential / fallback provider. Nothing is emitted into the stream here:
405
+ // the buffered (pre-content) events are discarded exactly like the rate-limit path, so
406
+ // the rotation target streams a clean turn.
407
+ return { result: "transient_exhausted", attemptAt, classification, attempts: attempt, failureMessage };
408
+ }
409
+ 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})`);
410
+ await activeSleep(backoffMs);
411
+ }
412
+ }
413
+ function emitTerminalError(stream, model, message) {
414
+ stream.push({ type: "error", reason: "error", error: toAssistantError(model, message) });
415
+ stream.end();
416
+ }
417
+ function enqueueFreshRouteCandidates(candidates, queuedRouteIds, knownRoutes, seenReadyLabels, cooldownAttempts, localCoolingDeadlines) {
418
+ const now = Date.now();
419
+ for (const [routeId, candidate] of knownRoutes) {
420
+ if (queuedRouteIds.has(routeId) || (localCoolingDeadlines.get(routeId) ?? 0) > now || !isManagedProvider(candidate.providerId))
421
+ continue;
422
+ const seenLabels = seenReadyLabels.get(routeId) ?? new Set();
423
+ const hasFreshCredential = readSubscriptionPool(candidate.providerId).some((credential) => {
424
+ if (credential.authFailure !== undefined || (credential.rateLimitedUntil || 0) > now)
425
+ return false;
426
+ const cooldownAttempt = cooldownAttempts.get(`${routeId}/${credential.label}`);
427
+ return !seenLabels.has(credential.label) || (cooldownAttempt !== undefined && credential.lastAttemptAt > cooldownAttempt);
428
+ });
429
+ if (hasFreshCredential) {
430
+ candidates.push(candidate);
431
+ queuedRouteIds.add(routeId);
432
+ }
433
+ }
434
+ }
435
+ async function switchToRoute(pi, ctx, candidate) {
436
+ if (ctx.model?.provider !== candidate.providerId || ctx.model.id !== candidate.model.id) {
437
+ if (!await pi.setModel(candidate.model))
438
+ return false;
439
+ }
440
+ pi.setThinkingLevel((candidate.thinkingLevel ?? 'off'));
441
+ return ctx.model?.provider === candidate.providerId && ctx.model.id === candidate.model.id;
442
+ }
443
+ function logRouteQueueAdvance(from, to, result) {
444
+ 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`);
445
+ }
446
+ // Drive the deterministic route queue from the live rank. Managed routes walk
447
+ // their pool rows; every other declared route is one configured credential.
448
+ async function runManagedProvider(state, model, context, options, stream) {
449
+ const liveThinking = state.runtimeContext?.thinkingLevel === undefined || state.runtimeContext?.thinkingLevel === 'off'
450
+ ? undefined
451
+ : state.runtimeContext.thinkingLevel;
452
+ const request = modelRequestFromConfig(`${model.provider}/${model.id}${liveThinking ? `:${liveThinking}` : ''}`, configuredValue(state, 'modelIntent', 'CRTR_MODEL_INTENT'), false);
453
+ const expanded = request ? expandModelCandidates(request, configuredValue(state, 'nodeCwd', 'CRTR_NODE_CWD'), configuredValue(state, 'profileId', 'CRTR_PROFILE_ID')) : [];
454
+ const liveRank = expanded.find((candidate) => candidate.providerId === model.provider && candidate.modelId === model.id && candidate.thinkingLevel === liveThinking)?.rank ?? 0;
455
+ const routeCandidates = expanded.slice(liveRank)
456
+ .map((route) => {
457
+ const registry = state.runtimeContext?.modelRegistry;
458
+ const routeModel = registry?.find(route.providerId, route.modelId);
459
+ if (!registry || !routeModel)
460
+ return undefined;
461
+ const availability = probeRouteAvailability(route, registry);
462
+ if (!availability.registered || (!isManagedProvider(route.providerId) && !availability.authenticated))
463
+ return undefined;
464
+ return { routeId: route.routeId, credentialSource: route.credentialSource, rank: route.rank, providerId: route.providerId, model: routeModel, thinkingLevel: route.thinkingLevel };
465
+ })
466
+ .filter((candidate) => candidate !== undefined);
467
+ const isLiveRoute = (candidate) => candidate.providerId === model.provider && candidate.model.id === model.id && candidate.thinkingLevel === liveThinking;
468
+ // A cooling or unauthenticated managed route contributes its diagnostic and may
469
+ // later re-enter when a credential becomes ready, but it is never selected now.
470
+ const candidates = routeCandidates.filter((candidate) => isLiveRoute(candidate) || !isManagedProvider(candidate.providerId) || readSubscriptionPool(candidate.providerId).some((entry) => entry.authFailure === undefined && (entry.rateLimitedUntil || 0) <= Date.now()));
471
+ if (!candidates.some(isLiveRoute)) {
472
+ candidates.unshift({ routeId: `live:${model.provider}/${model.id}`, credentialSource: 'live', rank: liveRank, providerId: model.provider, model, thinkingLevel: liveThinking });
473
+ }
474
+ const queuedRouteIds = new Set(candidates.map((candidate) => candidate.routeId));
475
+ const knownRoutes = new Map([...routeCandidates, ...candidates].map((candidate) => [candidate.routeId, candidate]));
476
+ const attemptedRoutes = new Set();
477
+ const authFailures = new Set();
478
+ const localCoolingDeadlines = new Map();
479
+ const seenReadyLabels = new Map();
480
+ const cooldownAttempts = new Map();
481
+ let terminalModel = model;
482
+ let lastTransientFailure;
483
+ for (let candidate = candidates.shift(); candidate !== undefined; candidate = candidates.shift()) {
484
+ const { routeId, providerId, model: candidateModel } = candidate;
485
+ queuedRouteIds.delete(routeId);
486
+ knownRoutes.set(routeId, candidate);
487
+ attemptedRoutes.add(routeId);
488
+ terminalModel = candidateModel;
489
+ if (!await switchToRoute(state.extensionAPI, state.runtimeContext, candidate)) {
490
+ emitTerminalError(stream, candidateModel, `Provider route ${routeId} is unavailable: failed to select ${providerId}/${candidateModel.id}`);
491
+ return;
492
+ }
493
+ if (!isManagedProvider(providerId)) {
494
+ const result = await streamProviderAttempt(state, providerId, candidateModel, context, options, undefined, stream);
495
+ if (result.result === 'success') {
496
+ stream.end();
497
+ return;
498
+ }
499
+ if (result.result === 'fatal') {
500
+ stream.end();
501
+ return;
502
+ }
503
+ const cooldownMs = result.result === 'rate_limited' ? result.retryAfterMs ?? DEFAULT_RATE_LIMIT_BACKOFF_MS : TRANSIENT_ROTATE_COOLDOWN_MS;
504
+ if (result.result === 'transient_exhausted')
505
+ lastTransientFailure = result;
506
+ localCoolingDeadlines.set(routeId, Math.max(localCoolingDeadlines.get(routeId) ?? 0, Date.now() + cooldownMs));
507
+ logRouteQueueAdvance(candidate, candidates[0], result);
508
+ continue;
509
+ }
510
+ const rawPool = readSubscriptionPool(providerId);
511
+ for (const entry of rawPool)
512
+ if (entry.authFailure === 'invalid_grant')
513
+ authFailures.add(`${providerId}/${entry.label}`);
514
+ const available = rawPool.filter((entry) => entry.authFailure === undefined && (!entry.rateLimitedUntil || entry.rateLimitedUntil <= Date.now()));
515
+ const seenLabels = seenReadyLabels.get(routeId) ?? new Set();
516
+ for (const credential of available)
517
+ seenLabels.add(credential.label);
518
+ seenReadyLabels.set(routeId, seenLabels);
519
+ let advanceResult;
520
+ for (const credential of available) {
521
+ let freshCredential;
522
+ try {
523
+ freshCredential = await refreshCredentialIfNeeded(state, providerId, credential);
524
+ }
525
+ catch (error) {
526
+ if (!isInvalidRefreshTokenError(error))
527
+ throw error;
528
+ let reauthenticated;
529
+ try {
530
+ reauthenticated = await reauthenticateCredential(state, providerId, credential);
531
+ }
532
+ catch (loginError) {
533
+ state.runtimeContext?.ui.notify(`${getProviderLabel(providerId)} re-authentication failed: ${loginError instanceof Error ? loginError.message : String(loginError)}`, 'error');
534
+ }
535
+ if (!reauthenticated) {
536
+ const attemptAt = Date.now();
537
+ await recordSubscriptionMutation(`recording ${getProviderLabel(providerId)} authentication failure`, () => markSubscriptionAuthenticationFailed(providerId, credential.label, INVALID_REFRESH_TOKEN_BACKOFF_MS, attemptAt));
538
+ authFailures.add(`${providerId}/${credential.label}`);
539
+ continue;
540
+ }
541
+ freshCredential = reauthenticated;
542
+ }
543
+ const result = await streamProviderAttempt(state, providerId, candidateModel, context, options, freshCredential, stream);
544
+ if (result.result === 'success') {
545
+ if (freshCredential.rateLimitedUntil || freshCredential.authFailure)
546
+ await recordSubscriptionMutation(`recording ${getProviderLabel(providerId)} success`, () => markSubscriptionSuccess(providerId, freshCredential.label, result.attemptAt));
547
+ stream.end();
548
+ return;
549
+ }
550
+ if (result.result === 'fatal') {
551
+ stream.end();
552
+ return;
553
+ }
554
+ const cooldownMs = result.result === 'rate_limited' ? result.retryAfterMs ?? DEFAULT_RATE_LIMIT_BACKOFF_MS : TRANSIENT_ROTATE_COOLDOWN_MS;
555
+ if (result.result === 'transient_exhausted')
556
+ lastTransientFailure = result;
557
+ if (result.result === 'rate_limited' && result.retryAfterMs !== undefined) {
558
+ const persisted = await recordSubscriptionMutation(`recording ${getProviderLabel(providerId)} server cooldown`, () => markSubscriptionRateLimited(providerId, freshCredential.label, cooldownMs, result.attemptAt));
559
+ if (persisted)
560
+ cooldownAttempts.set(`${routeId}/${freshCredential.label}`, result.attemptAt);
561
+ else
562
+ localCoolingDeadlines.set(routeId, Math.max(localCoolingDeadlines.get(routeId) ?? 0, Date.now() + cooldownMs));
563
+ }
564
+ else {
565
+ localCoolingDeadlines.set(routeId, Math.max(localCoolingDeadlines.get(routeId) ?? 0, Date.now() + cooldownMs));
566
+ }
567
+ advanceResult = result;
568
+ }
569
+ enqueueFreshRouteCandidates(candidates, queuedRouteIds, knownRoutes, seenReadyLabels, cooldownAttempts, localCoolingDeadlines);
570
+ if (advanceResult !== undefined)
571
+ logRouteQueueAdvance(candidate, candidates[0], advanceResult);
572
+ }
573
+ const coolingDeadline = earliestCoolingDeadline([...knownRoutes.values()], localCoolingDeadlines);
574
+ const tried = [...attemptedRoutes].join(', ');
575
+ if (lastTransientFailure !== undefined) {
576
+ const detail = lastTransientFailure.failureMessage ?? lastTransientFailure.classification?.reason ?? 'unknown connection failure';
577
+ emitTerminalError(stream, terminalModel, `Provider routes failed after transient retries: ${detail} (tried ${tried})`);
578
+ return;
579
+ }
580
+ if (coolingDeadline !== undefined) {
581
+ emitCoolingError(stream, terminalModel, new ManagedProviderCoolingError(`All provider routes are rate limited (cooling down ~${Math.ceil((coolingDeadline - Date.now()) / 1000)}s; tried ${tried})`, coolingDeadline));
582
+ return;
583
+ }
584
+ const authFailure = [...authFailures][0];
585
+ if (authFailure !== undefined) {
586
+ emitTerminalError(stream, terminalModel, `Provider authentication failed (invalid_grant) for ${authFailure}; re-authenticate the credential and retry (tried ${tried})`);
587
+ return;
588
+ }
589
+ emitTerminalError(stream, terminalModel, `All provider routes exhausted (tried ${tried})`);
590
+ }
591
+ // The earliest strictly-future deadline only when every attempted route is cooling.
592
+ function earliestCoolingDeadline(attempted, localCoolingDeadlines) {
593
+ const now = Date.now();
594
+ let earliest;
595
+ let sawRoute = false;
596
+ for (const route of attempted) {
597
+ const localUntil = localCoolingDeadlines.get(route.routeId) ?? 0;
598
+ if (localUntil > now) {
599
+ sawRoute = true;
600
+ earliest = earliest === undefined ? localUntil : Math.min(earliest, localUntil);
601
+ continue;
602
+ }
603
+ if (!isManagedProvider(route.providerId))
604
+ return undefined;
605
+ const pool = readSubscriptionPool(route.providerId).filter((entry) => entry.authFailure === undefined);
606
+ // Routes with no usable credential were never runnable this turn, so they do
607
+ // not defeat the cooling outcome of another route. An auth-failed-only pool
608
+ // is likewise unauthenticated; a valid cooling row still contributes its
609
+ // deadline even when a sibling row carries invalid_grant.
610
+ if (pool.length === 0)
611
+ continue;
612
+ sawRoute = true;
613
+ if (pool.some((entry) => (entry.rateLimitedUntil || 0) <= now))
614
+ return undefined;
615
+ const routeEarliest = Math.min(...pool.map((entry) => entry.rateLimitedUntil || 0));
616
+ earliest = earliest === undefined ? routeEarliest : Math.min(earliest, routeEarliest);
617
+ }
618
+ return sawRoute ? earliest : undefined;
619
+ }
620
+ // Emit the typed cooling terminal error, attaching its diagnostic (the earliest
621
+ // deadline) to the assistant error message so the deadline survives verbatim into
622
+ // agent_end.messages for the stophook to read.
623
+ function emitCoolingError(stream, model, error) {
624
+ const message = toAssistantError(model, error.message);
625
+ message.diagnostics = [error.toDiagnostic()];
626
+ stream.push({ type: "error", reason: "error", error: message });
627
+ stream.end();
628
+ }
629
+ function streamManagedProvider(state, model, context, options) {
630
+ const stream = createAssistantMessageEventStream();
631
+ runManagedProvider(state, model, context, options, stream).catch((error) => {
632
+ stream.push({
633
+ type: "error",
634
+ reason: "error",
635
+ error: toAssistantError(model, error instanceof Error ? error.message : String(error)),
636
+ });
637
+ stream.end();
638
+ });
639
+ return stream;
640
+ }
641
+ function parseCommand(args) {
642
+ return args
643
+ .trim()
644
+ .split(/\s+/)
645
+ .map((part) => part.trim())
646
+ .filter(Boolean);
647
+ }
648
+ function parseProvider(value) {
649
+ return isManagedProvider(value) ? value : undefined;
650
+ }
651
+ function installDeclaredRouteProviders(state, pi, ctx) {
652
+ const config = readMergedLaunchConfig(ctx.cwd, configuredValue(state, 'profileId', 'CRTR_PROFILE_ID') || null);
653
+ const providerIds = new Set(Object.values(config.modelRoutes ?? {}).map((route) => route.providerId));
654
+ for (const providerId of providerIds) {
655
+ if (isManagedProvider(providerId) || state.delegatedProviderStreams.has(providerId))
656
+ continue;
657
+ const provider = ctx.modelRegistry.getProvider?.(providerId);
658
+ const model = ctx.modelRegistry.getAll?.().find((entry) => entry.provider === providerId);
659
+ if (!provider || !model)
660
+ continue;
661
+ state.delegatedProviderStreams.set(providerId, provider.streamSimple.bind(provider));
662
+ // `api` is required by pi whenever a registration supplies streamSimple.
663
+ // Re-registration merges this wrapper over the existing declaration, so the
664
+ // provider's models, auth, and captured composed transport remain intact.
665
+ pi.registerProvider(providerId, { api: model.api, streamSimple: (model, context, options) => streamManagedProvider(state, model, context, options) });
666
+ }
667
+ }
668
+ function updateRotationStatus(state, ctx) {
669
+ const current = getCurrentModelRef(ctx.model);
670
+ if (current && isManagedProvider(current.providerId))
671
+ setStatus(state, formatStatusLine(current.providerId));
672
+ }
673
+ /** Restore only when the deterministic route rank improves; no shared-home
674
+ * preferred-model state is needed or consulted. */
675
+ async function restoreHigherRankIfAvailable(state, pi, ctx) {
676
+ if (!readRotationConfig().revertWhenAvailable)
677
+ return false;
678
+ const liveThinking = ctx.thinkingLevel === undefined || ctx.thinkingLevel === 'off' ? undefined : ctx.thinkingLevel;
679
+ const request = modelRequestFromConfig(`${ctx.model?.provider ?? ''}/${ctx.model?.id ?? ''}${liveThinking ? `:${liveThinking}` : ''}`, configuredValue(state, 'modelIntent', 'CRTR_MODEL_INTENT'), false);
680
+ if (!request || !ctx.model)
681
+ return false;
682
+ const candidates = expandModelCandidates(request, configuredValue(state, 'nodeCwd', 'CRTR_NODE_CWD'), configuredValue(state, 'profileId', 'CRTR_PROFILE_ID'));
683
+ const liveRank = candidates.find((candidate) => candidate.providerId === ctx.model?.provider && candidate.modelId === ctx.model?.id && candidate.thinkingLevel === liveThinking)?.rank;
684
+ if (liveRank === undefined)
685
+ return false;
686
+ const preferred = candidates.find((candidate) => candidate.rank < liveRank && resolveProviderCandidates([{
687
+ ...candidate,
688
+ ...probeRouteAvailability(candidate, ctx.modelRegistry),
689
+ automaticFallback: true,
690
+ }]).candidates.length > 0);
691
+ if (!preferred)
692
+ return false;
693
+ const model = ctx.modelRegistry.find(preferred.providerId, preferred.modelId);
694
+ if (!model || !await pi.setModel(model))
695
+ return false;
696
+ pi.setThinkingLevel((preferred.thinkingLevel ?? 'off'));
697
+ return true;
698
+ }
699
+ // Manual-code input is a human-in-the-loop OAuth round-trip (open link, log in on
700
+ // whatever device, copy the localhost redirect URL back) that routinely exceeds
701
+ // DEFAULT_DIALOG_TIMEOUT_MS -- give it a human-scale bound instead of the ordinary
702
+ // confirm/select default.
703
+ const OAUTH_MANUAL_INPUT_TIMEOUT_MS = 600_000;
704
+ /**
705
+ * pi-ai races the manual-code-paste dialog against its loopback callback server. When the
706
+ * callback supplies a code, the manual-input promise can remain pending, so `run` receives:
707
+ * - `readCode`, a `ctx.ui.input` wrapper with an AbortSignal. Aborting it removes the pending
708
+ * broker entry and emits an `extension_ui_dismiss` frame for exactly that dialog.
709
+ * - `dismissManual`, which aborts the signal at the post-code `onProgress` boundary before
710
+ * the token exchange completes. `onProgress` is emitted only after a code is available, so
711
+ * dismissing then cannot close a dialog the user still needs; `finally` covers paths without
712
+ * progress, and repeated AbortController calls are inert.
713
+ *
714
+ * `undefined` from `readCode` means cancellation or timeout. It throws so OAuth ends directly
715
+ * instead of opening a second paste-only dialog.
716
+ */
717
+ export async function loginWithManualInputCleanup(ctx, run) {
718
+ const controller = new AbortController();
719
+ const readCode = async (title, placeholder) => {
720
+ const result = await ctx.ui.input(title, placeholder, { timeout: OAUTH_MANUAL_INPUT_TIMEOUT_MS, signal: controller.signal });
721
+ if (result === undefined)
722
+ throw new Error("OAuth login cancelled");
723
+ return result;
724
+ };
725
+ try {
726
+ return await run(readCode, () => controller.abort());
727
+ }
728
+ finally {
729
+ controller.abort();
730
+ }
731
+ }
732
+ // The manual-code dialog carries the authorize URL in its title (rendered as a tappable link
733
+ // on web), so a headless remote box where `openBrowser` opens nothing still has a real path
734
+ // through login. pi's own `manual_code` message is URL-less, hence these per-provider titles.
735
+ const MANUAL_CODE_DIALOG = {
736
+ [ANTHROPIC_PROVIDER_ID]: {
737
+ title: (url) => `Open this URL to authenticate Claude:\n${url}\n\nPaste the redirect URL or code`,
738
+ placeholder: "http://localhost:53692/callback?...",
739
+ },
740
+ [OPENAI_CODEX_PROVIDER_ID]: {
741
+ title: (url) => `Open this URL to authenticate OpenAI Codex:\n${url}\n\nPaste the redirect URL or code`,
742
+ placeholder: "http://localhost:1455/auth/callback?...",
743
+ },
744
+ };
745
+ // This extension adapts the provider OAuth interaction onto `ctx.ui`. The attach viewer and
746
+ // `crtr sys setup` use LoginDialogComponent with `core/auth-interaction.ts`; the broker uses
747
+ // the dialog primitives below.
748
+ async function loginProvider(providerId, ctx) {
749
+ const oauth = oauthFlow(providerId, ctx);
750
+ const manual = MANUAL_CODE_DIALOG[providerId];
751
+ let authorizeUrl;
752
+ return loginWithManualInputCleanup(ctx, (readCode, dismissManual) => oauth.login({
753
+ notify: (event) => {
754
+ switch (event.type) {
755
+ case "auth_url":
756
+ authorizeUrl = event.url;
757
+ openBrowser(event.url);
758
+ if (event.instructions)
759
+ ctx.ui.notify(event.instructions, "info");
760
+ break;
761
+ case "device_code":
762
+ openBrowser(event.verificationUri);
763
+ ctx.ui.notify(`Enter code ${event.userCode} at ${event.verificationUri}`, "info");
764
+ break;
765
+ case "progress":
766
+ // Progress follows receipt of an OAuth code, so dismiss the now-unneeded manual
767
+ // dialog before token exchange. The browser path may emit no progress; `finally`
768
+ // closes that dialog when the OAuth attempt settles.
769
+ dismissManual();
770
+ ctx.ui.notify(event.message, "info");
771
+ break;
772
+ case "info":
773
+ ctx.ui.notify(event.message, "info");
774
+ break;
775
+ }
776
+ },
777
+ prompt: async (prompt) => {
778
+ if (prompt.type === "manual_code")
779
+ return readCode(manual.title(authorizeUrl), manual.placeholder);
780
+ if (prompt.type === "select") {
781
+ // Codex now opens its login with a browser-vs-device-code choice.
782
+ const labels = prompt.options.map((option) => option.label);
783
+ const chosen = await ctx.ui.select(prompt.message, labels);
784
+ const selected = chosen === undefined ? undefined : prompt.options[labels.indexOf(chosen)];
785
+ if (!selected)
786
+ throw new Error("OAuth login cancelled");
787
+ return selected.id;
788
+ }
789
+ return (await ctx.ui.input(prompt.message, prompt.placeholder ?? "")) ?? "";
790
+ },
791
+ }));
792
+ }
793
+ export function createProviderRotationExtension(options) {
794
+ const state = createProviderRotationState(options);
795
+ return async (pi) => {
796
+ state.extensionAPI = pi;
797
+ pi.registerProvider(ANTHROPIC_PROVIDER_ID, {
798
+ api: "anthropic-messages",
799
+ streamSimple: (model, context, streamOptions) => streamManagedProvider(state, model, context, streamOptions),
800
+ });
801
+ pi.registerProvider(OPENAI_CODEX_PROVIDER_ID, {
802
+ api: "openai-codex-responses",
803
+ streamSimple: (model, context, streamOptions) => streamManagedProvider(state, model, context, streamOptions),
804
+ });
805
+ pi.on("session_start", async (_event, ctx) => {
806
+ state.runtimeContext = ctx;
807
+ installDeclaredRouteProviders(state, pi, ctx);
808
+ updateRotationStatus(state, ctx);
809
+ });
810
+ pi.on("before_agent_start", async (_event, ctx) => {
811
+ state.runtimeContext = ctx;
812
+ const restored = await restoreHigherRankIfAvailable(state, pi, ctx);
813
+ if (restored && ctx.model && isManagedProvider(ctx.model.provider)) {
814
+ logRotation(`restored higher-ranked route ${ctx.model.provider}/${ctx.model.id}`);
815
+ setStatus(state, formatStatusLine(ctx.model.provider));
816
+ }
817
+ });
818
+ pi.on("model_select", async (_event, ctx) => {
819
+ state.runtimeContext = ctx;
820
+ updateRotationStatus(state, ctx);
821
+ });
822
+ pi.registerCommand("provider-sub", {
823
+ description: "Manage subscription credential pools for provider rotation",
824
+ handler: async (args, ctx) => {
825
+ const [providerArg, verb, ...rest] = parseCommand(args);
826
+ const providerId = parseProvider(providerArg);
827
+ if (!providerId) {
828
+ ctx.ui.notify("Usage: /provider-sub <anthropic|openai-codex> <list|add|select|rm>", "error");
829
+ return;
830
+ }
831
+ const action = verb === "rm" ? "remove" : verb || "list";
832
+ if (action === "status" || action === "list") {
833
+ const pool = readSubscriptionPool(providerId);
834
+ const lines = [formatStatusLine(providerId), ""];
835
+ for (const [index, entry] of pool.entries()) {
836
+ const cooldown = entry.rateLimitedUntil > Date.now() ? ` cooldown=${Math.ceil((entry.rateLimitedUntil - Date.now()) / 1000)}s` : "";
837
+ const account = entry.accountId ? ` account=${shortAccountId(entry.accountId)}` : "";
838
+ lines.push(`${index + 1}. ${entry.label}${account}${cooldown}`);
839
+ }
840
+ ctx.ui.notify(lines.join("\n"), "info");
841
+ return;
842
+ }
843
+ if (action === "add") {
844
+ if (!ctx.hasUI) {
845
+ ctx.ui.notify(`${getProviderLabel(providerId)} login requires an interactive UI.`, "error");
846
+ return;
847
+ }
848
+ const pool = readSubscriptionPool(providerId);
849
+ const label = (rest.join(" ") || (await ctx.ui.input(`${getProviderLabel(providerId)} subscription label`, "")) || "").trim();
850
+ if (!label)
851
+ return;
852
+ if (pool.some((entry) => entry.label.toLowerCase() === label.toLowerCase())) {
853
+ ctx.ui.notify(`${getProviderLabel(providerId)} subscription already exists: ${label}`, "error");
854
+ return;
855
+ }
856
+ try {
857
+ const cred = await activeLoginForProvider(providerId, ctx);
858
+ const identity = await enrichManagedLoginIdentity(providerId, cred);
859
+ // Explicit add always creates a new label, so a matching real account is rejected by
860
+ // the locked uniqueness gate rather than creating a second quota slot.
861
+ await addSubscription(providerId, {
862
+ label,
863
+ refresh: cred.refresh,
864
+ access: cred.access,
865
+ expires: cred.expires,
866
+ ...(identity.accountId ? { accountId: identity.accountId } : {}),
867
+ rateLimitedUntil: 0,
868
+ lastAttemptAt: 0,
869
+ lastRateLimitedAt: 0,
870
+ });
871
+ logRotation(`added ${getProviderLabel(providerId)} subscription "${label}"`);
872
+ ctx.ui.notify(`Added ${getProviderLabel(providerId)} subscription: ${label}`, "info");
873
+ updateRotationStatus(state, ctx);
874
+ }
875
+ catch (error) {
876
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
877
+ }
878
+ return;
879
+ }
880
+ if (action === "select") {
881
+ const ref = rest.join(" ");
882
+ if (!ref) {
883
+ ctx.ui.notify(`Usage: /provider-sub ${providerId} select <label|index>`, "error");
884
+ return;
885
+ }
886
+ try {
887
+ await promoteSubscription(providerId, ref);
888
+ logRotation(`selected ${getProviderLabel(providerId)} subscription "${ref}"`);
889
+ ctx.ui.notify(`Preferred ${getProviderLabel(providerId)} subscription: ${ref}`, "info");
890
+ updateRotationStatus(state, ctx);
891
+ }
892
+ catch (error) {
893
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
894
+ }
895
+ return;
896
+ }
897
+ if (action === "remove") {
898
+ const ref = rest.join(" ");
899
+ if (!ref) {
900
+ ctx.ui.notify(`Usage: /provider-sub ${providerId} rm <label|index>`, "error");
901
+ return;
902
+ }
903
+ try {
904
+ await removeManagedAccount(providerId, ref);
905
+ logRotation(`removed ${getProviderLabel(providerId)} subscription "${ref}"`);
906
+ ctx.ui.notify(`Removed ${getProviderLabel(providerId)} subscription: ${ref}`, "info");
907
+ updateRotationStatus(state, ctx);
908
+ }
909
+ catch (error) {
910
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
911
+ }
912
+ return;
913
+ }
914
+ ctx.ui.notify(`Unknown /provider-sub verb: ${verb}`, "error");
915
+ },
916
+ });
917
+ };
918
+ }
919
+ const defaultProviderRotationExtension = createProviderRotationExtension();
920
+ export default defaultProviderRotationExtension;