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
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** The config AS SEEN BY A RUN — the per-seam resolution half of the config views, split out
|
|
3
|
+
* of config.ts: which provider/model/thinking a role, the reviewer, or the conflict resolver
|
|
4
|
+
* resolves to, and which timeout a reviewer run gets. The budget/tier fallback side — which
|
|
5
|
+
* free pair the team runs on at the daily cap, and the per-role fallback config (fallbackPair,
|
|
6
|
+
* resolveTierFallbacks, applyFallbackModel, …) — lives in config-fallbacks.ts, which builds on
|
|
7
|
+
* this module one-way. No I/O and no caching — the persistence side (defaults, loading,
|
|
8
|
+
* saving, the role-selection helpers) lives in config.ts, the write side in config-write.ts. */
|
|
6
9
|
import { MODEL_TIERS } from "./config-schema.js";
|
|
7
|
-
import { formatModelSelector, parseModelSelector, routeModel } from "./model-selector.js";
|
|
8
|
-
import { isJsonObject } from "../files/json-object.js";
|
|
10
|
+
import { formatModelSelector, isAgentId, parseModelSelector, routeModel } from "./model-selector.js";
|
|
9
11
|
import { modelPairName } from "../budget/budget.js";
|
|
10
|
-
import {
|
|
12
|
+
import { isJsonObject } from "../files/json-object.js";
|
|
13
|
+
import { DIRECTOR_ROLE, roleById } from "../roles/roles.js";
|
|
11
14
|
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
12
|
-
|
|
13
|
-
/** The effective `agents.<id>` entry as a run sees it (plans/agent-backends.md "Back-compat"):
|
|
15
|
+
/** The effective `backends.<id>` entry as a run sees it (plans/agent-backends.md "Back-compat"):
|
|
14
16
|
* the file's own entry, with the legacy top-level `agentBin`/`piArgs` read as the pi backend's
|
|
15
17
|
* `bin`/`args` when the new form leaves a field absent. Pure — it never writes those legacy
|
|
16
|
-
* values back into `config.
|
|
18
|
+
* values back into `config.backends`, so a config write that re-serializes the merged config
|
|
17
19
|
* cannot leak a second spelling into the file (which validateConfig would then reject).
|
|
18
20
|
* pi-backend.ts and claude-backend.ts read `.bin` through resolveAgentBin (pi-bin.ts);
|
|
19
21
|
* claude-backend.ts reads `.args` for passthrough argv. */
|
|
20
22
|
export function resolvedAgentConfig(config, id) {
|
|
21
|
-
const entry = config.
|
|
23
|
+
const entry = config.backends?.[id];
|
|
22
24
|
// A view must own its array: returning the config's own `args` reference would let a caller
|
|
23
25
|
// that appends passthrough argv silently mutate the loaded config, breaking the
|
|
24
26
|
// purity promised above. `args` is the only mutable field, so a sliced copy suffices.
|
|
@@ -31,7 +33,7 @@ export function resolvedAgentConfig(config, id) {
|
|
|
31
33
|
}
|
|
32
34
|
/** True when `s` names one of the three tiers — the form `roles.<id>.model` /
|
|
33
35
|
* `review.model` may take to point at that tier of the top-level `model` map. */
|
|
34
|
-
function isTierName(s) {
|
|
36
|
+
export function isTierName(s) {
|
|
35
37
|
return MODEL_TIERS.includes(s);
|
|
36
38
|
}
|
|
37
39
|
/** The top-level `model` resolved at one tier (plans/model-tiers.md "Which tier each seam
|
|
@@ -40,7 +42,7 @@ function isTierName(s) {
|
|
|
40
42
|
* else `default`'s (a tier left out inherits `default`), as a pure selector. Undefined
|
|
41
43
|
* leaves pi's own default in charge. Map-form + legacy provider is a validation error, so
|
|
42
44
|
* no legacy provider is in scope for a map's entries. */
|
|
43
|
-
function topTierSelector(config, tier) {
|
|
45
|
+
export function topTierSelector(config, tier) {
|
|
44
46
|
if (typeof config.model === "string")
|
|
45
47
|
return parseModelSelector(config.model, config.provider);
|
|
46
48
|
if (isJsonObject(config.model)) {
|
|
@@ -50,7 +52,7 @@ function topTierSelector(config, tier) {
|
|
|
50
52
|
return undefined;
|
|
51
53
|
}
|
|
52
54
|
/** The selector one seam's tier resolves to at the top level — the tier's own map entry,
|
|
53
|
-
* else `default`'s, else none (pi's own default). The pricing view (
|
|
55
|
+
* else `default`'s, else none (pi's own default). The pricing view (teamModelsFree) and
|
|
54
56
|
* the later tiered-fallback parts read this instead of re-deriving the resolution, so the
|
|
55
57
|
* argv builder and the budget cannot disagree about which model a tier names. */
|
|
56
58
|
export function tierModel(config, tier) {
|
|
@@ -66,8 +68,13 @@ export function tierModel(config, tier) {
|
|
|
66
68
|
* key it overrides would.
|
|
67
69
|
*
|
|
68
70
|
* `tier` is the seam's effective tier, already resolved by the caller from any tier-name
|
|
69
|
-
* override — so `o.model` here is always a selector string, never a tier reference.
|
|
70
|
-
|
|
71
|
+
* override — so `o.model` here is always a selector string, never a tier reference.
|
|
72
|
+
*
|
|
73
|
+
* Exported so imageRerouteTarget (src/agent/image-input.ts) can probe a peer tier's
|
|
74
|
+
* top-level model (`withModelOverrides(config, tier, {})`, the same shape resolverConfig
|
|
75
|
+
* builds strong with) and reshape a tier's fallback pair into a runnable config — the
|
|
76
|
+
* tier-config builder the plan names. */
|
|
77
|
+
export function withModelOverrides(config, tier, o) {
|
|
71
78
|
// The top-level model resolves at the seam's tier: a string form is the default tier's
|
|
72
79
|
// selector; a map form takes the tier's entry, else `default`'s.
|
|
73
80
|
const topSel = topTierSelector(config, tier);
|
|
@@ -76,7 +83,7 @@ function withModelOverrides(config, tier, o) {
|
|
|
76
83
|
const model = ownSel ? ownSel.model : topSel?.model;
|
|
77
84
|
// The routed backend decides whether the provider half survives: a pi provider name is kept,
|
|
78
85
|
// a backend namespace such as `claude` is dropped (that backend takes no provider).
|
|
79
|
-
const routed = routeModel(provider, model, config.
|
|
86
|
+
const routed = routeModel(provider, model, config.backend ?? "pi", tier);
|
|
80
87
|
return {
|
|
81
88
|
...config,
|
|
82
89
|
provider: routed.agent === "pi" ? provider : undefined,
|
|
@@ -85,6 +92,16 @@ function withModelOverrides(config, tier, o) {
|
|
|
85
92
|
agent: routed.agent,
|
|
86
93
|
};
|
|
87
94
|
}
|
|
95
|
+
/** The provider/model/thinking overrides a config section (a role entry or `review`) carries
|
|
96
|
+
* into withModelOverrides: a tier-name `model` already chose the seam's tier, so it is not a
|
|
97
|
+
* selector and drops out here. Shared by configForRole and reviewConfig. */
|
|
98
|
+
function seamOverrides(s) {
|
|
99
|
+
return {
|
|
100
|
+
provider: s.provider,
|
|
101
|
+
model: isTierName(s.model) ? undefined : s.model,
|
|
102
|
+
thinking: s.thinking,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
88
105
|
/** The tier a role's model resolves at: a tier-name `roles.<id>.model` names it directly, and
|
|
89
106
|
* every other role rides the model catalog's assignment. Exported for the per-tier budget gate's
|
|
90
107
|
* pause set (src/gates/gate-polls.ts, part 5c/8) and the dashboards' per-role budget pause
|
|
@@ -94,39 +111,43 @@ export function roleSeamTier(config, role) {
|
|
|
94
111
|
const m = config.roles[baseRoleOf(role)]?.model;
|
|
95
112
|
if (isTierName(m))
|
|
96
113
|
return m;
|
|
114
|
+
if (role === DIRECTOR_ROLE)
|
|
115
|
+
return "strong";
|
|
97
116
|
return roleById(role)?.tier ?? "default";
|
|
98
117
|
}
|
|
99
118
|
/** The config as seen by one role: role-level provider/model/thinking overrides applied
|
|
100
|
-
* over the top-level values, plus the per-role
|
|
119
|
+
* over the top-level values, plus the per-role minRunIntervalSeconds (a slow clock for
|
|
101
120
|
* roles that should act rarely) falling back to the global value when unset. The role's
|
|
102
121
|
* model resolves at its effective tier (roleSeamTier): a `model` map serves the tier the
|
|
103
122
|
* catalog assigns (or `roles.<id>.model` names), a string form is the default tier. */
|
|
104
123
|
export function configForRole(config, role) {
|
|
105
124
|
const rc = config.roles[baseRoleOf(role)];
|
|
106
|
-
const resolved = withModelOverrides(config, roleSeamTier(config, role), rc
|
|
107
|
-
? {
|
|
108
|
-
provider: rc.provider,
|
|
109
|
-
model: isTierName(rc.model) ? undefined : rc.model,
|
|
110
|
-
thinking: rc.thinking,
|
|
111
|
-
}
|
|
112
|
-
: {});
|
|
125
|
+
const resolved = withModelOverrides(config, roleSeamTier(config, role), rc ? seamOverrides(rc) : {});
|
|
113
126
|
if (!rc)
|
|
114
127
|
return resolved;
|
|
115
128
|
return {
|
|
116
129
|
...resolved,
|
|
117
|
-
|
|
130
|
+
minRunIntervalSeconds: rc.minRunIntervalSeconds ?? config.minRunIntervalSeconds,
|
|
118
131
|
};
|
|
119
132
|
}
|
|
120
133
|
/** The config a role's ticks SCHEDULE from: configForRole, except that while the new-project
|
|
121
134
|
* bootstrap is active (plans/work-ratio.md, part 2/2) the plan role schedules on the GLOBAL
|
|
122
|
-
*
|
|
123
|
-
* steady stream of plans for feature
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
|
|
135
|
+
* minRunIntervalSeconds rather than its per-role 3600 s slow clock, so a young project gets a
|
|
136
|
+
* steady stream of plans for feature; and while the brief still carries the placeholder status
|
|
137
|
+
* (descriptionIncomplete) the plan role schedules with a ZERO min gap, so a fresh project plans
|
|
138
|
+
* back-to-back until a run changes nothing — the zero gap is tighter than bootstrap. One home
|
|
139
|
+
* for the rule: isEligible enforces the same gap when it decides whether plan may start, and
|
|
140
|
+
* loop-tick.ts hands this config to applyTickOutcome so nextRunAt is scheduled from the same
|
|
141
|
+
* figure instead of the role default. `descriptionIncomplete` means the brief's status section
|
|
142
|
+
* is still the placeholder; the initial prompt half is always present (init rejects an empty
|
|
143
|
+
* one), so the placeholder status is the machine-detectable "description not yet complete". */
|
|
144
|
+
export function scheduleConfigForRole(config, role, bootstrapActive, descriptionIncomplete = false) {
|
|
127
145
|
const resolved = configForRole(config, role);
|
|
146
|
+
if (descriptionIncomplete && baseRoleOf(role) === "plan") {
|
|
147
|
+
return { ...resolved, minRunIntervalSeconds: 0 };
|
|
148
|
+
}
|
|
128
149
|
if (bootstrapActive && baseRoleOf(role) === "plan") {
|
|
129
|
-
return { ...resolved,
|
|
150
|
+
return { ...resolved, minRunIntervalSeconds: config.minRunIntervalSeconds };
|
|
130
151
|
}
|
|
131
152
|
return resolved;
|
|
132
153
|
}
|
|
@@ -139,16 +160,12 @@ export function scheduleConfigForRole(config, role, bootstrapActive) {
|
|
|
139
160
|
* `review.model` names another tier. */
|
|
140
161
|
export function reviewConfig(config) {
|
|
141
162
|
const rm = config.review.model;
|
|
142
|
-
return withModelOverrides(config, isTierName(rm) ? rm : "strong",
|
|
143
|
-
provider: config.review.provider,
|
|
144
|
-
model: isTierName(rm) ? undefined : rm,
|
|
145
|
-
thinking: config.review.thinking,
|
|
146
|
-
});
|
|
163
|
+
return withModelOverrides(config, isTierName(rm) ? rm : "strong", seamOverrides(config.review));
|
|
147
164
|
}
|
|
148
|
-
/** The config the
|
|
149
|
-
* strong tier's model installed over the config the
|
|
150
|
-
* tolerant of latency, and edits code inside
|
|
151
|
-
* matters most. It reads whatever config the
|
|
165
|
+
/** The config the merge conflict resolver runs on (plans/model-tiers.md, part 4/8): the
|
|
166
|
+
* strong tier's model installed over the config the merge was handed — resolution is rare,
|
|
167
|
+
* tolerant of latency, and edits code inside the merge, so it rides the seam where model quality
|
|
168
|
+
* matters most. It reads whatever config the merge carries — the role's per-tier budget
|
|
152
169
|
* fallback view — so it follows the budget fallback's config (part 5a/8). No other overrides:
|
|
153
170
|
* the session dir, raw log,
|
|
154
171
|
* transient retry, and usage folding stay the authoring run's. */
|
|
@@ -156,38 +173,38 @@ export function resolverConfig(config) {
|
|
|
156
173
|
return withModelOverrides(config, "strong", {});
|
|
157
174
|
}
|
|
158
175
|
/** Default wall-clock budget for one reviewer run (`review.timeoutSeconds`). The reviewer holds
|
|
159
|
-
* the
|
|
176
|
+
* the merge slot the whole queue waits on, and its only other limits were the tick's budget
|
|
160
177
|
* and the quiet watchdog: reviews measured 2026-09-22/23 ran 2.3 min median, 9.2 min p90 and
|
|
161
178
|
* 24.6 min max, while earlier days had 1.5–3.5 h reviews. 15 min clears the ordinary review and
|
|
162
179
|
* kills a wedged one in minutes instead of the tick's hours. */
|
|
163
180
|
export const REVIEW_TIMEOUT_S = 900;
|
|
164
|
-
/** The reviewer's wall-clock floor while the budget fallback carries the
|
|
181
|
+
/** The reviewer's wall-clock floor while the budget fallback carries the team. The free model
|
|
165
182
|
* is typically local and far slower per turn than the budgeted one it replaces, but a review
|
|
166
183
|
* still needs the same number of turns: measured 2026-09-30, 1,081 reviews on the budgeted
|
|
167
184
|
* model took 7 turns at p50 and 15 at p90, while the local fallback managed one turn every
|
|
168
185
|
* ~150–220 s — so REVIEW_TIMEOUT_S fit 4–7 turns, and none of the 21 reviews started in the
|
|
169
186
|
* 09-28..09-30 fallback windows ever returned a verdict (14 timed out; BUGS.md 2026-09-30).
|
|
170
|
-
* Every code change then looped review → timeout → re-
|
|
187
|
+
* Every code change then looped review → timeout → re-merge until midnight. An hour fits ~20
|
|
171
188
|
* turns, past the p90 review. */
|
|
172
189
|
export const FALLBACK_REVIEW_TIMEOUT_S = 3600;
|
|
173
190
|
/** The config as seen by the gate's reviewer run: the reviewer's model wiring (reviewConfig)
|
|
174
191
|
* with its own time budget — `review.timeoutSeconds`, default REVIEW_TIMEOUT_S — overriding
|
|
175
|
-
* the tick's. A smaller
|
|
192
|
+
* the tick's. A smaller runTimeoutSeconds still wins. A still-progressing gate run gets one
|
|
176
193
|
* extension of the same length (src/pi/pi-watchdogs.ts), so the effective ceiling on a large
|
|
177
194
|
* diff's review is twice this budget. */
|
|
178
195
|
export function reviewRunConfig(config) {
|
|
179
196
|
const cfg = reviewConfig(config);
|
|
180
197
|
return {
|
|
181
198
|
...cfg,
|
|
182
|
-
|
|
199
|
+
runTimeoutSeconds: Math.min(cfg.runTimeoutSeconds, config.review.timeoutSeconds ?? REVIEW_TIMEOUT_S),
|
|
183
200
|
};
|
|
184
201
|
}
|
|
185
202
|
/** The optional provider/model/thinking fields of a resolved model seam, with each unset
|
|
186
203
|
* (empty) field left out so the result carries only what was resolved — the one home of that
|
|
187
|
-
* assembly.
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
204
|
+
* assembly. The fallback layer (config-fallbacks.ts's fallbackSelectorFields and
|
|
205
|
+
* fallbackPair/tierOwnFallback/modelFallbackView) and role-view's RoleViewPayload (its
|
|
206
|
+
* model fields) all build a triple this way and follow the same rule — an absent or empty
|
|
207
|
+
* value is
|
|
191
208
|
* omitted rather than written as undefined/"" (an empty selector field would read as
|
|
192
209
|
* "override present," so it must not appear). */
|
|
193
210
|
export function resolvedModelFields(provider, model, thinking) {
|
|
@@ -200,19 +217,11 @@ export function resolvedModelFields(provider, model, thinking) {
|
|
|
200
217
|
fields.thinking = thinking;
|
|
201
218
|
return fields;
|
|
202
219
|
}
|
|
203
|
-
/** The resolved model fields of a pure `fallback` selector string: parsed as a pure selector
|
|
204
|
-
* (no legacy provider in scope), with the ambient top-level thinking when the selector
|
|
205
|
-
* carries none. fallbackPair's map and string branches and tierOwnFallback share this
|
|
206
|
-
* assembly, so all three read one rule for how a `fallback` selector resolves. */
|
|
207
|
-
function fallbackSelectorFields(selector, config) {
|
|
208
|
-
const parsed = parseModelSelector(selector);
|
|
209
|
-
return resolvedModelFields(parsed.provider, parsed.model, parsed.thinking ?? config.thinking);
|
|
210
|
-
}
|
|
211
220
|
/** The `model` selector a run actually starts on, as an event/payload field: the resolved
|
|
212
221
|
* provider/model/thinking triple formatted as one selector string, or an empty object when
|
|
213
222
|
* no model id is resolved (pi's own default) — the key stays absent rather than empty, the
|
|
214
223
|
* same empty-means-unset rule resolvedModelFields applies to the raw halves. One home of
|
|
215
|
-
* that assembly and guard, shared by the two run-start event writers (loop-tick.ts's
|
|
224
|
+
* that assembly and guard, shared by the two run-start event writers (loop-tick.ts's run_start,
|
|
216
225
|
* review.ts's review_start) and the per-loop status snapshot (status-data.ts) so the three
|
|
217
226
|
* observability surfaces cannot disagree about which model a run starts on. A non-pi agent is
|
|
218
227
|
* named as the selector's provider namespace (agentQualifiedSelector), so a claude run reads
|
|
@@ -230,22 +239,52 @@ export function agentQualifiedSelector(cfg) {
|
|
|
230
239
|
const base = formatModelSelector({ provider: cfg.provider, model: cfg.model, thinking: cfg.thinking });
|
|
231
240
|
return cfg.agent && cfg.agent !== "pi" ? `${cfg.agent}/${base}` : base;
|
|
232
241
|
}
|
|
233
|
-
/** The key a resolved seam's
|
|
242
|
+
/** The key a resolved seam's team hold is filed under and the scheduler checks a role
|
|
234
243
|
* against: a non-pi backend's own id (claude seams carry no pi provider), else the pi
|
|
235
244
|
* provider (undefined for pi's own default). The failure hold and the subscription-window
|
|
236
245
|
* hold both key on this, so a hold and the roles it gates cannot drift two ways. */
|
|
237
246
|
export function seamHoldKey(cfg) {
|
|
238
247
|
return cfg.agent !== undefined && cfg.agent !== "pi" ? cfg.agent : cfg.provider;
|
|
239
248
|
}
|
|
240
|
-
/** The
|
|
249
|
+
/** The `modelPairName` a resolved seam's run is charged under (LoopState.dayCostByPair): the
|
|
250
|
+
* routed backend's id stands in for the provider on a non-pi seam, exactly as the fallback
|
|
251
|
+
* pairs carry it (config-fallbacks.ts's seamKeyedFallbackProvider), so a run and the chain
|
|
252
|
+
* step it ran on name one key. */
|
|
253
|
+
export function seamPairName(cfg) {
|
|
254
|
+
return modelPairName({ provider: seamHoldKey(cfg), model: cfg.model });
|
|
255
|
+
}
|
|
256
|
+
/** The key a resolved seam's run counts against the per-provider cap
|
|
257
|
+
* (plans/provider-concurrency.md §2): its hold key, else — for a pi seam that names no model
|
|
258
|
+
* at all and so runs pi's saved default — the default provider from pi's settings
|
|
259
|
+
* (`piDefault`, the caller's cached readPiDefaultModel result, so this view stays free of
|
|
260
|
+
* I/O). A bare id with no provider, or a seam whose pi settings cannot name a default, yields
|
|
261
|
+
* undefined: only the team total bounds it. HF sub-provider pins (`huggingface/…:deepinfra`)
|
|
262
|
+
* share the base provider, and billing does not split claude, because this reuses the hold key
|
|
263
|
+
* — "omlx is held" and "omlx is full" name the same thing on every surface. */
|
|
264
|
+
export function seamConcurrencyKey(cfg, piDefault) {
|
|
265
|
+
const hold = seamHoldKey(cfg);
|
|
266
|
+
if (hold !== undefined)
|
|
267
|
+
return hold;
|
|
268
|
+
if (cfg.model === undefined)
|
|
269
|
+
return piDefault?.provider;
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
/** The team's model wiring as one human-readable label, for the live-reload `model_changed`
|
|
241
273
|
* event: null when no tier resolves to a model (pi's own default), the single selector when
|
|
242
274
|
* every set tier agrees (the string-form `model`), and the `small=…, default=…, strong=…`
|
|
243
275
|
* list when the tiers differ. Reading EVERY tier makes an edit to any tier's entry change the
|
|
244
|
-
* label.
|
|
245
|
-
|
|
276
|
+
* label. Each tier's label is agent-qualified (agentQualifiedSelector), so flipping the
|
|
277
|
+
* top-level `agent` over a bare-id model relabels every tier and the reload now emits
|
|
278
|
+
* `model_changed` — a team's backend is part of what it runs (PLANS.md "Mixed claude + pi
|
|
279
|
+
* fleets, part 3/4"), and an edit that moves a bare id to another backend must not read as
|
|
280
|
+
* "nothing changed". */
|
|
281
|
+
export function teamModelLabel(config) {
|
|
246
282
|
const entries = MODEL_TIERS.map((tier) => {
|
|
247
283
|
const sel = tierModel(config, tier);
|
|
248
|
-
|
|
284
|
+
if (!sel)
|
|
285
|
+
return null;
|
|
286
|
+
const { agent } = routeModel(sel.provider, sel.model, config.backend ?? "pi", tier);
|
|
287
|
+
return { tier, label: agentQualifiedSelector({ ...sel, agent }) };
|
|
249
288
|
}).filter((e) => e !== null);
|
|
250
289
|
if (entries.length === 0)
|
|
251
290
|
return null;
|
|
@@ -253,276 +292,19 @@ export function fleetModelLabel(config) {
|
|
|
253
292
|
return entries[0].label;
|
|
254
293
|
return entries.map((e) => `${e.tier}=${e.label}`).join(", ");
|
|
255
294
|
}
|
|
256
|
-
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* the
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
* values, the same precedence every other override section uses. Private — reached only
|
|
272
|
-
* through tierOwnFallback (the default tier's own fallback) and so fallbackPair's resolution,
|
|
273
|
-
* which keeps the object and the string/map forms on one rule without recursing. */
|
|
274
|
-
function legacyFallbackPair(config) {
|
|
275
|
-
const fbo = config.fallbackModel;
|
|
276
|
-
if (!fbo)
|
|
277
|
-
return null;
|
|
278
|
-
// The top-level model is a selector too (parsed the same way withModelOverrides does), so a
|
|
279
|
-
// fallback field that omits model borrows the parsed id, never the raw selector string.
|
|
280
|
-
const topSel = topTierSelector(config, "default");
|
|
281
|
-
const provider = fbo.provider ?? topSel?.provider ?? config.provider;
|
|
282
|
-
const model = fbo.model ?? topSel?.model;
|
|
283
|
-
const thinking = fbo.thinking ?? topSel?.thinking ?? config.thinking;
|
|
284
|
-
return resolvedModelFields(provider, model, thinking);
|
|
285
|
-
}
|
|
286
|
-
/** The order a tier borrows other tiers' own fallbacks in, after trying its own: small
|
|
287
|
-
* climbs (small → default → strong), default keeps near itself (default → strong → small),
|
|
288
|
-
* and strong never drops to a small model (strong → default → pause) — a weak reviewer or
|
|
289
|
-
* planner does more harm than a paused one. */
|
|
290
|
-
const BORROW_ORDER = {
|
|
291
|
-
small: ["small", "default", "strong"],
|
|
292
|
-
default: ["default", "strong", "small"],
|
|
293
|
-
strong: ["strong", "default"],
|
|
294
|
-
};
|
|
295
|
-
/** A tier's OWN fallback pair — what the `fallback` key declares at that tier's map entry,
|
|
296
|
-
* or (default only) the string form of `fallback` or the legacy `fallbackModel` object — or
|
|
297
|
-
* undefined when the tier declares nothing or declares `"pause"` (an explicit opt-out, which
|
|
298
|
-
* also makes it unborrowable: a tier with no own fallback offers nothing to borrow). Parsed
|
|
299
|
-
* like fallbackPair: a pure selector, with the ambient top-level thinking when the selector
|
|
300
|
-
* carries none. */
|
|
301
|
-
function tierOwnFallback(config, tier) {
|
|
302
|
-
const fb = config.fallback;
|
|
303
|
-
let selector;
|
|
304
|
-
if (fb !== undefined) {
|
|
305
|
-
// The top-level string form names the default tier's own fallback; `"pause"` is rejected
|
|
306
|
-
// by validation but defensively names no model (matching the map entries below).
|
|
307
|
-
if (typeof fb === "string")
|
|
308
|
-
selector = tier === "default" && fb !== "pause" ? fb : undefined;
|
|
309
|
-
else {
|
|
310
|
-
const raw = fb[tier];
|
|
311
|
-
if (raw !== undefined && raw !== "pause")
|
|
312
|
-
selector = raw;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
else if (tier === "default") {
|
|
316
|
-
// The legacy `fallbackModel` object is the default tier's own fallback, with its
|
|
317
|
-
// provider/thinking fields and the top-level borrow precedence.
|
|
318
|
-
return legacyFallbackPair(config) ?? undefined;
|
|
319
|
-
}
|
|
320
|
-
if (selector === undefined)
|
|
321
|
-
return undefined;
|
|
322
|
-
return fallbackSelectorFields(selector, config);
|
|
323
|
-
}
|
|
324
|
-
/** Which free pair each tier runs on at the daily cap (plans/model-tiers.md "Budget fallback
|
|
325
|
-
* by tier", part 5a/8). A tier runs its own declared fallback when `usable(pair)`; otherwise
|
|
326
|
-
* it borrows another tier's OWN fallback — never one that tier itself borrowed — in
|
|
327
|
-
* BORROW_ORDER's order; otherwise it pauses (`pair: null`). The engine is pure: the caller
|
|
328
|
-
* supplies `usable` (part 5b/8 wires it to pair pricing and the fallback breaker), so the
|
|
329
|
-
* same rules are testable with a stub and the gates cannot disagree with each other. A
|
|
330
|
-
* config with no fallback at all resolves every tier to pause, and a single `fallback: F`
|
|
331
|
-
* reads as default's own pair, which small and strong then borrow — today's behavior. */
|
|
332
|
-
export function resolveTierFallbacks(config, usable) {
|
|
333
|
-
const own = (tier) => {
|
|
334
|
-
const pair = tierOwnFallback(config, tier);
|
|
335
|
-
return pair !== undefined && usable(pair) ? pair : undefined;
|
|
336
|
-
};
|
|
337
|
-
const resolve = (tier) => {
|
|
338
|
-
const mine = own(tier);
|
|
339
|
-
if (mine)
|
|
340
|
-
return { pair: mine, from: null };
|
|
341
|
-
for (const t of BORROW_ORDER[tier]) {
|
|
342
|
-
if (t === tier)
|
|
343
|
-
continue;
|
|
344
|
-
const theirs = own(t);
|
|
345
|
-
if (theirs)
|
|
346
|
-
return { pair: theirs, from: t };
|
|
347
|
-
}
|
|
348
|
-
return { pair: null, from: null };
|
|
349
|
-
};
|
|
350
|
-
return { small: resolve("small"), default: resolve("default"), strong: resolve("strong") };
|
|
351
|
-
}
|
|
352
|
-
/** The config a role's ticks run on while a model-fallback episode is active (PLANS.md "Model
|
|
353
|
-
* failure fallback, part 1/2"): the role's tier pair from resolveTierFallbacks (the tier's own
|
|
354
|
-
* pair, else BORROW_ORDER's borrow), shaped like configForRole — provider/model/thinking over
|
|
355
|
-
* the role's own and top-level values, plus the role's minTickIntervalSeconds. Null when the
|
|
356
|
-
* tier resolves to pause (no fallback configured), so the feature stays off. Pure. */
|
|
357
|
-
export function fallbackRoleConfig(config, role) {
|
|
358
|
-
const tier = roleSeamTier(config, role);
|
|
359
|
-
const pair = resolveTierFallbacks(config, () => true)[tier].pair;
|
|
360
|
-
if (pair === null)
|
|
361
|
-
return null;
|
|
362
|
-
const rc = config.roles[baseRoleOf(role)];
|
|
363
|
-
return {
|
|
364
|
-
...withModelOverrides(config, tier, {
|
|
365
|
-
provider: pair.provider,
|
|
366
|
-
model: pair.model,
|
|
367
|
-
thinking: pair.thinking,
|
|
368
|
-
}),
|
|
369
|
-
minTickIntervalSeconds: rc?.minTickIntervalSeconds ?? config.minTickIntervalSeconds,
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
/** Build a ModelFallbackView for an episode, or null while the episode is not running the
|
|
373
|
-
* fallback (no episode, or a due probe on the primary). role-view.ts's
|
|
374
|
-
* RoleViewPayload.modelFallback and status/status-data.ts's per-loop row both build this
|
|
375
|
-
* shape, so one function keeps the two surfaces from disagreeing about the pair, its start,
|
|
376
|
-
* and its reason. `now` is the caller's clock (the status snapshot passes its own poll
|
|
377
|
-
* clock), so a row and its poll agree on what "active" means. */
|
|
378
|
-
export function modelFallbackView(episode, config, role, now = Date.now()) {
|
|
379
|
-
if (episode === undefined || !modelFallbackActive(episode, now))
|
|
380
|
-
return null;
|
|
381
|
-
const episodeConfig = fallbackRoleConfig(config, role);
|
|
382
|
-
if (episodeConfig === null)
|
|
383
|
-
return null;
|
|
384
|
-
return {
|
|
385
|
-
...resolvedModelFields(episodeConfig.provider, episodeConfig.model, episodeConfig.thinking),
|
|
386
|
-
since: episode.since,
|
|
387
|
-
reason: episode.reason,
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
/** The per-tier fallback labels the observability surfaces render (part 7b/8): tier →
|
|
391
|
-
* `provider/model[ (from <tier>)]` for every tier the resolution gives a pair, in tier order.
|
|
392
|
-
* The `(from …)` suffix marks a borrowed pair — a strong-tier borrow is the fact an operator
|
|
393
|
-
* reading the feed or the badge can act on. Tiers resolved to pause are left out: the badge
|
|
394
|
-
* and the event name the pairs that RUN, the pause story lives on the pause events and the
|
|
395
|
-
* per-role rows. */
|
|
396
|
-
export function tierFallbackLabels(resolved) {
|
|
397
|
-
const out = {};
|
|
398
|
-
for (const tier of MODEL_TIERS) {
|
|
399
|
-
const r = resolved[tier];
|
|
400
|
-
if (r.pair !== null)
|
|
401
|
-
out[tier] = modelPairName(r.pair) + (r.from !== null ? ` (from ${r.from})` : "");
|
|
402
|
-
}
|
|
403
|
-
return out;
|
|
404
|
-
}
|
|
405
|
-
/** True when the resolution hands out two or more DISTINCT pairs — the condition under which
|
|
406
|
-
* the tiered surfaces (the budget_fallback event, the header badge) switch from today's
|
|
407
|
-
* single-pair text to the tier list: a single fallback whose pair the other tiers borrow
|
|
408
|
-
* resolves every tier to the same pair, and that stays on the single-pair text, byte
|
|
409
|
-
* identical to the pre-tier surfaces. */
|
|
410
|
-
export function tiersResolveDistinctPairs(resolved) {
|
|
411
|
-
const pairs = MODEL_TIERS.map((t) => resolved[t].pair)
|
|
412
|
-
.filter((p) => p !== null)
|
|
413
|
-
.map((p) => modelPairName(p));
|
|
414
|
-
return new Set(pairs).size >= 2;
|
|
415
|
-
}
|
|
416
|
-
/** Drop a model-override seam's provider/model/thinking fields — the one rule the fallback
|
|
417
|
-
* view's role and reviewer strips share, so a field on that triple cannot be dropped from one
|
|
418
|
-
* and left on the other (where it would reach a priced model after the cap). */
|
|
419
|
-
function stripModel(o) {
|
|
420
|
-
const { provider: _p, model: _m, thinking: _t, ...rest } = o;
|
|
421
|
-
return rest;
|
|
422
|
-
}
|
|
423
|
-
/** The reviewer config as the fallback view sees it: model seam stripped, and its time budget
|
|
424
|
-
* raised to at least FALLBACK_REVIEW_TIMEOUT_S (a budget sized for the budgeted model's turns
|
|
425
|
-
* times out every review on a slower free one; a configured value above the floor stands). The
|
|
426
|
-
* one home of that rule, shared by both fallback branches. */
|
|
427
|
-
function fallbackReview(review) {
|
|
428
|
-
return {
|
|
429
|
-
...stripModel(review),
|
|
430
|
-
timeoutSeconds: Math.max(review.timeoutSeconds ?? REVIEW_TIMEOUT_S, FALLBACK_REVIEW_TIMEOUT_S),
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
/** Every `roles.<id>` entry with its model seam stripped. With `keepTierNames`, an entry whose
|
|
434
|
-
* model is a tier name is left untouched: the resolved tier map now serves that tier itself, so
|
|
435
|
-
* the reference stays valid (stripping it would lose the role's tier). */
|
|
436
|
-
function stripRoleModels(roles, keepTierNames) {
|
|
437
|
-
return Object.fromEntries(Object.entries(roles).map(([id, rc]) => [
|
|
438
|
-
id,
|
|
439
|
-
keepTierNames && isTierName(rc?.model) ? rc : stripModel(rc),
|
|
440
|
-
]));
|
|
441
|
-
}
|
|
442
|
-
/** Every `roles.<id>` entry with its model seam stripped, except a seam `free` calls free — a
|
|
443
|
-
* subscription claude role keeps its own model through the cap rather than moving to the
|
|
444
|
-
* fallback (plans/agent-backends.md "Free-ness"). With `keepTierNames`, an entry whose model is
|
|
445
|
-
* a tier name is left untouched: the resolved tier map now serves that tier itself, so the
|
|
446
|
-
* reference stays valid (stripping it would lose the role's tier). */
|
|
447
|
-
function freeRoleModels(config, free) {
|
|
448
|
-
return Object.fromEntries(Object.entries(config.roles).map(([id, rc]) => {
|
|
449
|
-
if (rc && free?.(withModelOverrides(config, roleSeamTier(config, id), rc)))
|
|
450
|
-
return [id, rc];
|
|
451
|
-
return [id, isTierName(rc?.model) ? rc : stripModel(rc)];
|
|
452
|
-
}));
|
|
453
|
-
}
|
|
454
|
-
/** The config as seen by a role loop running on the free fallback model
|
|
455
|
-
* (plans/fallback-model.md): the fallback's provider/model/thinking installed as the top-level
|
|
456
|
-
* values AND every per-role and reviewer model override dropped, so that EVERY seam that could
|
|
457
|
-
* otherwise reach a priced model — an author run, its reviewer, a conflict resolver — resolves
|
|
458
|
-
* to the one free pair. Dropping the overrides is the point: a role pinned to a paid model in
|
|
459
|
-
* tumwater.json must not keep spending after the cap is reached. The one other change is the
|
|
460
|
-
* reviewer's time budget, raised to at least FALLBACK_REVIEW_TIMEOUT_S: a budget sized for the
|
|
461
|
-
* budgeted model's turns times out every review on a slower free one (a configured
|
|
462
|
-
* `review.timeoutSeconds` above the floor stands, and reviewRunConfig still caps it at
|
|
463
|
-
* tickTimeoutSeconds). Everything else (intervals, thresholds, exempt paths, the cap itself) is
|
|
464
|
-
* unchanged when no fallback is configured. With a per-tier `resolved` map (resolveTierFallbacks,
|
|
465
|
-
* part 5a/8) the model map is rewritten to the resolved pairs instead — each seam keeps its
|
|
466
|
-
* tier: `model[tier]` becomes the tier's pair as a selector string, a tier resolved to pause
|
|
467
|
-
* is left out (it inherits default's entry, harmless because the gate holds its roles), raw
|
|
468
|
-
* per-role selector overrides are dropped so a paid pin cannot keep spending, and tier-name
|
|
469
|
-
* role overrides (`roles.<id>.model: "strong"`) are kept since the map now serves the tier
|
|
470
|
-
* itself. The director's exemption lives in the gates (roleForConfig), not here. A
|
|
471
|
-
* subscription claude seam is never moved (`free`): the gate passes one predicate over the
|
|
472
|
-
* resolved seam, so a free tier keeps its own primary in the map — with the ambient thinking
|
|
473
|
-
* level it resolved with — a free role keeps its own override, and a free reviewer keeps its own
|
|
474
|
-
* model, while every priced seam still lands on its tier's fallback pair. */
|
|
475
|
-
export function applyFallbackModel(config, resolved, free) {
|
|
476
|
-
if (resolved) {
|
|
477
|
-
const selectorOf = (f) => f.pair
|
|
478
|
-
? formatModelSelector({
|
|
479
|
-
provider: f.pair.provider,
|
|
480
|
-
model: f.pair.model ?? "",
|
|
481
|
-
thinking: f.pair.thinking,
|
|
482
|
-
})
|
|
483
|
-
: undefined;
|
|
484
|
-
const model = {};
|
|
485
|
-
for (const tier of MODEL_TIERS) {
|
|
486
|
-
// A free seam keeps the tier's own primary selector — the backend namespace a resolved
|
|
487
|
-
// seam has cleared, plus the ambient thinking level `topTierSelector` alone omits. A moved
|
|
488
|
-
// seam gets its pair selector, which already carries that ambient level.
|
|
489
|
-
const seam = withModelOverrides(config, tier, {});
|
|
490
|
-
if (free?.(seam)) {
|
|
491
|
-
const own = topTierSelector(config, tier);
|
|
492
|
-
if (own !== undefined)
|
|
493
|
-
model[tier] = formatModelSelector({ ...own, thinking: own.thinking ?? config.thinking });
|
|
494
|
-
// A tier with no top-level selector — `agent: "claude"` riding its agent default —
|
|
495
|
-
// has no own selector to carry the level, so name the routed default plus it.
|
|
496
|
-
else if (seam.model !== undefined)
|
|
497
|
-
model[tier] = formatModelSelector({
|
|
498
|
-
provider: seam.provider,
|
|
499
|
-
model: seam.model,
|
|
500
|
-
thinking: seam.thinking,
|
|
501
|
-
});
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
const sel = selectorOf(resolved[tier]);
|
|
505
|
-
if (sel !== undefined)
|
|
506
|
-
model[tier] = sel;
|
|
507
|
-
}
|
|
508
|
-
return {
|
|
509
|
-
...config,
|
|
510
|
-
provider: undefined,
|
|
511
|
-
model,
|
|
512
|
-
thinking: undefined,
|
|
513
|
-
review: free?.(reviewConfig(config)) ? config.review : fallbackReview(config.review),
|
|
514
|
-
roles: freeRoleModels(config, free),
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
const pair = fallbackPair(config);
|
|
518
|
-
if (!pair)
|
|
519
|
-
return config;
|
|
520
|
-
return {
|
|
521
|
-
...config,
|
|
522
|
-
provider: pair.provider,
|
|
523
|
-
model: pair.model,
|
|
524
|
-
thinking: pair.thinking,
|
|
525
|
-
review: fallbackReview(config.review),
|
|
526
|
-
roles: stripRoleModels(config.roles, false),
|
|
527
|
-
};
|
|
295
|
+
/** One fallback pair rendered for display, agent-qualified (PLANS.md "Mixed claude + pi
|
|
296
|
+
* fleets, part 3/4" gap 7): a pair routed to a non-pi backend — its `provider` is that
|
|
297
|
+
* backend's id, per seamKeyedFallbackProvider, so `claude` — renders as `claude/claude-sonnet-5-5`,
|
|
298
|
+
* the same agent-qualified form every other surface carries; a pi pair keeps the bare model
|
|
299
|
+
* byte-identical to before (the badge, the team alert and `tumwater role` all name the
|
|
300
|
+
* model a pi fallback runs, never its provider). Falls back through the provider to the
|
|
301
|
+
* caller's `<agent> default` label when both halves are absent. One home so the three display
|
|
302
|
+
* surfaces cannot render one fallback two ways. */
|
|
303
|
+
export function fallbackDisplayPair(pair, agentDefault) {
|
|
304
|
+
const p = pair ?? {};
|
|
305
|
+
if (p.model !== undefined)
|
|
306
|
+
return p.provider !== undefined && isAgentId(p.provider) ? `${p.provider}/${p.model}` : p.model;
|
|
307
|
+
if (p.provider !== undefined)
|
|
308
|
+
return p.provider;
|
|
309
|
+
return agentDefault;
|
|
528
310
|
}
|