tumwater 0.4.2 → 0.4.4
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/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -2,23 +2,20 @@
|
|
|
2
2
|
* event-and-verdict half (src/gates/budget-gates.ts) so the two halves each have a plain
|
|
3
3
|
* function boundary and their own tests (the same split stage-path-checks.ts made of
|
|
4
4
|
* stage-check.ts). This module owns everything pollBudgetGate does from its `onFallback` seam
|
|
5
|
-
* through the end of the function: the seam/backends walk
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* part 5c/8, 6a/7) hold here. */
|
|
5
|
+
* through the end of the function: the seam/backends walk, the motion key (`resolvedKey`,
|
|
6
|
+
* `fromKey`), the fallback view derivation (`fallbackConfig`), `chainInfo`, and the `pairs`
|
|
7
|
+
* handback list. It no longer moves any tier for a used-up subscription window (part 5d/6): only
|
|
8
|
+
* a spent `maxDailyCostUsd` triggers a move, so the view's one job is the cap's free-seam
|
|
9
|
+
* install. Its one entry reads the inputs pollBudgetGate's first half already resolved and
|
|
10
|
+
* returns the view fields `BudgetGatePoll` carries, so pollBudgetGate keeps its call signature
|
|
11
|
+
* and src/gates/gate-polls.ts's destructure of it is unchanged. The plan references the moved
|
|
12
|
+
* comments carry (part 5b/8, part 5c/8, 6a/7) hold here. */
|
|
14
13
|
import { MODEL_TIERS, } from "../config/config-schema.js";
|
|
15
14
|
import { modelPairName, pairDailyCost } from "../budget/budget.js";
|
|
16
15
|
import { fallbackServingPair, } from "../budget/fallback-breaker.js";
|
|
17
|
-
import { applyFallbackModel,
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { formatModelSelector, seamAgent } from "../config/model-selector.js";
|
|
21
|
-
import { configForRole, isTierName, reviewConfig, roleSeamTier, withModelOverrides, } from "../config/config-views.js";
|
|
16
|
+
import { applyFallbackModel, tierChainSteps, } from "../config/config-fallbacks.js";
|
|
17
|
+
import { seamAgent } from "../config/config-model-selector.js";
|
|
18
|
+
import { withModelOverrides } from "../config/config-views.js";
|
|
22
19
|
import { piPairFree, seamFree } from "../pi/pi-models.js";
|
|
23
20
|
import { dayAt } from "../text/datetime.js";
|
|
24
21
|
/** Can the account not take extra usage: some window is rejected, or the newest non-reset
|
|
@@ -47,225 +44,76 @@ export function subscriptionBackendOf(x, accounts) {
|
|
|
47
44
|
const account = accounts(id);
|
|
48
45
|
return account?.billing === "subscription" ? id : null;
|
|
49
46
|
}
|
|
50
|
-
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// free pair — a demotion must never promote them to the priced model the cap already spent.
|
|
60
|
-
const onFallback = reached && pairNames.length > 0;
|
|
61
|
-
// A used-up window moves the subscription jobs of the backend whose window stands to their
|
|
62
|
-
// tier's fallback below the cap too; with no fallback to move to, nothing changes and that
|
|
63
|
-
// backend's hold applies. Every backend with a subscription seam in the config counts
|
|
64
|
-
// (seamsInUse enumerates them: every enabled role, the reviewer, the resolver, the director,
|
|
65
|
-
// and every tier fallback step), so a used-up window with no seam on that backend (or no
|
|
66
|
-
// fallback) moves nothing.
|
|
67
|
-
const seamBackends = new Set();
|
|
68
|
-
for (const seam of seamsInUse(liveConfig)) {
|
|
69
|
-
if (seam.agent === undefined)
|
|
70
|
-
continue;
|
|
71
|
-
const acc = accounts(seam.agent);
|
|
72
|
-
if (acc?.billing === "subscription")
|
|
73
|
-
seamBackends.add(seam.agent);
|
|
74
|
-
}
|
|
75
|
-
// The used-up windows behind this poll's move, keyed by backend: a backend counts only when
|
|
76
|
-
// a subscription seam of it is in the config, its own window is used up, and there is a
|
|
77
|
-
// fallback pair to move to. `windowOut` names only those backends; a healthy or absent
|
|
78
|
-
// backend's window is untouched.
|
|
79
|
-
const windowOut = {};
|
|
80
|
-
const movedBackends = new Set();
|
|
81
|
-
for (const backend of seamBackends) {
|
|
82
|
-
const win = windowOutFor(backend);
|
|
83
|
-
if (win !== null && pairNames.length > 0) {
|
|
84
|
-
windowOut[backend] = win;
|
|
85
|
-
movedBackends.add(backend);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const windowMove = movedBackends.size > 0;
|
|
89
|
-
// The resolved pairs are part of the key: a priced step crossing its own daily cap changes
|
|
90
|
-
// the resolution without a config or window change, and the view must follow it.
|
|
91
|
-
const resolvedKey = MODEL_TIERS.map((t) => {
|
|
92
|
-
const rp = priceResolved[t].pair;
|
|
93
|
-
return rp ? modelPairName(rp) : "-";
|
|
94
|
-
}).join(",");
|
|
95
|
-
// The set of window-moved backends is part of the key too (not just the `w` flag): a second
|
|
96
|
-
// backend's window can come or go while the move still stands, and the view must re-derive
|
|
97
|
-
// for the roles it newly blocks or frees.
|
|
98
|
-
const movedKey = [...movedBackends].sort().join("+");
|
|
99
|
-
const fromKey = `${windowMove ? "w" : ""}${reached ? "c" : ""}|${movedKey}|${resolvedKey}`;
|
|
100
|
-
return { onFallback, windowOut, movedBackends, windowMove, resolvedKey, fromKey };
|
|
101
|
-
}
|
|
102
|
-
/** Install `state.fallbackConfig` when the fallback or a window move changed the view key,
|
|
103
|
-
* including the pinned-role and pinned-reviewer escapes. Read by the pinned loops. */
|
|
104
|
-
function installFallbackConfig(a, o) {
|
|
105
|
-
const { state, config: liveConfig, accounts, providers, catalog, priceResolved, reached } = a;
|
|
106
|
-
const { onFallback, windowMove, windowOut, movedBackends, fromKey } = o;
|
|
107
|
-
if ((onFallback || windowMove) && (state.from !== liveConfig || state.fromKey !== fromKey)) {
|
|
47
|
+
/** Install `state.fallbackConfig` when the fallback changed the view key: at the cap, the free
|
|
48
|
+
* pair installed top-level and every per-role/reviewer model override dropped, so no seam can
|
|
49
|
+
* reach a priced model. A breaker-demoted fallback keeps the view too: its gate is `paused`,
|
|
50
|
+
* but a run parked in the semaphore when it tripped, the half-open probe, and the merges must
|
|
51
|
+
* still run on the free pair — a demotion must never promote them to the priced model the cap
|
|
52
|
+
* already spent. */
|
|
53
|
+
function installFallbackConfig(a, fromKey) {
|
|
54
|
+
const { state, config: liveConfig, providers, catalog, accounts, priceResolved, reached } = a;
|
|
55
|
+
if (reached && (state.from !== liveConfig || state.fromKey !== fromKey)) {
|
|
108
56
|
state.from = liveConfig;
|
|
109
57
|
state.fromKey = fromKey;
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
return b !== null && windowOut[b] !== undefined;
|
|
115
|
-
};
|
|
116
|
-
const available = (seam) => !seamBlocked(seam) && (!reached || seamFree(providers, seam, accounts, catalog));
|
|
117
|
-
const view = applyFallbackModel(liveConfig, priceResolved, available);
|
|
118
|
-
// A role pinned to a subscription backend under a tier of another backend has no tier pair
|
|
119
|
-
// to fall onto — dropping its override would put it on the tier's priced primary — so it
|
|
120
|
-
// keeps its pin and that backend's hold stops it, as before the window move.
|
|
121
|
-
if (movedBackends.size > 0) {
|
|
122
|
-
for (const role of enabledRoleIds(liveConfig)) {
|
|
123
|
-
const tier = roleSeamTier(liveConfig, role);
|
|
124
|
-
const pin = liveConfig.roles[role];
|
|
125
|
-
const roleBackend = subscriptionBackendOf(configForRole(liveConfig, role), accounts);
|
|
126
|
-
const tierBackend = subscriptionBackendOf(withModelOverrides(liveConfig, tier, {}), accounts);
|
|
127
|
-
if (pin && roleBackend !== null && roleBackend !== tierBackend)
|
|
128
|
-
view.roles[role] = pin;
|
|
129
|
-
}
|
|
130
|
-
// A reviewer pinned to a subscription backend under such a tier would likewise end up on
|
|
131
|
-
// the tier's priced primary; it goes to the tier's fallback pair instead (a held reviewer
|
|
132
|
-
// would park the roles a pre-merge review gates).
|
|
133
|
-
const rm = liveConfig.review.model;
|
|
134
|
-
const reviewTier = isTierName(rm) ? rm : "strong";
|
|
135
|
-
const reviewPair = priceResolved[reviewTier].pair;
|
|
136
|
-
const reviewBackend = subscriptionBackendOf(reviewConfig(liveConfig), accounts);
|
|
137
|
-
const reviewTierBackend = subscriptionBackendOf(withModelOverrides(liveConfig, reviewTier, {}), accounts);
|
|
138
|
-
// Only the reviewer's own backend's window counts: a reviewer on a healthy backend must
|
|
139
|
-
// stay put even while another backend's window moves its tier.
|
|
140
|
-
if (reviewPair &&
|
|
141
|
-
reviewBackend !== null &&
|
|
142
|
-
windowOut[reviewBackend] !== undefined &&
|
|
143
|
-
reviewBackend !== reviewTierBackend)
|
|
144
|
-
view.review = {
|
|
145
|
-
...view.review,
|
|
146
|
-
backend: reviewPair.backend ?? "pi",
|
|
147
|
-
provider: undefined,
|
|
148
|
-
model: formatModelSelector({
|
|
149
|
-
provider: reviewPair.backend === undefined ? reviewPair.provider : undefined,
|
|
150
|
-
model: reviewPair.model ?? "",
|
|
151
|
-
thinking: reviewPair.thinking,
|
|
152
|
-
}),
|
|
153
|
-
thinking: undefined,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
state.fallbackConfig = view;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
/** The tiers the window move put on a fallback pair: those whose own seam is a subscription
|
|
160
|
-
* backend whose window stands. */
|
|
161
|
-
function deriveWindowTiers(a, windowOut, windowMove) {
|
|
162
|
-
const { config: liveConfig, accounts, priceResolved } = a;
|
|
163
|
-
// The tiers the window move put on a fallback pair: those whose own seam is a subscription
|
|
164
|
-
// backend whose window stands.
|
|
165
|
-
const windowTiers = {};
|
|
166
|
-
if (windowMove) {
|
|
167
|
-
const moved = new Set();
|
|
168
|
-
for (const tier of MODEL_TIERS) {
|
|
169
|
-
const b = subscriptionBackendOf(withModelOverrides(liveConfig, tier, {}), accounts);
|
|
170
|
-
if (b !== null && windowOut[b] !== undefined)
|
|
171
|
-
moved.add(tier);
|
|
172
|
-
}
|
|
173
|
-
const labels = tierFallbackLabels(priceResolved);
|
|
174
|
-
for (const tier of MODEL_TIERS)
|
|
175
|
-
if (moved.has(tier) && labels[tier] !== undefined)
|
|
176
|
-
windowTiers[tier] = labels[tier];
|
|
177
|
-
}
|
|
178
|
-
return windowTiers;
|
|
179
|
-
}
|
|
180
|
-
/** Install the director's `state.directorFrom`/`directorKey`/`directorView` and return whether
|
|
181
|
-
* the director moved. */
|
|
182
|
-
function installDirectorFallback(a, resolvedKey, windowOut) {
|
|
183
|
-
const { state, config: liveConfig, accounts, pairNames, directorWindowOutFor, priceResolved } = a;
|
|
184
|
-
// The director follows availability, not the cap: while the window move stands and its own
|
|
185
|
-
// model job is a subscription backend, its tier's resolved pair replaces the live config (only
|
|
186
|
-
// subscription jobs of a used-up window are unavailable, so a priced director primary keeps
|
|
187
|
-
// its model over the cap). With no fallback (windowMove false) it stays on the live config and
|
|
188
|
-
// the scheduler holds it on that backend's hold.
|
|
189
|
-
const directorBackend = subscriptionBackendOf(configForRole(liveConfig, DIRECTOR_ROLE), accounts);
|
|
190
|
-
const directorMoved = directorBackend !== null && pairNames.length > 0 && directorWindowOutFor(directorBackend) !== null;
|
|
191
|
-
if (!directorMoved)
|
|
192
|
-
state.directorFrom = null;
|
|
193
|
-
else if (state.directorFrom !== liveConfig || state.directorKey !== resolvedKey) {
|
|
194
|
-
state.directorFrom = liveConfig;
|
|
195
|
-
state.directorKey = resolvedKey;
|
|
196
|
-
state.directorView = applyFallbackModel(liveConfig, priceResolved, (seam) => {
|
|
197
|
-
const b = subscriptionBackendOf(seam, accounts);
|
|
198
|
-
return b === null || windowOut[b] === undefined;
|
|
199
|
-
});
|
|
58
|
+
// At the cap only free seams are available (a subscription claude seam is free money), so
|
|
59
|
+
// every role/reviewer override that resolves to a priced seam drops to the fallback pair.
|
|
60
|
+
const available = (seam) => seamFree(providers, seam, accounts, catalog);
|
|
61
|
+
state.fallbackConfig = applyFallbackModel(liveConfig, priceResolved, available);
|
|
200
62
|
}
|
|
201
|
-
return directorMoved;
|
|
202
63
|
}
|
|
203
|
-
/**
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const windowResumed = windowResumedBackends.length > 0;
|
|
208
|
-
state.prevWindowMoved = movedBackends;
|
|
209
|
-
return { windowResumed, windowResumedBackends };
|
|
210
|
-
}
|
|
211
|
-
/** The tiers `applyFallbackModel` moved: those whose own seam fails the `available` test above.
|
|
212
|
-
* Feeds the `chain` block's `moved` set. */
|
|
213
|
-
function deriveMovedTiers(a, windowOut, windowMove) {
|
|
214
|
-
const { config: liveConfig, accounts, providers, catalog, reached } = a;
|
|
64
|
+
/** The tiers `applyFallbackModel` moved at the cap: those whose own seam fails the `available`
|
|
65
|
+
* test above (a priced seam under a spent cap). Feeds the `chain` block's `moved` set. */
|
|
66
|
+
function deriveMovedTiers(a) {
|
|
67
|
+
const { config: liveConfig, providers, accounts, catalog, reached } = a;
|
|
215
68
|
const movedTiers = new Set();
|
|
216
|
-
// The tiers `applyFallbackModel` moved: those whose own seam fails the `available` test above.
|
|
217
69
|
for (const tier of MODEL_TIERS) {
|
|
218
70
|
const seam = withModelOverrides(liveConfig, tier, {});
|
|
219
|
-
|
|
220
|
-
const out = (windowMove && b !== null && windowOut[b] !== undefined) || (reached && !seamFree(providers, seam, accounts, catalog));
|
|
221
|
-
if (out)
|
|
71
|
+
if (reached && !seamFree(providers, seam, accounts, catalog))
|
|
222
72
|
movedTiers.add(tier);
|
|
223
73
|
}
|
|
224
74
|
return movedTiers;
|
|
225
75
|
}
|
|
226
76
|
/** Derive the fallback view for one poll from the verdict half's already-resolved inputs
|
|
227
|
-
* (`priceResolved`, `servingResolved`, `chainPairs`, `pairNames`) and the `reached` flag
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
77
|
+
* (`priceResolved`, `servingResolved`, `chainPairs`, `pairNames`) and the `reached` flag and
|
|
78
|
+
* the shared reads (`state`, `config`, `accounts`, `providers`, `catalog`), writing the
|
|
79
|
+
* cross-poll view state (`from`/`fromKey`/`fallbackConfig`) and returning the view fields
|
|
80
|
+
* pollBudgetGate hands back. The director always keeps the live config (part 5d/6): with the
|
|
81
|
+
* window move gone no input moves it, so `directorConfig` is the live config by construction. */
|
|
232
82
|
export function budgetFallbackView(a) {
|
|
233
|
-
const { state, config: liveConfig,
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
83
|
+
const { state, config: liveConfig, reached, priceResolved } = a;
|
|
84
|
+
const onFallback = reached && a.pairNames.length > 0;
|
|
85
|
+
// The resolved pairs are part of the key: a priced step crossing its own daily cap changes
|
|
86
|
+
// the resolution without a config or window change, and the view must follow it.
|
|
87
|
+
const resolvedKey = MODEL_TIERS.map((t) => {
|
|
88
|
+
const rp = priceResolved[t].pair;
|
|
89
|
+
return rp ? modelPairName(rp) : "-";
|
|
90
|
+
}).join(",");
|
|
91
|
+
const fromKey = `${reached ? "c" : ""}|${resolvedKey}`;
|
|
92
|
+
installFallbackConfig(a, fromKey);
|
|
93
|
+
const movedTiers = deriveMovedTiers(a);
|
|
240
94
|
const chain = chainInfo({
|
|
241
95
|
config: liveConfig,
|
|
242
|
-
resolved: servingResolved,
|
|
96
|
+
resolved: a.servingResolved,
|
|
243
97
|
moved: movedTiers,
|
|
244
98
|
breakers: state.breakers,
|
|
245
|
-
states,
|
|
246
|
-
now,
|
|
247
|
-
|
|
248
|
-
accounts,
|
|
249
|
-
isPriced: (p) => !piPairFree(providers, p, accounts, catalog),
|
|
99
|
+
states: a.states,
|
|
100
|
+
now: a.now,
|
|
101
|
+
accounts: a.accounts,
|
|
102
|
+
isPriced: (p) => !piPairFree(a.providers, p, a.accounts, a.catalog),
|
|
250
103
|
});
|
|
251
104
|
return {
|
|
252
105
|
onFallback,
|
|
253
106
|
chain,
|
|
254
|
-
roleConfig: onFallback
|
|
255
|
-
pairs: distinctPairs(priceResolved, chainPairs),
|
|
256
|
-
|
|
257
|
-
windowTiers,
|
|
258
|
-
windowResumed,
|
|
259
|
-
windowResumedBackends,
|
|
260
|
-
directorConfig: directorMoved ? state.directorView : liveConfig,
|
|
107
|
+
roleConfig: onFallback ? state.fallbackConfig : liveConfig,
|
|
108
|
+
pairs: distinctPairs(priceResolved, a.chainPairs),
|
|
109
|
+
directorConfig: liveConfig,
|
|
261
110
|
};
|
|
262
111
|
}
|
|
263
112
|
/** The `chain` block: for each tier this poll moved off its primary, the step it runs on and
|
|
264
113
|
* why the position before it cannot run (plans/fallback-chain.md "Events and surfaces").
|
|
265
114
|
* `moved` names the tiers the poll moved; a tier the gate pauses (`pair: null`) gets no entry.
|
|
266
115
|
* The reason is read from the position just before the active step — the previous chain step,
|
|
267
|
-
* or the tier's primary at step 0 — in the order demoted, step-cap,
|
|
268
|
-
* cap. */
|
|
116
|
+
* or the tier's primary at step 0 — in the order demoted, step-cap, overage, cap. */
|
|
269
117
|
export function chainInfo(a) {
|
|
270
118
|
const out = {};
|
|
271
119
|
const steps = tierChainSteps(a.config);
|
|
@@ -274,47 +122,56 @@ export function chainInfo(a) {
|
|
|
274
122
|
const r = a.resolved[tier];
|
|
275
123
|
if (!a.moved.has(tier) || r.pair === null)
|
|
276
124
|
continue;
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const info = { pair: modelPairName(r.pair), step: idx + 1, of: own.length, reason: "cap" };
|
|
286
|
-
const priorName = prior ? modelPairName(prior) : "";
|
|
287
|
-
if (prior && a.breakers[priorName] !== undefined && !fallbackServingPair(a.breakers, priorName)) {
|
|
288
|
-
info.reason = "demoted";
|
|
289
|
-
}
|
|
290
|
-
else if (prior?.maxDailyCostUsd !== undefined &&
|
|
291
|
-
pairDailyCost(a.states, priorName, a.now) >= prior.maxDailyCostUsd) {
|
|
292
|
-
info.reason = "step-cap";
|
|
293
|
-
info.returnsAt = midnight;
|
|
294
|
-
}
|
|
295
|
-
else if (windowBackend !== null && a.windowOutFor(windowBackend) !== null) {
|
|
296
|
-
const win = a.windowOutFor(windowBackend);
|
|
297
|
-
info.reason = win.reserve ? "reserve" : "window";
|
|
298
|
-
info.returnsAt = win.resetsAt;
|
|
299
|
-
}
|
|
300
|
-
else if (prior &&
|
|
301
|
-
subscriptionBackendOf(prior, a.accounts) !== null &&
|
|
302
|
-
prior.extraUsage &&
|
|
303
|
-
a.accounts("claude") &&
|
|
304
|
-
claudeOverageRefused(a.accounts("claude"))) {
|
|
305
|
-
info.reason = "overage";
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
info.returnsAt = midnight;
|
|
309
|
-
}
|
|
310
|
-
if (r.pair.maxDailyCostUsd !== undefined && a.isPriced(r.pair)) {
|
|
311
|
-
info.spentUsd = pairDailyCost(a.states, modelPairName(r.pair), a.now);
|
|
312
|
-
info.capUsd = r.pair.maxDailyCostUsd;
|
|
313
|
-
}
|
|
125
|
+
const position = movedTierPosition(steps, tier, r);
|
|
126
|
+
const info = {
|
|
127
|
+
pair: modelPairName(r.pair),
|
|
128
|
+
step: position.step,
|
|
129
|
+
of: position.of,
|
|
130
|
+
...movedTierReason(a, position.prior, position.priorName, midnight),
|
|
131
|
+
};
|
|
132
|
+
Object.assign(info, movedTierSpend(a, r.pair));
|
|
314
133
|
out[tier] = info;
|
|
315
134
|
}
|
|
316
135
|
return out;
|
|
317
136
|
}
|
|
137
|
+
/** The step a moved tier runs on and the pair just before it: the tier's own chain steps
|
|
138
|
+
* (`steps.filter((s) => s.tier === (r.from ?? tier))`) resolved to their pairs, positioned at
|
|
139
|
+
* `r.step ?? 0`, with the previous step's pair (or null at step 0). Answers "which step does
|
|
140
|
+
* this moved tier run on, and which pair precedes it?" (plans/fallback-chain.md "Events and
|
|
141
|
+
* surfaces"). */
|
|
142
|
+
function movedTierPosition(steps, tier, r) {
|
|
143
|
+
const own = steps.filter((s) => s.tier === (r.from ?? tier)).map((s) => s.pair);
|
|
144
|
+
const idx = r.step ?? 0;
|
|
145
|
+
const prior = idx > 0 ? own[idx - 1] : null;
|
|
146
|
+
return { step: idx + 1, of: own.length, prior, priorName: prior ? modelPairName(prior) : "" };
|
|
147
|
+
}
|
|
148
|
+
/** Why the position before the active step cannot run, in INV-BN1's fixed order (demoted,
|
|
149
|
+
* step-cap, overage, cap). A demoted or overage result carries no `returnsAt`; a step-cap or
|
|
150
|
+
* cap result pins it to `midnight` (INV-BN2: local midnight only for step-cap and cap).
|
|
151
|
+
* Answers "why can't the position before the active step run, and when does it come back?". */
|
|
152
|
+
function movedTierReason(a, prior, priorName, midnight) {
|
|
153
|
+
if (prior && a.breakers[priorName] !== undefined && !fallbackServingPair(a.breakers, priorName)) {
|
|
154
|
+
return { reason: "demoted" };
|
|
155
|
+
}
|
|
156
|
+
if (prior?.maxDailyCostUsd !== undefined && pairDailyCost(a.states, priorName, a.now) >= prior.maxDailyCostUsd) {
|
|
157
|
+
return { reason: "step-cap", returnsAt: midnight };
|
|
158
|
+
}
|
|
159
|
+
if (prior &&
|
|
160
|
+
subscriptionBackendOf(prior, a.accounts) !== null &&
|
|
161
|
+
prior.extraUsage &&
|
|
162
|
+
a.accounts("claude") &&
|
|
163
|
+
claudeOverageRefused(a.accounts("claude"))) {
|
|
164
|
+
return { reason: "overage" };
|
|
165
|
+
}
|
|
166
|
+
return { reason: "cap", returnsAt: midnight };
|
|
167
|
+
}
|
|
168
|
+
/** What the active step has spent against its own cap: both keys absent unless the step is a
|
|
169
|
+
* priced capped one (INV-BN2). Answers "what has the active step spent against its cap?". */
|
|
170
|
+
function movedTierSpend(a, pair) {
|
|
171
|
+
if (pair.maxDailyCostUsd === undefined || !a.isPriced(pair))
|
|
172
|
+
return {};
|
|
173
|
+
return { spentUsd: pairDailyCost(a.states, modelPairName(pair), a.now), capUsd: pair.maxDailyCostUsd };
|
|
174
|
+
}
|
|
318
175
|
/** The distinct non-null fallback pairs a resolution hands out (tier order, first occurrence
|
|
319
176
|
* first) — the handback match list: a reopen hands back every in-flight run on any of
|
|
320
177
|
* them (part 5c/8's per-tier handback). */
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { MODEL_TIERS } from "../config/config-schema.js";
|
|
2
|
-
import { publishInfoFields } from "../team/orchestrator-info.js";
|
|
2
|
+
import { publishInfoFields } from "../team/team-orchestrator-info.js";
|
|
3
3
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
4
4
|
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
5
5
|
import { fallbackDemotion } from "./fallback-breaker.js";
|
|
6
|
-
import { gateRoleConfig, runOnPair,
|
|
7
|
-
import {
|
|
6
|
+
import { gateRoleConfig, runOnPair, } from "../gates/budget-gates.js";
|
|
7
|
+
import { roleSeamTier } from "../config/config-views.js";
|
|
8
8
|
function publishBudgetWiring(root, info, budget, view) {
|
|
9
9
|
// Publish what observers cannot derive themselves: every pair's demotion — a per-tier
|
|
10
10
|
// fallback can demote a pair the legacy single `fallback` never names, and a dashboard that
|
|
@@ -54,51 +54,31 @@ function publishBudgetWiring(root, info, budget, view) {
|
|
|
54
54
|
}
|
|
55
55
|
function assignRunnerBudgetConfig(runners, liveConfig, view) {
|
|
56
56
|
// The director keeps the live config — an explicit human prompt outranks the
|
|
57
|
-
//
|
|
57
|
+
// (gateRoleConfig). Assigned every poll (not only on transitions)
|
|
58
58
|
// so a runner created mid-gate, or one left behind by a broken-file poll that skipped
|
|
59
59
|
// the reload, can never run on the wrong model.
|
|
60
60
|
for (const r of runners) {
|
|
61
|
-
r.config = gateRoleConfig(r.role, liveConfig, view.roleConfig
|
|
61
|
+
r.config = gateRoleConfig(r.role, liveConfig, view.roleConfig);
|
|
62
|
+
// An image reroute must never spend past the daily cap: while the gate has the team on
|
|
63
|
+
// its free fallback pairs, a reroute looks only among those pairs (plans/image-input.md
|
|
64
|
+
// "Reroute target" "Regime"). Set beside the config assignment so the scheduler's regime
|
|
65
|
+
// and the roles' never disagree.
|
|
66
|
+
r.budgetFallbackActive = view.onFallback;
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
|
-
function handBackFallbackRuns(root, runners,
|
|
69
|
+
function handBackFallbackRuns(root, runners, view) {
|
|
65
70
|
// The budget reopened: a run that started on a fallback pair keeps it until it ends, so hand
|
|
66
71
|
// those runs back — abort them resumably (session and worktree edits kept) and let their next
|
|
67
72
|
// run continue the same session on the primary, whose config the assignment above already
|
|
68
73
|
// installed (PLANS.md 2026-09-30). Per-tier fallbacks put different roles on different pairs
|
|
69
|
-
// (part 5c/8), so the match runs over EVERY pair the price-based resolution hands out, not
|
|
70
|
-
// one. A run started on the primary keeps running, the director is exempt, and the merges
|
|
71
|
-
// slot's own runs) are untouched. One event per pair names who was handed back, so the
|
|
72
|
-
// aborted runs read as the budget reopening, not as unexplained failures. The
|
|
73
|
-
// window reset
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
// The window handback names the backend whose window resumed (the entry's goal) — not the
|
|
77
|
-
// handed-back pair's own backend: for claude-only the fallback pair carries no backend (it would
|
|
78
|
-
// read undefined), and with two subscription backends it would name the opposite one. A pair
|
|
79
|
-
// belongs to those resumed backends whose tiers' price-resolved pair it is.
|
|
80
|
-
const resumedBackendSet = new Set(view.windowResumedBackends);
|
|
81
|
-
const resumedBackendForPair = (leftPair) => {
|
|
82
|
-
for (const tier of MODEL_TIERS) {
|
|
83
|
-
const p = view.priceResolved[tier]?.pair;
|
|
84
|
-
if (p && p.provider === leftPair.provider && p.model === leftPair.model) {
|
|
85
|
-
const b = subscriptionBackendOf(withModelOverrides(liveConfig, tier, {}), accounts);
|
|
86
|
-
if (b !== null && resumedBackendSet.has(b))
|
|
87
|
-
return b;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return undefined;
|
|
91
|
-
};
|
|
92
|
-
if (handBack && view.pairs.length > 0) {
|
|
93
|
-
// A cap reopen while the window is still used up leaves the window's tiers on their
|
|
94
|
-
// fallback pairs: handing those runs back would abort them to restart on the same pair.
|
|
95
|
-
const stillMoved = Object.keys(view.windowTiers).flatMap((t) => {
|
|
96
|
-
const p = view.priceResolved[t].pair;
|
|
97
|
-
return p ? [p] : [];
|
|
98
|
-
});
|
|
74
|
+
// (part 5c/8), so the match runs over EVERY pair the price-based resolution hands out, not
|
|
75
|
+
// only one. A run started on the primary keeps running, the director is exempt, and the merges
|
|
76
|
+
// (the slot's own runs) are untouched. One event per pair names who was handed back, so the
|
|
77
|
+
// resulting aborted runs read as the budget reopening, not as unexplained failures. The
|
|
78
|
+
// subscription-window reset below the cap handed back too, but that move is gone (part 5d/6):
|
|
79
|
+
// only the cap reopen hands back now.
|
|
80
|
+
if (view.resumed && view.pairs.length > 0) {
|
|
99
81
|
for (const leftPair of view.pairs) {
|
|
100
|
-
if (stillMoved.some((p) => p.provider === leftPair.provider && p.model === leftPair.model))
|
|
101
|
-
continue;
|
|
102
82
|
const matching = runners.filter((r) => r.role !== DIRECTOR_ROLE && runOnPair(r.runModelPair(), leftPair));
|
|
103
83
|
if (matching.length > 0) {
|
|
104
84
|
logEventBestEffort(root, {
|
|
@@ -107,9 +87,6 @@ function handBackFallbackRuns(root, runners, liveConfig, accounts, view) {
|
|
|
107
87
|
roles: matching.map((r) => r.role),
|
|
108
88
|
provider: leftPair.provider,
|
|
109
89
|
model: leftPair.model,
|
|
110
|
-
...(view.resumed
|
|
111
|
-
? {}
|
|
112
|
-
: { reason: "window", backend: resumedBackendForPair(leftPair) }),
|
|
113
90
|
});
|
|
114
91
|
for (const r of matching)
|
|
115
92
|
r.handBackRun();
|
|
@@ -120,43 +97,30 @@ function handBackFallbackRuns(root, runners, liveConfig, accounts, view) {
|
|
|
120
97
|
/** Perform the budget gate's wiring for one poll, exactly as `pollTeamGates` did inline:
|
|
121
98
|
* publish the demotions and the switch/serve info fields, log the one `fallback_switched`
|
|
122
99
|
* transition event per tier change, assign every runner's config and cap flag, and hand
|
|
123
|
-
* in-flight fallback runs back on a cap reopen
|
|
100
|
+
* in-flight fallback runs back on a cap reopen. */
|
|
124
101
|
export function applyBudgetGateWiring(input) {
|
|
125
|
-
const { root, runners, liveConfig, info,
|
|
102
|
+
const { root, runners, liveConfig, info, budget, view } = input;
|
|
126
103
|
publishBudgetWiring(root, info, budget, view);
|
|
127
104
|
assignRunnerBudgetConfig(runners, liveConfig, view);
|
|
128
|
-
handBackFallbackRuns(root, runners,
|
|
105
|
+
handBackFallbackRuns(root, runners, view);
|
|
129
106
|
}
|
|
130
|
-
/** The per-tier budget pause subject
|
|
107
|
+
/** The per-tier budget pause subject set (part 5c/8), computed exactly as `pollTeamGates` did
|
|
131
108
|
* inline: `budgetPausedRoles` holds every role whose model tier resolved to no usable free pair
|
|
132
|
-
* while the cap is reached
|
|
133
|
-
* a
|
|
134
|
-
*
|
|
135
|
-
*
|
|
109
|
+
* while the cap is reached. The window-moved set is gone (part 5d/6): below the cap nothing
|
|
110
|
+
* pauses on a usage window anymore. The director is exempt (as under every autonomous gate),
|
|
111
|
+
* and the demoted pair's probe pierces the set for its own tier's roles only — probeRoles in
|
|
112
|
+
* the orchestrator's scheduling pass. */
|
|
136
113
|
export function budgetPausedSubjects(input) {
|
|
137
|
-
const { runners, liveConfig,
|
|
114
|
+
const { runners, liveConfig, budgetActive, servingResolved } = input;
|
|
138
115
|
const budgetPausedRoles = new Set();
|
|
139
|
-
const windowMovedRoles = new Set();
|
|
140
|
-
const movedTiers = Object.keys(windowTiers);
|
|
141
|
-
// The backend whose used-up window moved a given tier: the tier's own seam's backend — the same
|
|
142
|
-
// read budget-gates made to put it in `windowTiers`. A role on a moved tier whose own backend is
|
|
143
|
-
// DIFFERENT (a healthy backend riding the same tier) keeps its pair: only the role whose own
|
|
144
|
-
// backend owns the out window is moved.
|
|
145
|
-
const tierMovedBackend = (tier) => subscriptionBackendOf(withModelOverrides(liveConfig, tier, {}), accounts);
|
|
146
116
|
for (const r of runners) {
|
|
147
117
|
if (r.role === DIRECTOR_ROLE)
|
|
148
118
|
continue;
|
|
149
119
|
const tier = roleSeamTier(liveConfig, r.role);
|
|
150
|
-
|
|
151
|
-
// (it is not on a pair); a role on the same tier pinning a healthy backend does not.
|
|
152
|
-
if (movedTiers.includes(tier) &&
|
|
153
|
-
subscriptionBackendOf(configForRole(liveConfig, r.role), accounts) === tierMovedBackend(tier))
|
|
154
|
-
windowMovedRoles.add(r.role);
|
|
155
|
-
// Below the cap only a window-moved role can sit on a demoted fallback.
|
|
156
|
-
if (!budgetActive && !windowMovedRoles.has(r.role))
|
|
120
|
+
if (!budgetActive)
|
|
157
121
|
continue;
|
|
158
122
|
if (servingResolved[tier].pair === null)
|
|
159
123
|
budgetPausedRoles.add(r.role);
|
|
160
124
|
}
|
|
161
|
-
return { budgetPausedRoles
|
|
125
|
+
return { budgetPausedRoles };
|
|
162
126
|
}
|
|
@@ -152,11 +152,10 @@ export function modelPairName(p) {
|
|
|
152
152
|
* `strong` tiers resolved to a usable free pair (resolveTierFallbacks over the full usable
|
|
153
153
|
* predicate — price AND breaker serving) — plus whether the review gate is on. The gate is
|
|
154
154
|
* `paused` when `default` resolves to pause (nothing for the team's bulk to run on), or when
|
|
155
|
-
* review is on and `strong` does
|
|
156
|
-
* their
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* display it, so what an operator sees is what the scheduler is doing. */
|
|
155
|
+
* review is on and `strong` does; `fallback` otherwise, with the tiers that did resolve running
|
|
156
|
+
* their pairs and the rest held per role. This is the value the dashboards display; the
|
|
157
|
+
* orchestrator's role holds come from the per-tier set `budgetPausedRoles` (a tier whose own
|
|
158
|
+
* pair did not resolve), not from a reviewer-derived team-wide pause. */
|
|
160
159
|
export function budgetGate(reached, tiers, reviewOn) {
|
|
161
160
|
if (!reached)
|
|
162
161
|
return "open";
|
|
@@ -114,8 +114,8 @@ export function recordFallbackRun(b, ranOn, result, probe, now, policy = FALLBAC
|
|
|
114
114
|
return { ...b, failures, probeAt: now + policy.cooldownMs, cooldownMs: policy.cooldownMs, probing: false };
|
|
115
115
|
}
|
|
116
116
|
/** What orchestrator.json publishes while the breaker holds the fallback demoted
|
|
117
|
-
* (team/orchestrator-info.ts's OrchestratorInfo.fallbackDemoted). Observers need
|
|
118
|
-
* price check alone would advertise the demoted pair as a working fallback —
|
|
117
|
+
* (team/team-orchestrator-info.ts's OrchestratorInfo.fallbackDemoted). Observers need
|
|
118
|
+
* it because the price check alone would advertise the demoted pair as a working fallback —
|
|
119
119
|
* status/status-data.ts nulls the snapshot's `fallback` from it and `tumwater doctor` names it.
|
|
120
120
|
* Undefined while the fallback is trusted or not engaged. */
|
|
121
121
|
export function fallbackDemotion(b) {
|