tumwater 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { configForRole, modelSelectorField, roleSeamTier, } from "../config/config-views.js";
|
|
3
|
-
import { modelFallbackView } from "../config/config-fallbacks.js";
|
|
4
3
|
import { isCustomRole } from "../config/config.js";
|
|
5
4
|
import { baseRoleOf, configuredInstances, instanceIndex, runnerIds, runnerPaused } from "../roles/runner-ids.js";
|
|
6
5
|
import { errorStreakAtWarn, QUIET_KILL_RESUME_LIMIT } from "../run/run-apply.js";
|
|
7
6
|
import { queuedMergeRoles } from "../merge/merge-queue.js";
|
|
8
7
|
import { hasOutstandingWork } from "../runner/runner-state.js";
|
|
9
|
-
import { formatModelSelector } from "../config/model-selector.js";
|
|
8
|
+
import { formatModelSelector } from "../config/config-model-selector.js";
|
|
10
9
|
import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
|
|
11
10
|
import { isJsonObject } from "../files/json-object.js";
|
|
12
11
|
import { runnerStateForPoll } from "./status-polls.js";
|
|
@@ -14,14 +13,14 @@ import { roleImageInfo } from "../agent/image-input.js";
|
|
|
14
13
|
import { backendDefaultLabel } from "../agent/backend-traits.js";
|
|
15
14
|
import { readPiListedModels } from "../pi/pi-listed-models.js";
|
|
16
15
|
import { seamContextWindowForRole } from "../pi/pi-models.js";
|
|
17
|
-
/** One row per enabled role id (part 5b/7). `configForRole`,
|
|
16
|
+
/** One row per enabled role id (part 5b/7). `configForRole`, the leased /
|
|
18
17
|
* pinned slot and the pi-default seam all resolve against the values `snapshot()` already
|
|
19
18
|
* read once for the poll — the info and slots pins (threaded as `slots`) keep rows, `running`
|
|
20
|
-
* and `disk` on the same shared reads
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
export function collectRoleRows(root, cfg,
|
|
19
|
+
* and `disk` on the same shared reads. One read of pi's own saved default per poll
|
|
20
|
+
* (docs/backends.md) serves the rows whose seam runs pi and resolves no model — its
|
|
21
|
+
* settings.json sits beside the models catalog this snapshot reads (piSettingsPath), so one
|
|
22
|
+
* seam serves both. */
|
|
23
|
+
export function collectRoleRows(root, cfg, slots, modelsPath, paused, queued = queuedMergeRoles(root)) {
|
|
25
24
|
// A surplus runner (index above `instances`, from the runner pool of plans/fast-merge.md
|
|
26
25
|
// "Lever 1") with nothing to show gets no row, so the table does not list idle extras.
|
|
27
26
|
// The exceptions are exactly the surplus runners an operator most needs to see and resume
|
|
@@ -31,7 +30,6 @@ export function collectRoleRows(root, cfg, now, slots, modelsPath, paused) {
|
|
|
31
30
|
// run after run" alert keys on, so a surplus runner the alert would report is never
|
|
32
31
|
// hidden. Without these rows the role is invisible and unresumable (BUGS.md 2026-10-23),
|
|
33
32
|
// and the alerts — which read the rows — cannot name it.
|
|
34
|
-
const queued = queuedMergeRoles(root);
|
|
35
33
|
const roles = runnerIds(cfg).filter((r) => {
|
|
36
34
|
if (instanceIndex(r) <= configuredInstances(cfg, r))
|
|
37
35
|
return true;
|
|
@@ -59,19 +57,13 @@ export function collectRoleRows(root, cfg, now, slots, modelsPath, paused) {
|
|
|
59
57
|
// only with a tier map declared, where it names the tier a role rides.
|
|
60
58
|
const eff = configForRole(cfg, r);
|
|
61
59
|
// The image-input surface (plans/image-input.md "Surfaces", part 5/5): the role's own
|
|
62
|
-
// capability
|
|
63
|
-
//
|
|
64
|
-
// prompt would run.
|
|
60
|
+
// capability, one shared resolver so the status rows, the role page, and the submit gate
|
|
61
|
+
// cannot disagree about who reads images.
|
|
65
62
|
const image = roleImageInfo(cfg, r, modelsPath, listed);
|
|
66
63
|
// The peak-ctx share denominator (the role's context window, part 3c): resolved once per
|
|
67
64
|
// poll here, beside `eff` (the same seam the role page resolves and names) — the same seam
|
|
68
65
|
// `seamContextWindow` run against, so row, page and drawer cannot disagree.
|
|
69
66
|
const contextWindow = seamContextWindowForRole(cfg, r, root, modelsPath);
|
|
70
|
-
// Model failure fallback, part 2/2: while the role's episode is active its agent runs use
|
|
71
|
-
// the tier-resolved fallback pair, so the row names that pair, the episode's start, and
|
|
72
|
-
// the failure that tripped it. `now` is the poll's own clock (the snapshot seam), so
|
|
73
|
-
// every row agrees on what "active" means.
|
|
74
|
-
const episode = modelFallbackView(base.modelFallback, cfg, r, now);
|
|
75
67
|
// A seam that runs pi and resolves neither a provider nor a model starts on pi's own
|
|
76
68
|
// saved default — name it in the row (modelFromPi) so the operator sees which model the
|
|
77
69
|
// role runs. The config's `thinking` wins over the default's because tumwater passes it
|
|
@@ -106,7 +98,6 @@ export function collectRoleRows(root, cfg, now, slots, modelsPath, paused) {
|
|
|
106
98
|
? {}
|
|
107
99
|
: { model: backendDefaultLabel(eff.agent) }),
|
|
108
100
|
...(isJsonObject(cfg.model) ? { modelTier: roleSeamTier(cfg, r) } : {}),
|
|
109
|
-
...(episode !== null ? { fallback: episode } : {}),
|
|
110
101
|
...(contextWindow !== null ? { contextWindow } : {}),
|
|
111
102
|
...(slot ? { slot: path.basename(slot.dir), slotPinned: pinned !== undefined } : {}),
|
|
112
103
|
...(baseRoleOf(r) === r ? {} : { instanceOf: baseRoleOf(r) }),
|
|
@@ -22,7 +22,7 @@ export const ERROR_STORM_ROLES = 3;
|
|
|
22
22
|
export const ERROR_STORM_QUIET = { key: null, roles: [] };
|
|
23
23
|
/** The config knob a shared cause points at, when one is known. Only the timeout cause is
|
|
24
24
|
* mapped: `timed out after <dur>` is the reducer's pooled rendering of the run-timeout errors
|
|
25
|
-
* (src/agent/run
|
|
25
|
+
* (src/agent/agent-run.ts's two `timed out after ${runTimeoutSeconds}s` shapes, pooled into one
|
|
26
26
|
* key by poolTimeoutKey — src/failure/failure-cluster.ts owns the two shapes and their
|
|
27
27
|
* pooling), and a team-wide
|
|
28
28
|
* run of it means the run budget does not fit the serving model — the one cause with a knob
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* 2026-09-29 connection-storm entry): when many roles each fail a few times on one provider
|
|
3
3
|
* failure kind, no existing bar trips — the per-role streak alarm needs one role DEEP
|
|
4
4
|
* (ERROR_STREAK_WARN consecutive, src/run/run-apply.ts), the error-storm warning needs several
|
|
5
|
-
* roles deep at once (src/team/error-storm.ts), and the team hold needs failures COALESCED
|
|
6
|
-
* a two-minute window (src/team/team-hold.ts). A degraded backend that fails the team
|
|
7
|
-
* shallowly — six runs across as many roles, each failure minutes apart — rides all
|
|
8
|
-
* gaps in silence. This alarm counts raw failures of one kind across roles in a rolling
|
|
5
|
+
* roles deep at once (src/team/team-error-storm.ts), and the team hold needs failures COALESCED
|
|
6
|
+
* within a two-minute window (src/team/team-hold.ts). A degraded backend that fails the team
|
|
7
|
+
* widely and shallowly — six runs across as many roles, each failure minutes apart — rides all
|
|
8
|
+
* three gaps in silence. This alarm counts raw failures of one kind across roles in a rolling
|
|
9
9
|
* window, independent of any role's streak depth: the fourth bar, shaped for the spread the
|
|
10
|
-
* other three cannot see. POLICY, on the shape of its siblings (src/team/error-storm.ts,
|
|
10
|
+
* other three cannot see. POLICY, on the shape of its siblings (src/team/team-error-storm.ts,
|
|
11
11
|
* src/team/team-hold.ts): a pure reducer over the observations the orchestrator collects each
|
|
12
12
|
* poll and the previous state, so the rule is unit-testable without a team and the
|
|
13
13
|
* orchestrator owns only the wiring (the warning event). Like both siblings it has memory —
|
|
@@ -78,15 +78,17 @@ export function heldProviders(holds) {
|
|
|
78
78
|
/** The holds that STAND, as a structured list for observers (the dashboards and `tumwater
|
|
79
79
|
* status` name them; the event feed and failure digest already do): iterate the map, skip an
|
|
80
80
|
* entry whose `until` is null (a lifted hold kept only for relapse memory) and one whose kind
|
|
81
|
-
* is `"subscription-window"` (
|
|
82
|
-
*
|
|
83
|
-
* one
|
|
84
|
-
* `
|
|
81
|
+
* is `"subscription-window"` (the account-level window throttle, not a failure-storm hold the
|
|
82
|
+
* standing list names; its display and crossing events are gone — parts 5u/6–5x/6), then
|
|
83
|
+
* emit one `StandingHold` each, sorted by provider name so two reads of one map agree. An
|
|
84
|
+
* `undefined` provider means pi's default backend, the same meaning `heldProviders` gives it,
|
|
85
|
+
* so it is omitted rather than written as null. */
|
|
85
86
|
export function standingHolds(holds) {
|
|
86
87
|
const out = [];
|
|
87
88
|
for (const [provider, hold] of holds) {
|
|
88
89
|
// A lifted hold (until null) and a null kind (never standing) never publish; a
|
|
89
|
-
// subscription-window hold
|
|
90
|
+
// subscription-window hold is an account-level throttle, not a storm hold for this list,
|
|
91
|
+
// and its display and crossing events are gone (parts 5u/6–5x/6).
|
|
90
92
|
if (hold.until === null || hold.kind === null || hold.kind === "subscription-window")
|
|
91
93
|
continue;
|
|
92
94
|
out.push({ ...(provider !== undefined ? { provider } : {}), until: hold.until, kind: hold.kind });
|
|
@@ -94,88 +96,121 @@ export function standingHolds(holds) {
|
|
|
94
96
|
out.sort((a, b) => (a.provider ?? "").localeCompare(b.provider ?? ""));
|
|
95
97
|
return out;
|
|
96
98
|
}
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
// A rejected subscription usage window is the account-level throttle: it holds every seam on
|
|
128
|
-
// its provider until `resetsAt`, whatever the role count — the one observation kind the
|
|
129
|
-
// per-role storm rule below does not gate. Overage is real money and never holds (the run
|
|
130
|
-
// continues into paid overflow); a window with no future `resetsAt` holds nothing.
|
|
99
|
+
/** Decide whether the current hold still stands or the deadline re-opens it: return `prev`
|
|
100
|
+
* itself while open or still held (and not a re-evaluated reserve), else the re-opened
|
|
101
|
+
* skeleton. A reserve hold is re-evaluated every poll: the window may since have been
|
|
102
|
+
* rejected (the director keeps running) or the reserve removed from the config, and either
|
|
103
|
+
* must show now. The deadline passed: re-open, keeping provider/kind/escalation as the next
|
|
104
|
+
* storm's relapse memory, then fall through so a still-rejected subscription window (or a
|
|
105
|
+
* storm whose observations survived) holds again on this very poll rather than leaving one
|
|
106
|
+
* poll open. kind and provider stay because the next storm's relapse test compares against
|
|
107
|
+
* the hold that just ended. */
|
|
108
|
+
function reopenTeamHold(prev, now) {
|
|
109
|
+
if (prev.until === null)
|
|
110
|
+
return prev;
|
|
111
|
+
if (now < prev.until && prev.reserve !== true)
|
|
112
|
+
return prev;
|
|
113
|
+
return {
|
|
114
|
+
provider: prev.provider,
|
|
115
|
+
until: null,
|
|
116
|
+
kind: prev.kind,
|
|
117
|
+
roles: [],
|
|
118
|
+
escalation: prev.escalation,
|
|
119
|
+
reopenedAt: prev.reserve === true ? prev.reopenedAt : now,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** The account-level subscription window hold, or undefined when none is rejected. A rejected
|
|
123
|
+
* subscription usage window is the account-level throttle: it holds every seam on its
|
|
124
|
+
* provider until `resetsAt`, whatever the role count — the one observation kind the
|
|
125
|
+
* per-role storm rule below does not gate. Overage is real money and never holds (the run
|
|
126
|
+
* continues into paid overflow); a window with no future `resetsAt` holds nothing. */
|
|
127
|
+
function subscriptionWindowHold(observations, now, reopenedAt) {
|
|
131
128
|
const window = observations.find((o) => o.kind === "subscription-window" && (o.resetsAt ?? 0) > now);
|
|
132
|
-
if (window
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
129
|
+
if (window === undefined)
|
|
130
|
+
return undefined;
|
|
131
|
+
return {
|
|
132
|
+
provider: window.provider,
|
|
133
|
+
until: window.resetsAt,
|
|
134
|
+
kind: "subscription-window",
|
|
135
|
+
roles: [],
|
|
136
|
+
escalation: 0,
|
|
137
|
+
reopenedAt,
|
|
138
|
+
...(window.reserve === true ? { reserve: true } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** The observations that can still count toward the next storm: failures after the last
|
|
142
|
+
* re-open (the storm that hold already answered, and any an in-flight run hit while it
|
|
143
|
+
* held — that run's own retry is already waiting it out) and inside the storm window. */
|
|
144
|
+
function recentStormObservations(prev, observations, now) {
|
|
145
|
+
return observations.filter((o) => (prev.reopenedAt === null || o.at > prev.reopenedAt) && now - o.at <= HOLD_STORM_WINDOW_MS);
|
|
146
|
+
}
|
|
147
|
+
/** The first provider+kind group (in first-seen order, so the pick is deterministic) with
|
|
148
|
+
* HOLD_STORM_ROLES distinct roles behind it, or undefined when none qualifies — the answer
|
|
149
|
+
* to "which storm wins one poll?", since one poll can hold about only one kind. Undefined
|
|
150
|
+
* providers share one key: an unconfigured team is one backend by construction. */
|
|
151
|
+
function firstStormGroup(recent) {
|
|
147
152
|
const byProviderKind = new Map();
|
|
148
153
|
for (const o of recent) {
|
|
149
154
|
const key = `${o.provider ?? ""}\n${o.kind}`;
|
|
150
155
|
getOrCreate(byProviderKind, key, () => ({ provider: o.provider, kind: o.kind, group: [] })).group.push(o);
|
|
151
156
|
}
|
|
152
|
-
let storm = [];
|
|
153
|
-
let kind = null;
|
|
154
|
-
let provider;
|
|
155
157
|
for (const entry of byProviderKind.values()) {
|
|
156
158
|
const roles = new Set(entry.group.map((o) => o.role));
|
|
157
159
|
if (roles.size >= HOLD_STORM_ROLES) {
|
|
158
|
-
|
|
159
|
-
kind = entry.kind;
|
|
160
|
-
provider = entry.provider;
|
|
161
|
-
break;
|
|
160
|
+
return { provider: entry.provider, kind: entry.kind, storm: entry.group };
|
|
162
161
|
}
|
|
163
162
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
/** The hold severity this storm earns: +1 on the previous escalation when it is a relapse of
|
|
166
|
+
* the just-ended hold, else 0. A relapse is per provider AND kind: the same storm re-tripping
|
|
167
|
+
* inside the window escalates, while a different kind — or the same kind at a DIFFERENT
|
|
168
|
+
* provider — after a re-open is a new incident at the base. */
|
|
169
|
+
function holdEscalation(prev, provider, kind, now) {
|
|
170
170
|
const relapse = prev.kind !== null &&
|
|
171
171
|
prev.kind === kind &&
|
|
172
172
|
prev.provider === provider &&
|
|
173
173
|
prev.reopenedAt !== null &&
|
|
174
174
|
now - prev.reopenedAt <= HOLD_RELAPSE_MS;
|
|
175
|
-
|
|
175
|
+
return relapse ? prev.escalation + 1 : 0;
|
|
176
|
+
}
|
|
177
|
+
/** How long this storm's hold lasts: the longest of the escalated base hold and, for a
|
|
178
|
+
* rate-limit storm, the furthest Retry-After deadline among its observations (each measured
|
|
179
|
+
* from its own 429, since that is when the provider said it), capped at HOLD_CAP_MS. */
|
|
180
|
+
function holdDurationMs(escalation, storm, now) {
|
|
176
181
|
const retryAfterMs = Math.max(0, ...storm.map((o) => o.at + (o.retryAfterSeconds ?? 0) * 1000 - now));
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
return Math.min(HOLD_CAP_MS, Math.max(HOLD_BASE_MS * 2 ** escalation, retryAfterMs));
|
|
183
|
+
}
|
|
184
|
+
/** Step the hold by one orchestrator poll: from `prev` and every role's latest failure
|
|
185
|
+
* observations, decide what holds at `now`. A held team stays held until `until` and then
|
|
186
|
+
* re-opens by itself — nothing can get stuck, the same guarantee the budget gate's stateless
|
|
187
|
+
* re-evaluation gives. An open team trips when HOLD_STORM_ROLES distinct roles each
|
|
188
|
+
* ended a run on the same failure kind within the storm window (and after the last re-open) —
|
|
189
|
+
* the first qualifying kind in observation order wins, since one poll can hold about only one
|
|
190
|
+
* kind — for the longest of the escalated base hold and, for a rate-limit storm, the furthest
|
|
191
|
+
* Retry-After deadline among those observations (each measured from its own 429, since that is
|
|
192
|
+
* when the provider said it), capped at HOLD_CAP_MS. Returns `prev` itself when
|
|
193
|
+
* nothing changed, so a caller can compare `until` across the step to find transitions. */
|
|
194
|
+
export function teamHold(prev, observations, now) {
|
|
195
|
+
const reopened = reopenTeamHold(prev, now);
|
|
196
|
+
if (reopened.until !== null)
|
|
197
|
+
return reopened;
|
|
198
|
+
const window = subscriptionWindowHold(observations, now, reopened.reopenedAt);
|
|
199
|
+
if (window !== undefined)
|
|
200
|
+
return window;
|
|
201
|
+
const group = firstStormGroup(recentStormObservations(reopened, observations, now));
|
|
202
|
+
if (group === undefined)
|
|
203
|
+
return reopened;
|
|
204
|
+
const escalation = holdEscalation(reopened, group.provider, group.kind, now);
|
|
205
|
+
const roles = sortedRoles(new Set(group.storm.map((o) => o.role)));
|
|
206
|
+
return {
|
|
207
|
+
provider: group.provider,
|
|
208
|
+
until: now + holdDurationMs(escalation, group.storm, now),
|
|
209
|
+
kind: group.kind,
|
|
210
|
+
roles,
|
|
211
|
+
escalation,
|
|
212
|
+
reopenedAt: reopened.reopenedAt,
|
|
213
|
+
};
|
|
179
214
|
}
|
|
180
215
|
/** The providers whose standing hold is the account's rejected subscription window (kind
|
|
181
216
|
* "subscription-window", not a reserve crossing — the director is exempt from the reserve): the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
2
|
+
import { cachedByStat } from "../files/stat-cache.js";
|
|
2
3
|
import { finiteNumber, isJsonObject } from "../files/json-object.js";
|
|
3
4
|
import { pidAlive } from "../process/process.js";
|
|
4
5
|
import { orchestratorStatePath } from "../paths.js";
|
|
@@ -41,13 +42,28 @@ export function publishInfoFields(root, info, fields) {
|
|
|
41
42
|
* trusted because the `OrchestratorInfo` annotation says so: a foreign shape (a scalar
|
|
42
43
|
* `fallbackDemotions`, a `disk` missing its numbers) would otherwise throw out of a render —
|
|
43
44
|
* `modelPairName(p) in publishedDemotions` or `formatGB(undefined)` — on every second's
|
|
44
|
-
* poll. An unreadable field is dropped, so observers read it as absent.
|
|
45
|
+
* poll. An unreadable field is dropped, so observers read it as absent. The value is served
|
|
46
|
+
* from a stat-keyed cache (infoCache): the TUI, GUI and `tumwater status` poll snapshot every
|
|
47
|
+
* second, but the orchestrator writes the info file only at start and whenever a published
|
|
48
|
+
* field (build, budget, disk, permits) changes — between writes it sits unchanged for minutes.
|
|
49
|
+
* An unchanged file costs one stat syscall per poll instead of re-reading, re-parsing, and
|
|
50
|
+
* re-normalizing JSON; any write invalidates via dev/ino/mtime/size (writeJsonAtomic's
|
|
51
|
+
* tmp+rename is the same freshness contract the other cachedByStat consumers rely on). */
|
|
52
|
+
// Per-root info cache (stat-cache.cachedByStat), keyed by state-file path so distinct roots
|
|
53
|
+
// never collide; capped inside cachedByStat so many short-lived roots in tests cannot grow it
|
|
54
|
+
// unbounded. readOrchestratorInfo serves an unchanged file from this cache on a poll, and the
|
|
55
|
+
// orchestrator's own writes (tmp+rename) invalidate it exactly like every other cachedByStat
|
|
56
|
+
// consumer.
|
|
57
|
+
const infoCache = new Map();
|
|
45
58
|
export function readOrchestratorInfo(root) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
const file = orchestratorStatePath(root);
|
|
60
|
+
return (cachedByStat(infoCache, file, file, () => {
|
|
61
|
+
const info = readJsonFile(file);
|
|
62
|
+
if (info === null)
|
|
63
|
+
return null;
|
|
64
|
+
normalizeInfoFields(info);
|
|
65
|
+
return info;
|
|
66
|
+
}, (info) => ({ ...info })) ?? null);
|
|
51
67
|
}
|
|
52
68
|
/** Number type-guard over the shared `finiteNumber` read (json-object.ts): that helper is the
|
|
53
69
|
* one home of the "the field is the number or it is nothing" rule, narrowed here so callers
|
|
@@ -82,11 +98,12 @@ function parseBuild(v) {
|
|
|
82
98
|
build.restartPending = v.restartPending;
|
|
83
99
|
if (typeof v.restartBlocked === "string")
|
|
84
100
|
build.restartBlocked = v.restartBlocked;
|
|
101
|
+
const waiting = v.waitingForCheck;
|
|
102
|
+
if (isJsonObject(waiting) && typeof waiting.head === "string" && isFiniteNumber(waiting.aheadCommits))
|
|
103
|
+
build.waitingForCheck = { head: waiting.head, aheadCommits: waiting.aheadCommits };
|
|
85
104
|
return build;
|
|
86
105
|
}
|
|
87
|
-
/** The published permit snapshot, or undefined for any foreign shape.
|
|
88
|
-
* carry its two numbers; `limit` is kept only when it is a number, so a malformed provider
|
|
89
|
-
* map is dropped to `undefined` rather than reaching a render that reads it as a valid cap. */
|
|
106
|
+
/** The published permit snapshot, or undefined for any foreign shape. */
|
|
90
107
|
/** One entry of the published provider-holds list, or undefined for any foreign shape: a
|
|
91
108
|
* well-formed entry is an object with a finite `until` and a string `kind`; a `provider` that
|
|
92
109
|
* is present but not a string drops the entry, so a guarded list keeps its well-formed
|
|
@@ -106,18 +123,7 @@ function parseCheckPermits(v) {
|
|
|
106
123
|
function parsePermits(v) {
|
|
107
124
|
if (!isJsonObject(v) || !isFiniteNumber(v.inUse) || !isFiniteNumber(v.limit) || !isFiniteNumber(v.reserve))
|
|
108
125
|
return undefined;
|
|
109
|
-
|
|
110
|
-
return undefined;
|
|
111
|
-
const byKey = {};
|
|
112
|
-
for (const [key, value] of Object.entries(v.byKey)) {
|
|
113
|
-
if (!isJsonObject(value) || !isFiniteNumber(value.inUse) || !isFiniteNumber(value.waiting))
|
|
114
|
-
continue;
|
|
115
|
-
const entry = { inUse: value.inUse, waiting: value.waiting };
|
|
116
|
-
if (isFiniteNumber(value.limit))
|
|
117
|
-
entry.limit = value.limit;
|
|
118
|
-
byKey[key] = entry;
|
|
119
|
-
}
|
|
120
|
-
return { inUse: v.inUse, limit: v.limit, reserve: v.reserve, byKey };
|
|
126
|
+
return { inUse: v.inUse, limit: v.limit, reserve: v.reserve };
|
|
121
127
|
}
|
|
122
128
|
function parseSlotPool(v) {
|
|
123
129
|
if (!isJsonObject(v) || !isFiniteNumber(v.leased) || !isFiniteNumber(v.limit) || !isFiniteNumber(v.waiting))
|
|
@@ -143,14 +149,9 @@ function parseOrDelete(rec, key, parse) {
|
|
|
143
149
|
else
|
|
144
150
|
rec[key] = parsed;
|
|
145
151
|
}
|
|
146
|
-
/**
|
|
147
|
-
*
|
|
148
|
-
function
|
|
149
|
-
parseOrDelete(rec, "build", parseBuild);
|
|
150
|
-
parseOrDelete(rec, "fallbackDemoted", parseDemotion);
|
|
151
|
-
parseOrDelete(rec, "permits", parsePermits);
|
|
152
|
-
parseOrDelete(rec, "checkPermits", parseCheckPermits);
|
|
153
|
-
parseOrDelete(rec, "slotPool", parseSlotPool);
|
|
152
|
+
/** Drop a non-object `fallbackDemotions` or each entry that does not parse, keeping
|
|
153
|
+
* well-formed entries unchanged. */
|
|
154
|
+
function normalizeFallbackDemotions(rec) {
|
|
154
155
|
const demotions = rec.fallbackDemotions;
|
|
155
156
|
if (!isJsonObject(demotions))
|
|
156
157
|
delete rec.fallbackDemotions;
|
|
@@ -163,13 +164,9 @@ function normalizeInfoFields(rec) {
|
|
|
163
164
|
demotions[key] = parsed;
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (rec.chain !== undefined && !isJsonObject(rec.chain))
|
|
170
|
-
delete rec.chain;
|
|
171
|
-
if (rec.stopsAt !== undefined && !isFiniteNumber(rec.stopsAt))
|
|
172
|
-
delete rec.stopsAt;
|
|
167
|
+
}
|
|
168
|
+
/** Drop a non-array `providerHolds` or keep each well-formed entry, dropping foreign ones. */
|
|
169
|
+
function normalizeProviderHolds(rec) {
|
|
173
170
|
const providerHolds = rec.providerHolds;
|
|
174
171
|
if (!Array.isArray(providerHolds))
|
|
175
172
|
delete rec.providerHolds;
|
|
@@ -182,6 +179,9 @@ function normalizeInfoFields(rec) {
|
|
|
182
179
|
}
|
|
183
180
|
rec.providerHolds = kept;
|
|
184
181
|
}
|
|
182
|
+
}
|
|
183
|
+
/** Drop a `disk` that lacks finite numbers or a boolean `held`, or an invalid `lastReclaim`. */
|
|
184
|
+
function normalizeDisk(rec) {
|
|
185
185
|
const disk = rec.disk;
|
|
186
186
|
if (!isJsonObject(disk) ||
|
|
187
187
|
!isFiniteNumber(disk.freeGB) ||
|
|
@@ -194,6 +194,25 @@ function normalizeInfoFields(rec) {
|
|
|
194
194
|
delete disk.lastReclaim;
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
+
/** Degrade each known nested field to a usable shape or drop it, mutating the freshly parsed
|
|
198
|
+
* object in place so a well-formed file keeps its values unchanged. */
|
|
199
|
+
function normalizeInfoFields(rec) {
|
|
200
|
+
parseOrDelete(rec, "build", parseBuild);
|
|
201
|
+
parseOrDelete(rec, "fallbackDemoted", parseDemotion);
|
|
202
|
+
parseOrDelete(rec, "permits", parsePermits);
|
|
203
|
+
parseOrDelete(rec, "checkPermits", parseCheckPermits);
|
|
204
|
+
parseOrDelete(rec, "slotPool", parseSlotPool);
|
|
205
|
+
normalizeFallbackDemotions(rec);
|
|
206
|
+
const budget = rec.budget;
|
|
207
|
+
if (!isJsonObject(budget) || !isFiniteNumber(budget.spentUsd))
|
|
208
|
+
delete rec.budget;
|
|
209
|
+
if (rec.chain !== undefined && !isJsonObject(rec.chain))
|
|
210
|
+
delete rec.chain;
|
|
211
|
+
if (rec.stopsAt !== undefined && !isFiniteNumber(rec.stopsAt))
|
|
212
|
+
delete rec.stopsAt;
|
|
213
|
+
normalizeProviderHolds(rec);
|
|
214
|
+
normalizeDisk(rec);
|
|
215
|
+
}
|
|
197
216
|
/** Publish the running orchestrator's info file atomically (writeJsonAtomic's tmp + rename).
|
|
198
217
|
* The only writer of that path, so a process killed mid-write — the orchestrator's own poll
|
|
199
218
|
* publishes budget/disk/build changes while `tumwater start` in another terminal reads it to
|