mixdog 1.3.5 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +2 -2
- package/README.md +2 -2
- package/package.json +2 -11
- package/scripts/daemon-stub.mjs +1 -1
- package/scripts/gate-local.mjs +4 -2
- package/scripts/native-release-status.mjs +2 -2
- package/scripts/recall-history-cases.json +1 -1
- package/scripts/suite-health.mjs +10 -1
- package/scripts/test.mjs +7 -11
- package/src/agents/reviewer/AGENT.md +7 -11
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +17 -1
- package/src/defaults/skills/browser-use/SKILL.md +59 -11
- package/src/defaults/skills/code-tidy/SKILL.md +142 -270
- package/src/defaults/skills/code-tidy/references/agent-cleanup.md +4 -4
- package/src/defaults/skills/code-tidy/references/cleanup-report.md +31 -76
- package/src/defaults/skills/code-tidy/references/dead-code.md +6 -12
- package/src/defaults/skills/computer-use/SKILL.md +33 -9
- package/src/defaults/skills/setup/SKILL.md +22 -0
- package/src/defaults/skills/setup/references/actions.md +39 -5
- package/src/defaults/skills/setup/references/surfaces.md +11 -2
- package/src/defaults/skills/visualize/SKILL.md +2 -1
- package/src/lib/rules-builder.cjs +10 -8
- package/src/rules/agent/AGENT.md +6 -6
- package/src/rules/lead/LEAD.md +18 -20
- package/src/rules/shared/05-tool-calls.md +5 -6
- package/src/rules/shared/10-tool-workflow.md +2 -0
- package/src/rules/shared/60-verification.md +7 -5
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -1
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
- package/src/runtime/agent/orchestrator/config.mjs +17 -1
- package/src/runtime/agent/orchestrator/context/collect.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +72 -67
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +54 -1
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -3
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +176 -72
- package/src/runtime/agent/orchestrator/mcp/client.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +181 -133
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +42 -6
- package/src/runtime/agent/orchestrator/providers/anthropic-cache-warm.mjs +165 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +14 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +145 -66
- package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +25 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +64 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +135 -87
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +123 -102
- package/src/runtime/agent/orchestrator/providers/cache-warm-admission.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +25 -9
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -13
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +52 -10
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +8 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +11 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-send/send-context.mjs +22 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +43 -5
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/handshake-failure.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/stream-failure.mjs +6 -3
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +65 -1
- package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/registry.mjs +26 -0
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +39 -7
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +6 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +12 -7
- package/src/runtime/agent/orchestrator/runtime-core/compaction-config-keys.mjs +62 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +37 -16
- package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +100 -23
- package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +19 -4
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +125 -30
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +792 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +133 -22
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +475 -58
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +42 -17
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -4
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +18 -22
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +17 -3
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/after-hook.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -10
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +19 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +133 -90
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +31 -4
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -18
- package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +23 -13
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +129 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +102 -15
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +18 -5
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +19 -8
- package/src/runtime/agent/orchestrator/session/prompt-cache-idle-gaps.mjs +113 -0
- package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +127 -0
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +36 -9
- package/src/runtime/agent/orchestrator/session/sensitive-projection.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/sensitive-values.mjs +257 -0
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +20 -7
- package/src/runtime/agent/orchestrator/session/side-question.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/skill-update-notice.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/store/save-delta.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/store/session-persistence-events.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -14
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +84 -3
- package/src/runtime/agent/orchestrator/session/stored-attachment-history.mjs +32 -0
- package/src/runtime/agent/orchestrator/session/thinking-binding.mjs +108 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +9 -7
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-text.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/turn-failure.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +240 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/shell-cwd.mjs +38 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +25 -5
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +65 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -13
- package/src/runtime/browser-bridge/action-schema.mjs +13 -2
- package/src/runtime/browser-bridge/client.mjs +73 -8
- package/src/runtime/browser-bridge/input-fields.mjs +8 -9
- package/src/runtime/browser-bridge/mail-links.mjs +107 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
- package/src/runtime/channels/lib/config.mjs +6 -61
- package/src/runtime/channels/lib/crash-log.mjs +1 -1
- package/src/runtime/channels/lib/event-pipeline.mjs +4 -19
- package/src/runtime/channels/lib/event-queue.mjs +3 -18
- package/src/runtime/channels/lib/owned-runtime/automation-services.mjs +2 -2
- package/src/runtime/channels/lib/owned-runtime/automation-start.mjs +3 -4
- package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +4 -59
- package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +33 -115
- package/src/runtime/channels/lib/owned-runtime/owned-stop.mjs +5 -22
- package/src/runtime/channels/lib/owned-runtime/state.mjs +2 -8
- package/src/runtime/channels/lib/owned-runtime.mjs +6 -23
- package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -43
- package/src/runtime/channels/lib/scheduler.mjs +33 -147
- package/src/runtime/channels/lib/schedules-db.mjs +34 -11
- package/src/runtime/channels/lib/status-snapshot.mjs +1 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +1 -2
- package/src/runtime/channels/lib/webhooks-db.mjs +5 -9
- package/src/runtime/channels/lib/worker-bootstrap.mjs +0 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +0 -1
- package/src/runtime/channels/lib/worker-main.mjs +10 -34
- package/src/runtime/computer-bridge/action-schema.mjs +42 -1
- package/src/runtime/computer-bridge/actions.mjs +9 -1
- package/src/runtime/computer-bridge/client.mjs +180 -8
- package/src/runtime/computer-bridge/core-actions.mjs +2 -2
- package/src/runtime/computer-bridge/error-recovery.mjs +27 -2
- package/src/runtime/computer-bridge/linux-readiness.mjs +65 -23
- package/src/runtime/computer-bridge/pending-continuation.mjs +1 -1
- package/src/runtime/connectors/connector-accounts-mcp.mjs +230 -0
- package/src/runtime/connectors/gmail-app-password.mjs +172 -39
- package/src/runtime/connectors/google-mcp.mjs +10 -12
- package/src/runtime/memory/lib/pg/adapter.mjs +77 -70
- package/src/runtime/memory/lib/pg/supervisor.mjs +24 -12
- package/src/runtime/memory/lib/runtime-fetcher.mjs +23 -5
- package/src/runtime/session-search/session-search-db.mjs +73 -9
- package/src/runtime/session-search/session-search-ingest.mjs +110 -20
- package/src/runtime/shared/abort-race.mjs +89 -0
- package/src/runtime/shared/agent-avatar.d.mts +0 -1
- package/src/runtime/shared/agent-kind.mjs +4 -3
- package/src/runtime/shared/bounded-download.mjs +8 -3
- package/src/runtime/shared/config.mjs +2 -13
- package/src/runtime/shared/connector-accounts.mjs +20 -10
- package/src/runtime/shared/connector-catalog.d.mts +29 -0
- package/src/runtime/shared/connector-catalog.mjs +150 -0
- package/src/runtime/shared/connector-triggers.mjs +22 -19
- package/src/runtime/shared/defaults-separation.mjs +2 -14
- package/src/runtime/shared/delegated-approval-host.mjs +103 -11
- package/src/runtime/shared/err-text.mjs +24 -15
- package/src/runtime/shared/file-permissions.d.mts +0 -8
- package/src/runtime/shared/file-permissions.mjs +0 -11
- package/src/runtime/shared/git-direct.test-support.mjs +19 -0
- package/src/runtime/shared/heartbeat-interval.d.mts +4 -0
- package/src/runtime/shared/heartbeat-interval.mjs +19 -0
- package/src/runtime/shared/idle-auto-clear-gate.mjs +104 -0
- package/src/runtime/shared/llm/usage-rollup.mjs +1 -1
- package/src/runtime/shared/mixy-presence.mjs +22 -0
- package/src/runtime/shared/orchestration.d.mts +2 -0
- package/src/runtime/shared/pg-schema-db.mjs +27 -16
- package/src/runtime/shared/profile-config.cjs +1 -1
- package/src/runtime/shared/prompt-origin.d.mts +4 -0
- package/src/runtime/shared/prompt-origin.mjs +21 -0
- package/src/runtime/shared/provider-accounts.mjs +7 -1
- package/src/runtime/shared/provider-display-name.mjs +29 -0
- package/src/runtime/shared/provider-timeout-phase.mjs +2 -0
- package/src/runtime/shared/routine-trigger.d.mts +2 -0
- package/src/runtime/shared/routine-trigger.mjs +10 -0
- package/src/runtime/shared/sensitive-path-rules.d.mts +2 -10
- package/src/runtime/shared/sensitive-path-rules.mjs +19 -41
- package/src/runtime/shared/session-activity-signal.mjs +34 -1
- package/src/runtime/shared/session-sidebar-host.d.mts +1 -1
- package/src/runtime/shared/session-sidebar-host.mjs +7 -3
- package/src/runtime/shared/session-turn-prelude.mjs +18 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -14
- package/src/runtime/shared/worktrees/test-support.mjs +9 -4
- package/src/runtime/terminal-bridge/client.mjs +10 -1
- package/src/runtime/visualize-bridge/client.mjs +27 -8
- package/src/runtime/visualize-bridge/tool-defs.mjs +22 -8
- package/src/runtime/visualize-bridge/tool-meta.d.mts +5 -0
- package/src/runtime/visualize-bridge/tool-meta.mjs +22 -0
- package/src/runtime/web-search/lib/fetch-pipeline.mjs +2 -2
- package/src/session-runtime/agent-home-delivery.mjs +448 -185
- package/src/session-runtime/agent-task-sessions.mjs +329 -180
- package/src/session-runtime/boot/apis.mjs +9 -0
- package/src/session-runtime/boot/infrastructure.mjs +4 -0
- package/src/session-runtime/boot/providers.mjs +3 -0
- package/src/session-runtime/boot/tools.mjs +6 -1
- package/src/session-runtime/config-lifecycle.mjs +6 -0
- package/src/session-runtime/connector-merge-migration.mjs +305 -0
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +132 -28
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +2 -0
- package/src/session-runtime/lifecycle/inheritance.mjs +16 -3
- package/src/session-runtime/lifecycle/session-catalog.mjs +3 -1
- package/src/session-runtime/lifecycle/teardown/runtime-stops.mjs +50 -4
- package/src/session-runtime/lifecycle/teardown.mjs +23 -5
- package/src/session-runtime/mcp-glue/server-catalog.mjs +55 -3
- package/src/session-runtime/mcp-oauth-login.mjs +5 -3
- package/src/session-runtime/mixy-heartbeat.mjs +153 -33
- package/src/session-runtime/notification-bus.mjs +31 -117
- package/src/session-runtime/notification-completion-queue.mjs +54 -0
- package/src/session-runtime/notification-listeners.mjs +76 -0
- package/src/session-runtime/owned-automation-run.mjs +2 -1
- package/src/session-runtime/prewarm/channel-start.mjs +3 -7
- package/src/session-runtime/resource-mcp-api.mjs +143 -224
- package/src/session-runtime/resource-mcp-connector-accounts.mjs +163 -0
- package/src/session-runtime/resource-mcp-oauth-api.mjs +217 -0
- package/src/session-runtime/resource-mcp-secrets.mjs +82 -0
- package/src/session-runtime/resource-mcp-servers.mjs +78 -0
- package/src/session-runtime/resource-mcp-store.mjs +49 -0
- package/src/session-runtime/routine-owner.mjs +40 -9
- package/src/session-runtime/services/agent-tool/close-flow.mjs +3 -0
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +7 -2
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +1 -0
- package/src/session-runtime/services/agent-tool/worker-index/awaiting-user-sync.mjs +51 -0
- package/src/session-runtime/services/agent-tool/worker-index/disposal.mjs +55 -0
- package/src/session-runtime/services/agent-tool/worker-index/released-store.mjs +123 -0
- package/src/session-runtime/services/agent-tool/worker-index/restart-settlement.mjs +112 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-reads.mjs +36 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +17 -4
- package/src/session-runtime/services/agent-tool/worker-index/row-writes.mjs +58 -0
- package/src/session-runtime/services/agent-tool/worker-index.mjs +41 -241
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +3 -0
- package/src/session-runtime/services/channel-admin.mjs +7 -13
- package/src/session-runtime/services/channel-session-router.mjs +4 -2
- package/src/session-runtime/services/seeds.mjs +0 -28
- package/src/session-runtime/session-compaction-sync.mjs +27 -0
- package/src/session-runtime/session-create/session-options.mjs +1 -0
- package/src/session-runtime/settings-builtin-tools-api.mjs +11 -0
- package/src/session-runtime/settings-compaction-api.mjs +2 -40
- package/src/session-runtime/settings-system-api.mjs +13 -1
- package/src/session-runtime/setup-tool/agent-settings-actions.mjs +65 -0
- package/src/session-runtime/setup-tool/executor.mjs +145 -22
- package/src/session-runtime/setup-tool/extended-actions.mjs +8 -3
- package/src/session-runtime/setup-tool/session-route-actions.mjs +152 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +4 -3
- package/src/session-runtime/setup-tool/tool-defs.mjs +64 -5
- package/src/session-runtime/tool-policy/agent-profile-fold.mjs +17 -13
- package/src/session-runtime/tool-policy/policy-stage.mjs +16 -2
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +4 -0
- package/src/standalone/channel-transport/client-registry/client-records.mjs +65 -0
- package/src/standalone/channel-transport/client-registry/client-registration.mjs +118 -0
- package/src/standalone/channel-transport/client-registry.mjs +24 -163
- package/src/standalone/channel-transport.mjs +1 -1
- package/src/standalone/daemon-channels-loader.mjs +10 -8
- package/src/standalone/daemon-session-control/list-page.mjs +72 -0
- package/src/standalone/daemon-session-control/request.mjs +51 -0
- package/src/standalone/daemon-session-control/room-host.mjs +57 -0
- package/src/standalone/daemon-session-control/route-control.mjs +48 -0
- package/src/standalone/daemon-session-control/send-turn.mjs +60 -0
- package/src/standalone/daemon-session-control/worktree-control.mjs +40 -0
- package/src/standalone/daemon-session-control.mjs +124 -188
- package/src/standalone/daemon.mjs +26 -3
- package/src/standalone/session-client.mjs +7 -3
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +1 -0
- package/src/standalone/session-service/quota-resume.mjs +3 -0
- package/src/standalone/session-service/retention/busy.mjs +65 -1
- package/src/standalone/session-service/session-calls/session-action.mjs +23 -2
- package/src/standalone/session-service/session-calls/session-views.mjs +8 -0
- package/src/standalone/session-tool-output.mjs +15 -7
- package/src/tui/app/model-options.mjs +3 -28
- package/src/tui/dist/index.mjs +58 -64
- package/src/tui/session/agent-job-feed/model-notification.mjs +3 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/notice-surface.mjs +7 -1
- package/src/tui/session/queue-helpers.mjs +3 -42
- package/src/tui/session/session-api/routes-agent-definitions.mjs +14 -2
- package/src/tui/session/session-api/routes-write.mjs +5 -1
- package/src/tui/session/session-api/routes.mjs +1 -0
- package/src/tui/session/session-api/settings.mjs +17 -1
- package/src/tui/session/session-flow/auto-clear.mjs +4 -33
- package/src/tui/session/session-flow/drain-closed-queue.mjs +38 -0
- package/src/tui/session/session-flow/drain-enqueue.mjs +86 -0
- package/src/tui/session/session-flow/drain-retry.mjs +35 -0
- package/src/tui/session/session-flow/drain.mjs +118 -149
- package/src/tui/session/session-flow/queue/entry-shape.mjs +5 -0
- package/src/tui/session/session-flow/queue/take-entries.mjs +5 -2
- package/src/tui/session/tool-approval-delegation.mjs +64 -0
- package/src/tui/session/tool-approval-request.mjs +39 -0
- package/src/tui/session/tool-approval-settle.mjs +62 -0
- package/src/tui/session/tool-approval.mjs +69 -115
- package/src/tui/session/tui-steering-persist.mjs +8 -1
- package/src/tui/session/turn-stream.mjs +19 -2
- package/src/tui/session/turn.mjs +1 -0
- package/src/vendor/statusline/bin/statusline-route.mjs +1 -219
- package/src/vendor/statusline/src/gateway/route-meta.mjs +35 -13
- package/scripts/code-graph-description-contract.mjs +0 -188
- package/src/runtime/channels/lib/owned-runtime/ownership-refresh.mjs +0 -36
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ACCOUNT_PROVIDERS,
|
|
3
|
+
MAX_ACCOUNTS,
|
|
3
4
|
accountScopedSessionId,
|
|
4
5
|
readProviderAccountPool,
|
|
5
6
|
chooseProviderAccount,
|
|
@@ -358,110 +359,182 @@ async function accountQuotaExhausted(providerName, row, model, error, account) {
|
|
|
358
359
|
evidence = providerAccountExhausted(providerAccountWithBlocks(entry), Date.now(), model, options);
|
|
359
360
|
exhausted = exhausted || evidence || providerAccountExhausted(entry, Date.now(), model, options);
|
|
360
361
|
}
|
|
361
|
-
|
|
362
|
+
const reason = evidence ? 'quota' : 'unknown';
|
|
363
|
+
return { exhausted, delay, evidence, ...(exhausted ? blockUntil(evidence, reason) : {}) };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Record an exhausted verdict on the roster. An account-level block holds
|
|
367
|
+
// every model, so it carries no model or limit scope.
|
|
368
|
+
function blockRefusedAccount(providerName, accountId, model, error, verdict) {
|
|
369
|
+
const { evidence, until, source, reset, accountBlocked } = verdict;
|
|
370
|
+
return blockProviderAccount(providerName, accountId, until, accountBlocked ? '' : model, source, {
|
|
371
|
+
quota: evidence === true,
|
|
372
|
+
reset: reset === true,
|
|
373
|
+
limit: accountBlocked ? '' : quotaLimitOf(error),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* A refusal `error` that account `accountId` gave outside the pool's send (a
|
|
379
|
+
* cache warm, which never rotates): judged and recorded on the roster exactly
|
|
380
|
+
* as the send would, with `instance` as that account's provider. Resolves
|
|
381
|
+
* whether the account was blocked.
|
|
382
|
+
*/
|
|
383
|
+
export async function recordAccountRefusal(providerName, accountId, model, error, instance) {
|
|
384
|
+
const row = readProviderAccountPool(providerName).accounts.find((entry) => entry.id === accountId);
|
|
385
|
+
if (!row) return false;
|
|
386
|
+
const verdict = await accountQuotaExhausted(providerName, row, model, error, () => instance);
|
|
387
|
+
if (!verdict.exhausted) return false;
|
|
388
|
+
await blockRefusedAccount(providerName, row.id, model, error, verdict);
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Whether every account in `ids` (default: the roster) is exhausted with quota
|
|
393
|
+
// evidence: observed in this send (`evidenced`), or a full window.
|
|
394
|
+
function rosterExhaustion(providerName, model, evidenced, ids) {
|
|
395
|
+
const pool = readProviderAccountPool(providerName);
|
|
396
|
+
// Manual policy: the eligible set is the selected account alone.
|
|
397
|
+
const scope = ids || (providerAccountPolicy(pool) === 'manual' ? [pool.selectedId] : null);
|
|
398
|
+
const rows = scope ? pool.accounts.filter((entry) => scope.includes(entry.id)) : pool.accounts;
|
|
399
|
+
const opts = { useCredits: pool.useCredits === true };
|
|
400
|
+
const now = Date.now();
|
|
401
|
+
const all =
|
|
402
|
+
rows.length > 0 &&
|
|
403
|
+
rows.every((entry) =>
|
|
404
|
+
providerAccountExhausted(
|
|
405
|
+
evidenced.has(entry.id) ? entry : providerAccountWithBlocks(entry, (block) => block.blockedQuota === true),
|
|
406
|
+
now,
|
|
407
|
+
model,
|
|
408
|
+
opts
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
return { rows, opts, now, all };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Mark `error` only when every account in `ids` is exhausted with quota
|
|
415
|
+
// evidence (`rosterExhaustion`). Without an error of its own, the send reports
|
|
416
|
+
// quota exhaustion only on that evidence, and a rate-limit cooldown otherwise.
|
|
417
|
+
function exhaustedSendError(providerName, model, evidenced, error, ids) {
|
|
418
|
+
const { rows, opts, now, all } = rosterExhaustion(providerName, model, evidenced, ids);
|
|
419
|
+
if (all) {
|
|
420
|
+
return quotaExhausted(error || accountsExhaustedError(opts.useCredits), providerName, model, rows, opts);
|
|
421
|
+
}
|
|
422
|
+
return error || accountsCoolingDownError(rows, now, model);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// The provider's bound on the time one send spends waiting for cooldowns.
|
|
426
|
+
function cooldownWaitBoundMs(providerName, account, ids) {
|
|
427
|
+
const id = ids?.[0] || readProviderAccountPool(providerName).selectedId || 'default';
|
|
428
|
+
return finiteMs(cooldownPolicyOf(account(id))?.waitForCooldownMs, 0);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// A cooldown-only eligible set whose earliest cooldown ends inside the
|
|
432
|
+
// remaining wait budget is waited out; otherwise the send fails as before.
|
|
433
|
+
// `ledger` is the send's own record of the roster (see `poolSend`).
|
|
434
|
+
async function waitOutCooldownOrThrow(request, ledger, error, ids) {
|
|
435
|
+
const { providerName, model, account, options } = request;
|
|
436
|
+
const { rows, now, all } = rosterExhaustion(providerName, model, ledger.evidenced, ids);
|
|
437
|
+
const bound = cooldownWaitBoundMs(providerName, account, ids);
|
|
438
|
+
// Persisting a short cooldown can outlast its deadline. Release only
|
|
439
|
+
// attempts cooled by this send, and only under an enabled wait policy.
|
|
440
|
+
const expired = bound > 0 && rows.some((row) => ledger.cooled.has(row.id) && !(coolingUntil(row, model) > now));
|
|
441
|
+
let wait = null;
|
|
442
|
+
if (!all) wait = expired ? 0 : earliestCooldownMs(rows, now, model);
|
|
443
|
+
if (wait == null || ledger.waitedMs + wait > bound) {
|
|
444
|
+
throw exhaustedSendError(providerName, model, ledger.evidenced, error, ids);
|
|
445
|
+
}
|
|
446
|
+
options.onStageChange?.('reconnecting', {
|
|
447
|
+
message: `Rate limited — every account is cooling down; retrying in ${Math.ceil(wait / 1000)}s`,
|
|
448
|
+
reason: 'rate_limited',
|
|
449
|
+
waitMs: wait,
|
|
450
|
+
retryAt: now + wait,
|
|
451
|
+
});
|
|
452
|
+
// A zero-duration sleep does not check cancellation, including on the
|
|
453
|
+
// final allowed send when no next loop iteration will check it either.
|
|
454
|
+
options.signal?.throwIfAborted();
|
|
455
|
+
await sleepWithAbort(wait, options.signal);
|
|
456
|
+
ledger.waitedMs += wait;
|
|
457
|
+
const lifted = Date.now();
|
|
458
|
+
for (const row of readProviderAccountPool(providerName).accounts) {
|
|
459
|
+
if (ledger.cooled.has(row.id) && !(coolingUntil(row, model) > lifted)) {
|
|
460
|
+
ledger.cooled.delete(row.id);
|
|
461
|
+
ledger.attempted.delete(row.id);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// No roster: with credit use off, a stored reading showing the plan's window
|
|
467
|
+
// spent refuses before the implicit account sends.
|
|
468
|
+
function throwIfPlanQuotaSpent(providerName, pool, model) {
|
|
469
|
+
if (pool.useCredits !== true) {
|
|
470
|
+
const stored = readCachedOAuthUsageSnapshot({ provider: providerName, model: '' });
|
|
471
|
+
// A snapshot cached before per-model gating is stale evidence.
|
|
472
|
+
if (stored?.quotaWindows?.length && !staleQuotaWindows(providerName, stored.quotaWindows)) {
|
|
473
|
+
const row = { usage: { windows: stored.quotaWindows, checkedAt: stored.cachedAt } };
|
|
474
|
+
if (providerAccountExhausted(row, Date.now(), model)) {
|
|
475
|
+
throw quotaExhausted(accountsExhaustedError(false), providerName, model, [row]);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Headroom policy: a session's first pick, or its move off its account on a
|
|
482
|
+
// limit, becomes the selection; continuing on its account does not, so
|
|
483
|
+
// concurrent sessions never flip the selection back and forth. A move forced
|
|
484
|
+
// by a model-scoped limit serves this request only (`scoped`). Stamps the
|
|
485
|
+
// serving account on `result` and resolves whether the selection may commit.
|
|
486
|
+
function bindServedAccount(providerName, result, rowId, model, binding) {
|
|
487
|
+
const { policy, boundId, sessionKey, switched, refusalScoped } = binding;
|
|
488
|
+
const leftBound = Boolean(boundId) && rowId !== boundId;
|
|
489
|
+
const scoped = switched
|
|
490
|
+
? refusalScoped
|
|
491
|
+
: policy === 'headroom' && leftBound && accountExhaustedForModelOnly(providerName, boundId, model);
|
|
492
|
+
recordServingAccount(providerName, result, rowId, scoped && boundId ? boundId : rowId);
|
|
493
|
+
if (sessionKey && policy === 'headroom' && !scoped) rememberSessionAccount(sessionKey, rowId);
|
|
494
|
+
return { scoped, commit: policy !== 'headroom' || !boundId || leftBound };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// A failed attempt is never re-issued once output reached the caller or the
|
|
498
|
+
// attempt was unsafe to retry (`unsafe`, judged by the caller and read only
|
|
499
|
+
// when nothing was emitted).
|
|
500
|
+
function attemptUnretryable(bound, error, unsafe) {
|
|
501
|
+
return Boolean(bound.emitted() || unsafe() || error.liveTextEmitted || error.emittedToolCall);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function rotationReason(evidence, accountBlocked) {
|
|
505
|
+
if (evidence) return 'Quota exhausted';
|
|
506
|
+
if (accountBlocked) return 'Account blocked';
|
|
507
|
+
return 'Rate limited (cooldown)';
|
|
362
508
|
}
|
|
363
509
|
|
|
364
510
|
// The pool's send: walks the eligible accounts until one answers, blocking
|
|
365
511
|
// the ones whose quota window is exhausted along the way.
|
|
366
512
|
function poolSend(providerName, account) {
|
|
367
513
|
return async function send(messages, model, tools, options = {}) {
|
|
368
|
-
const
|
|
514
|
+
const request = { providerName, model, account, options };
|
|
515
|
+
// This send's record of the roster: the accounts it tried, those it found
|
|
516
|
+
// exhausted with quota evidence, those it blocked on a rate-limit
|
|
517
|
+
// cooldown, and the time it spent waiting for cooldowns.
|
|
518
|
+
const ledger = { attempted: new Set(), evidenced: new Set(), cooled: new Set(), waitedMs: 0 };
|
|
369
519
|
let lastError;
|
|
370
520
|
let probed = false;
|
|
371
|
-
|
|
521
|
+
let sends = 0;
|
|
372
522
|
const sessionKey = options.sessionId ? `${providerName}:${options.sessionId}` : null;
|
|
373
523
|
const boundId = sessionAccountId(providerName, sessionKey, messages, options);
|
|
374
524
|
// Whether this send already left an account, and whether the latest quota
|
|
375
525
|
// refusal held only for this model's family.
|
|
376
526
|
let switched = false;
|
|
377
527
|
let refusalScoped = false;
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
// Without an error of its own, the send reports quota exhaustion only on
|
|
381
|
-
// that evidence, and a rate-limit cooldown otherwise.
|
|
382
|
-
const exhaustion = (ids) => {
|
|
383
|
-
const pool = readProviderAccountPool(providerName);
|
|
384
|
-
// Manual policy: the eligible set is the selected account alone.
|
|
385
|
-
const scope = ids || (providerAccountPolicy(pool) === 'manual' ? [pool.selectedId] : null);
|
|
386
|
-
const rows = scope ? pool.accounts.filter((entry) => scope.includes(entry.id)) : pool.accounts;
|
|
387
|
-
const opts = { useCredits: pool.useCredits === true };
|
|
388
|
-
const now = Date.now();
|
|
389
|
-
const all =
|
|
390
|
-
rows.length > 0 &&
|
|
391
|
-
rows.every((entry) =>
|
|
392
|
-
providerAccountExhausted(
|
|
393
|
-
evidenced.has(entry.id) ? entry : providerAccountWithBlocks(entry, (block) => block.blockedQuota === true),
|
|
394
|
-
now,
|
|
395
|
-
model,
|
|
396
|
-
opts
|
|
397
|
-
)
|
|
398
|
-
);
|
|
399
|
-
return { rows, opts, now, all };
|
|
400
|
-
};
|
|
401
|
-
const markExhausted = (error, ids) => {
|
|
402
|
-
const { rows, opts, now, all } = exhaustion(ids);
|
|
403
|
-
if (all) {
|
|
404
|
-
return quotaExhausted(error || accountsExhaustedError(opts.useCredits), providerName, model, rows, opts);
|
|
405
|
-
}
|
|
406
|
-
return error || accountsCoolingDownError(rows, now, model);
|
|
407
|
-
};
|
|
408
|
-
// Accounts this send blocked on a rate-limit cooldown, the time it spent
|
|
409
|
-
// waiting for cooldowns, and the provider's wait bound.
|
|
410
|
-
const cooled = new Set();
|
|
411
|
-
let waitedMs = 0;
|
|
412
|
-
let sends = 0;
|
|
413
|
-
const waitBoundMs = (ids) => {
|
|
414
|
-
const id = ids?.[0] || readProviderAccountPool(providerName).selectedId || 'default';
|
|
415
|
-
return finiteMs(cooldownPolicyOf(account(id))?.waitForCooldownMs, 0);
|
|
416
|
-
};
|
|
417
|
-
// A cooldown-only eligible set whose earliest cooldown ends inside the
|
|
418
|
-
// remaining wait budget is waited out; otherwise the send fails as before.
|
|
419
|
-
const waitOrThrow = async (error, ids) => {
|
|
420
|
-
const { rows, now, all } = exhaustion(ids);
|
|
421
|
-
const bound = waitBoundMs(ids);
|
|
422
|
-
// Persisting a short cooldown can outlast its deadline. Release only
|
|
423
|
-
// attempts cooled by this send, and only under an enabled wait policy.
|
|
424
|
-
const expired = bound > 0 && rows.some((row) => cooled.has(row.id) && !(coolingUntil(row, model) > now));
|
|
425
|
-
let wait = null;
|
|
426
|
-
if (!all) wait = expired ? 0 : earliestCooldownMs(rows, now, model);
|
|
427
|
-
if (wait == null || waitedMs + wait > bound) throw markExhausted(error, ids);
|
|
428
|
-
options.onStageChange?.('reconnecting', {
|
|
429
|
-
message: `Rate limited — every account is cooling down; retrying in ${Math.ceil(wait / 1000)}s`,
|
|
430
|
-
reason: 'rate_limited',
|
|
431
|
-
waitMs: wait,
|
|
432
|
-
retryAt: now + wait,
|
|
433
|
-
});
|
|
434
|
-
// A zero-duration sleep does not check cancellation, including on the
|
|
435
|
-
// final allowed send when no next loop iteration will check it either.
|
|
436
|
-
options.signal?.throwIfAborted();
|
|
437
|
-
await sleepWithAbort(wait, options.signal);
|
|
438
|
-
waitedMs += wait;
|
|
439
|
-
const lifted = Date.now();
|
|
440
|
-
for (const row of readProviderAccountPool(providerName).accounts) {
|
|
441
|
-
if (cooled.has(row.id) && !(coolingUntil(row, model) > lifted)) {
|
|
442
|
-
cooled.delete(row.id);
|
|
443
|
-
attempted.delete(row.id);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
while (attempted.size < 20 && sends < 20) {
|
|
528
|
+
// One try per connectable account at most (provider-accounts.mjs MAX_ACCOUNTS).
|
|
529
|
+
while (ledger.attempted.size < MAX_ACCOUNTS && sends < MAX_ACCOUNTS) {
|
|
448
530
|
options.signal?.throwIfAborted();
|
|
449
531
|
const pool = readProviderAccountPool(providerName);
|
|
450
532
|
if (!pool.accounts.length) {
|
|
451
|
-
|
|
452
|
-
const stored = readCachedOAuthUsageSnapshot({ provider: providerName, model: '' });
|
|
453
|
-
// A snapshot cached before per-model gating is stale evidence.
|
|
454
|
-
if (stored?.quotaWindows?.length && !staleQuotaWindows(providerName, stored.quotaWindows)) {
|
|
455
|
-
const row = { usage: { windows: stored.quotaWindows, checkedAt: stored.cachedAt } };
|
|
456
|
-
if (providerAccountExhausted(row, Date.now(), model)) {
|
|
457
|
-
throw quotaExhausted(accountsExhaustedError(false), providerName, model, [row]);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
533
|
+
throwIfPlanQuotaSpent(providerName, pool, model);
|
|
461
534
|
return account('default').send(messages, model, tools, options);
|
|
462
535
|
}
|
|
463
536
|
const policy = providerAccountPolicy(pool);
|
|
464
|
-
const row = chooseProviderAccount(pool, attempted, Date.now(), model, { sessionAccountId: boundId });
|
|
537
|
+
const row = chooseProviderAccount(pool, ledger.attempted, Date.now(), model, { sessionAccountId: boundId });
|
|
465
538
|
if (!row) {
|
|
466
539
|
// Once per send: a refusal built on a stale meter must not outlive the
|
|
467
540
|
// meter. Re-measuring can only widen the roster, so it is tried before
|
|
@@ -470,26 +543,22 @@ function poolSend(providerName, account) {
|
|
|
470
543
|
probed = true;
|
|
471
544
|
if (await probeRefusedAccounts(providerName, pool, model, account)) continue;
|
|
472
545
|
}
|
|
473
|
-
await
|
|
546
|
+
await waitOutCooldownOrThrow(request, ledger, lastError);
|
|
474
547
|
continue;
|
|
475
548
|
}
|
|
476
|
-
attempted.add(row.id);
|
|
549
|
+
ledger.attempted.add(row.id);
|
|
477
550
|
sends += 1;
|
|
478
551
|
const bound = boundAccountSend(providerName, row, messages, options);
|
|
479
552
|
try {
|
|
480
553
|
const result = await account(row.id).send(bound.history, model, tools, bound.opts);
|
|
481
554
|
quotaStrikes.delete(`${providerName}:${row.id}`);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
: policy === 'headroom' && leftBound && accountExhaustedForModelOnly(providerName, boundId, model);
|
|
490
|
-
recordServingAccount(providerName, result, row.id, scoped && boundId ? boundId : row.id);
|
|
491
|
-
if (sessionKey && policy === 'headroom' && !scoped) rememberSessionAccount(sessionKey, row.id);
|
|
492
|
-
const commit = policy !== 'headroom' || !boundId || leftBound;
|
|
555
|
+
const { scoped, commit } = bindServedAccount(providerName, result, row.id, model, {
|
|
556
|
+
policy,
|
|
557
|
+
boundId,
|
|
558
|
+
sessionKey,
|
|
559
|
+
switched,
|
|
560
|
+
refusalScoped,
|
|
561
|
+
});
|
|
493
562
|
if (commit && (await commitFallbackAccount(providerName, pool, row, model, scoped, policy))) {
|
|
494
563
|
// Publish only a committed selection, never a speculative retry or
|
|
495
564
|
// one superseded by a manual switch. The existing progress channel
|
|
@@ -508,62 +577,41 @@ function poolSend(providerName, account) {
|
|
|
508
577
|
} catch (error) {
|
|
509
578
|
if (options.signal?.aborted) throw error;
|
|
510
579
|
if (error?.reauthRequired === true) {
|
|
511
|
-
if (
|
|
512
|
-
policy === 'manual' ||
|
|
513
|
-
bound.emitted() ||
|
|
514
|
-
error.unsafeToRetry ||
|
|
515
|
-
error.liveTextEmitted ||
|
|
516
|
-
error.emittedToolCall
|
|
517
|
-
)
|
|
518
|
-
throw error;
|
|
580
|
+
if (policy === 'manual' || attemptUnretryable(bound, error, () => error.unsafeToRetry)) throw error;
|
|
519
581
|
lastError = error;
|
|
520
582
|
switched = true;
|
|
521
583
|
refusalScoped = false;
|
|
522
584
|
options.onStageChange?.('reconnecting', { message: 'Sign-in expired — switching to the next account' });
|
|
523
585
|
continue;
|
|
524
586
|
}
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
row,
|
|
528
|
-
model,
|
|
529
|
-
error,
|
|
530
|
-
account
|
|
531
|
-
);
|
|
587
|
+
const verdict = await accountQuotaExhausted(providerName, row, model, error, account);
|
|
588
|
+
const { exhausted, evidence, accountBlocked } = verdict;
|
|
532
589
|
if (options.signal?.aborted || !exhausted) throw error;
|
|
533
|
-
if (evidence) evidenced.add(row.id);
|
|
534
|
-
else cooled.add(row.id);
|
|
535
|
-
|
|
536
|
-
// limit scope.
|
|
537
|
-
await blockProviderAccount(providerName, row.id, until, accountBlocked ? '' : model, source, {
|
|
538
|
-
quota: evidence === true,
|
|
539
|
-
reset: reset === true,
|
|
540
|
-
limit: accountBlocked ? '' : quotaLimitOf(error),
|
|
541
|
-
});
|
|
590
|
+
if (evidence) ledger.evidenced.add(row.id);
|
|
591
|
+
else ledger.cooled.add(row.id);
|
|
592
|
+
await blockRefusedAccount(providerName, row.id, model, error, verdict);
|
|
542
593
|
// An account block is unsafe to re-issue on THAT account only; with no
|
|
543
594
|
// output emitted, the next account may serve the same request.
|
|
544
|
-
if (
|
|
545
|
-
bound.emitted() ||
|
|
546
|
-
(error.unsafeToRetry && accountBlocked !== true) ||
|
|
547
|
-
error.liveTextEmitted ||
|
|
548
|
-
error.emittedToolCall
|
|
549
|
-
)
|
|
550
|
-
throw error;
|
|
595
|
+
if (attemptUnretryable(bound, error, () => error.unsafeToRetry && accountBlocked !== true)) throw error;
|
|
551
596
|
// Manual policy: the eligible set is the selected account alone.
|
|
552
597
|
if (policy === 'manual') {
|
|
553
|
-
await
|
|
598
|
+
await waitOutCooldownOrThrow(request, ledger, error, [row.id]);
|
|
554
599
|
lastError = error;
|
|
555
600
|
continue;
|
|
556
601
|
}
|
|
557
602
|
lastError = error;
|
|
558
603
|
switched = true;
|
|
559
604
|
refusalScoped = accountExhaustedForModelOnly(providerName, row.id, model);
|
|
560
|
-
|
|
561
|
-
if (evidence) reason = 'Quota exhausted';
|
|
562
|
-
else if (accountBlocked) reason = 'Account blocked';
|
|
605
|
+
const reason = rotationReason(evidence, accountBlocked);
|
|
563
606
|
options.onStageChange?.('reconnecting', { message: `${reason} — switching to the next account` });
|
|
564
607
|
}
|
|
565
608
|
}
|
|
566
|
-
throw
|
|
609
|
+
throw exhaustedSendError(
|
|
610
|
+
providerName,
|
|
611
|
+
model,
|
|
612
|
+
ledger.evidenced,
|
|
613
|
+
lastError || new Error('Provider account attempts exhausted.')
|
|
614
|
+
);
|
|
567
615
|
};
|
|
568
616
|
}
|
|
569
617
|
|
|
@@ -264,7 +264,9 @@ export class ProviderAdmissionScheduler {
|
|
|
264
264
|
running.parentAbort = () => {
|
|
265
265
|
try {
|
|
266
266
|
controller.abort(item.signal.reason);
|
|
267
|
-
} catch {
|
|
267
|
+
} catch {
|
|
268
|
+
// the controller may already be aborted
|
|
269
|
+
}
|
|
268
270
|
};
|
|
269
271
|
item.signal.addEventListener('abort', running.parentAbort, { once: true });
|
|
270
272
|
}
|
|
@@ -301,7 +303,9 @@ export class ProviderAdmissionScheduler {
|
|
|
301
303
|
if (running.parentAbort && running.parentSignal) {
|
|
302
304
|
try {
|
|
303
305
|
running.parentSignal.removeEventListener('abort', running.parentAbort);
|
|
304
|
-
} catch {
|
|
306
|
+
} catch {
|
|
307
|
+
// the abort listener may already be gone
|
|
308
|
+
}
|
|
305
309
|
}
|
|
306
310
|
this.running.delete(running);
|
|
307
311
|
lane.active -= 1;
|
|
@@ -312,6 +316,32 @@ export class ProviderAdmissionScheduler {
|
|
|
312
316
|
if (this._canDeleteLane(lane)) this._deleteLane(key, lane);
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Why a request that must not wait (a cache warm) cannot start on lane
|
|
321
|
+
* `key` now, or null: the lane's rate-limit cooldown, or a lane throttled
|
|
322
|
+
* after a 429 that would queue it.
|
|
323
|
+
*/
|
|
324
|
+
immediateBlock(key) {
|
|
325
|
+
if (this.closedReason) return 'provider scheduler closed';
|
|
326
|
+
const lane = this.lanes.get(String(key || ''));
|
|
327
|
+
if (!lane) return null;
|
|
328
|
+
if (lane.adaptive && lane.cooldownUntil > this.now()) return 'rate-limit cooldown';
|
|
329
|
+
if (lane.queue.length > 0 || lane.active >= lane.limit) return 'account throttled';
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Record a rate limit a request outside `run` (a cache warm) received on
|
|
334
|
+
* lane `key`, into the same cooldown state (and broadcast) as a send's. */
|
|
335
|
+
recordRateLimit(key, error) {
|
|
336
|
+
const laneKey = String(key || '');
|
|
337
|
+
if (!laneKey || this.closedReason) return false;
|
|
338
|
+
const lane = this.lanes.get(laneKey) || createAdmissionLane(laneKey, this.concurrency);
|
|
339
|
+
this.lanes.set(laneKey, lane);
|
|
340
|
+
const recorded = this._recordFailure(laneKey, lane, error);
|
|
341
|
+
if (this._canDeleteLane(lane)) this._deleteLane(laneKey, lane);
|
|
342
|
+
return recorded;
|
|
343
|
+
}
|
|
344
|
+
|
|
315
345
|
snapshot() {
|
|
316
346
|
const now = this.now();
|
|
317
347
|
return {
|
|
@@ -428,7 +458,9 @@ export class ProviderAdmissionScheduler {
|
|
|
428
458
|
if (item.onAbort && item.signal) {
|
|
429
459
|
try {
|
|
430
460
|
item.signal.removeEventListener('abort', item.onAbort);
|
|
431
|
-
} catch {
|
|
461
|
+
} catch {
|
|
462
|
+
// the abort listener may already be gone
|
|
463
|
+
}
|
|
432
464
|
item.onAbort = null;
|
|
433
465
|
}
|
|
434
466
|
}
|
|
@@ -453,7 +485,9 @@ export class ProviderAdmissionScheduler {
|
|
|
453
485
|
for (const running of this.running) {
|
|
454
486
|
try {
|
|
455
487
|
running.controller.abort(this.closedReason);
|
|
456
|
-
} catch {
|
|
488
|
+
} catch {
|
|
489
|
+
// the controller may already be aborted
|
|
490
|
+
}
|
|
457
491
|
}
|
|
458
492
|
}
|
|
459
493
|
}
|
|
@@ -476,7 +510,7 @@ function digest(value) {
|
|
|
476
510
|
.digest('hex');
|
|
477
511
|
}
|
|
478
512
|
|
|
479
|
-
function providerAdmissionKey(providerName, provider) {
|
|
513
|
+
export function providerAdmissionKey(providerName, provider) {
|
|
480
514
|
const name = String(providerName || provider?.name || 'provider');
|
|
481
515
|
const stableAnthropicOAuthPath =
|
|
482
516
|
name.toLowerCase() === 'anthropic-oauth'
|
|
@@ -556,7 +590,9 @@ export function wrapProviderAdmission(provider, providerName, scheduler = provid
|
|
|
556
590
|
// time. Provider-local retry remains the sole retry owner.
|
|
557
591
|
try {
|
|
558
592
|
opts.onStageChange?.('requesting');
|
|
559
|
-
} catch {
|
|
593
|
+
} catch {
|
|
594
|
+
// the stage observer is advisory
|
|
595
|
+
}
|
|
560
596
|
const admittedOpts = {
|
|
561
597
|
...opts,
|
|
562
598
|
signal: admissionSignal,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// anthropic-cache-warm.mjs — the cache-warm mode both Anthropic providers
|
|
2
|
+
// expose as `sendCacheWarm` (session/cache-warm.mjs): one POST of the
|
|
3
|
+
// request the provider's own builder produces, no retry, fallback, re-send,
|
|
4
|
+
// wait or account rotation, and the stream cut at the first content block
|
|
5
|
+
// once the usage (cache read / write) has been reported. Right before the
|
|
6
|
+
// POST the account gate (cache-warm-admission.mjs) may skip it; a refusal is
|
|
7
|
+
// fed into the shared rate-limit and roster state. The request is bounded by
|
|
8
|
+
// the providers' own watchdogs: the first-byte timeout until the usage
|
|
9
|
+
// arrives, then the SSE idle timeout between events. Usage is handed to
|
|
10
|
+
// `onUsage` as it arrives, so a warm that fails or is aborted after the
|
|
11
|
+
// provider accepted it is still accounted. Every exit cancels the stream and
|
|
12
|
+
// aborts the transport.
|
|
13
|
+
import { PROVIDER_FIRST_BYTE_TIMEOUT_MS, PROVIDER_SSE_IDLE_TIMEOUT_MS } from '../stall-policy.mjs';
|
|
14
|
+
import { updateTurnUsage } from './anthropic-sse-turn/turn-state.mjs';
|
|
15
|
+
import { _anthropicSseError } from './anthropic-sse.mjs';
|
|
16
|
+
import { anthropicQuotaError } from './anthropic-oauth-request/initial-status.mjs';
|
|
17
|
+
import { cacheWarmAccountBlock, cacheWarmBlockedError, recordCacheWarmRefusal } from './cache-warm-admission.mjs';
|
|
18
|
+
import { scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
|
|
19
|
+
|
|
20
|
+
const SSE_LABELS = Object.freeze({ display: 'Anthropic cache warm' });
|
|
21
|
+
|
|
22
|
+
function usageSnapshot(usage) {
|
|
23
|
+
const { raw: _raw, ...rest } = usage;
|
|
24
|
+
return { ...rest };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// An abort deadline: `arm(ms)` (re)starts it; it fires `controller.abort`.
|
|
28
|
+
function watchdog(controller) {
|
|
29
|
+
let timer = null;
|
|
30
|
+
return {
|
|
31
|
+
arm(ms, what) {
|
|
32
|
+
if (timer) clearTimeout(timer);
|
|
33
|
+
timer = setTimeout(() => controller.abort(new Error(`cache warm ${what} timed out after ${ms}ms`)), ms);
|
|
34
|
+
timer.unref?.();
|
|
35
|
+
},
|
|
36
|
+
clear() {
|
|
37
|
+
if (timer) clearTimeout(timer);
|
|
38
|
+
timer = null;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read a warm's SSE stream until usage is known and the first content block
|
|
45
|
+
* starts, or the message ends. Resolves the usage; a stream error rejects
|
|
46
|
+
* with `error.usage` set to what was reported before it. `onChunk` hears
|
|
47
|
+
* every chunk (the idle watchdog). The stream is cancelled on every exit.
|
|
48
|
+
*/
|
|
49
|
+
export async function readCacheWarmStream(response, { onUsage = () => {}, onChunk = () => {} } = {}) {
|
|
50
|
+
const usage = { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, raw: null };
|
|
51
|
+
let reported = false;
|
|
52
|
+
const report = (raw) => {
|
|
53
|
+
if (!raw || typeof raw !== 'object') return;
|
|
54
|
+
updateTurnUsage(usage, raw);
|
|
55
|
+
reported = true;
|
|
56
|
+
onUsage(usageSnapshot(usage));
|
|
57
|
+
};
|
|
58
|
+
const reader = response.body.getReader();
|
|
59
|
+
const decoder = new TextDecoder();
|
|
60
|
+
let buffer = '';
|
|
61
|
+
try {
|
|
62
|
+
for (;;) {
|
|
63
|
+
const { value, done } = await reader.read();
|
|
64
|
+
if (done) break;
|
|
65
|
+
buffer += decoder.decode(value, { stream: true });
|
|
66
|
+
let cut = buffer.indexOf('\n\n');
|
|
67
|
+
while (cut >= 0) {
|
|
68
|
+
const frame = buffer.slice(0, cut);
|
|
69
|
+
buffer = buffer.slice(cut + 2);
|
|
70
|
+
cut = buffer.indexOf('\n\n');
|
|
71
|
+
const data = frame
|
|
72
|
+
.split('\n')
|
|
73
|
+
.filter((line) => line.startsWith('data:'))
|
|
74
|
+
.map((line) => line.slice(5).trimStart())
|
|
75
|
+
.join('\n');
|
|
76
|
+
if (!data) continue;
|
|
77
|
+
let event;
|
|
78
|
+
try {
|
|
79
|
+
event = JSON.parse(data);
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (event?.type === 'error' || event?.error) throw _anthropicSseError(event, SSE_LABELS);
|
|
84
|
+
if (event.type === 'message_start') report(event.message?.usage);
|
|
85
|
+
else if (event.type === 'message_delta') report(event.usage);
|
|
86
|
+
else if (event.type === 'content_block_start' && reported) {
|
|
87
|
+
// The prefix was read (and the cache refreshed) before any content:
|
|
88
|
+
// nothing generated after this point is needed.
|
|
89
|
+
return usageSnapshot(usage);
|
|
90
|
+
} else if (event.type === 'message_stop') return usageSnapshot(usage);
|
|
91
|
+
}
|
|
92
|
+
onChunk(reported);
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error && typeof error === 'object' && error.usage === undefined) {
|
|
96
|
+
error.usage = reported ? usageSnapshot(usage) : null;
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
} finally {
|
|
100
|
+
await reader.cancel().catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
return usageSnapshot(usage);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* One warm on account instance `instance` (roster account `accountId`, empty
|
|
107
|
+
* for a provider without a roster) of `providerName`: the account gate, then
|
|
108
|
+
* `post(signal)` (the provider's single POST of its built body), then the
|
|
109
|
+
* stream. A refusal reaches the shared rate-limit and roster state; the
|
|
110
|
+
* transport is aborted on every exit.
|
|
111
|
+
*/
|
|
112
|
+
export async function sendAnthropicCacheWarm({
|
|
113
|
+
providerName,
|
|
114
|
+
instance,
|
|
115
|
+
accountId = '',
|
|
116
|
+
model,
|
|
117
|
+
opts = {},
|
|
118
|
+
post,
|
|
119
|
+
onUsage,
|
|
120
|
+
}) {
|
|
121
|
+
const blocked = cacheWarmAccountBlock(providerName, instance, {
|
|
122
|
+
accountId,
|
|
123
|
+
model,
|
|
124
|
+
sessionId: opts.session?.id || opts.sessionId || '',
|
|
125
|
+
});
|
|
126
|
+
if (blocked) throw cacheWarmBlockedError(blocked);
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const callerSignal = opts.signal;
|
|
129
|
+
const onCallerAbort = () => controller.abort(callerSignal.reason);
|
|
130
|
+
if (callerSignal?.aborted) controller.abort(callerSignal.reason);
|
|
131
|
+
else callerSignal?.addEventListener?.('abort', onCallerAbort, { once: true });
|
|
132
|
+
const deadline = watchdog(controller);
|
|
133
|
+
deadline.arm(PROVIDER_FIRST_BYTE_TIMEOUT_MS, 'first byte');
|
|
134
|
+
try {
|
|
135
|
+
const response = await post(controller.signal);
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
const bodyText = await response.text().catch(() => '');
|
|
138
|
+
throw anthropicQuotaError(response.status, response.headers, bodyText);
|
|
139
|
+
}
|
|
140
|
+
return await readCacheWarmStream(response, {
|
|
141
|
+
onUsage,
|
|
142
|
+
onChunk: (reported) => {
|
|
143
|
+
if (reported) deadline.arm(PROVIDER_SSE_IDLE_TIMEOUT_MS, 'stream');
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// The warm's own error is what the caller needs; a failure to persist the
|
|
148
|
+
// refusal (quota / roster state) is reported, scrubbed, like the other
|
|
149
|
+
// credential and account-state write failures of the Anthropic providers.
|
|
150
|
+
await recordCacheWarmRefusal(providerName, instance, error, {
|
|
151
|
+
accountId,
|
|
152
|
+
model,
|
|
153
|
+
aborted: Boolean(callerSignal?.aborted),
|
|
154
|
+
}).catch((recordError) => {
|
|
155
|
+
process.stderr.write(
|
|
156
|
+
`[anthropic-cache-warm] refusal record failed: ${scrubOAuthSecrets(recordError?.message || String(recordError))}\n`
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
throw error;
|
|
160
|
+
} finally {
|
|
161
|
+
callerSignal?.removeEventListener?.('abort', onCallerAbort);
|
|
162
|
+
deadline.clear();
|
|
163
|
+
controller.abort(new Error('cache warm finished'));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -10,7 +10,7 @@ import { createNpmVersionSource, maxSemver } from './npm-cli-version.mjs';
|
|
|
10
10
|
// source release just to advance the user-agent version.
|
|
11
11
|
// Claude Opus 5.5 requires 2.1.280 or newer. The release workflow raises this
|
|
12
12
|
// to the published CLI version (scripts/sync-client-version-floors.mjs).
|
|
13
|
-
export const DEFAULT_CLI_VERSION = '2.1.
|
|
13
|
+
export const DEFAULT_CLI_VERSION = '2.1.311';
|
|
14
14
|
export const CLAUDE_CLI_NPM_PACKAGE = '@anthropic-ai/claude-code';
|
|
15
15
|
|
|
16
16
|
const CACHE_SCHEMA_VERSION = 1;
|
|
@@ -15,6 +15,7 @@ import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs
|
|
|
15
15
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
16
16
|
import { modelProxyClientMode, serverModelCredentialsDisabledError } from '../../../shared/model-proxy-mode.mjs';
|
|
17
17
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
18
|
+
import { TIMEOUT_PHASE } from '../../../shared/provider-timeout-phase.mjs';
|
|
18
19
|
import { claudeCliUserAgent, resolveCliVersion } from './anthropic-oauth-client-version.mjs';
|
|
19
20
|
import {
|
|
20
21
|
accountIdentityFields,
|
|
@@ -375,7 +376,9 @@ async function _refreshOAuthCredentialsUnlocked(creds) {
|
|
|
375
376
|
return refreshed;
|
|
376
377
|
} catch (err) {
|
|
377
378
|
if (err?.name === 'AbortError') {
|
|
378
|
-
throw new Error('Anthropic OAuth token refresh timed out after 30000ms')
|
|
379
|
+
throw Object.assign(new Error('Anthropic OAuth token refresh timed out after 30000ms'), {
|
|
380
|
+
timeoutPhase: TIMEOUT_PHASE.AUTH,
|
|
381
|
+
});
|
|
379
382
|
}
|
|
380
383
|
throw err;
|
|
381
384
|
} finally {
|