@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.
- package/dist/api/client.d.ts +37 -11
- package/dist/api/client.js +84 -15
- package/dist/api/dto/human.d.ts +53 -39
- package/dist/api/dto/human.js +2 -3
- package/dist/api/dto/inbox.d.ts +0 -1
- package/dist/api/dto/nodes.d.ts +12 -0
- package/dist/api/dto/review-comments.d.ts +137 -0
- package/dist/api/dto/review-comments.js +5 -0
- package/dist/api/dto/reviews.d.ts +109 -0
- package/dist/api/dto/reviews.js +5 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/routes.d.ts +17 -4
- package/dist/api/routes.js +21 -5
- package/dist/build-root.d.ts +1 -16
- package/dist/build-root.js +1 -25
- package/dist/builtin-memory/00-runtime-base.md +7 -5
- package/dist/builtin-memory/04-base-worker.md +1 -1
- package/dist/builtin-memory/04-orchestration-kernel.md +7 -7
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/review/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +20 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -7
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +3 -3
- package/dist/builtin-memory/05-kinds/spec/requirements.md +3 -3
- package/dist/builtin-memory/design.md +2 -2
- package/dist/builtin-memory/insights/capture.md +50 -0
- package/dist/builtin-memory/insights/init.md +45 -0
- package/dist/builtin-memory/insights/listen.md +9 -0
- package/dist/builtin-memory/internal/agent-shaping.md +2 -2
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/plugins.md +12 -11
- package/dist/builtin-memory/internal/storage-tiers.md +2 -2
- package/dist/builtin-memory/{planning.md → plan/roadmap.md} +3 -3
- package/dist/builtin-memory/plan.md +19 -0
- package/dist/builtin-memory/spec/guide.md +38 -0
- package/dist/builtin-memory/spec/requirements.md +31 -0
- package/dist/builtin-memory/spec/roadmap.md +35 -0
- package/dist/builtin-memory/spec.md +10 -86
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +3 -4
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +49 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +56 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +920 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +221 -156
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +4 -5
- package/dist/cli.js +2 -6
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +7 -35
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +32 -16
- package/dist/clients/attach/__tests__/context-message.test.js +142 -6
- package/dist/clients/attach/__tests__/crtr-output.test.js +29 -45
- package/dist/clients/attach/__tests__/diagram.test.js +1 -1
- package/dist/clients/attach/__tests__/file-review-focus.test.js +48 -0
- package/dist/clients/attach/__tests__/group-activity.test.js +266 -0
- package/dist/clients/attach/__tests__/input-controller-extension-command.test.js +32 -0
- package/dist/clients/attach/__tests__/session-identity-refresh.test.js +32 -0
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -2
- package/dist/clients/attach/chrome/header.d.ts +10 -0
- package/dist/clients/attach/chrome/header.js +30 -0
- package/dist/clients/attach/chrome/recap.d.ts +7 -0
- package/dist/clients/attach/chrome/recap.js +19 -0
- package/dist/clients/attach/chrome/status-line.d.ts +3 -0
- package/dist/clients/attach/chrome/status-line.js +5 -0
- package/dist/clients/attach/chrome/widgets.js +11 -21
- package/dist/clients/attach/command.js +11 -7
- package/dist/clients/attach/config.d.ts +4 -0
- package/dist/clients/attach/config.js +3 -0
- package/dist/clients/attach/input/controller.d.ts +19 -0
- package/dist/clients/attach/input/controller.js +62 -28
- package/dist/clients/attach/input/titled-editor.d.ts +0 -4
- package/dist/clients/attach/input/titled-editor.js +2 -10
- package/dist/clients/attach/overlays/file-review.d.ts +9 -5
- package/dist/clients/attach/overlays/file-review.js +9 -70
- package/dist/clients/attach/overlays/help.d.ts +33 -0
- package/dist/clients/attach/overlays/help.js +204 -0
- package/dist/clients/attach/render/activity-slot.d.ts +4 -0
- package/dist/clients/attach/render/activity-slot.js +13 -0
- package/dist/clients/attach/render/chat-view.d.ts +117 -12
- package/dist/clients/attach/render/chat-view.js +604 -135
- package/dist/clients/attach/render/context-message.d.ts +2 -1
- package/dist/clients/attach/render/context-message.js +17 -13
- package/dist/clients/attach/render/crtr-output.js +1 -3
- package/dist/clients/attach/render/diagram.js +2 -2
- package/dist/clients/attach/render/edit-diff.d.ts +4 -1
- package/dist/clients/attach/render/edit-diff.js +7 -11
- package/dist/clients/attach/render/group-activity.d.ts +32 -0
- package/dist/clients/attach/render/group-activity.js +206 -0
- package/dist/clients/attach/render/group-recap.d.ts +10 -0
- package/dist/clients/attach/render/group-recap.js +93 -0
- package/dist/clients/attach/render/html-markdown.d.ts +17 -0
- package/dist/clients/attach/render/html-markdown.js +228 -0
- package/dist/clients/attach/render/measured-container.d.ts +96 -0
- package/dist/clients/attach/render/measured-container.js +322 -0
- package/dist/clients/attach/render/scroll-animation.d.ts +8 -0
- package/dist/clients/attach/render/scroll-animation.js +80 -0
- package/dist/clients/attach/render/tool-calls.js +6 -6
- package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
- package/dist/clients/attach/render/transcript-copy.js +81 -0
- package/dist/clients/attach/render/viewport.d.ts +29 -0
- package/dist/clients/attach/render/viewport.js +159 -0
- package/dist/clients/attach/session/bindings.d.ts +10 -7
- package/dist/clients/attach/session/bindings.js +23 -8
- package/dist/clients/attach/session/connection.js +4 -5
- package/dist/clients/attach/session/context.d.ts +6 -4
- package/dist/clients/attach/session/frame.d.ts +34 -0
- package/dist/clients/attach/session/frame.js +176 -0
- package/dist/clients/attach/session/frames.d.ts +4 -1
- package/dist/clients/attach/session/frames.js +14 -1
- package/dist/clients/attach/session/identity.js +4 -3
- package/dist/clients/attach/session/input-wiring.d.ts +14 -4
- package/dist/clients/attach/session/input-wiring.js +15 -13
- package/dist/clients/attach/session/invariant.d.ts +9 -0
- package/dist/clients/attach/session/invariant.js +15 -0
- package/dist/clients/attach/session/keys.d.ts +10 -2
- package/dist/clients/attach/session/keys.js +13 -5
- package/dist/clients/attach/session/layout.d.ts +10 -8
- package/dist/clients/attach/session/layout.js +33 -34
- package/dist/clients/attach/session/mouse.d.ts +18 -0
- package/dist/clients/attach/session/mouse.js +64 -0
- package/dist/clients/attach/session/pane-tag.d.ts +3 -0
- package/dist/clients/attach/session/pane-tag.js +51 -8
- package/dist/clients/attach/session/reconnect.d.ts +3 -0
- package/dist/clients/attach/session/reconnect.js +42 -5
- package/dist/clients/attach/session/surface-frame.d.ts +20 -0
- package/dist/clients/attach/session/surface-frame.js +73 -0
- package/dist/clients/attach/session/surface-host.d.ts +64 -0
- package/dist/clients/attach/session/surface-host.js +255 -0
- package/dist/clients/attach/session/whip.d.ts +17 -0
- package/dist/clients/attach/session/whip.js +334 -0
- package/dist/clients/attach/slash/dispatch.d.ts +13 -1
- package/dist/clients/attach/slash/dispatch.js +51 -29
- package/dist/clients/attach/viewer.js +907 -819
- package/dist/clients/conversation/conversation.d.ts +65 -0
- package/dist/clients/conversation/conversation.js +318 -0
- package/dist/clients/conversation/frames.d.ts +30 -0
- package/dist/clients/conversation/frames.js +52 -0
- package/dist/clients/conversation/region.d.ts +51 -0
- package/dist/clients/conversation/region.js +265 -0
- package/dist/clients/conversation/submit.d.ts +38 -0
- package/dist/clients/conversation/submit.js +51 -0
- package/dist/clients/inbox/__tests__/inbox-controller.test.js +247 -0
- package/dist/clients/inbox/__tests__/mount-panel.test.js +776 -0
- package/dist/clients/inbox/controller.d.ts +79 -0
- package/dist/clients/inbox/controller.js +448 -0
- package/dist/clients/inbox/deck-adapter.d.ts +29 -0
- package/dist/clients/inbox/deck-adapter.js +62 -0
- package/dist/clients/inbox/layout.d.ts +8 -0
- package/dist/clients/inbox/layout.js +9 -0
- package/dist/clients/inbox/resolve.d.ts +10 -0
- package/dist/clients/inbox/resolve.js +34 -0
- package/dist/clients/inbox/review/__tests__/editor-roundtrip.test.js +29 -0
- package/dist/clients/inbox/review/__tests__/remap.test.js +196 -0
- package/dist/clients/inbox/review/__tests__/review-core.test.js +341 -0
- package/dist/clients/inbox/review/anchors.d.ts +41 -0
- package/dist/clients/inbox/review/anchors.js +89 -0
- package/dist/clients/inbox/review/columns.d.ts +22 -0
- package/dist/clients/inbox/review/columns.js +40 -0
- package/dist/clients/inbox/review/comments-client.d.ts +66 -0
- package/dist/clients/inbox/review/comments-client.js +98 -0
- package/dist/clients/inbox/review/composition.d.ts +60 -0
- package/dist/clients/inbox/review/composition.js +278 -0
- package/dist/clients/inbox/review/document-region.d.ts +120 -0
- package/dist/clients/inbox/review/document-region.js +902 -0
- package/dist/clients/inbox/review/frame.d.ts +45 -0
- package/dist/clients/inbox/review/frame.js +215 -0
- package/dist/clients/inbox/review/keys.d.ts +67 -0
- package/dist/clients/inbox/review/keys.js +130 -0
- package/dist/clients/inbox/review/launch.d.ts +10 -0
- package/dist/clients/inbox/review/launch.js +129 -0
- package/dist/clients/inbox/review/live-source.d.ts +24 -0
- package/dist/clients/inbox/review/live-source.js +160 -0
- package/dist/clients/inbox/review/nvim.d.ts +13 -0
- package/dist/clients/inbox/review/nvim.js +42 -0
- package/dist/clients/inbox/review/remap.d.ts +51 -0
- package/dist/clients/inbox/review/remap.js +157 -0
- package/dist/clients/inbox/review/review-client.d.ts +30 -0
- package/dist/clients/inbox/review/review-client.js +107 -0
- package/dist/clients/inbox/review/roundtrip.d.ts +5 -0
- package/dist/clients/inbox/review/roundtrip.js +36 -0
- package/dist/clients/inbox/review/state.d.ts +143 -0
- package/dist/clients/inbox/review/state.js +282 -0
- package/dist/clients/inbox/review-adapter.d.ts +29 -0
- package/dist/clients/inbox/review-adapter.js +92 -0
- package/dist/clients/inbox/review-visibility.d.ts +5 -0
- package/dist/clients/inbox/review-visibility.js +19 -0
- package/dist/clients/inbox/surface.d.ts +19 -0
- package/dist/clients/inbox/surface.js +128 -0
- package/dist/clients/inbox/tui/ansi.d.ts +66 -0
- package/dist/clients/inbox/tui/ansi.js +317 -0
- package/dist/clients/inbox/tui/clipboard.d.ts +2 -0
- package/dist/clients/inbox/tui/clipboard.js +26 -0
- package/dist/clients/inbox/tui/input.d.ts +7 -0
- package/dist/clients/inbox/tui/input.js +653 -0
- package/dist/clients/inbox/tui/keys.d.ts +28 -0
- package/dist/clients/inbox/tui/keys.js +111 -0
- package/dist/clients/inbox/tui/panel.d.ts +2 -0
- package/dist/clients/inbox/tui/panel.js +217 -0
- package/dist/clients/inbox/tui/render.d.ts +37 -0
- package/dist/clients/inbox/tui/render.js +476 -0
- package/dist/clients/inbox/tui/types.d.ts +57 -0
- package/dist/clients/inbox/tui/types.js +1 -0
- package/dist/clients/inbox/tui.d.ts +6 -0
- package/dist/clients/inbox/tui.js +74 -0
- package/dist/clients/surfaces/host.d.ts +66 -0
- package/dist/clients/surfaces/host.js +1 -0
- package/dist/clients/surfaces/region.d.ts +16 -0
- package/dist/clients/surfaces/region.js +1 -0
- package/dist/clients/web/__tests__/push-engine.test.js +14 -158
- package/dist/clients/web/dev-server.d.ts +3 -3
- package/dist/clients/web/dev-server.js +9 -23
- package/dist/clients/web/events.js +4 -6
- package/dist/clients/web/push-engine.d.ts +2 -66
- package/dist/clients/web/push-engine.js +30 -161
- package/dist/clients/web/push-registry.d.ts +0 -4
- package/dist/clients/web/push-registry.js +2 -7
- package/dist/clients/web/server.js +3 -35
- package/dist/clients/web/web-client/shared/protocol.d.ts +0 -108
- package/dist/clients/web/web-cmd.js +2 -2
- package/dist/commands/__tests__/human.test.js +27 -15
- package/dist/commands/api-client.js +3 -3
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/canvas-issue.js +1 -1
- package/dist/commands/canvas-use.js +1 -1
- package/dist/commands/human/doc.d.ts +2 -0
- package/dist/commands/human/doc.js +98 -0
- package/dist/commands/human/inbox.d.ts +2 -0
- package/dist/commands/human/inbox.js +101 -0
- package/dist/commands/human/prompts.d.ts +2 -3
- package/dist/commands/human/prompts.js +27 -180
- package/dist/commands/human/queue.d.ts +1 -5
- package/dist/commands/human/queue.js +54 -216
- package/dist/commands/human/review.d.ts +2 -0
- package/dist/commands/human/review.js +298 -0
- package/dist/commands/human/shared.d.ts +2 -69
- package/dist/commands/human/shared.js +1 -181
- package/dist/commands/human.js +15 -20
- package/dist/commands/memory/__tests__/lint.test.d.ts +1 -0
- package/dist/commands/memory/__tests__/lint.test.js +15 -0
- package/dist/commands/memory/delete.js +2 -2
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +21 -3
- package/dist/commands/memory/read.js +4 -4
- package/dist/commands/node.js +8 -7
- package/dist/commands/pkg/plugin-manage.js +143 -68
- package/dist/commands/surface-inspect.js +2 -2
- package/dist/commands/surface.js +6 -12
- package/dist/commands/sys/__tests__/setup-core.test.js +9 -5
- package/dist/commands/sys/__tests__/setup-front-door.test.js +7 -42
- package/dist/commands/sys/config.js +29 -3
- package/dist/commands/sys/feedback.js +1 -1
- package/dist/commands/sys/setup-core.d.ts +12 -11
- package/dist/commands/sys/setup-core.js +55 -46
- package/dist/commands/sys/setup-wizard.d.ts +4 -1
- package/dist/commands/sys/setup-wizard.js +50 -9
- package/dist/commands/sys/sync-deps.js +1 -1
- package/dist/commands/sys.js +4 -6
- package/dist/core/__tests__/broker-sdk-wiring.test.js +6 -1
- package/dist/core/__tests__/broker-snapshot-history.test.js +9 -1
- package/dist/core/__tests__/dead-node-policy-table.test.js +3 -4
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +8 -5
- package/dist/core/__tests__/fixtures/fake-engine.js +28 -2
- package/dist/core/__tests__/full/broker-provider-retry.test.js +87 -2
- package/dist/core/__tests__/full/broker-whip-interrupt.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-whip-interrupt.test.js +39 -0
- package/dist/core/__tests__/full/detach-focus.test.js +1 -1
- package/dist/core/__tests__/human-deliver-e2e.test.js +64 -64
- package/dist/core/__tests__/human-deliver.test.js +71 -345
- package/dist/core/__tests__/inline-memory-refs.test.js +2 -12
- package/dist/core/__tests__/memory-resolver-precedence.test.js +12 -15
- package/dist/core/__tests__/migration.test.js +10 -0
- package/dist/core/__tests__/model-pin-durability.test.js +14 -0
- package/dist/core/__tests__/phase4-review-migration.test.d.ts +1 -0
- package/dist/core/__tests__/phase4-review-migration.test.js +100 -0
- package/dist/core/__tests__/phase4-review-store.test.d.ts +1 -0
- package/dist/core/__tests__/phase4-review-store.test.js +143 -0
- package/dist/core/__tests__/remote-canvas-target.test.js +2 -2
- package/dist/core/__tests__/revive.test.js +22 -53
- package/dist/core/__tests__/session-cycles.test.js +48 -18
- package/dist/core/__tests__/spawn-no-kickoff.test.js +8 -12
- package/dist/core/__tests__/stop-guard.test.js +16 -1
- package/dist/core/__tests__/tmux-surface.test.js +21 -25
- package/dist/core/asset-root.d.ts +7 -0
- package/dist/core/asset-root.js +18 -0
- package/dist/core/broker-client/client.d.ts +0 -7
- package/dist/core/broker-client/client.js +4 -10
- package/dist/core/broker-client/transport-relay.d.ts +3 -4
- package/dist/core/broker-client/transport-relay.js +3 -4
- package/dist/core/canvas/__tests__/attention.test.js +1 -1
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +1 -1
- package/dist/core/canvas/__tests__/remote-transport.test.d.ts +1 -0
- package/dist/core/{view/__tests__/transport-remote.test.js → canvas/__tests__/remote-transport.test.js} +2 -2
- package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
- package/dist/core/canvas/attention.js +4 -4
- package/dist/core/canvas/canvas.d.ts +6 -11
- package/dist/core/canvas/canvas.js +11 -33
- package/dist/core/canvas/db.js +207 -38
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +8 -1
- package/dist/core/canvas/index.d.ts +0 -1
- package/dist/core/canvas/index.js +0 -1
- package/dist/core/canvas/nav-model.d.ts +1 -1
- package/dist/core/canvas/nav-model.js +1 -1
- package/dist/core/canvas/paths.d.ts +5 -0
- package/dist/core/canvas/paths.js +16 -0
- package/dist/core/canvas/remote-canvas-source.js +2 -2
- package/dist/core/canvas/remote-transport.d.ts +7 -0
- package/dist/core/{view/transport-remote.js → canvas/remote-transport.js} +3 -4
- package/dist/core/canvas/source.js +1 -1
- package/dist/core/canvas/status-glyph.js +4 -12
- package/dist/core/canvas/types.d.ts +10 -35
- package/dist/core/clipboard-text.d.ts +8 -0
- package/dist/core/clipboard-text.js +18 -0
- package/dist/core/command-manifests/registry.d.ts +1 -3
- package/dist/core/command-plugins/bundle.d.ts +29 -0
- package/dist/core/command-plugins/bundle.js +198 -0
- package/dist/core/command-plugins/discovery.d.ts +0 -8
- package/dist/core/command-plugins/discovery.js +2 -39
- package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
- package/dist/core/command-plugins/transport/http-fetch.js +17 -95
- package/dist/core/command.d.ts +1 -13
- package/dist/core/command.js +1 -16
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +28 -3
- package/dist/core/document-lines.d.ts +4 -0
- package/dist/core/document-lines.js +6 -0
- package/dist/core/feed/feed.d.ts +2 -0
- package/dist/core/feed/feed.js +11 -7
- package/dist/core/human/__tests__/inbox-cancel-worker.d.ts +1 -0
- package/dist/core/human/__tests__/inbox-cancel-worker.js +7 -0
- package/dist/core/human/__tests__/inbox-claim-worker.d.ts +1 -0
- package/dist/core/human/__tests__/inbox-claim-worker.js +17 -0
- package/dist/core/human/__tests__/inbox-core.test.d.ts +1 -0
- package/dist/core/human/__tests__/inbox-core.test.js +144 -0
- package/dist/core/human/__tests__/inbox-stale-claim-worker.d.ts +1 -0
- package/dist/core/human/__tests__/inbox-stale-claim-worker.js +8 -0
- package/dist/core/human/__tests__/visible-paths.test.d.ts +1 -0
- package/dist/{clients/attach → core/human}/__tests__/visible-paths.test.js +9 -0
- package/dist/core/human/claim.d.ts +23 -0
- package/dist/core/human/claim.js +67 -0
- package/dist/core/human/convention.d.ts +30 -0
- package/dist/core/human/convention.js +202 -0
- package/dist/core/human/deck-factories.d.ts +8 -0
- package/dist/core/human/deck-factories.js +15 -0
- package/dist/core/human/deck-schema.d.ts +70 -0
- package/dist/core/human/deck-schema.js +92 -0
- package/dist/core/human/root.d.ts +7 -0
- package/dist/core/human/root.js +37 -0
- package/dist/core/human/scan.d.ts +3 -0
- package/dist/core/human/scan.js +76 -0
- package/dist/core/human/summary.d.ts +8 -0
- package/dist/core/human/summary.js +44 -0
- package/dist/core/human/tickets.d.ts +49 -0
- package/dist/core/human/tickets.js +190 -0
- package/dist/core/human/types.d.ts +136 -0
- package/dist/core/human/types.js +5 -0
- package/dist/{clients/attach → core/human}/visible-paths.d.ts +1 -1
- package/dist/{clients/attach → core/human}/visible-paths.js +3 -2
- package/dist/core/inspector/chrome.d.ts +6 -0
- package/dist/core/inspector/chrome.js +14 -0
- package/dist/core/inspector/contract.d.ts +105 -0
- package/dist/core/inspector/contract.js +1 -0
- package/dist/core/inspector/core.d.ts +4 -5
- package/dist/core/inspector/core.js +3 -5
- package/dist/core/inspector/host.d.ts +46 -0
- package/dist/core/{tui → inspector}/host.js +42 -208
- package/dist/core/inspector/text.d.ts +2 -2
- package/dist/core/inspector/transport.d.ts +6 -0
- package/dist/core/inspector/transport.js +36 -0
- package/dist/core/inspector/tui.d.ts +2 -2
- package/dist/core/inspector/tui.js +15 -15
- package/dist/core/keybindings/__tests__/inbox-affordance.test.js +11 -11
- package/dist/core/keybindings/__tests__/resolve.test.js +10 -19
- package/dist/core/keybindings/attach-control.d.ts +9 -3
- package/dist/core/keybindings/attach-control.js +3 -1
- package/dist/core/keybindings/catalog.d.ts +6 -5
- package/dist/core/keybindings/catalog.js +61 -133
- package/dist/core/keybindings/inbox.d.ts +5 -6
- package/dist/core/keybindings/inbox.js +9 -10
- package/dist/core/keybindings/index.d.ts +3 -3
- package/dist/core/keybindings/index.js +3 -3
- package/dist/core/keybindings/persistence.d.ts +5 -1
- package/dist/core/keybindings/persistence.js +24 -6
- package/dist/core/keybindings/resolve.d.ts +6 -0
- package/dist/core/keybindings/resolve.js +10 -3
- package/dist/core/memory/inline-ref-grammar.js +2 -3
- package/dist/core/memory-resolver.d.ts +27 -29
- package/dist/core/memory-resolver.js +45 -39
- package/dist/core/model-routes.d.ts +33 -0
- package/dist/core/model-routes.js +42 -3
- package/dist/core/preview-registry.js +17 -55
- package/dist/core/profiles/select.js +3 -3
- package/dist/core/{view/remote-canvas-target.d.ts → remote-canvas-target.d.ts} +2 -2
- package/dist/core/{view/remote-canvas-target.js → remote-canvas-target.js} +4 -4
- package/dist/core/review/__tests__/capture-origin.test.d.ts +1 -0
- package/dist/core/review/__tests__/capture-origin.test.js +139 -0
- package/dist/core/review/__tests__/comments.test.d.ts +1 -0
- package/dist/core/review/__tests__/comments.test.js +98 -0
- package/dist/core/review/__tests__/project.test.d.ts +1 -0
- package/dist/core/review/__tests__/project.test.js +71 -0
- package/dist/core/review/__tests__/remap.test.d.ts +1 -0
- package/dist/core/review/__tests__/remap.test.js +159 -0
- package/dist/core/review/__tests__/stage-identity.test.d.ts +1 -0
- package/dist/core/review/__tests__/stage-identity.test.js +140 -0
- package/dist/core/review/birth.d.ts +7 -0
- package/dist/core/review/birth.js +23 -0
- package/dist/core/review/comments.d.ts +69 -0
- package/dist/core/review/comments.js +295 -0
- package/dist/core/review/companion.d.ts +35 -0
- package/dist/core/review/companion.js +205 -0
- package/dist/core/review/document.d.ts +27 -0
- package/dist/core/review/document.js +138 -0
- package/dist/core/review/project.d.ts +13 -0
- package/dist/core/review/project.js +58 -0
- package/dist/core/review/realize.d.ts +19 -0
- package/dist/core/review/realize.js +217 -0
- package/dist/core/review/remap.d.ts +24 -0
- package/dist/core/review/remap.js +156 -0
- package/dist/core/review/signal.d.ts +5 -0
- package/dist/core/review/signal.js +31 -0
- package/dist/core/review/stage.d.ts +26 -0
- package/dist/core/review/stage.js +184 -0
- package/dist/core/review/store.d.ts +46 -0
- package/dist/core/review/store.js +163 -0
- package/dist/core/review/ticket-filter.d.ts +17 -0
- package/dist/core/review/ticket-filter.js +32 -0
- package/dist/core/review/types.d.ts +130 -0
- package/dist/core/review/types.js +23 -0
- package/dist/core/runtime/__tests__/session-visibility.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/session-visibility.test.js +119 -0
- package/dist/core/runtime/bearings.d.ts +5 -17
- package/dist/core/runtime/bearings.js +88 -71
- package/dist/core/runtime/broker-protocol.d.ts +31 -8
- package/dist/core/runtime/broker.d.ts +15 -1
- package/dist/core/runtime/broker.js +573 -141
- package/dist/core/runtime/canvas-extensions.d.ts +4 -5
- package/dist/core/runtime/canvas-extensions.js +5 -5
- package/dist/core/runtime/front-door.js +4 -4
- package/dist/core/runtime/headless-pi.d.ts +24 -0
- package/dist/core/runtime/headless-pi.js +152 -0
- package/dist/core/runtime/kickoff.d.ts +1 -5
- package/dist/core/runtime/kickoff.js +1 -5
- package/dist/core/runtime/model-registry.d.ts +3 -0
- package/dist/core/runtime/model-registry.js +13 -0
- package/dist/core/runtime/naming-persist.d.ts +4 -0
- package/dist/core/runtime/naming-persist.js +27 -1
- package/dist/core/runtime/naming.d.ts +17 -12
- package/dist/core/runtime/naming.js +91 -140
- package/dist/core/runtime/node-read.js +19 -10
- package/dist/core/runtime/nodes.d.ts +9 -14
- package/dist/core/runtime/nodes.js +3 -53
- package/dist/core/runtime/package-health.d.ts +6 -21
- package/dist/core/runtime/package-health.js +12 -123
- package/dist/core/runtime/placement-tmux.d.ts +2 -1
- package/dist/core/runtime/placement-tmux.js +6 -4
- package/dist/core/runtime/placement.d.ts +1 -0
- package/dist/core/runtime/placement.js +4 -8
- package/dist/core/runtime/promote.js +3 -0
- package/dist/core/runtime/recap.d.ts +2 -7
- package/dist/core/runtime/recap.js +35 -69
- package/dist/core/runtime/revive.js +34 -77
- package/dist/core/runtime/session-cycles.d.ts +4 -0
- package/dist/core/runtime/session-cycles.js +14 -1
- package/dist/core/runtime/session-visibility.d.ts +37 -0
- package/dist/core/runtime/session-visibility.js +83 -0
- package/dist/core/runtime/spawn.js +10 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -13
- package/dist/core/runtime/surface-bg.js +1 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +35 -7
- package/dist/core/runtime/tmux.js +439 -62
- package/dist/core/runtime/tool-group-summary.d.ts +19 -0
- package/dist/core/runtime/tool-group-summary.js +88 -0
- package/dist/core/scope.d.ts +2 -7
- package/dist/core/scope.js +9 -35
- package/dist/core/substrate/schema.d.ts +2 -2
- package/dist/core/termrender/code-doc.d.ts +12 -0
- package/dist/core/termrender/code-doc.js +92 -0
- package/dist/core/termrender/display.d.ts +12 -0
- package/dist/core/termrender/display.js +19 -0
- package/dist/core/termrender/termrender.d.ts +73 -0
- package/dist/core/termrender/termrender.js +795 -0
- package/dist/core/termrender/version.d.ts +1 -0
- package/dist/core/termrender/version.js +1 -0
- package/dist/core/tui/terminal.js +1 -2
- package/dist/core/user-settings.d.ts +71 -3
- package/dist/core/user-settings.js +83 -1
- package/dist/daemon/api/__tests__/inbox.test.js +44 -133
- package/dist/daemon/api/handlers/human.d.ts +0 -10
- package/dist/daemon/api/handlers/human.js +104 -702
- package/dist/daemon/api/handlers/inbox.js +113 -142
- package/dist/daemon/api/handlers/review-comments.d.ts +2 -0
- package/dist/daemon/api/handlers/review-comments.js +500 -0
- package/dist/daemon/api/handlers/reviews.d.ts +2 -0
- package/dist/daemon/api/handlers/reviews.js +287 -0
- package/dist/daemon/api/map.d.ts +22 -7
- package/dist/daemon/api/map.js +159 -11
- package/dist/daemon/api/server.js +4 -0
- package/dist/daemon/cron-run.js +6 -9
- package/dist/daemon/crtrd.js +17 -33
- package/dist/daemon/fleet.d.ts +1 -8
- package/dist/daemon/fleet.js +4 -30
- package/dist/daemon/human/finish.d.ts +14 -0
- package/dist/daemon/human/finish.js +115 -0
- package/dist/daemon/human/sweep.d.ts +6 -0
- package/dist/daemon/human/sweep.js +49 -0
- package/dist/daemon/messaging/node-message.d.ts +18 -0
- package/dist/daemon/messaging/node-message.js +70 -0
- package/dist/daemon/review/comment-notify.d.ts +26 -0
- package/dist/daemon/review/comment-notify.js +107 -0
- package/dist/daemon/review/deliver.d.ts +7 -0
- package/dist/daemon/review/deliver.js +56 -0
- package/dist/daemon/review/finish.d.ts +22 -0
- package/dist/daemon/review/finish.js +254 -0
- package/dist/daemon/review/sweep.d.ts +11 -0
- package/dist/daemon/review/sweep.js +120 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -2
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +42 -64
- package/dist/pi-extensions/__tests__/canvas-recap.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-recap.test.js +159 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +16 -38
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +8 -11
- package/dist/pi-extensions/canvas-context-intro.js +50 -104
- package/dist/pi-extensions/canvas-recap.d.ts +15 -2
- package/dist/pi-extensions/canvas-recap.js +192 -69
- package/dist/pi-extensions/canvas-review-boundary.d.ts +59 -0
- package/dist/pi-extensions/canvas-review-boundary.js +81 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +3 -3
- package/dist/pi-extensions/canvas-stophook.js +78 -128
- package/dist/pi-extensions/canvas-structured-output.js +52 -8
- package/dist/shared/generated-context.d.ts +29 -4
- package/dist/shared/generated-context.js +122 -7
- package/dist/shared/tool-groups.d.ts +23 -0
- package/dist/shared/tool-groups.js +60 -0
- package/dist/shared/working-activity.d.ts +8 -0
- package/dist/shared/working-activity.js +26 -0
- package/dist/types.d.ts +36 -2
- package/dist/types.js +51 -0
- package/dist/web-client/assets/index-CCPaLeW7.css +2 -0
- package/dist/web-client/assets/index-kyOWsnZK.js +86 -0
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/docs/compat/hearth-crtr-v4.md +2 -2
- package/docs/compat/hearth-crtr-v5.md +4 -2
- package/docs/public-api.md +3 -3
- package/package.json +21 -14
- package/runtime.lock.json +4661 -1453
- package/scripts/postinstall.mjs +15 -2
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +0 -36
- package/dist/builtin-pi-packages/pi-mode-switch/bin/mode +0 -36
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +0 -449
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +0 -14
- package/dist/builtin-pi-packages/pi-mode-switch/tsconfig.json +0 -9
- package/dist/builtin-views/_lib/states.mjs +0 -161
- package/dist/builtin-views/canvas/core.mjs +0 -657
- package/dist/builtin-views/canvas/text.mjs +0 -58
- package/dist/builtin-views/canvas/tui.mjs +0 -168
- package/dist/builtin-views/canvas/web.jsx +0 -121
- package/dist/builtin-views/chat/core.mjs +0 -684
- package/dist/builtin-views/chat/text.mjs +0 -101
- package/dist/builtin-views/chat/tui.mjs +0 -360
- package/dist/builtin-views/chat/web.jsx +0 -362
- package/dist/builtin-views/git-pr/core.mjs +0 -673
- package/dist/builtin-views/git-pr/text.mjs +0 -84
- package/dist/builtin-views/git-pr/tui.mjs +0 -301
- package/dist/builtin-views/git-pr/web.jsx +0 -216
- package/dist/builtin-views/inbox/_lib/render.mjs +0 -175
- package/dist/builtin-views/inbox/core.mjs +0 -1273
- package/dist/builtin-views/inbox/text.mjs +0 -73
- package/dist/builtin-views/inbox/tui.mjs +0 -314
- package/dist/builtin-views/inbox/web.jsx +0 -188
- package/dist/builtin-views/linkedin/core.mjs +0 -906
- package/dist/builtin-views/linkedin/text.mjs +0 -69
- package/dist/builtin-views/linkedin/tui.mjs +0 -413
- package/dist/builtin-views/linkedin/web.jsx +0 -206
- package/dist/builtin-views/prompt-review/core.mjs +0 -735
- package/dist/builtin-views/prompt-review/text.mjs +0 -15
- package/dist/builtin-views/prompt-review/tui.mjs +0 -196
- package/dist/builtin-views/prompt-review/web.jsx +0 -484
- package/dist/builtin-views/workspace-sidebar/__tests__/core.test.js +0 -50
- package/dist/builtin-views/workspace-sidebar/__tests__/core.test.ts +0 -53
- package/dist/builtin-views/workspace-sidebar/core.mjs +0 -670
- package/dist/builtin-views/workspace-sidebar/text.mjs +0 -53
- package/dist/builtin-views/workspace-sidebar/tui.mjs +0 -141
- package/dist/builtin-views/workspace-sidebar/web.jsx +0 -109
- package/dist/clients/attach/session/mode.d.ts +0 -25
- package/dist/clients/attach/session/mode.js +0 -97
- package/dist/commands/sys/promptstudio.d.ts +0 -2
- package/dist/commands/sys/promptstudio.js +0 -65
- package/dist/commands/view-cycle.d.ts +0 -2
- package/dist/commands/view-cycle.js +0 -130
- package/dist/commands/view-list.d.ts +0 -2
- package/dist/commands/view-list.js +0 -66
- package/dist/commands/view-new.d.ts +0 -2
- package/dist/commands/view-new.js +0 -74
- package/dist/commands/view-pick.d.ts +0 -6
- package/dist/commands/view-pick.js +0 -129
- package/dist/commands/view-run.d.ts +0 -2
- package/dist/commands/view-run.js +0 -214
- package/dist/commands/view.d.ts +0 -2
- package/dist/commands/view.js +0 -31
- package/dist/commands/workspace.d.ts +0 -2
- package/dist/commands/workspace.js +0 -165
- package/dist/core/__tests__/chat-view-reconnect.test.js +0 -105
- package/dist/core/__tests__/full/consult-startup-failure.test.js +0 -124
- package/dist/core/canvas/__tests__/human-work-outbox.test.js +0 -123
- package/dist/core/canvas/human-work-outbox.d.ts +0 -73
- package/dist/core/canvas/human-work-outbox.js +0 -261
- package/dist/core/command-plugins/store.d.ts +0 -16
- package/dist/core/command-plugins/store.js +0 -64
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +0 -40
- package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
- package/dist/core/runtime/fault-recovery-nudge.js +0 -3
- package/dist/core/tui/__tests__/host-keybindings.test.js +0 -113
- package/dist/core/tui/host.d.ts +0 -66
- package/dist/core/view/__tests__/transport-cache.test.js +0 -62
- package/dist/core/view/bridge.d.ts +0 -10
- package/dist/core/view/bridge.js +0 -31
- package/dist/core/view/chrome.d.ts +0 -9
- package/dist/core/view/chrome.js +0 -22
- package/dist/core/view/contract.d.ts +0 -205
- package/dist/core/view/contract.js +0 -23
- package/dist/core/view/loader.d.ts +0 -31
- package/dist/core/view/loader.js +0 -188
- package/dist/core/view/stream-local.d.ts +0 -3
- package/dist/core/view/stream-local.js +0 -231
- package/dist/core/view/transport-cache.d.ts +0 -8
- package/dist/core/view/transport-cache.js +0 -38
- package/dist/core/view/transport-local.d.ts +0 -7
- package/dist/core/view/transport-local.js +0 -83
- package/dist/core/view/transport-remote.d.ts +0 -2
- package/dist/core/view/transport.d.ts +0 -10
- package/dist/core/view/transport.js +0 -15
- package/dist/pi-extensions/naming-tool.d.ts +0 -24
- package/dist/pi-extensions/naming-tool.js +0 -67
- package/dist/prompts/view.d.ts +0 -7
- package/dist/prompts/view.js +0 -172
- package/dist/web/ViewChrome.d.ts +0 -7
- package/dist/web/ViewChrome.js +0 -28
- package/dist/web/ViewPane.d.ts +0 -39
- package/dist/web/ViewPane.js +0 -48
- package/dist/web/index.d.ts +0 -8
- package/dist/web/index.js +0 -22
- package/dist/web/runtime.d.ts +0 -39
- package/dist/web/runtime.js +0 -232
- package/dist/web/states.d.ts +0 -24
- package/dist/web/states.js +0 -24
- package/dist/web/transport-http.d.ts +0 -5
- package/dist/web/transport-http.js +0 -34
- package/dist/web/transport-stream.d.ts +0 -3
- package/dist/web/transport-stream.js +0 -204
- package/dist/web-client/assets/index-U_NZ66VE.js +0 -79
- package/dist/web-client/assets/index-aaeu4adv.css +0 -2
- /package/dist/{builtin-views/workspace-sidebar/__tests__/core.test.d.ts → clients/attach/__tests__/file-review-focus.test.d.ts} +0 -0
- /package/dist/clients/attach/__tests__/{visible-paths.test.d.ts → group-activity.test.d.ts} +0 -0
- /package/dist/{core/__tests__/chat-view-reconnect.test.d.ts → clients/attach/__tests__/input-controller-extension-command.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/consult-startup-failure.test.d.ts → clients/attach/__tests__/session-identity-refresh.test.d.ts} +0 -0
- /package/dist/{core/canvas/__tests__/human-work-outbox.test.d.ts → clients/inbox/__tests__/inbox-controller.test.d.ts} +0 -0
- /package/dist/{core/keybindings/__tests__/bespoke-consumers.test.d.ts → clients/inbox/__tests__/mount-panel.test.d.ts} +0 -0
- /package/dist/{core/tui/__tests__/host-keybindings.test.d.ts → clients/inbox/review/__tests__/editor-roundtrip.test.d.ts} +0 -0
- /package/dist/{core/view/__tests__/transport-cache.test.d.ts → clients/inbox/review/__tests__/remap.test.d.ts} +0 -0
- /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;
|