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,9 @@
|
|
|
1
|
-
import { errorStorm, errorStormKnob } from "./error-storm.js";
|
|
1
|
+
import { errorStorm, errorStormKnob } from "./team-error-storm.js";
|
|
2
2
|
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
3
3
|
import { teamHold, TEAM_OPEN } from "./team-hold.js";
|
|
4
|
-
import { claudeWindowUsedUp, windowReserved } from "../agent/claude/claude-account.js";
|
|
5
4
|
import { sortedRoles } from "../failure/failure-cluster.js";
|
|
6
|
-
import { FAILURE_SPREAD_WINDOW_MS, failureSpread } from "./failure-spread.js";
|
|
5
|
+
import { FAILURE_SPREAD_WINDOW_MS, failureSpread } from "./team-failure-spread.js";
|
|
7
6
|
import { groupBy } from "../collections.js";
|
|
8
|
-
import { eventsSince } from "../events/event-window.js";
|
|
9
|
-
import { DAY_MS } from "../text/datetime.js";
|
|
10
7
|
/** The HoldObservation list both team-wide failure polls step their reducer with, gathered
|
|
11
8
|
* from each runner's two episodic fields: lastRateLimit (stamped "rate-limit", carrying the
|
|
12
9
|
* server's retry-after when present) and lastBackendFailure. One copy of that flatMap so the
|
|
@@ -37,15 +34,14 @@ function holdObservations(runners) {
|
|
|
37
34
|
* heldProviders() (until non-null), never bare key presence: a lifted hold must never
|
|
38
35
|
* keep blocking. Events carry the provider when one is configured (an undefined provider
|
|
39
36
|
* — pi's default — omits it, so an unconfigured team's events render exactly as before).
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const steps = [...holdObservations(runners), ...observations];
|
|
37
|
+
* Each provider's hold observations come only from its runners (holdObservations): the
|
|
38
|
+
* per-backend subscription-account observations and their subscription-window crossing
|
|
39
|
+
* events were removed with their last reader (Subtract parts 5u/6, 5w/6 and 5x/6), so the
|
|
40
|
+
* hold poll logs only the rate-limit/backend-failure hold events. Returns the new map for
|
|
41
|
+
* the caller to keep.
|
|
42
|
+
* Exported as a unit-test seam, like its sibling polls below. */
|
|
43
|
+
export function pollTeamHold(root, prev, runners, now) {
|
|
44
|
+
const steps = holdObservations(runners);
|
|
49
45
|
const byProvider = groupBy(steps, (o) => o.provider);
|
|
50
46
|
const next = new Map();
|
|
51
47
|
// Every provider the map already knows, plus every provider this poll observed: a hold
|
|
@@ -55,195 +51,51 @@ export function pollTeamHold(root, prev, runners, now, observations = [], window
|
|
|
55
51
|
const pPrev = prev.get(provider) ?? { ...TEAM_OPEN, provider };
|
|
56
52
|
const pNext = teamHold(pPrev, byProvider.get(provider) ?? [], now);
|
|
57
53
|
next.set(provider, pNext);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// (or would open) its hold, whose window type the limited event names. The events' backend
|
|
61
|
-
// is the provider itself (a subscription-window hold always carries a string provider — a
|
|
62
|
-
// rejected window or reserve crossing never keys the undefined-pi default) — the reset, in
|
|
63
|
-
// particular, fires after the window's observation has already gone (the account flipped to
|
|
64
|
-
// reset), so it cannot read its backend back from a still-present observation.
|
|
65
|
-
const subObs = byProvider.get(provider)?.find((o) => o.kind === "subscription-window");
|
|
66
|
-
const limitedEvent = () => {
|
|
67
|
-
// The subscription window's own crossing event, once per rejected window rather than
|
|
68
|
-
// once per failure storm. It names the window type and the deadline the hold lifts at.
|
|
69
|
-
logEventBestEffort(root, {
|
|
70
|
-
source: TUMWATER_SOURCE,
|
|
71
|
-
type: "subscription_window_limited",
|
|
72
|
-
backend: provider,
|
|
73
|
-
...providerFields,
|
|
74
|
-
windowType: subObs?.windowType,
|
|
75
|
-
resetsAt: pNext.until,
|
|
76
|
-
// Where each moved tier's work went (the budget poll's window move); absent when
|
|
77
|
-
// nothing moved, so a no-fallback team's event is today's.
|
|
78
|
-
...(Object.keys(windowTiers).length > 0 ? { tiers: windowTiers } : {}),
|
|
79
|
-
});
|
|
80
|
-
};
|
|
81
|
-
if (pPrev.until !== null && pPrev.kind === "subscription-window" && pPrev.reserve === true &&
|
|
82
|
-
pNext.until !== null && pNext.kind === "subscription-window" && pNext.reserve !== true) {
|
|
83
|
-
// A reserve hold hardened into a rejected window: the operator hears the limit now.
|
|
84
|
-
limitedEvent();
|
|
85
|
-
}
|
|
86
|
-
else if (pPrev.until === null && pNext.until !== null) {
|
|
87
|
-
if (pNext.kind === "subscription-window" && pNext.reserve === true) {
|
|
88
|
-
// A reserve crossing logs subscription_window_reserved (pollClaudeWindowReserve), not this.
|
|
89
|
-
}
|
|
90
|
-
else if (pNext.kind === "subscription-window") {
|
|
91
|
-
limitedEvent();
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
logEventBestEffort(root, {
|
|
95
|
-
source: TUMWATER_SOURCE,
|
|
96
|
-
type: "rate_limit_hold",
|
|
97
|
-
...providerFields,
|
|
98
|
-
kind: pNext.kind,
|
|
99
|
-
roles: pNext.roles,
|
|
100
|
-
holdMs: pNext.until - now,
|
|
101
|
-
escalation: pNext.escalation,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
54
|
+
if (pPrev.until === null && pNext.until !== null) {
|
|
55
|
+
logHoldStart(root, provider, pNext, now);
|
|
104
56
|
}
|
|
105
57
|
else if (pPrev.until !== null && pNext.until === null) {
|
|
106
|
-
|
|
107
|
-
logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "subscription_window_reset", backend: provider, ...providerFields });
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
// The ended hold's kind rides the resumed event: the hold's own event names its kind, so
|
|
111
|
-
// the lift must be able to name what actually ended too — "429 hold lifted" after a
|
|
112
|
-
// connection-error hold is the same lie the hold line's kind split removed
|
|
113
|
-
// (BUGS.md 2026-09-29).
|
|
114
|
-
logEventBestEffort(root, {
|
|
115
|
-
source: TUMWATER_SOURCE,
|
|
116
|
-
type: "rate_limit_resumed",
|
|
117
|
-
...providerFields,
|
|
118
|
-
kind: pNext.kind,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
58
|
+
logHoldEnd(root, provider, pNext);
|
|
121
59
|
}
|
|
122
60
|
}
|
|
123
61
|
return next;
|
|
124
62
|
}
|
|
125
|
-
/** The
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
provider: backend,
|
|
139
|
-
kind: "subscription-window",
|
|
140
|
-
at: now,
|
|
141
|
-
windowType: furthest.type,
|
|
142
|
-
resetsAt: furthest.resetsAt,
|
|
143
|
-
...(furthest.reserve === true ? { reserve: true } : {}),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
/** A claude usage window is open for at most `seven_day`, so any window still open was logged
|
|
147
|
-
* within this span of the event log. */
|
|
148
|
-
const CLAUDE_WINDOW_LOOKBACK_MS = 7 * DAY_MS;
|
|
149
|
-
/** The `backend:windowType:resetsAt` keys of the still-open windows whose `type` event is already
|
|
150
|
-
* in the event log, so a restart does not re-announce a window that is still open. Keyed by the
|
|
151
|
-
* event's own `backend` (falling back to `"claude"` for an event written before the window
|
|
152
|
-
* events carried one), so two backends' windows of the same type and reset instant keep their
|
|
153
|
-
* own keys. Events without a numeric `resetsAt` cannot name their window and are skipped. */
|
|
154
|
-
export function loggedWindowKeys(root, type, now) {
|
|
155
|
-
const keys = new Set();
|
|
156
|
-
for (const e of eventsSince(root, CLAUDE_WINDOW_LOOKBACK_MS, now, (ev) => ev.type === type)) {
|
|
157
|
-
if (typeof e.windowType === "string" && typeof e.resetsAt === "number" && e.resetsAt > now) {
|
|
158
|
-
keys.add(`${typeof e.backend === "string" ? e.backend : "claude"}:${e.windowType}:${e.resetsAt}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return keys;
|
|
162
|
-
}
|
|
163
|
-
/** Log each subscription-window event once per window key. The reserve and the warning polls
|
|
164
|
-
* (below) both walk every window of the account and log one event for each that crosses its mark
|
|
165
|
-
* this poll, deduplicated by the `backend:type:resetsAt` key so many polls across one window log
|
|
166
|
-
* once (a later window's crossing is its own key). They differ only in WHICH window marks (the
|
|
167
|
-
* reserve fraction vs the `allowed_warning` status) and the event's payload, so the traversal, the
|
|
168
|
-
* "reset instant already past" skip, the key build, the key dedup, and the returned-set contract
|
|
169
|
-
* live here and each caller supplies the mark-and-payload predicate (`eventFor` returns the event
|
|
170
|
-
* to log, or null to skip that window). The returned set drops keys whose reset instant has passed,
|
|
171
|
-
* and is `logged` itself when nothing was added or dropped. */
|
|
172
|
-
function logWindowEventOnce(root, logged, account, now, backend, eventFor) {
|
|
173
|
-
if (account?.billing !== "subscription")
|
|
174
|
-
return logged;
|
|
175
|
-
let next = null;
|
|
176
|
-
for (const key of logged) {
|
|
177
|
-
const suffix = key.slice(key.lastIndexOf(":") + 1);
|
|
178
|
-
if (suffix !== "" && Number(suffix) <= now)
|
|
179
|
-
(next ??= new Set(logged)).delete(key);
|
|
180
|
-
}
|
|
181
|
-
for (const [type, w] of Object.entries(account.windows)) {
|
|
182
|
-
const ev = eventFor(type, w);
|
|
183
|
-
if (ev === null)
|
|
184
|
-
continue;
|
|
185
|
-
// A window whose reset instant is in the past (or unreadable) no longer marks a crossing.
|
|
186
|
-
if (typeof w.resetsAt === "number" && w.resetsAt <= now)
|
|
187
|
-
continue;
|
|
188
|
-
// Keyed by this poll's backend (the `backend:windowType:resetsAt` shape loggedWindowKeys
|
|
189
|
-
// seeds), so a different backend's identical window keeps its own key. The backend rides
|
|
190
|
-
// the event, stamped here so each caller's event needs no backend of its own.
|
|
191
|
-
const key = `${backend}:${type}:${w.resetsAt ?? ""}`;
|
|
192
|
-
if ((next ?? logged).has(key))
|
|
193
|
-
continue;
|
|
194
|
-
logEventBestEffort(root, { ...ev, backend });
|
|
195
|
-
(next ??= new Set(logged)).add(key);
|
|
196
|
-
}
|
|
197
|
-
return next ?? logged;
|
|
198
|
-
}
|
|
199
|
-
/** One poll of the reserve crossing: each window at or past its windowReserve fraction (read
|
|
200
|
-
* through `windowReserveFor(config, "claude")`) logs `subscription_window_reserved` once per
|
|
201
|
-
* window key (`backend:type:resetsAt`, kept in `logged`), so several windows crossing in one
|
|
202
|
-
* poll each log their own event. Returns the keys to keep. */
|
|
203
|
-
export function pollClaudeWindowReserve(root, logged, account, now, backend, reserve) {
|
|
204
|
-
if (reserve === undefined)
|
|
205
|
-
return logged;
|
|
206
|
-
return logWindowEventOnce(root, logged, account, now, backend, (type, w) => {
|
|
207
|
-
const fraction = reserve[type];
|
|
208
|
-
if (!windowReserved(w, fraction) || typeof w.resetsAt !== "number")
|
|
209
|
-
return null;
|
|
210
|
-
return {
|
|
211
|
-
source: TUMWATER_SOURCE,
|
|
212
|
-
type: "subscription_window_reserved",
|
|
213
|
-
windowType: type,
|
|
214
|
-
utilization: w.utilization,
|
|
215
|
-
reserve: fraction,
|
|
216
|
-
resetsAt: w.resetsAt,
|
|
217
|
-
};
|
|
63
|
+
/** The quiet→held crossing event: a provider whose hold just opened logs one `rate_limit_hold`
|
|
64
|
+
* naming the kind, the roles, how long it holds (until the re-open), and the escalation depth.
|
|
65
|
+
* The provider is carried only when one is configured (an undefined provider — pi's default —
|
|
66
|
+
* omits it, so an unconfigured team's events render exactly as before). */
|
|
67
|
+
function logHoldStart(root, provider, pNext, now) {
|
|
68
|
+
logEventBestEffort(root, {
|
|
69
|
+
source: TUMWATER_SOURCE,
|
|
70
|
+
type: "rate_limit_hold",
|
|
71
|
+
...(provider === undefined ? {} : { provider }),
|
|
72
|
+
kind: pNext.kind,
|
|
73
|
+
roles: pNext.roles,
|
|
74
|
+
holdMs: pNext.until - now,
|
|
75
|
+
escalation: pNext.escalation,
|
|
218
76
|
});
|
|
219
77
|
}
|
|
220
|
-
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
source: TUMWATER_SOURCE,
|
|
231
|
-
type: "subscription_window_warning",
|
|
232
|
-
windowType: type,
|
|
233
|
-
...(typeof w.utilization === "number" ? { utilization: w.utilization } : {}),
|
|
234
|
-
...(typeof w.resetsAt === "number" ? { resetsAt: w.resetsAt } : {}),
|
|
235
|
-
};
|
|
78
|
+
/** The held→quiet lift event: a provider whose hold just re-opened logs one `rate_limit_resumed`.
|
|
79
|
+
* The ended hold's kind rides the resumed event: the hold's own event names its kind, so the
|
|
80
|
+
* lift must be able to name what actually ended too — "429 hold lifted" after a
|
|
81
|
+
* connection-error hold is the same lie the hold line's kind split removed (BUGS.md 2026-09-29). */
|
|
82
|
+
function logHoldEnd(root, provider, pNext) {
|
|
83
|
+
logEventBestEffort(root, {
|
|
84
|
+
source: TUMWATER_SOURCE,
|
|
85
|
+
type: "rate_limit_resumed",
|
|
86
|
+
...(provider === undefined ? {} : { provider }),
|
|
87
|
+
kind: pNext.kind,
|
|
236
88
|
});
|
|
237
89
|
}
|
|
238
|
-
/** One poll of the team-wide error-storm warning (src/team/error-storm.ts): gather each
|
|
239
|
-
* current error streak (RunnerState.consecutiveErrors/lastError — the same fields the
|
|
240
|
-
* "consecutive run failures" warning reads, the director's included, since its failures
|
|
241
|
-
* the team's evidence too), step the pure reducer, and log exactly one warning per episode,
|
|
90
|
+
/** One poll of the team-wide error-storm warning (src/team/team-error-storm.ts): gather each
|
|
91
|
+
* runner's current error streak (RunnerState.consecutiveErrors/lastError — the same fields the
|
|
92
|
+
* per-role "consecutive run failures" warning reads, the director's included, since its failures
|
|
93
|
+
* are the team's evidence too), step the pure reducer, and log exactly one warning per episode,
|
|
242
94
|
* like the per-role warning's once-per-streak-crossing shape: on the quiet→storm crossing,
|
|
243
|
-
* naming the roles, the shared normalized cause, and the config knob when the cause has one
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
95
|
+
* naming the roles, the shared normalized cause, and the config knob when the cause has one (a
|
|
96
|
+
* team timing out together means runTimeoutSeconds does not fit the serving model). Storms clear
|
|
97
|
+
* silently — the members' own recoveries already tell that story — and the reducer's `prev`
|
|
98
|
+
* pass-through keeps a held storm event-free. Returns the new storm for the
|
|
247
99
|
* caller to keep. Exported as a unit-test seam, like pollTeamHold above. */
|
|
248
100
|
export function pollErrorStorm(root, prev, runners) {
|
|
249
101
|
const observations = runners.map((r) => ({
|
|
@@ -267,15 +119,15 @@ export function pollErrorStorm(root, prev, runners) {
|
|
|
267
119
|
}
|
|
268
120
|
return next;
|
|
269
121
|
}
|
|
270
|
-
/** One poll of the team-wide wide-shallow storm alarm (src/team/failure-spread.ts): gather
|
|
271
|
-
* runner's latest provider-failure observations — the same two the team hold reads, the
|
|
272
|
-
* director's included, since its failures are the team's evidence too — step the pure
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
122
|
+
/** One poll of the team-wide wide-shallow storm alarm (src/team/team-failure-spread.ts): gather
|
|
123
|
+
* each runner's latest provider-failure observations — the same two the team hold reads, the
|
|
124
|
+
* director's included, since its failures are the team's evidence too — step the pure reducer,
|
|
125
|
+
* and log exactly one warning per episode: on the quiet→active crossing (and again when a
|
|
126
|
+
* DIFFERENT kind becomes the episode while one is sounding, the reducer's new-episode rule),
|
|
127
|
+
* naming the failure count, the kind, the window, and the roles behind it. The wiring keys its
|
|
128
|
+
* log off active/kind, not object identity — `recent` moves almost every poll, so unlike the
|
|
129
|
+
* hold's poll there is no prev pass-through to compare. Exported as a unit-test seam, like
|
|
130
|
+
* pollTeamHold above. */
|
|
279
131
|
export function pollFailureSpread(root, prev, runners, now) {
|
|
280
132
|
const observations = holdObservations(runners);
|
|
281
133
|
const next = failureSpread(prev, observations, now);
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* an interrupted run's uncommitted edits survive.
|
|
7
7
|
*
|
|
8
8
|
* The candidate list and the guard that keeps a clean off the primary checkout live here; the
|
|
9
|
-
* in-use half lives in src/git/worktree-use.ts and the hold half in src/gates/disk-
|
|
9
|
+
* in-use half lives in src/git/worktree-use.ts and the hold half in src/gates/disk-gates.ts. */
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { HOUR_MS } from "../text/datetime.js";
|
|
12
12
|
import { worktreesDir } from "../paths.js";
|
|
13
13
|
import { git, gitTry } from "../git/git-run.js";
|
|
14
14
|
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
15
15
|
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
16
|
-
import { BYTES_PER_GB, sampleFreeBytes } from "../gates/disk-
|
|
16
|
+
import { BYTES_PER_GB, sampleFreeBytes } from "../gates/disk-gates.js";
|
|
17
17
|
import { subdirectoryNames } from "../files/files.js";
|
|
18
18
|
import { claimForReclaim, isReclaimInProgress, isWorktreeInUse, readWorktreeUse, releaseReclaim, updateWorktreeUse, } from "../git/worktree-use.js";
|
|
19
19
|
/** Worktree basenames that pressure reclaim never touches: the self-hosting mirrors the
|
|
@@ -126,55 +126,85 @@ async function cleanIgnored(dir) {
|
|
|
126
126
|
const out = await git(dir, "clean", "-fdX");
|
|
127
127
|
return out.split("\n").some((line) => line.startsWith("Removing "));
|
|
128
128
|
}
|
|
129
|
-
/**
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
/** True when a pressure or manual pass should stop: the sampler reports free space at or
|
|
130
|
+
* above `reclaimGB`. A null sample (sampling failed) is not the floor, so it keeps reclaiming. */
|
|
131
|
+
function pressureFloorReached(free, reclaimGB) {
|
|
132
|
+
return free !== null && reclaimGB > 0 && free >= reclaimGB * BYTES_PER_GB;
|
|
133
|
+
}
|
|
134
|
+
/** Clean one candidate, treating a guard failure as a skip, not a reason to abort the pass. */
|
|
135
|
+
async function reclaimOneCandidate(root, candidate) {
|
|
136
|
+
try {
|
|
137
|
+
return await reclaimWorktree(root, candidate.dir);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// A guard failure (a directory that stopped being a linked worktree) is not a reason to
|
|
141
|
+
// abort the pass; the remaining candidates are still safe to clean.
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Walk the candidates once, collecting the ones actually cleaned. Idle mode keeps only the
|
|
146
|
+
* `isIdleExpired` candidates and never re-samples; pressure and manual modes re-sample before
|
|
147
|
+
* each and stop once the pressure floor is reached. */
|
|
148
|
+
async function collectReclaimed(root, candidates, mode, idleHours, reclaimGB, sample, startedAt) {
|
|
137
149
|
const cleaned = [];
|
|
138
150
|
for (const candidate of candidates) {
|
|
139
151
|
if (mode === "idle") {
|
|
140
|
-
if (!isIdleExpired(candidate,
|
|
152
|
+
if (!isIdleExpired(candidate, idleHours, startedAt))
|
|
141
153
|
continue;
|
|
142
154
|
}
|
|
143
|
-
else {
|
|
144
|
-
|
|
145
|
-
if (free !== null && opts.reclaimGB > 0 && free >= opts.reclaimGB * BYTES_PER_GB)
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
try {
|
|
149
|
-
if (await reclaimWorktree(root, candidate.dir))
|
|
150
|
-
cleaned.push(candidate.name);
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// A guard failure (a directory that stopped being a linked worktree) is not a reason to
|
|
154
|
-
// abort the pass; the remaining candidates are still safe to clean.
|
|
155
|
+
else if (pressureFloorReached(sample(root), reclaimGB)) {
|
|
156
|
+
break;
|
|
155
157
|
}
|
|
158
|
+
if (await reclaimOneCandidate(root, candidate))
|
|
159
|
+
cleaned.push(candidate.name);
|
|
156
160
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
return cleaned;
|
|
162
|
+
}
|
|
163
|
+
/** Freed space as the before/after free-space difference, clamped at zero: another writer can
|
|
164
|
+
* consume space during the pass, and a negative "freed" would render as nonsense in the event
|
|
165
|
+
* and the digest. A null sample keeps the zero fallback. */
|
|
166
|
+
function reclaimFreedGB(before, after) {
|
|
167
|
+
return before !== null && after !== null ? Math.max(0, (after - before) / BYTES_PER_GB) : 0;
|
|
168
|
+
}
|
|
169
|
+
/** The pass result: the cleaned worktrees, the clamped freed and current free figures, and the
|
|
170
|
+
* pass duration. `freeGB` falls back to whichever sample we have when the other is null. */
|
|
171
|
+
function buildReclaimResult(cleaned, before, after, startedAt) {
|
|
160
172
|
const freeBytes = after ?? before ?? 0;
|
|
161
|
-
|
|
173
|
+
return {
|
|
162
174
|
worktrees: cleaned,
|
|
163
|
-
|
|
164
|
-
// would render as nonsense in the event and the digest.
|
|
165
|
-
freedGB: before !== null && after !== null ? Math.max(0, (after - before) / BYTES_PER_GB) : 0,
|
|
175
|
+
freedGB: reclaimFreedGB(before, after),
|
|
166
176
|
freeGB: freeBytes / BYTES_PER_GB,
|
|
167
177
|
durationMs: Date.now() - startedAt,
|
|
168
178
|
};
|
|
179
|
+
}
|
|
180
|
+
/** Log exactly one `disk_reclaim` event with the result's figures. */
|
|
181
|
+
function logReclaim(root, mode, result) {
|
|
169
182
|
logEventBestEffort(root, {
|
|
170
183
|
source: TUMWATER_SOURCE,
|
|
171
184
|
type: "disk_reclaim",
|
|
172
185
|
mode,
|
|
173
|
-
worktrees:
|
|
186
|
+
worktrees: result.worktrees,
|
|
174
187
|
freedGB: result.freedGB,
|
|
175
188
|
freeGB: result.freeGB,
|
|
176
189
|
durationMs: result.durationMs,
|
|
177
190
|
});
|
|
191
|
+
}
|
|
192
|
+
/** Clean pressure candidates until free space reaches `reclaimGB`, re-sampling after each, and
|
|
193
|
+
* log exactly one `disk_reclaim` when anything was cleaned. `idle` mode ignores the threshold
|
|
194
|
+
* and walks every candidate once (part 3/4). Returns null when nothing was reclaimed. */
|
|
195
|
+
export async function reclaimPass(root, mode, opts) {
|
|
196
|
+
const sample = opts.sample ?? sampleFreeBytes;
|
|
197
|
+
const candidates = opts.candidates ?? reclaimCandidates(root);
|
|
198
|
+
const startedAt = Date.now();
|
|
199
|
+
const before = sample(root);
|
|
200
|
+
// With no candidates the pass resolves synchronously (no re-sampling or cleaning to await),
|
|
201
|
+
// so a no-live-team Ctrl+E flashes "nothing to reclaim" within the caller's same turn.
|
|
202
|
+
const cleaned = candidates.length === 0 ? [] : await collectReclaimed(root, candidates, mode, opts.idleHours ?? 0, opts.reclaimGB, sample, startedAt);
|
|
203
|
+
if (cleaned.length === 0)
|
|
204
|
+
return null;
|
|
205
|
+
const after = sample(root);
|
|
206
|
+
const result = buildReclaimResult(cleaned, before, after, startedAt);
|
|
207
|
+
logReclaim(root, mode, result);
|
|
178
208
|
return result;
|
|
179
209
|
}
|
|
180
210
|
/** Dry run: every reclaim candidate with its idle age and what `git clean -ndX` would remove,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
3
2
|
import { finiteNumber, stringList } from "../files/json-object.js";
|
|
4
|
-
import { removeQuiet } from "../files/files.js";
|
|
3
|
+
import { removeIfPresent, removeQuiet } from "../files/files.js";
|
|
5
4
|
import { withStateLock } from "../concurrency/lock.js";
|
|
6
5
|
import { collapseWhitespace, truncate } from "../text/text.js";
|
|
7
6
|
import { pausedPath, pausedRolesLockPath, pausedRolesPath, scheduledStopPath } from "../paths.js";
|
|
@@ -128,15 +127,11 @@ export function pauseTeam(root, untilMs, reason) {
|
|
|
128
127
|
return true;
|
|
129
128
|
}
|
|
130
129
|
/** Resume the team by removing its marker (a no-op if absent); returns whether a marker was
|
|
131
|
-
* there to lift, mirroring pauseTeam's changed-state contract. Uses
|
|
130
|
+
* there to lift, mirroring pauseTeam's changed-state contract. Uses removeIfPresent's
|
|
132
131
|
* never-throws delete: a marker vanishing between the check and the unlink is success, not a
|
|
133
132
|
* failure worth surfacing from a toggle. */
|
|
134
133
|
export function resumeTeam(root) {
|
|
135
|
-
|
|
136
|
-
if (!fs.existsSync(marker))
|
|
137
|
-
return false;
|
|
138
|
-
removeQuiet(marker);
|
|
139
|
-
return true;
|
|
134
|
+
return removeIfPresent(pausedPath(root));
|
|
140
135
|
}
|
|
141
136
|
/** Schedule a graceful team stop at `untilMs` (epoch ms): `tumwater stop --in`. Overwrites the
|
|
142
137
|
* marker, so the last write wins. */
|
|
@@ -156,11 +151,7 @@ export function scheduledStopAt(root) {
|
|
|
156
151
|
}
|
|
157
152
|
/** Drop the scheduled stop; returns whether a marker was there (resumeTeam's contract). */
|
|
158
153
|
export function cancelScheduledStop(root) {
|
|
159
|
-
|
|
160
|
-
if (!fs.existsSync(marker))
|
|
161
|
-
return false;
|
|
162
|
-
removeQuiet(marker);
|
|
163
|
-
return true;
|
|
154
|
+
return removeIfPresent(scheduledStopPath(root));
|
|
164
155
|
}
|
|
165
156
|
/** The roles the operator has individually paused (`tumwater pause --role <id>`): a role in
|
|
166
157
|
* this set starts no new runs — in-flight ones finish, every other role keeps running, the
|
|
@@ -98,7 +98,7 @@ export function daysBetween(fromKey, toKey) {
|
|
|
98
98
|
const [y = 0, m = 1, d = 1] = key.split("-").map(Number);
|
|
99
99
|
return Date.UTC(y, m - 1, d);
|
|
100
100
|
};
|
|
101
|
-
return Math.round((utc(toKey) - utc(fromKey)) /
|
|
101
|
+
return Math.round((utc(toKey) - utc(fromKey)) / DAY_MS);
|
|
102
102
|
}
|
|
103
103
|
/** The `MM-DD` half of a `YYYY-MM-DD` day key (dayKey/formatDate): the redundant year
|
|
104
104
|
* dropped for a line whose date lies inside the current window. Shared by the failure
|
|
@@ -168,15 +168,24 @@ export function humanSeconds(s) {
|
|
|
168
168
|
export function humanUntil(atMs, now) {
|
|
169
169
|
return humanSeconds(secondsUntil(atMs, now));
|
|
170
170
|
}
|
|
171
|
-
/**
|
|
172
|
-
* the past-direction
|
|
173
|
-
*
|
|
174
|
-
* merge-
|
|
175
|
-
*
|
|
171
|
+
/** Human duration since a past epoch-ms instant, as a bare span — "how long ago" without
|
|
172
|
+
* the ` ago` wording: the `humanUntil` past-direction twin, collapsing the four sites that
|
|
173
|
+
* spelled the two-step `humanSeconds(secondsSince(...))` composition out (merge-queue-render.ts's
|
|
174
|
+
* merge and stage elapseds and doctor-backlog.ts's stale-queue and inbox-prompt waits), so the
|
|
175
|
+
* s/m/h/d bucketing cannot drift per consumer. Lives here beside the other time phrasing. */
|
|
176
|
+
export function humanSince(sinceMs, now) {
|
|
177
|
+
return humanSeconds(secondsSince(sinceMs, now));
|
|
178
|
+
}
|
|
179
|
+
/** `humanSince`'s past-direction phrase with the ` ago` suffix: how long since an epoch-ms
|
|
180
|
+
* instant, as `NS ago` — the phrase five renderers each built as
|
|
181
|
+
* `{humanSeconds(secondsSince(...))} ago` (status-render.ts's ago against the real clock,
|
|
182
|
+
* role-render.ts's last-run and merge-queue-render.ts's enqueue ages, inbox.ts's queued-age
|
|
183
|
+
* suffix, and doctor-notify.ts's latest-failed-page age, which had inlined the clamp itself),
|
|
184
|
+
* so the clamp, the rounding,
|
|
176
185
|
* and the " ago" wording cannot drift per consumer. Lives here beside the other time
|
|
177
186
|
* phrasing so core modules (which may not import src/ui) share the same form. */
|
|
178
187
|
export function humanAgo(sinceMs, now) {
|
|
179
|
-
return `${
|
|
188
|
+
return `${humanSince(sinceMs, now)} ago`;
|
|
180
189
|
}
|
|
181
190
|
/** A compact age clamped at zero and floored to whole units — `12m`, `4h`, `6d` — the bare
|
|
182
191
|
* (no `ago`) parenthetical the `qa` coverage block and the `feedback` "previously filed" block
|
|
@@ -22,7 +22,7 @@ export function markdownTable(header, rows, align = []) {
|
|
|
22
22
|
* the `map((item, i) => `${i + 1}. ${item}`).join("\n")` step, shared by the review prompt's
|
|
23
23
|
* prior objections, its stage-fix findings, and the rejected/revision notes
|
|
24
24
|
* (prompt/gate-notes.ts), `tumwater questions`' open-question list
|
|
25
|
-
* (backlog/question-answer.ts) and `tumwater wake --list`'s wake lines
|
|
25
|
+
* (backlog/backlog-question-answer.ts) and `tumwater wake --list`'s wake lines
|
|
26
26
|
* (operator/operator-intent.ts `renderWakeList`), so the numbering shape cannot drift between
|
|
27
27
|
* those surfaces (the TUI queued-prompt rows and the merge queue rows number their own
|
|
28
28
|
* lines). `empty` is returned verbatim when `items` is empty
|
package/dist/src/text/phrases.js
CHANGED
|
@@ -25,8 +25,9 @@ export const PREFILL_REASON = "it rides into every run's prefill";
|
|
|
25
25
|
* drift across the four surfaces that show just that first reason: the review gate's returned
|
|
26
26
|
* detail (review.ts), the event feed's one-line reject (event-format.ts), the failure digest's
|
|
27
27
|
* rejection clusters (failure-data.ts), and the error a role's next run records (run-apply.ts).
|
|
28
|
-
* Sites that render the whole reason list, or intentionally omit the fallback
|
|
29
|
-
* first-reason example, review.ts's rejection event `reason`), keep
|
|
28
|
+
* Sites that render the whole reason list, or intentionally omit the fallback
|
|
29
|
+
* (failure-time-spend.ts's first-reason example, review.ts's rejection event `reason`), keep
|
|
30
|
+
* their own handling. */
|
|
30
31
|
export function firstReason(reasons) {
|
|
31
32
|
const first = reasons?.[0];
|
|
32
33
|
return typeof first === "string" ? first : NO_REASONS_GIVEN;
|
|
@@ -169,6 +170,15 @@ export function rolesPhrase(roles, fallback, max, format = String) {
|
|
|
169
170
|
list.length = Math.min(list.length, max);
|
|
170
171
|
return list.length > 0 ? list.join(", ") : fallback;
|
|
171
172
|
}
|
|
173
|
+
/** The trailing-day median-and-extreme clause every doctor timing row renders — `… median, …
|
|
174
|
+
* slowest|longest (N unit)` — the one home of that bucketing, shared by the `check` row's
|
|
175
|
+
* checkDurationPhrase ("passing merge checks … took … slowest") and the `merge queue` row's
|
|
176
|
+
* mergeWaitPhrase ("merges … waited … longest"), so the `shortSpanPhrase` median/extreme shape
|
|
177
|
+
* cannot drift between them. Each caller supplies its own verbed prefix ("took"/"waited", the
|
|
178
|
+
* one differing word) and the extreme's label and unit. */
|
|
179
|
+
export function trailingDayMedianPhrase(medianMs, extremeMs, extreme, n, unit) {
|
|
180
|
+
return `${shortSpanPhrase(medianMs)} median, ${shortSpanPhrase(extremeMs)} ${extreme} (${plural(n, unit)})`;
|
|
181
|
+
}
|
|
172
182
|
/** The `$<spent> of $<cap>` fragment every budget-transition event renders — the one home of that
|
|
173
183
|
* phrasing, shared by the event feed (event-format.ts) and the failure digest's Team state changes
|
|
174
184
|
* lines (src/failure/failure-state-change.ts), so a budget transition reads the same on both
|
|
@@ -241,6 +251,21 @@ function withoutLeadingCd(command) {
|
|
|
241
251
|
rest = rest.replace(LEADING_CD, "");
|
|
242
252
|
return rest.trim() ? rest : command;
|
|
243
253
|
}
|
|
254
|
+
/** The first present candidate string in a tool call's args, or "" when none is a string.
|
|
255
|
+
* The read order — path, file_path, command, cmd, pattern, url — is the order INV-AE4 pins. */
|
|
256
|
+
function toolCallCandidate(args) {
|
|
257
|
+
const candidate = args.path ?? args.file_path ?? args.command ?? args.cmd ?? args.pattern ?? args.url;
|
|
258
|
+
return typeof candidate === "string" ? candidate : "";
|
|
259
|
+
}
|
|
260
|
+
/** How one candidate reads as a detail: a path-like key reduces to its file name, a
|
|
261
|
+
* command-like key drops a leading `cd`, and any other key shows verbatim. */
|
|
262
|
+
function toolCallDetail(candidate, args) {
|
|
263
|
+
if (candidate === args.path || candidate === args.file_path)
|
|
264
|
+
return path.basename(candidate);
|
|
265
|
+
if (candidate === args.command || candidate === args.cmd)
|
|
266
|
+
return withoutLeadingCd(candidate);
|
|
267
|
+
return candidate;
|
|
268
|
+
}
|
|
244
269
|
/** One-line description of a tool call from its name and args — shared by live progress data
|
|
245
270
|
* collection (LiveProgress.lastTool), transcript rendering, and the harness's stalled-tool-call
|
|
246
271
|
* warning (src/pi/pi-stream.ts builds the label src/pi/pi-watchdogs.ts renders). Path-like keys
|
|
@@ -248,19 +273,8 @@ function withoutLeadingCd(command) {
|
|
|
248
273
|
* is dropped from a command, because the run's cwd is implied and the path would use up the
|
|
249
274
|
* 32-character budget. */
|
|
250
275
|
export function describeToolCall(toolName, args) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const candidate = args.path ?? args.file_path ?? args.command ?? args.cmd ?? args.pattern ?? args.url;
|
|
254
|
-
if (typeof candidate === "string") {
|
|
255
|
-
detail =
|
|
256
|
-
candidate === args.path || candidate === args.file_path
|
|
257
|
-
? path.basename(candidate)
|
|
258
|
-
: candidate === args.command || candidate === args.cmd
|
|
259
|
-
? withoutLeadingCd(candidate)
|
|
260
|
-
: candidate;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
detail = squash(detail, 32);
|
|
276
|
+
const raw = isJsonObject(args) ? toolCallDetail(toolCallCandidate(args), args) : "";
|
|
277
|
+
const detail = squash(raw, 32);
|
|
264
278
|
// An empty name (pi omits toolName on some start events) yields the bare detail — no
|
|
265
279
|
// leading space in front of it.
|
|
266
280
|
return detail ? (toolName ? `${toolName} ${detail}` : detail) : toolName;
|
package/dist/src/text/suggest.js
CHANGED
|
@@ -21,7 +21,7 @@ function editDistance(a, b) {
|
|
|
21
21
|
}
|
|
22
22
|
/** The candidate closest to a mistyped `input` by case-insensitive edit distance, or null
|
|
23
23
|
* when nothing is close enough to suggest: the "did you mean" behind typo'd command names
|
|
24
|
-
* (help.ts) and config keys (config-write.ts, config-commands.ts, config-validation's
|
|
24
|
+
* (help.ts) and config keys (config-write.ts, cli-config-commands.ts, config-validation's
|
|
25
25
|
* checkKnownKeys/checkKnownRoleId). Capped at `maxDistance`
|
|
26
26
|
* (two edits by default — a typo's distance, not a different word's), so only a near miss
|
|
27
27
|
* gets a hint and the suggestion can never fire as an auto-correction; the caller still
|