tumwater 0.1.2 → 0.2.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/README.md +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
* fallback config view it derives, as the orchestrator polls it each cycle. Extracted from
|
|
3
3
|
* orchestrator.ts's poll loop: the spend check, the fallback breaker's re-key, the
|
|
4
4
|
* edge-triggered budget_* events, and the fallback view derivation are one concern beside the
|
|
5
|
-
* other
|
|
5
|
+
* other team gates (src/gates/pause-gates.ts, src/team/team-hold.ts's team hold) — the
|
|
6
6
|
* orchestrator owns only the wiring (the demotion publish and the per-runner config assignment). */
|
|
7
7
|
import { MODEL_TIERS } from "../config/config-schema.js";
|
|
8
|
-
import { budgetGate, budgetReached, budgetSpend, budgetWarning, modelPairName, } from "../budget/budget.js";
|
|
8
|
+
import { budgetGate, budgetReached, budgetSpend, budgetWarning, modelPairName, pairDailyCost, } from "../budget/budget.js";
|
|
9
9
|
import { fallbackServingPair, rekeyFallbackBreakers, } from "../budget/fallback-breaker.js";
|
|
10
|
-
import { applyFallbackModel, fallbackPair, resolveTierFallbacks, tierFallbackLabels, tiersResolveDistinctPairs, } from "../config/config-
|
|
10
|
+
import { applyFallbackModel, fallbackPair, resolveTierFallbacks, tierChainSteps, tierFallbackLabels, tiersResolveDistinctPairs, } from "../config/config-fallbacks.js";
|
|
11
11
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
12
|
-
import { logEventBestEffort } from "../events/events.js";
|
|
12
|
+
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
13
|
+
import { enabledRoleIds } from "../config/config.js";
|
|
14
|
+
import { formatModelSelector, seamAgent } from "../config/model-selector.js";
|
|
15
|
+
import { configForRole, isTierName, reviewConfig, roleSeamTier, withModelOverrides } from "../config/config-views.js";
|
|
13
16
|
import { piPairFree, readPiProviders, seamFree } from "../pi/pi-models.js";
|
|
14
|
-
import {
|
|
17
|
+
import { readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
|
|
18
|
+
import { claudeWindowUsedUp, readClaudeAccount } from "../agent/claude/claude-account.js";
|
|
19
|
+
import { dayAt } from "../text/datetime.js";
|
|
15
20
|
/** A fresh budget-gate poll state: gate open, no fallback breakers yet, no fallback view cached
|
|
16
21
|
* yet (so the first poll of an unchanged config logs nothing). */
|
|
17
22
|
export function newBudgetGateState(config) {
|
|
@@ -20,10 +25,33 @@ export function newBudgetGateState(config) {
|
|
|
20
25
|
breakers: {},
|
|
21
26
|
engaged: null,
|
|
22
27
|
from: null,
|
|
28
|
+
fromKey: "",
|
|
29
|
+
prevWindowMoved: false,
|
|
30
|
+
directorFrom: null,
|
|
31
|
+
directorView: config,
|
|
32
|
+
directorKey: "",
|
|
23
33
|
fallbackConfig: config,
|
|
24
34
|
warned: false,
|
|
35
|
+
prevChain: {},
|
|
25
36
|
};
|
|
26
37
|
}
|
|
38
|
+
/** Is this fallback pair a claude one (the agent namespace in the pair's provider)? */
|
|
39
|
+
function pairIsClaude(p) {
|
|
40
|
+
return p.provider === "claude";
|
|
41
|
+
}
|
|
42
|
+
/** Can the account not take extra usage: some window is rejected, or the newest non-reset
|
|
43
|
+
* window's `overageStatus` is `rejected`. */
|
|
44
|
+
function claudeOverageRefused(account) {
|
|
45
|
+
const live = Object.values(account.windows).filter((w) => w.status !== "reset");
|
|
46
|
+
if (live.some((w) => w.status === "rejected"))
|
|
47
|
+
return true;
|
|
48
|
+
const newest = live.reduce((a, w) => (a === null || w.observedAt > a.observedAt ? w : a), null);
|
|
49
|
+
return newest?.overageStatus === "rejected";
|
|
50
|
+
}
|
|
51
|
+
/** Does this model seam route to the claude backend? */
|
|
52
|
+
function seamIsClaude(seam) {
|
|
53
|
+
return seamAgent(seam) === "claude";
|
|
54
|
+
}
|
|
27
55
|
/** Does an in-flight tick's captured model pair (LoopRunner.tickModel()) match the fallback
|
|
28
56
|
* pair a reopening budget gate just left? The orchestrator hands back exactly those ticks
|
|
29
57
|
* (PLANS.md 2026-09-30): both sides must be present — no fallback pair means no tick ran on
|
|
@@ -36,16 +64,18 @@ export function tickOnPair(tickModel, pair) {
|
|
|
36
64
|
tickModel.provider === pair.provider &&
|
|
37
65
|
tickModel.model === pair.model);
|
|
38
66
|
}
|
|
39
|
-
/** The config a runner runs on while the budget gate stands: the director keeps the live
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
/** The config a runner runs on while the budget gate stands: the director keeps the live config —
|
|
68
|
+
* an explicit human prompt outranks the autonomous-spend cap — except while a used-up window has
|
|
69
|
+
* its own subscription claude model unable to run, when it takes `directorConfig` (pollBudgetGate's
|
|
70
|
+
* view: the tier's fallback pair; omitted means the live config). Every other role takes the gate's
|
|
71
|
+
* derived view (pollBudgetGate's returned `roleConfig`). Single home for the rule, so the
|
|
72
|
+
* scheduler's per-poll assignment (src/gates/gate-polls.ts) and a throwaway merge author's
|
|
73
|
+
* construction (src/merge/merge-verify.ts's resolveAuthor) cannot drift apart — say, an exemption
|
|
74
|
+
* granted to one more role in one copy and not the other. */
|
|
75
|
+
export function gateRoleConfig(role, liveConfig, roleConfig, directorConfig = liveConfig) {
|
|
76
|
+
return role === DIRECTOR_ROLE ? directorConfig : roleConfig;
|
|
47
77
|
}
|
|
48
|
-
/** Poll the daily cost budget gate: once the
|
|
78
|
+
/** Poll the daily cost budget gate: once the team's spend for the local day has reached
|
|
49
79
|
* maxDailyCostUsd, role loops either switch to the configured cost-free fallback model and
|
|
50
80
|
* keep working, or — with no usable one — start no new ticks at all (scheduled, main-moved
|
|
51
81
|
* wake, or startup). The director is outside both: an explicit human prompt outranks the
|
|
@@ -66,20 +96,54 @@ export function pollBudgetGate(state, ctx) {
|
|
|
66
96
|
const reviewOn = liveConfig.review.enabled;
|
|
67
97
|
// Whether the fallback is usable is a live question too: models.json is stat-cached
|
|
68
98
|
// inside pi-models.ts, so an unchanged catalog costs one stat per poll, and an operator
|
|
69
|
-
// who fixes a mistyped model id sees the
|
|
99
|
+
// who fixes a mistyped model id sees the team switch over within a cycle. One snapshot
|
|
70
100
|
// serves both the engaged pair's check and the per-tier resolution below.
|
|
71
101
|
const providers = readPiProviders(modelsPath);
|
|
72
102
|
// The persisted billing answer (6a/7): a subscription claude seam is free money, so the
|
|
73
103
|
// predicates below never move it to a fallback and accept a subscription claude pair as a
|
|
74
104
|
// budget fallback. One read per poll; an absent file reads as "no subscription".
|
|
75
105
|
const account = readClaudeAccount(root);
|
|
106
|
+
// The cached opencode models catalog (part 6b/7), seeded at the startup gate and read once
|
|
107
|
+
// per poll so every free-ness verdict below prices opencode seams against the same snapshot.
|
|
108
|
+
// Absent (no cache yet), an opencode seam reads NOT free — the safe direction until the
|
|
109
|
+
// startup seed fills the file in.
|
|
110
|
+
const catalog = readOpencodeCache(root);
|
|
76
111
|
// The usable predicate's two halves (part 5b/8): pi prices the pair at zero — a pair
|
|
77
112
|
// routed to a non-pi backend (e.g. `claude/<id>`) is never free, the same predicate the
|
|
78
113
|
// badge uses — and its breaker entry holds no demotion, tiers sharing a pair share the
|
|
79
114
|
// verdict. The resolution below deliberately prices alone (the map's keys are the pairs
|
|
80
115
|
// that would run absent a demotion, so a demotion cannot churn them); part 5c/8 wires the
|
|
81
116
|
// full predicate into the resolution so a demoted pair re-resolves only the tiers using it.
|
|
82
|
-
|
|
117
|
+
// A used-up subscription window (claudeWindowUsedUp, the rule the team hold reads too): while
|
|
118
|
+
// it stands a subscription claude pair is no fallback — it would meet the same window.
|
|
119
|
+
// `claudeWindowReserve` makes a window past its fraction count as used up for the team; the
|
|
120
|
+
// director's own reading (directorWindowOut) ignores it, as it runs the operator's prompts.
|
|
121
|
+
const windowOut = claudeWindowUsedUp(account, now, true, liveConfig.claudeWindowReserve);
|
|
122
|
+
const directorWindowOut = claudeWindowUsedUp(account, now);
|
|
123
|
+
// A priced step (one pi does not price at zero, or claude on API billing) is usable only
|
|
124
|
+
// while the team is under its cap and the step is under its own `maxDailyCostUsd`, so no cap
|
|
125
|
+
// can be passed; at the cap only free steps remain, as before.
|
|
126
|
+
const pairFreeAt = (p) => {
|
|
127
|
+
if (account?.billing === "subscription" && pairIsClaude(p)) {
|
|
128
|
+
// The first Claude model runs on the subscription only; a later one (extraUsage) may
|
|
129
|
+
// spend extra usage within its cap while no window is rejected or refuses overage.
|
|
130
|
+
if (!p.extraUsage)
|
|
131
|
+
return windowOut === null && piPairFree(providers, p.provider, p.model, account, catalog);
|
|
132
|
+
// Past the reserve this step would spend the reserved quota before reaching extra usage:
|
|
133
|
+
// it waits until a reading shows the account already spending overage.
|
|
134
|
+
if (windowOut?.reserve === true && !Object.values(account.windows).some((w) => w.isUsingOverage === true))
|
|
135
|
+
return false;
|
|
136
|
+
return (!reached &&
|
|
137
|
+
!claudeOverageRefused(account) &&
|
|
138
|
+
(p.maxDailyCostUsd === undefined || pairDailyCost(states, modelPairName(p), now) < p.maxDailyCostUsd));
|
|
139
|
+
}
|
|
140
|
+
if (piPairFree(providers, p.provider, p.model, account, catalog))
|
|
141
|
+
return !(windowOut !== null && pairIsClaude(p));
|
|
142
|
+
if (reached)
|
|
143
|
+
return false;
|
|
144
|
+
return (p.maxDailyCostUsd === undefined ||
|
|
145
|
+
pairDailyCost(states, modelPairName(p), now) < p.maxDailyCostUsd);
|
|
146
|
+
};
|
|
83
147
|
// The breaker map is keyed by every pair the tiers would run on absent a demotion
|
|
84
148
|
// (resolveTierFallbacks over the price check alone), re-keyed per pair each poll: a pair's
|
|
85
149
|
// judgment survives while its subject (pair + cap) is unchanged, and a changed cap, a pair
|
|
@@ -90,10 +154,10 @@ export function pollBudgetGate(state, ctx) {
|
|
|
90
154
|
// runs on it — and the paused tiers the demotion produces are held by the scheduler's
|
|
91
155
|
// per-role set, so nobody but the probe ever reaches the demoted pair.
|
|
92
156
|
const priceResolved = resolveTierFallbacks(liveConfig, pairFreeAt);
|
|
93
|
-
// The
|
|
157
|
+
// The team-level engagement is the price-resolved `default` tier's pair: a map with no
|
|
94
158
|
// `default` entry still reports the free pair the tier borrows, so state.engaged and the
|
|
95
159
|
// budget_fallback event name what the gate actually engages instead of reading null while
|
|
96
|
-
// the
|
|
160
|
+
// the team runs one. fallbackReady is exactly a non-null resolved default. rejectedPair is
|
|
97
161
|
// the default tier's own declared pair (fallbackPair's borrow order included) — the pair
|
|
98
162
|
// the operator asked for when the price check refused it, named by the pause event.
|
|
99
163
|
const pair = priceResolved.default.pair;
|
|
@@ -109,12 +173,22 @@ export function pollBudgetGate(state, ctx) {
|
|
|
109
173
|
pairNames.push(n);
|
|
110
174
|
}
|
|
111
175
|
}
|
|
176
|
+
// Every price-free step of every tier's chain gets a breaker entry too: a demoted step's
|
|
177
|
+
// successor must have one, or fallbackServingPair would read it as not serving.
|
|
178
|
+
const chainPairs = tierChainSteps(liveConfig)
|
|
179
|
+
.map((s) => s.pair)
|
|
180
|
+
.filter(pairFreeAt);
|
|
181
|
+
for (const p of chainPairs) {
|
|
182
|
+
const n = modelPairName(p);
|
|
183
|
+
if (!pairNames.includes(n))
|
|
184
|
+
pairNames.push(n);
|
|
185
|
+
}
|
|
112
186
|
state.breakers = rekeyFallbackBreakers(state.breakers, pairNames, liveConfig.maxDailyCostUsd);
|
|
113
187
|
state.engaged = reached && fallbackReady ? pairName : null;
|
|
114
188
|
// The demotion-aware resolution (part 5c/8): the same engine over the FULL usable predicate —
|
|
115
189
|
// priced at zero AND its breaker entry serving — so a demoted pair re-resolves only the tiers
|
|
116
190
|
// that use it: a demoted strong pair holds the strong roles beside the per-role cap set while
|
|
117
|
-
// small and default keep their own pairs, instead of the
|
|
191
|
+
// small and default keep their own pairs, instead of the team-wide pause the single-pair
|
|
118
192
|
// gate produced.
|
|
119
193
|
const usable = (p) => pairFreeAt(p) && fallbackServingPair(state.breakers, modelPairName(p));
|
|
120
194
|
const servingResolved = resolveTierFallbacks(liveConfig, usable);
|
|
@@ -132,7 +206,7 @@ export function pollBudgetGate(state, ctx) {
|
|
|
132
206
|
// doubles the page on the poll the cap itself is reached.
|
|
133
207
|
const warn = gate === "open" && budgetWarning({ spentUsd, capUsd });
|
|
134
208
|
if (warn && !state.warned) {
|
|
135
|
-
logEventBestEffort(root, {
|
|
209
|
+
logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "budget_warning", spentUsd, capUsd });
|
|
136
210
|
state.warned = true;
|
|
137
211
|
}
|
|
138
212
|
else if (!warn) {
|
|
@@ -140,7 +214,7 @@ export function pollBudgetGate(state, ctx) {
|
|
|
140
214
|
}
|
|
141
215
|
if (gate !== state.prevGate) {
|
|
142
216
|
logEventBestEffort(root, {
|
|
143
|
-
|
|
217
|
+
source: TUMWATER_SOURCE,
|
|
144
218
|
type: gate === "open" ? "budget_resumed" : gate === "fallback" ? "budget_fallback" : "budget_paused",
|
|
145
219
|
spentUsd,
|
|
146
220
|
capUsd,
|
|
@@ -180,37 +254,179 @@ export function pollBudgetGate(state, ctx) {
|
|
|
180
254
|
});
|
|
181
255
|
state.prevGate = gate;
|
|
182
256
|
}
|
|
183
|
-
// While the fallback holds, role loops run under the derived view — the free pair
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
// the priced model the cap already spent.
|
|
257
|
+
// While the fallback holds, role loops run under the derived view — the free pair installed
|
|
258
|
+
// top-level and every per-role/reviewer model override dropped, so no seam can reach a priced
|
|
259
|
+
// model. A breaker-demoted fallback keeps the view too: its gate is `paused`, but a tick parked
|
|
260
|
+
// in the semaphore when it tripped, the half-open probe, and the merges must still run on the
|
|
261
|
+
// free pair — a demotion must never promote them to the priced model the cap already spent.
|
|
189
262
|
const onFallback = reached && pairNames.length > 0;
|
|
190
|
-
|
|
263
|
+
// A used-up window moves the subscription claude jobs to their tier's fallback below the cap
|
|
264
|
+
// too; with no fallback to move to, nothing changes and the claude hold applies.
|
|
265
|
+
const windowMove = windowOut !== null && pairNames.length > 0;
|
|
266
|
+
// The resolved pairs are part of the key: a priced step crossing its own daily cap changes
|
|
267
|
+
// the resolution without a config or window change, and the view must follow it.
|
|
268
|
+
const resolvedKey = MODEL_TIERS.map((t) => {
|
|
269
|
+
const rp = priceResolved[t].pair;
|
|
270
|
+
return rp ? modelPairName(rp) : "-";
|
|
271
|
+
}).join(",");
|
|
272
|
+
const fromKey = `${windowMove ? "w" : ""}${reached ? "c" : ""}|${resolvedKey}`;
|
|
273
|
+
if ((onFallback || windowMove) && (state.from !== liveConfig || state.fromKey !== fromKey)) {
|
|
191
274
|
state.from = liveConfig;
|
|
192
|
-
state.
|
|
275
|
+
state.fromKey = fromKey;
|
|
276
|
+
// A job keeps its own model while that model is available: below the cap every seam but a
|
|
277
|
+
// subscription claude one during a used-up window; at the cap it must also be free.
|
|
278
|
+
const available = (seam) => !(windowOut !== null && seamIsClaude(seam)) && (!reached || seamFree(providers, seam, account, catalog));
|
|
279
|
+
const view = applyFallbackModel(liveConfig, priceResolved, available);
|
|
280
|
+
// A role pinned to subscription claude under a tier that is not claude has no tier pair to fall
|
|
281
|
+
// onto — dropping its override would put it on the tier's priced primary — so it keeps its
|
|
282
|
+
// pin and the claude hold stops it, as before the window move.
|
|
283
|
+
if (windowOut !== null) {
|
|
284
|
+
for (const role of enabledRoleIds(liveConfig)) {
|
|
285
|
+
const tier = roleSeamTier(liveConfig, role);
|
|
286
|
+
const pin = liveConfig.roles[role];
|
|
287
|
+
if (pin && seamIsClaude(configForRole(liveConfig, role)) && !seamIsClaude(withModelOverrides(liveConfig, tier, {})))
|
|
288
|
+
view.roles[role] = pin;
|
|
289
|
+
}
|
|
290
|
+
// A reviewer pinned to subscription claude under such a tier would likewise end up on the
|
|
291
|
+
// tier's priced primary; it goes to the tier's fallback pair instead (a held reviewer would
|
|
292
|
+
// stop the whole team).
|
|
293
|
+
const rm = liveConfig.review.model;
|
|
294
|
+
const reviewTier = isTierName(rm) ? rm : "strong";
|
|
295
|
+
const reviewPair = priceResolved[reviewTier].pair;
|
|
296
|
+
if (reviewPair &&
|
|
297
|
+
seamIsClaude(reviewConfig(liveConfig)) &&
|
|
298
|
+
!seamIsClaude(withModelOverrides(liveConfig, reviewTier, {})))
|
|
299
|
+
view.review = {
|
|
300
|
+
...view.review,
|
|
301
|
+
provider: undefined,
|
|
302
|
+
model: formatModelSelector({
|
|
303
|
+
provider: reviewPair.provider,
|
|
304
|
+
model: reviewPair.model ?? "",
|
|
305
|
+
thinking: reviewPair.thinking,
|
|
306
|
+
}),
|
|
307
|
+
thinking: undefined,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
state.fallbackConfig = view;
|
|
311
|
+
}
|
|
312
|
+
// The tiers the window move put on a fallback pair: those whose own seam is subscription claude.
|
|
313
|
+
const windowTiers = {};
|
|
314
|
+
if (windowMove) {
|
|
315
|
+
const moved = new Set();
|
|
316
|
+
for (const tier of MODEL_TIERS)
|
|
317
|
+
if (seamIsClaude(withModelOverrides(liveConfig, tier, {})))
|
|
318
|
+
moved.add(tier);
|
|
319
|
+
const labels = tierFallbackLabels(priceResolved);
|
|
320
|
+
for (const tier of MODEL_TIERS)
|
|
321
|
+
if (moved.has(tier) && labels[tier] !== undefined)
|
|
322
|
+
windowTiers[tier] = labels[tier];
|
|
323
|
+
}
|
|
324
|
+
// The director follows availability, not the cap: while the window move stands and its own
|
|
325
|
+
// model job is subscription claude, its tier's resolved pair replaces the live config (only
|
|
326
|
+
// subscription claude jobs are unavailable, so a priced director primary keeps its model over
|
|
327
|
+
// the cap). With no fallback (windowMove false) it stays on the live config and the scheduler
|
|
328
|
+
// holds it on the claude hold.
|
|
329
|
+
const directorMoved = directorWindowOut !== null && pairNames.length > 0 && seamIsClaude(configForRole(liveConfig, DIRECTOR_ROLE));
|
|
330
|
+
if (!directorMoved)
|
|
331
|
+
state.directorFrom = null;
|
|
332
|
+
else if (state.directorFrom !== liveConfig || state.directorKey !== resolvedKey) {
|
|
333
|
+
state.directorFrom = liveConfig;
|
|
334
|
+
state.directorKey = resolvedKey;
|
|
335
|
+
state.directorView = applyFallbackModel(liveConfig, priceResolved, (seam) => !seamIsClaude(seam));
|
|
336
|
+
}
|
|
337
|
+
const windowResumed = state.prevWindowMoved && !windowMove;
|
|
338
|
+
state.prevWindowMoved = windowMove;
|
|
339
|
+
const movedTiers = new Set();
|
|
340
|
+
// The tiers `applyFallbackModel` moved: those whose own seam fails the `available` test above.
|
|
341
|
+
for (const tier of MODEL_TIERS) {
|
|
342
|
+
const seam = withModelOverrides(liveConfig, tier, {});
|
|
343
|
+
const out = (windowMove && seamIsClaude(seam)) || (reached && !seamFree(providers, seam, account, catalog));
|
|
344
|
+
if (out)
|
|
345
|
+
movedTiers.add(tier);
|
|
193
346
|
}
|
|
347
|
+
const chain = chainInfo({
|
|
348
|
+
config: liveConfig,
|
|
349
|
+
resolved: servingResolved,
|
|
350
|
+
moved: movedTiers,
|
|
351
|
+
breakers: state.breakers,
|
|
352
|
+
states,
|
|
353
|
+
now,
|
|
354
|
+
windowOut,
|
|
355
|
+
account,
|
|
356
|
+
isPriced: (p) => !piPairFree(providers, p.provider, p.model, account, catalog),
|
|
357
|
+
});
|
|
194
358
|
return {
|
|
195
359
|
gate,
|
|
360
|
+
chain,
|
|
196
361
|
onFallback,
|
|
197
|
-
roleConfig: onFallback ? state.fallbackConfig : liveConfig,
|
|
362
|
+
roleConfig: onFallback || windowMove ? state.fallbackConfig : liveConfig,
|
|
198
363
|
spentUsd,
|
|
199
364
|
capUsd,
|
|
200
365
|
resumed,
|
|
201
|
-
pairs: distinctPairs(priceResolved),
|
|
366
|
+
pairs: distinctPairs(priceResolved, chainPairs),
|
|
202
367
|
priceResolved,
|
|
203
368
|
servingResolved,
|
|
204
369
|
budgetActive: reached,
|
|
370
|
+
windowOut,
|
|
371
|
+
windowTiers,
|
|
372
|
+
windowResumed,
|
|
373
|
+
directorConfig: directorMoved ? state.directorView : liveConfig,
|
|
205
374
|
};
|
|
206
375
|
}
|
|
376
|
+
/** The `chain` block: for each tier this poll moved off its primary, the step it runs on and
|
|
377
|
+
* why the position before it cannot run (plans/fallback-chain.md "Events and surfaces").
|
|
378
|
+
* `moved` names the tiers the poll moved; a tier the gate pauses (`pair: null`) gets no entry.
|
|
379
|
+
* The reason is read from the position just before the active step — the previous chain step,
|
|
380
|
+
* or the tier's primary at step 0 — in the order demoted, step-cap, window/reserve, overage,
|
|
381
|
+
* cap. */
|
|
382
|
+
export function chainInfo(a) {
|
|
383
|
+
const out = {};
|
|
384
|
+
const steps = tierChainSteps(a.config);
|
|
385
|
+
const midnight = dayAt(-1, new Date(a.now)).getTime();
|
|
386
|
+
for (const tier of MODEL_TIERS) {
|
|
387
|
+
const r = a.resolved[tier];
|
|
388
|
+
if (!a.moved.has(tier) || r.pair === null)
|
|
389
|
+
continue;
|
|
390
|
+
const own = steps.filter((s) => s.tier === (r.from ?? tier)).map((s) => s.pair);
|
|
391
|
+
const idx = r.step ?? 0;
|
|
392
|
+
const prior = idx > 0 ? own[idx - 1] : null;
|
|
393
|
+
const primaryClaude = seamIsClaude(withModelOverrides(a.config, tier, {}));
|
|
394
|
+
const info = { pair: modelPairName(r.pair), step: idx + 1, of: own.length, reason: "cap" };
|
|
395
|
+
const priorName = prior ? modelPairName(prior) : "";
|
|
396
|
+
if (prior && a.breakers[priorName] !== undefined && !fallbackServingPair(a.breakers, priorName)) {
|
|
397
|
+
info.reason = "demoted";
|
|
398
|
+
}
|
|
399
|
+
else if (prior?.maxDailyCostUsd !== undefined &&
|
|
400
|
+
pairDailyCost(a.states, priorName, a.now) >= prior.maxDailyCostUsd) {
|
|
401
|
+
info.reason = "step-cap";
|
|
402
|
+
info.returnsAt = midnight;
|
|
403
|
+
}
|
|
404
|
+
else if ((prior !== null && pairIsClaude(prior) || primaryClaude) && a.windowOut !== null) {
|
|
405
|
+
info.reason = a.windowOut.reserve ? "reserve" : "window";
|
|
406
|
+
info.returnsAt = a.windowOut.resetsAt;
|
|
407
|
+
}
|
|
408
|
+
else if (prior && pairIsClaude(prior) && prior.extraUsage && a.account && claudeOverageRefused(a.account)) {
|
|
409
|
+
info.reason = "overage";
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
info.returnsAt = midnight;
|
|
413
|
+
}
|
|
414
|
+
if (r.pair.maxDailyCostUsd !== undefined && a.isPriced(r.pair)) {
|
|
415
|
+
info.spentUsd = pairDailyCost(a.states, modelPairName(r.pair), a.now);
|
|
416
|
+
info.capUsd = r.pair.maxDailyCostUsd;
|
|
417
|
+
}
|
|
418
|
+
out[tier] = info;
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
207
422
|
/** The distinct non-null fallback pairs a resolution hands out (tier order, first occurrence
|
|
208
423
|
* first) — the handback match list: a reopen hands back every in-flight tick running on any of
|
|
209
424
|
* them (part 5c/8's per-tier handback). */
|
|
210
|
-
function distinctPairs(resolved) {
|
|
425
|
+
function distinctPairs(resolved, chainPairs) {
|
|
211
426
|
const out = [];
|
|
212
|
-
|
|
213
|
-
|
|
427
|
+
// The resolved pairs first, then every other price-free chain step: a run can be on any step.
|
|
428
|
+
const candidates = [...MODEL_TIERS.map((t) => resolved[t].pair), ...chainPairs];
|
|
429
|
+
for (const p of candidates) {
|
|
214
430
|
if (p !== null && !out.some((q) => modelPairName(q) === modelPairName(p)))
|
|
215
431
|
out.push(p);
|
|
216
432
|
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
/** The disk floor's hold (plans/disk-floor.md, "Disk floor 1/4"): when the volume the harness
|
|
2
|
-
* worktrees live on runs low on free space, the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* worktrees live on runs low on free space, the team starts no new work — role ticks, the director,
|
|
3
|
+
* merge verifies and merges — so a team of build-heavy checkouts cannot fill the disk. The syscall,
|
|
4
|
+
* the hold's hysteresis, and the edge-triggered `disk_low`/`disk_ok` events live here; the
|
|
5
|
+
* orchestrator owns only the wiring, exactly like the quiet-hours and pause gates. Part 2/4
|
|
6
|
+
* reclaims build outputs so the hold rarely engages; this part turns "disk full" into "team held,
|
|
7
|
+
* with a notification". In-flight work runs on — killing it wastes the work without freeing
|
|
8
|
+
* anything a finished run would not free anyway.
|
|
9
9
|
*
|
|
10
10
|
* The measurement is deliberately language-agnostic: `fs.statfsSync` reports the bytes an
|
|
11
11
|
* unprivileged user can still write, whatever the project builds. */
|
|
12
12
|
import fs from "node:fs";
|
|
13
13
|
import { worktreesDir } from "../paths.js";
|
|
14
|
-
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
14
|
+
import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
15
15
|
/** GB means 10^9 bytes everywhere — in config and in display (plans/disk-floor.md). Exported
|
|
16
16
|
* so doctor's detail renders the same unit the gate holds on. */
|
|
17
17
|
export const BYTES_PER_GB = 1_000_000_000;
|
|
18
18
|
/** How far free space must climb back ABOVE `diskHoldGB` before the hold lifts. Without it a
|
|
19
|
-
*
|
|
19
|
+
* team hovering at the line would flap in and out of the hold every poll. */
|
|
20
20
|
export const DISK_HOLD_HYSTERESIS_GB = 5;
|
|
21
|
-
/** The volume the
|
|
22
|
-
* (a
|
|
21
|
+
/** The volume the team's builds write to: the worktrees dir once it exists, else the root
|
|
22
|
+
* (a team that has not created a worktree yet measures the checkout's own volume). One home
|
|
23
23
|
* of the choice so the sampler and doctor's detail name the same path. */
|
|
24
24
|
export function diskVolumePath(root) {
|
|
25
25
|
const worktrees = worktreesDir(root);
|
|
26
26
|
return fs.existsSync(worktrees) ? worktrees : root;
|
|
27
27
|
}
|
|
28
|
-
/** Free bytes available to an unprivileged user on the volume the
|
|
28
|
+
/** Free bytes available to an unprivileged user on the volume the team builds on, or null
|
|
29
29
|
* when statfs throws (an unsupported platform or filesystem). A null sample never holds —
|
|
30
|
-
* the
|
|
30
|
+
* the team keeps running rather than freezing on a measurement it cannot make. */
|
|
31
31
|
export function sampleFreeBytes(root, statfs = (p) => fs.statfsSync(p)) {
|
|
32
32
|
try {
|
|
33
33
|
const stats = statfs(diskVolumePath(root));
|
|
@@ -37,14 +37,14 @@ export function sampleFreeBytes(root, statfs = (p) => fs.statfsSync(p)) {
|
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
/** A fresh gate state: not held, warning unspent — so the first poll of a
|
|
40
|
+
/** A fresh gate state: not held, warning unspent — so the first poll of a team with room
|
|
41
41
|
* logs nothing. */
|
|
42
42
|
export function newDiskGateState() {
|
|
43
43
|
return { prevHeld: false, warnedUnmeasurable: false };
|
|
44
44
|
}
|
|
45
45
|
/** Poll the disk gate for one orchestrator cycle: with `freeBytes` in hand, decide the hold
|
|
46
46
|
* (edge-triggered with `DISK_HOLD_HYSTERESIS_GB` of hysteresis) and log exactly one
|
|
47
|
-
* `disk_low` / `disk_ok` event per crossing. `holdGB` 0 disables the hold outright: a
|
|
47
|
+
* `disk_low` / `disk_ok` event per crossing. `holdGB` 0 disables the hold outright: a team
|
|
48
48
|
* with the key set to 0 never holds, and a live edit to 0 lifts an active hold on the next
|
|
49
49
|
* poll (with its one `disk_ok`). A null sample (statfs threw) logs one `warning` per process
|
|
50
50
|
* and never holds. `waitForReclaim` defers ENTERING the hold while part 2/4's pressure reclaim
|
|
@@ -55,7 +55,7 @@ export function pollDiskGate(root, freeBytes, holdGB, state, waitForReclaim = fa
|
|
|
55
55
|
if (freeBytes === null) {
|
|
56
56
|
if (!state.warnedUnmeasurable) {
|
|
57
57
|
state.warnedUnmeasurable = true;
|
|
58
|
-
warnEventBestEffort(root,
|
|
58
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, "cannot measure free disk space (statfs failed) — the disk hold is off for this process");
|
|
59
59
|
}
|
|
60
60
|
state.prevHeld = false;
|
|
61
61
|
return false;
|
|
@@ -71,8 +71,8 @@ export function pollDiskGate(root, freeBytes, holdGB, state, waitForReclaim = fa
|
|
|
71
71
|
if (held !== state.prevHeld) {
|
|
72
72
|
state.prevHeld = held;
|
|
73
73
|
logEventBestEffort(root, held
|
|
74
|
-
? {
|
|
75
|
-
: {
|
|
74
|
+
? { source: TUMWATER_SOURCE, type: "disk_low", freeGB, holdGB }
|
|
75
|
+
: { source: TUMWATER_SOURCE, type: "disk_ok", freeGB });
|
|
76
76
|
}
|
|
77
77
|
return held;
|
|
78
78
|
}
|