tumwater 0.4.0 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -40,17 +40,45 @@ export function newTeamGateStates(config) {
|
|
|
40
40
|
}
|
|
41
41
|
/** Poll every team-wide gate and alarm, advancing `states` in place. */
|
|
42
42
|
export function pollTeamGates(states, ctx) {
|
|
43
|
-
const { root,
|
|
43
|
+
const { root, now } = ctx;
|
|
44
44
|
// The per-backend subscription account lookup (part 2e/9): each backend's account is read on
|
|
45
45
|
// demand — the window hold/reserve/warning loop, the window-move and the handback reads each
|
|
46
46
|
// call accounts fresh — so a second subscription backend is judged from its own account, never
|
|
47
47
|
// from claude's.
|
|
48
48
|
const accounts = (backend) => readAccount(root, backend, now);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const budget = pollBudgetPhase(states, ctx, accounts);
|
|
50
|
+
const holds = pollHoldGates(states, ctx);
|
|
51
|
+
const quietDisk = pollQuietAndDiskGates(states, ctx);
|
|
52
|
+
pollTeamFailureAlarms(states, ctx, accounts, budget.windowTiers);
|
|
53
|
+
return {
|
|
54
|
+
gate: budget.gate,
|
|
55
|
+
roleConfig: budget.roleConfig,
|
|
56
|
+
directorConfig: budget.directorConfig,
|
|
57
|
+
userPaused: holds.userPaused,
|
|
58
|
+
pausedRoles: holds.pausedRoles,
|
|
59
|
+
capPaused: holds.capPaused,
|
|
60
|
+
maintenanceQuota: holds.maintenanceQuota,
|
|
61
|
+
bootstrapHeld: holds.bootstrapHeld,
|
|
62
|
+
budgetPausedRoles: budget.budgetPausedRoles,
|
|
63
|
+
servingResolved: budget.servingResolved,
|
|
64
|
+
priceResolved: budget.priceResolved,
|
|
65
|
+
budgetActive: budget.budgetActive,
|
|
66
|
+
windowMovedRoles: budget.windowMovedRoles,
|
|
67
|
+
roleQuietHeld: quietDisk.roleQuietHeld,
|
|
68
|
+
quietNow: quietDisk.quietNow,
|
|
69
|
+
diskHeld: quietDisk.diskHeld,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Daily cost budget gate (src/gates/budget-gates.ts owns the reads, the edge-triggered
|
|
73
|
+
// budget_* events, and the breaker re-key; the fallback config view it derives lives in
|
|
74
|
+
// src/budget/budget-fallback-view.ts): the orchestrator
|
|
75
|
+
// owns only the wiring — the demotion publish below and the per-runner assignment at
|
|
76
|
+
// the bottom of this block — and the per-tier budget pause (src/budget/budget-wiring.ts,
|
|
77
|
+
// part 5c/8): a role whose model tier resolved to no usable free pair while the cap is
|
|
78
|
+
// reached starts no new runs, and a role on a window-moved tier whose own backend owns
|
|
79
|
+
// the out window is moved off it.
|
|
80
|
+
function pollBudgetPhase(states, ctx, accounts) {
|
|
81
|
+
const { root, runners, liveConfig, modelsPath, info } = ctx;
|
|
54
82
|
const { gate, roleConfig, spentUsd, capUsd, onFallback, resumed: gateResumed, pairs: fallbackPairs, priceResolved, servingResolved, budgetActive, windowTiers, windowResumedBackends, directorConfig, chain, } = pollBudgetGate(states.budget, {
|
|
55
83
|
root,
|
|
56
84
|
states: runners.map((r) => r.state),
|
|
@@ -82,9 +110,40 @@ export function pollTeamGates(states, ctx) {
|
|
|
82
110
|
capUsd,
|
|
83
111
|
},
|
|
84
112
|
});
|
|
85
|
-
// The pause
|
|
86
|
-
//
|
|
87
|
-
//
|
|
113
|
+
// The per-tier budget pause (src/budget/budget-wiring.ts, part 5c/8): a role whose model
|
|
114
|
+
// tier resolved to no usable free pair while the cap is reached starts no new runs, and a
|
|
115
|
+
// role on a window-moved tier whose own backend owns the out window is moved off it — the
|
|
116
|
+
// stateless verdict recomputed every poll from the SAME resolution the gate's value came
|
|
117
|
+
// from, beside `capPaused` (the scheduling pass folds the sets; the director is exempt, as
|
|
118
|
+
// under every autonomous gate, and the demoted pair's probe pierces the set for its own
|
|
119
|
+
// tier's roles only — probeRoles in the orchestrator's scheduling pass).
|
|
120
|
+
const { budgetPausedRoles, windowMovedRoles } = budgetPausedSubjects({
|
|
121
|
+
runners,
|
|
122
|
+
liveConfig,
|
|
123
|
+
accounts,
|
|
124
|
+
budgetActive,
|
|
125
|
+
servingResolved,
|
|
126
|
+
windowTiers,
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
gate,
|
|
130
|
+
roleConfig,
|
|
131
|
+
directorConfig,
|
|
132
|
+
budgetPausedRoles,
|
|
133
|
+
windowMovedRoles,
|
|
134
|
+
servingResolved,
|
|
135
|
+
priceResolved,
|
|
136
|
+
budgetActive,
|
|
137
|
+
windowTiers,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// The pause gates (src/gates/pause-gates.ts owns the reads, the edge-triggered pause/resume
|
|
141
|
+
// events, and the cross-poll bookkeeping): the operator pause's marker and the per-role pause's
|
|
142
|
+
// set, both read fresh per cycle so a marker change takes effect on the next poll — then the
|
|
143
|
+
// error-streak circuit breaker, the per-role daily cost cap, the maintenance allowance and the
|
|
144
|
+
// new-project bootstrap, each a stateless verdict folded into this poll's hold views.
|
|
145
|
+
function pollHoldGates(states, ctx) {
|
|
146
|
+
const { root, runners, liveConfig, now } = ctx;
|
|
88
147
|
const { userPaused, pausedRoles } = pollPauseGates(root, states.pause);
|
|
89
148
|
// The error-streak circuit breaker (src/gates/streak-gate.ts): a role past ERROR_STREAK_BREAKER
|
|
90
149
|
// consecutive failed runs is paused through the same per-role marker the operator's
|
|
@@ -101,21 +160,6 @@ export function pollTeamGates(states, ctx) {
|
|
|
101
160
|
for (const role of streakPaused)
|
|
102
161
|
states.pause.prevPausedRoles.add(role);
|
|
103
162
|
const pausedRolesNow = streakPaused.length > 0 ? new Set([...pausedRoles, ...streakPaused]) : pausedRoles;
|
|
104
|
-
// The per-tier budget pause (src/budget/budget-wiring.ts, part 5c/8): a role whose model
|
|
105
|
-
// tier resolved to no usable free pair while the cap is reached starts no new runs, and a
|
|
106
|
-
// role on a window-moved tier whose own backend owns the out window is moved off it — the
|
|
107
|
-
// stateless verdict recomputed every poll from the SAME resolution the gate's value came
|
|
108
|
-
// from, beside `capPaused` (the scheduling pass folds the sets; the director is exempt, as
|
|
109
|
-
// under every autonomous gate, and the demoted pair's probe pierces the set for its own
|
|
110
|
-
// tier's roles only — probeRoles in the orchestrator's scheduling pass).
|
|
111
|
-
const { budgetPausedRoles, windowMovedRoles } = budgetPausedSubjects({
|
|
112
|
-
runners,
|
|
113
|
-
liveConfig,
|
|
114
|
-
accounts,
|
|
115
|
-
budgetActive,
|
|
116
|
-
servingResolved,
|
|
117
|
-
windowTiers,
|
|
118
|
-
});
|
|
119
163
|
// The per-role daily cost cap (src/gates/role-cap-gates.ts): a role whose local-day spend has
|
|
120
164
|
// reached its own maxDailyCostUsdPerRole entry starts no new runs — the stateless verdict
|
|
121
165
|
// recomputed every poll, no pause marker written (the marker is anonymous; a cap pause must
|
|
@@ -143,12 +187,16 @@ export function pollTeamGates(states, ctx) {
|
|
|
143
187
|
// pass folds the held set in beside capPaused (with the fresh-wake/queued-prompt override the
|
|
144
188
|
// need-based deferral honors).
|
|
145
189
|
const bootstrapHeld = pollBootstrapGate(root, states.bootstrap, runners, liveConfig, now);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
190
|
+
return { userPaused, pausedRoles: pausedRolesNow, capPaused, maintenanceQuota, bootstrapHeld };
|
|
191
|
+
}
|
|
192
|
+
// Per-role quiet hours (src/scheduling/quiet-hours.ts): a role whose own quietHoursPerRole window
|
|
193
|
+
// covers `now` starts no new runs — the team window's semantics scoped to one role, the
|
|
194
|
+
// director exempt like every autonomous gate. Stateless, recomputed per poll exactly like
|
|
195
|
+
// capPaused: no crossing events, no state — a live config edit applies on the next poll
|
|
196
|
+
// by construction. A role held by BOTH windows is held once: the scheduling pass folds
|
|
197
|
+
// both verdicts into one hold condition, nothing counts a role twice.
|
|
198
|
+
function pollQuietAndDiskGates(states, ctx) {
|
|
199
|
+
const { root, runners, liveConfig, now, info } = ctx;
|
|
152
200
|
const roleQuietHeld = new Set();
|
|
153
201
|
for (const r of runners) {
|
|
154
202
|
if (r.role === DIRECTOR_ROLE)
|
|
@@ -188,19 +236,20 @@ export function pollTeamGates(states, ctx) {
|
|
|
188
236
|
...(ctx.reclaim?.lastReclaim ? { lastReclaim: ctx.reclaim.lastReclaim } : {}),
|
|
189
237
|
};
|
|
190
238
|
publishInfoFields(root, info, [["disk", disk]]);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
239
|
+
return { roleQuietHeld, quietNow, diskHeld };
|
|
240
|
+
}
|
|
241
|
+
// Each runner as the two provider-failure polls read it (HoldInputs) is built ONCE here so
|
|
242
|
+
// the hold and the spread read the same list, like holdObservations on the reducer side.
|
|
243
|
+
// Keying on the effective provider is what lets a storm on the fallback pair form its own
|
|
244
|
+
// hold while the abandoned primary stays clear (PLANS.md "Model failure fallback, part 1/2").
|
|
245
|
+
//
|
|
246
|
+
// Each stamp (lastRateLimit/lastBackendFailure) carries the hold key of the backend that
|
|
247
|
+
// ACTUALLY failed — a reviewer run folded through the author's role stamps `claude`, not
|
|
248
|
+
// the pi author's provider — and the polls' observation mapper files each stamp under that
|
|
249
|
+
// key, falling back to runProvider here for a stamp without one (PLANS.md "Mixed claude +
|
|
250
|
+
// pi fleets, part 2/4"). The spreads below carry the stamps wholesale, provider included.
|
|
251
|
+
function pollTeamFailureAlarms(states, ctx, accounts, windowTiers) {
|
|
252
|
+
const { root, runners, liveConfig, now, info } = ctx;
|
|
204
253
|
const holdInputs = (rs) => rs.map((r) => ({
|
|
205
254
|
role: r.role,
|
|
206
255
|
provider: r.runProvider(now),
|
|
@@ -268,22 +317,4 @@ export function pollTeamGates(states, ctx) {
|
|
|
268
317
|
// so a degraded backend that fails the team widely and shallowly still names itself.
|
|
269
318
|
// Observational only, like the error storm: it gates nothing.
|
|
270
319
|
states.failureSpread = pollFailureSpread(root, states.failureSpread, holdInputs(runners), now);
|
|
271
|
-
return {
|
|
272
|
-
gate,
|
|
273
|
-
roleConfig,
|
|
274
|
-
directorConfig,
|
|
275
|
-
userPaused,
|
|
276
|
-
pausedRoles: pausedRolesNow,
|
|
277
|
-
capPaused,
|
|
278
|
-
maintenanceQuota,
|
|
279
|
-
bootstrapHeld,
|
|
280
|
-
budgetPausedRoles,
|
|
281
|
-
servingResolved,
|
|
282
|
-
priceResolved,
|
|
283
|
-
budgetActive,
|
|
284
|
-
windowMovedRoles,
|
|
285
|
-
roleQuietHeld,
|
|
286
|
-
quietNow,
|
|
287
|
-
diskHeld,
|
|
288
|
-
};
|
|
289
320
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* set instead, and `resume --role` has no marker to touch (none is needed: the lift is a
|
|
17
17
|
* config edit or midnight).
|
|
18
18
|
* - An unknown role id in the caps map is a config-validation error
|
|
19
|
-
* (src/config/config-validation.ts, the
|
|
19
|
+
* (src/config/config-validation-roles.ts, the per-role map checks): explicit misconfiguration
|
|
20
20
|
* never silently no-ops a cap.
|
|
21
21
|
*
|
|
22
22
|
* Shape, like every gate in the family (src/gates/gate-polls.ts): the pure verdict and the
|
|
@@ -78,42 +78,28 @@ opencodeSeed = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
// Seed the claude account state before the first run (plans/agent-backends.md part 6a):
|
|
80
80
|
// `claude auth status` decides subscription vs API key, and an existing file is left alone.
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
// and a logged-out claude would otherwise spawn a bounded probe on every poll. If the seed
|
|
84
|
-
// fails, the first claude run's observation fills the file in.
|
|
85
|
-
if (!claudeSeedAttempted.has(root) && backendsInUse(config).includes("claude")) {
|
|
86
|
-
claudeSeedAttempted.add(root);
|
|
87
|
-
seedClaudeAccount(root, config);
|
|
88
|
-
}
|
|
81
|
+
// If the seed fails, the first claude run's observation fills the file in.
|
|
82
|
+
seedOnce(root, config, "claude", claudeSeedAttempted, () => seedClaudeAccount(root, config));
|
|
89
83
|
// Seed the codex account state before the first codex run (plans/codex-backend.md §7):
|
|
90
84
|
// `codex login status` decides subscription vs API key, and an existing file is left alone.
|
|
91
|
-
// Gated on a codex seam like the claude seed above, and attempted once per root per process.
|
|
92
85
|
// API-key billing is refused (decision 5): Codex reports no cost, so per-token billing would
|
|
93
86
|
// spend money the cap cannot see. If the seed fails or reads something else, the first codex
|
|
94
87
|
// run's observation fills the file in.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
problem: "Codex is logged in with an API key; tumwater runs Codex on a ChatGPT subscription only — run `CODEX_HOME=<home> codex login --device-auth`",
|
|
101
|
-
};
|
|
102
|
-
}
|
|
88
|
+
const codexAccount = seedOnce(root, config, "codex", codexSeedAttempted, () => seedCodexAccount(root, config));
|
|
89
|
+
if (codexAccount?.billing === "api") {
|
|
90
|
+
return {
|
|
91
|
+
problem: "Codex is logged in with an API key; tumwater runs Codex on a ChatGPT subscription only — run `CODEX_HOME=<home> codex login --device-auth`",
|
|
92
|
+
};
|
|
103
93
|
}
|
|
104
94
|
// Seed the opencode models catalog (part 6b/7) before the first run: the poll surfaces price
|
|
105
95
|
// opencode seams from the cached `.tumwater/state/opencode-models.json`, and an absent cache
|
|
106
|
-
// reads every opencode seam NOT free (the safe direction for the budget gate).
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
...opencodeSeed,
|
|
114
|
-
warn: (message) => warnEventBestEffort(root, TUMWATER_SOURCE, message),
|
|
115
|
-
});
|
|
116
|
-
}
|
|
96
|
+
// reads every opencode seam NOT free (the safe direction for the budget gate). A failed fetch
|
|
97
|
+
// keeps the last good cache (or an empty one) and never blocks startup; a failed fetch or
|
|
98
|
+
// cache write logs one warning event.
|
|
99
|
+
await seedOnce(root, config, "opencode", opencodeSeedAttempted, () => seedOpencodeCatalog(root, config, {
|
|
100
|
+
...opencodeSeed,
|
|
101
|
+
warn: (message) => warnEventBestEffort(root, TUMWATER_SOURCE, message),
|
|
102
|
+
}));
|
|
117
103
|
const branch = await resolveMainBranch(root, config, branchArg);
|
|
118
104
|
if ("problem" in branch)
|
|
119
105
|
return branch;
|
|
@@ -159,6 +145,20 @@ export function newOpencodeCatalogRefresh(root, seed, warn) {
|
|
|
159
145
|
},
|
|
160
146
|
};
|
|
161
147
|
}
|
|
148
|
+
/** Run a per-backend seed at most once per root per process, and only when that backend is in
|
|
149
|
+
* use — the guard the claude-account, codex-account and opencode-catalog seeds share
|
|
150
|
+
* (runStartupCheck is also the redeploy/supervisor poll's ask runStartupProblem, and a seed
|
|
151
|
+
* would otherwise spawn a bounded probe on every poll). Returns the seed body's value when it
|
|
152
|
+
* ran, undefined when the backend is unused or was already attempted this process; each caller
|
|
153
|
+
* decides what to do with the value (refuse on a codex API-key billing, ignore for claude and
|
|
154
|
+
* opencode). */
|
|
155
|
+
function seedOnce(root, config, backend, attempted, seed) {
|
|
156
|
+
if (!attempted.has(root) && backendsInUse(config).includes(backend)) {
|
|
157
|
+
attempted.add(root);
|
|
158
|
+
return seed();
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
162
|
/** Roots this process has already attempted a claude-account seed for (see runStartupCheck),
|
|
163
163
|
* so the supervisor's and redeploy's repeated startup checks do not respawn `claude auth
|
|
164
164
|
* status` on every poll when the probe fails. */
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* ERROR_STREAK_BREAKER failures.
|
|
30
30
|
*
|
|
31
31
|
* Accepted behavior, stated here and not fixed in this change: the dashboards' "failing run after
|
|
32
|
-
* run" alert (src/ui/team-
|
|
32
|
+
* run" alert (src/ui/team-alert-status.ts) reads consecutiveErrors regardless of pause, so it keeps
|
|
33
33
|
* listing a breaker-paused role until the streak clears; the role cell's paused badge explains why
|
|
34
34
|
* it is not running, and the role_streak_paused event tells the operator why. */
|
|
35
35
|
import { ERROR_STREAK_BREAKER } from "../run/run-apply.js";
|
package/dist/src/git/git.js
CHANGED
|
@@ -463,7 +463,7 @@ export async function commitMessage(cwd, sha) {
|
|
|
463
463
|
/** The subject (first line) of one commit, trimmed, or "" when it cannot be read — the
|
|
464
464
|
* non-throwing sibling of `commitMessage` and the one home of the `log -1 --format=%s`
|
|
465
465
|
* read, so the path that names a commit in a message (the post-merge reviewer naming the
|
|
466
|
-
* fix it landed, src/merge/merge-post-review.ts) cannot drift on its git spelling
|
|
466
|
+
* fix it landed, src/merge/merge-post-review-drain.ts) cannot drift on its git spelling
|
|
467
467
|
* or its trim. Its call is through gitTry, so an unreadable repo yields a "" subject, never a
|
|
468
468
|
* throw; it treats that as an honest degradation, not an error. (Distinct from
|
|
469
469
|
* commit-message.ts's commitSubject, which parses a subject out of a message string instead of
|
|
@@ -24,18 +24,18 @@ import fs from "node:fs";
|
|
|
24
24
|
import os from "node:os";
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
27
|
-
import { writeTextAtomic } from "../files/files.js";
|
|
27
|
+
import { ensureDir, writeTextAtomic } from "../files/files.js";
|
|
28
28
|
import { conflictedFiles } from "../merge/merge-git.js";
|
|
29
29
|
import { salvageDir, salvagePendingDir } from "../paths.js";
|
|
30
30
|
import { errorMessage } from "../text/text.js";
|
|
31
|
-
import { pad2 } from "../text/datetime.js";
|
|
31
|
+
import { formatDate, pad2 } from "../text/datetime.js";
|
|
32
|
+
import { plural } from "../text/phrases.js";
|
|
32
33
|
import { git, gitCouldNotStart, gitTry, runGit } from "./git-run.js";
|
|
33
34
|
import { isUsableWorktree } from "./worktree.js";
|
|
34
35
|
/** The patch file's name: a sortable local timestamp, the slot's directory name and the role that
|
|
35
36
|
* last held it, so a listing reads in order and names whose work each file is. */
|
|
36
37
|
function salvageFileName(now, slotDir, leftBy) {
|
|
37
|
-
const stamp = `${now.
|
|
38
|
-
`-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
38
|
+
const stamp = `${formatDate(now)}-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
39
39
|
return `${stamp}-${path.basename(slotDir)}-${leftBy ?? "unknown"}.patch`;
|
|
40
40
|
}
|
|
41
41
|
/** Save the uncommitted changes in `dir` (modified, deleted and untracked files, ignored ones
|
|
@@ -76,24 +76,24 @@ export async function salvageSlotLeftovers(root, dir, { leftBy, recoverFor, forR
|
|
|
76
76
|
const base = await git(dir, "rev-parse", "HEAD");
|
|
77
77
|
const home = recoverFor === null ? salvageDir(root) : salvagePendingDir(root, recoverFor);
|
|
78
78
|
const file = path.join(home, salvageFileName(now, dir, leftBy));
|
|
79
|
-
|
|
79
|
+
ensureDir(home);
|
|
80
80
|
// Atomic: the patch is the only copy left once the lease resets the workspace, so a kill
|
|
81
81
|
// mid-write must never leave a truncated .patch that a later restore reads and partially
|
|
82
82
|
// applies. Written to a temp name and renamed, a reader (restoreSalvagedWork) sees either
|
|
83
83
|
// no file or the complete patch, never a prefix.
|
|
84
|
-
writeTextAtomic(file, `# ${files
|
|
84
|
+
writeTextAtomic(file, `# ${plural(files, "uncommitted file")} ${by} left in workspace ${slot}, saved before ${forRole} reused it\n` +
|
|
85
85
|
`# base ${base}; apply with: git apply --index <this file> on a checkout of that commit\n` +
|
|
86
86
|
`${patch}\n`);
|
|
87
87
|
const saved = path.relative(root, file);
|
|
88
88
|
warnEventBestEffort(root, TUMWATER_SOURCE, recoverFor === null
|
|
89
|
-
? `workspace ${slot} still held ${files
|
|
90
|
-
: `workspace ${slot} still held ${files
|
|
89
|
+
? `workspace ${slot} still held ${plural(files, "uncommitted file")} ${by} left behind; saved them to ${saved} and reset it for ${forRole}`
|
|
90
|
+
: `workspace ${slot} still held ${plural(files, "uncommitted file")} from ${recoverFor}'s unfinished run; saved them to ${saved} for its next run and reset it for ${forRole}`);
|
|
91
91
|
return file;
|
|
92
92
|
}
|
|
93
93
|
catch (err) {
|
|
94
94
|
if (gitCouldNotStart(err))
|
|
95
95
|
throw err;
|
|
96
|
-
warnEventBestEffort(root, TUMWATER_SOURCE, `workspace ${slot} still held ${files
|
|
96
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `workspace ${slot} still held ${plural(files, "uncommitted file")} ${by} left behind, and saving them failed (${errorMessage(err)}); resetting it for ${forRole} discards them`);
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
99
|
finally {
|
|
@@ -72,14 +72,23 @@ function readSlotRecord(slot) {
|
|
|
72
72
|
...slot,
|
|
73
73
|
dir: slot.dir,
|
|
74
74
|
lease: isSlotLease(slot.lease) ? readSlotLease(slot.lease) : null,
|
|
75
|
-
pinnedFor:
|
|
75
|
+
pinnedFor: roleIdOrNull(slot.pinnedFor),
|
|
76
76
|
// Absent stays absent (an older writer's shape is preserved); present-but-foreign is null.
|
|
77
77
|
pinnedAt: slot.pinnedAt === undefined ? undefined : finiteNumber(slot.pinnedAt, null),
|
|
78
|
-
lastRole:
|
|
78
|
+
lastRole: roleIdOrNull(slot.lastRole),
|
|
79
79
|
lastReleasedAt: finiteNumber(slot.lastReleasedAt, null),
|
|
80
|
-
unsettledRun:
|
|
80
|
+
unsettledRun: roleIdOrNull(slot.unsettledRun),
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
+
/** A string role id read from a persisted slot record, canonicalized, or null when absent
|
|
84
|
+
* or foreign. The three canonical-id fields of SlotRecord (pinnedFor, lastRole,
|
|
85
|
+
* unsettledRun) share this one read, so they cannot drift on how a non-string or legacy id
|
|
86
|
+
* reads: a non-string rides along as null (a hand edit or older writer that dropped the
|
|
87
|
+
* field must not name a role), and a legacy id resolves through canonicalRoleId like every
|
|
88
|
+
* reader here. */
|
|
89
|
+
function roleIdOrNull(value) {
|
|
90
|
+
return typeof value === "string" ? canonicalRoleId(value) : null;
|
|
91
|
+
}
|
|
83
92
|
function writeSlotsStateUnlocked(root, state) {
|
|
84
93
|
writeJsonAtomic(slotsStatePath(root), state);
|
|
85
94
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** The worktree pool's one-time startup layout migration (plans/worktree-pool.md, parts 4c/5):
|
|
2
|
+
* move the pre-pool `.tumwater/worktrees/<role>` checkouts onto the pooled `_slot-<n>` layout, and
|
|
3
|
+
* pin the slots a previous process died in. Split out of worktree-pool.ts so that module — the
|
|
4
|
+
* runtime lease machinery every run and verify calls — does not also carry the passes the
|
|
5
|
+
* orchestrator runs exactly once on a team's start. These two passes read and rewrite the same
|
|
6
|
+
* persisted slots.json (updateSlotsState) and run after every RoleRunner is constructed but
|
|
7
|
+
* before any run can lease; they are exported only for orchestrator.ts and the worktree-pool
|
|
8
|
+
* tests. */
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { worktreesDir, isCanonicalSlotDir } from "../paths.js";
|
|
11
|
+
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
12
|
+
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
13
|
+
import { updateSlotsState } from "./slots-state.js";
|
|
14
|
+
import { removeWorktreeDir } from "./worktree.js";
|
|
15
|
+
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
16
|
+
import { errorMessage } from "../text/text.js";
|
|
17
|
+
import { subdirectoryNames } from "../files/files.js";
|
|
18
|
+
/** One-time startup migration (plans/worktree-pool.md "Legacy role worktrees", part 4c/5):
|
|
19
|
+
* retire the pre-pool `.tumwater/worktrees/<role>` checkouts now that runs lease pooled slots.
|
|
20
|
+
* A resumable enabled role keeps its legacy dir as a pinned slot so its pending resume runs in
|
|
21
|
+
* the same cwd; every other legacy dir is removed (its branch keeps any commit), and pins for
|
|
22
|
+
* roles outside `configured` are cleared. `configured` is every role id the config's roles
|
|
23
|
+
* enable (`runnerIds`), never a `--once --role` filter: a scoped run must leave the other roles'
|
|
24
|
+
* legacy dirs and pins alone, or it silently loses their pending resumes. It runs after
|
|
25
|
+
* `runOrchestrator` constructs every
|
|
26
|
+
* `RoleRunner` but before any run can call `leaseSlot`: the constructor infers `resumePending`
|
|
27
|
+
* from the on-disk `running` flag only in memory (it does not save it), so `loadRunnerState` here
|
|
28
|
+
* still reports the on-disk flags. *Removing* a dir is best-effort and logged: the slot records
|
|
29
|
+
* are already updated, so a leaked directory is only a leak the next prune collects. */
|
|
30
|
+
export async function retireLegacyRoleWorktrees(root, configured) {
|
|
31
|
+
const dir = worktreesDir(root);
|
|
32
|
+
// A missing worktrees dir (a team that has not started) yields no names to migrate; the pin
|
|
33
|
+
// retirement pass below still runs over whatever slots.json holds.
|
|
34
|
+
const names = subdirectoryNames(dir, (name) => name !== DIRECTOR_ROLE && !name.startsWith("_"));
|
|
35
|
+
const configuredSet = new Set(configured);
|
|
36
|
+
const toRemove = new Set();
|
|
37
|
+
updateSlotsState(root, (state) => {
|
|
38
|
+
for (const role of names) {
|
|
39
|
+
const legacyDir = path.join(dir, role);
|
|
40
|
+
const legacy = loadRunnerState(root, role);
|
|
41
|
+
const resumable = legacy.resumePending === true || legacy.running === true;
|
|
42
|
+
const ownRecord = state.slots.find((slot) => slot.dir === legacyDir);
|
|
43
|
+
if (resumable && configuredSet.has(role)) {
|
|
44
|
+
const owned = state.slots.find((slot) => slot.pinnedFor === role ||
|
|
45
|
+
(slot.lease?.role === role && slot.lease.purpose === "run"));
|
|
46
|
+
if (owned !== undefined) {
|
|
47
|
+
// The resume happens in the owned slot: drop the legacy record and remove its dir.
|
|
48
|
+
if (owned.dir !== legacyDir) {
|
|
49
|
+
state.slots = state.slots.filter((slot) => slot.dir !== legacyDir);
|
|
50
|
+
toRemove.add(legacyDir);
|
|
51
|
+
}
|
|
52
|
+
if (owned.pinnedFor !== role)
|
|
53
|
+
owned.pinnedAt = Date.now();
|
|
54
|
+
owned.pinnedFor = role;
|
|
55
|
+
}
|
|
56
|
+
else if (ownRecord !== undefined) {
|
|
57
|
+
// Already migrated (a restart before the resume ran): keep it pinned and inert,
|
|
58
|
+
// preserving the pin's original age for the doctor's stuck-pin warning.
|
|
59
|
+
ownRecord.lease = null;
|
|
60
|
+
if (ownRecord.pinnedFor !== role)
|
|
61
|
+
ownRecord.pinnedAt = Date.now();
|
|
62
|
+
ownRecord.pinnedFor = role;
|
|
63
|
+
ownRecord.lastRole = null;
|
|
64
|
+
ownRecord.lastReleasedAt = null;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
state.slots.push({
|
|
68
|
+
dir: legacyDir,
|
|
69
|
+
lease: null,
|
|
70
|
+
pinnedFor: role,
|
|
71
|
+
pinnedAt: Date.now(),
|
|
72
|
+
lastRole: null,
|
|
73
|
+
lastReleasedAt: null,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
state.slots = state.slots.filter((slot) => slot.dir !== legacyDir);
|
|
79
|
+
toRemove.add(legacyDir);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// A paused or removed role must not leak a permanently pinned checkout.
|
|
83
|
+
for (const slot of [...state.slots]) {
|
|
84
|
+
if (slot.pinnedFor === null || configuredSet.has(slot.pinnedFor))
|
|
85
|
+
continue;
|
|
86
|
+
slot.pinnedFor = null;
|
|
87
|
+
slot.pinnedAt = null;
|
|
88
|
+
if (!isCanonicalSlotDir(slot.dir)) {
|
|
89
|
+
state.slots = state.slots.filter((s) => s !== slot);
|
|
90
|
+
toRemove.add(slot.dir);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
for (const victim of toRemove) {
|
|
95
|
+
try {
|
|
96
|
+
await removeWorktreeDir(root, victim);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `could not remove legacy role workspace ${victim}: ${errorMessage(err)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Startup pass for runs the previous process died in (crash, kill -9, power loss): such a run's
|
|
104
|
+
* lease is still in slots.json with the dead pid, and its RoleRunner reads the persisted
|
|
105
|
+
* `running` flag as a resume that must run in the same checkout, the cwd its session used. Pin
|
|
106
|
+
* each such slot for its role, as the graceful shutdown's abort pin would have, unless the role
|
|
107
|
+
* already holds a pin; the next claim then clears the dead lease (marking the run unsettled) and
|
|
108
|
+
* the resume leases the pinned slot with its edits in place. Only configured roles count, as in
|
|
109
|
+
* retireLegacyRoleWorktrees, so a removed role never leaks a pin. Like that pass, it runs before
|
|
110
|
+
* any run can lease, while loadRunnerState still reports the on-disk flags. */
|
|
111
|
+
export function pinCrashedRuns(root, configured) {
|
|
112
|
+
const configuredSet = new Set(configured);
|
|
113
|
+
updateSlotsState(root, (state) => {
|
|
114
|
+
for (const slot of state.slots) {
|
|
115
|
+
const lease = slot.lease;
|
|
116
|
+
if (lease === null || lease.pid === process.pid || lease.purpose !== "run")
|
|
117
|
+
continue;
|
|
118
|
+
if (!configuredSet.has(lease.role) || state.slots.some((s) => s.pinnedFor === lease.role))
|
|
119
|
+
continue;
|
|
120
|
+
const runner = loadRunnerState(root, lease.role);
|
|
121
|
+
if (runner.running !== true && runner.resumePending !== true)
|
|
122
|
+
continue;
|
|
123
|
+
slot.pinnedFor = lease.role;
|
|
124
|
+
slot.pinnedAt = Date.now();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|