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
|
@@ -7,7 +7,7 @@ import { errCode } from "../errno.js";
|
|
|
7
7
|
import { primaryWorktreeLockPath } from "../paths.js";
|
|
8
8
|
/** How old a lock dir must be before it is stale on age alone — regardless of whether its
|
|
9
9
|
* recorded pid still looks alive, so a reused pid cannot latch a dead holder as live. */
|
|
10
|
-
const STALE_MS = 10 *
|
|
10
|
+
const STALE_MS = 10 * 60_000;
|
|
11
11
|
/** A creator publishes its lock dir with the holder's pid already inside (atomic rename), so a
|
|
12
12
|
* visible lock always carries a readable pid. This grace remains for a pid-less dir an older
|
|
13
13
|
* build or an interrupted hand operation left behind: past it, the dir is an orphan and safe
|
|
@@ -93,6 +93,10 @@ function releaseOwnedLock(dir) {
|
|
|
93
93
|
return;
|
|
94
94
|
rmLockDir(dir);
|
|
95
95
|
}
|
|
96
|
+
/** The ledger-facing release primitive: the same releaseOwnedLock every mutex exits with, but
|
|
97
|
+
* exported so src/concurrency/check-slots.ts can hand a suite's slot back without re-implementing
|
|
98
|
+
* the owned-pid guard (a holder whose lock was stolen must not delete its successor's dir). */
|
|
99
|
+
export const releaseOwnedLockDir = releaseOwnedLock;
|
|
96
100
|
function acquireStep(dir, timeoutMs, waitedMs) {
|
|
97
101
|
// An existing path is a held lock to wait on (and maybe break); publishLockDir is only for
|
|
98
102
|
// an absent path, so a read-only parent that already holds a stale lock reports contention
|
|
@@ -149,6 +153,24 @@ function publishLockDir(dir) {
|
|
|
149
153
|
throw err;
|
|
150
154
|
}
|
|
151
155
|
}
|
|
156
|
+
/** The one-shot claim step of the lock protocol, exported for a caller that wants a single
|
|
157
|
+
* best-effort claim rather than withLock's retry loop: claim `dir` by atomic publish, returning
|
|
158
|
+
* true on success and false when `dir` already exists (a live holder) or contention — never
|
|
159
|
+
* throwing, unlike publishLockDir. The suite-slot ledger (check-slots.ts) calls it per index as
|
|
160
|
+
* it walks a ledger, so it must not throw on a held slot to inspect the others. A genuine
|
|
161
|
+
* filesystem failure (ENOSPC, a read-only parent) also returns false; the ledger degrades to
|
|
162
|
+
* "run unpermitted" on an unable-to-create ledger, which suits a best-effort scheduling cap. */
|
|
163
|
+
export function tryClaimLockDir(dir) {
|
|
164
|
+
try {
|
|
165
|
+
if (fs.existsSync(dir))
|
|
166
|
+
return false; // A held slot (live or stale) — the caller walks on.
|
|
167
|
+
publishLockDir(dir);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
152
174
|
/** Remove `.acquiring-*` temp dirs left beside a lock by a creator that crashed before its
|
|
153
175
|
* rename — the one remnant atomic publication can leave. Only dirs older than STALE_MS are
|
|
154
176
|
* touched, far beyond any scheduling stall, so a live creator's in-flight temp is never
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** The AsyncLocalStorage store that carries a run's lease across its work: a lease rides here
|
|
2
|
+
* for the length of a run, so an idle stretch inside it (a failed review's in-place retry
|
|
3
|
+
* pause, called via idleWithoutPermit) can hand the team permit back and re-take it, letting a
|
|
4
|
+
* parked run use the slot meanwhile. The check-permit.ts `holdingPermit` store is the pattern:
|
|
5
|
+
* nothing here is a separate permit, only the single lease the run path already holds. */
|
|
6
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
const runPermitStore = new AsyncLocalStorage();
|
|
8
|
+
/** Run `run` with `lease` in the run-permit store, so an idle wait inside it can pause and
|
|
9
|
+
* resume the same team permit. The same lease object is released by the caller after the store
|
|
10
|
+
* is unwrapped. */
|
|
11
|
+
export function withRunPermit(lease, run) {
|
|
12
|
+
return runPermitStore.run({ lease }, run);
|
|
13
|
+
}
|
|
14
|
+
/** Run `wait`, an idle stretch inside the calling lease's work, with the lease's team permit
|
|
15
|
+
* handed back (SemaphoreLease.pause), so a parked run can use it meanwhile, then take it back
|
|
16
|
+
* (resume) before returning. A failed review's in-place retry pause is the caller: with one
|
|
17
|
+
* team permit, a reviewer that kept failing held it through two 30 s pauses and starved every
|
|
18
|
+
* other role. Once `signal` has fired, nothing is taken back and the caller's release() settles
|
|
19
|
+
* a still-paused lease. No-op without a lease in context. */
|
|
20
|
+
export async function idleWithoutPermit(wait, signal) {
|
|
21
|
+
const ctx = runPermitStore.getStore();
|
|
22
|
+
if (!ctx)
|
|
23
|
+
return wait();
|
|
24
|
+
ctx.lease.pause();
|
|
25
|
+
try {
|
|
26
|
+
await wait();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
if (!signal?.aborted)
|
|
30
|
+
await untilAborted(ctx.lease.resume(), signal);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** `p`, or an earlier return once `signal` fires: a resume parked on a full team must not keep an
|
|
34
|
+
* aborted verification waiting for a permit it no longer needs. */
|
|
35
|
+
async function untilAborted(p, signal) {
|
|
36
|
+
if (!signal)
|
|
37
|
+
return p;
|
|
38
|
+
let onAbort = () => { };
|
|
39
|
+
const aborted = new Promise((resolve) => {
|
|
40
|
+
onAbort = resolve;
|
|
41
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
await Promise.race([p, aborted]);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
signal.removeEventListener("abort", onAbort);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,39 +1,19 @@
|
|
|
1
1
|
/** In-process counting semaphore bounding concurrent work (the orchestrator uses it to
|
|
2
2
|
* cap simultaneous pi runs). The cross-PROCESS mutex lives in lock.ts; this one only
|
|
3
|
-
* coordinates within a single harness process.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* and setKeyCaps touch the keyed path. */
|
|
9
|
-
/** The entries of a `maxConcurrentPerProvider` map the semaphore enforces: positive integers
|
|
10
|
-
* only, so a non-positive or non-integer cap is dropped rather than read as a limit. */
|
|
11
|
-
export function enforcedKeyCaps(caps) {
|
|
12
|
-
return Object.entries(caps ?? {}).filter(([, v]) => Number.isInteger(v) && v > 0);
|
|
13
|
-
}
|
|
3
|
+
* coordinates within a single harness process. A run is admitted by the one team total
|
|
4
|
+
* (`maxConcurrent`) alone — there is no per-provider cap any more (the cap, its surfaces
|
|
5
|
+
* and its keyed machinery were subtracted in parts 5b/6 and 5i/6); a lease is a team
|
|
6
|
+
* permit that can pause and resume so an idle wait (a failed review's in-place retry)
|
|
7
|
+
* hands the slot back and re-takes it before the next attempt. */
|
|
14
8
|
export class Semaphore {
|
|
15
9
|
capacity;
|
|
16
|
-
/** A parked WAITING (admission) waiter: its scheduling tier
|
|
17
|
-
*
|
|
18
|
-
* never held by a key cap. */
|
|
10
|
+
/** A parked WAITING (admission) waiter: its scheduling tier and the resolver that hands
|
|
11
|
+
* it the permit. */
|
|
19
12
|
waiters = [];
|
|
20
|
-
/** A parked re-key waiter: it already holds a team permit, so it waits only for its key's
|
|
21
|
-
* headroom, ahead of every admission waiter (which would otherwise queue a team permit uselessly
|
|
22
|
-
* behind another provider's runs). `granted` flips when drive() hands the waiter the target key's
|
|
23
|
-
* slot, so the lease's release can tell a delivered grant (give it back) from a still-parked one
|
|
24
|
-
* (cancel it) when the two interleave. */
|
|
25
|
-
rekeyWaiters = [];
|
|
26
13
|
/** Permits currently held, tracked separately from capacity (rather than as an "available"
|
|
27
14
|
* count): setCapacity can shrink below the current in-use, and a free-permit count cannot
|
|
28
15
|
* represent that state — it would over-admit new work after a shrink. */
|
|
29
16
|
inUse = 0;
|
|
30
|
-
/** The per-key permits currently held: a lease or forced lease charges its key here, and a
|
|
31
|
-
* key at its cap withholds further grants on it. Grows and shrinks only through release and
|
|
32
|
-
* rekey — never preempted. */
|
|
33
|
-
keyInUse = new Map();
|
|
34
|
-
/** The live per-key caps, from `maxConcurrentPerProvider` (plans/provider-concurrency.md
|
|
35
|
-
* §1). An absent key is uncapped — bounded by `capacity` alone. */
|
|
36
|
-
keyCaps = new Map();
|
|
37
17
|
/** Permits withheld from tier ≥ 1 acquirers so a work role that has work to take can claim
|
|
38
18
|
* one without waiting for a maintenance run to release (plans/parallel-work-instances.md
|
|
39
19
|
* "Priority headroom", part 7/7). Tiers ≤ 0 (work, VERIFY_TIER, MERGE_TIER) ignore it. */
|
|
@@ -45,13 +25,12 @@ export class Semaphore {
|
|
|
45
25
|
get limit() {
|
|
46
26
|
return this.capacity;
|
|
47
27
|
}
|
|
48
|
-
/** How many acquirers are parked for a permit right now
|
|
49
|
-
* With no keys there are never re-key waiters, so this is exactly today's count. */
|
|
28
|
+
/** How many acquirers are parked for a permit right now. */
|
|
50
29
|
get waiting() {
|
|
51
|
-
return this.waiters.length
|
|
30
|
+
return this.waiters.length;
|
|
52
31
|
}
|
|
53
32
|
/** Take a free permit without parking: true when one was granted (the same fast path
|
|
54
|
-
* acquire takes), false when acquire would have to wait.
|
|
33
|
+
* acquire takes), false when acquire would have to wait. */
|
|
55
34
|
tryAcquire() {
|
|
56
35
|
if (this.inUse >= this.capacity)
|
|
57
36
|
return false;
|
|
@@ -62,7 +41,7 @@ export class Semaphore {
|
|
|
62
41
|
* requests only: on arrival a waiter inserts ahead of every parked waiter with a strictly
|
|
63
42
|
* greater tier (the orchestrator passes roleTier so work roles beat maintenance across
|
|
64
43
|
* polls), keeping stable FIFO within a tier. In-flight holders are never preempted — they
|
|
65
|
-
* run to completion and their release is what hands out the permit.
|
|
44
|
+
* run to completion and their release is what hands out the permit. */
|
|
66
45
|
async acquire(tier) {
|
|
67
46
|
if (this.inUse < this.capacity - this.reserveFor(tier)) {
|
|
68
47
|
this.inUse += 1;
|
|
@@ -72,25 +51,16 @@ export class Semaphore {
|
|
|
72
51
|
this.enqueue({ tier, resolve });
|
|
73
52
|
});
|
|
74
53
|
}
|
|
75
|
-
/** Admit by
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
* and the key is under its cap (an absent or uncapped key always passes). FIFO holds within
|
|
79
|
-
* a key, and a full key never delays a waiter on another. */
|
|
80
|
-
async acquireLease(tier, key) {
|
|
81
|
-
let currentKey = key;
|
|
54
|
+
/** Admit by the team total alone and return a lease handle. `tier` orders the wait exactly
|
|
55
|
+
* as acquire does. FIFO holds within a tier. */
|
|
56
|
+
async acquireLease(tier) {
|
|
82
57
|
// True once release() has run: a released lease owns no slot and must not undo one again —
|
|
83
|
-
// a second release
|
|
84
|
-
//
|
|
85
|
-
//
|
|
58
|
+
// a second release must be a no-op, never a negative count. The orchestrator releases each
|
|
59
|
+
// lease exactly once, but a cancelled parked resume relies on this guard so a cancelled
|
|
60
|
+
// lease never re-decrements what it took.
|
|
86
61
|
let closed = false;
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// CANCEL the pending grant rather than undo a slot this lease never took. Null when no
|
|
90
|
-
// rekey is parked.
|
|
91
|
-
let parkedRekey = null;
|
|
92
|
-
// True while pause() has handed the team permit and the key's slot back: release() then owes
|
|
93
|
-
// nothing, and resume() re-admits at this lease's tier on its current key.
|
|
62
|
+
// True while pause() has handed the team permit back: release() then owes nothing, and
|
|
63
|
+
// resume() re-admits at this lease's tier in queue order.
|
|
94
64
|
let paused = false;
|
|
95
65
|
// resume()'s admission waiter while it is queued. release() cancels it if drive has not
|
|
96
66
|
// granted it yet, and gives the grant back if it has.
|
|
@@ -109,102 +79,46 @@ export class Semaphore {
|
|
|
109
79
|
w.resolve();
|
|
110
80
|
return;
|
|
111
81
|
}
|
|
112
|
-
// drive granted it this microtask: its permit
|
|
82
|
+
// drive granted it this microtask: its permit is taken, so give it back.
|
|
113
83
|
paused = false;
|
|
114
84
|
}
|
|
115
85
|
if (paused)
|
|
116
|
-
return; // pause() already gave the permit
|
|
117
|
-
if (parkedRekey !== null) {
|
|
118
|
-
// A re-key is parked waiting on its new key. Cancel it: pull it off the queue (so drive
|
|
119
|
-
// can't grant it) and, if the grant already arrived this microtask, give the new key's slot
|
|
120
|
-
// back. The lease holds no slot either way, so the team permit is the only thing to free.
|
|
121
|
-
const w = parkedRekey;
|
|
122
|
-
const idx = this.rekeyWaiters.indexOf(w);
|
|
123
|
-
if (idx >= 0)
|
|
124
|
-
this.rekeyWaiters.splice(idx, 1);
|
|
125
|
-
if (w.granted) {
|
|
126
|
-
this.bumpKeyInUse(w.key, -1);
|
|
127
|
-
}
|
|
128
|
-
w.resolve();
|
|
129
|
-
parkedRekey = null;
|
|
130
|
-
}
|
|
131
|
-
else if (currentKey !== undefined) {
|
|
132
|
-
this.bumpKeyInUse(currentKey, -1);
|
|
133
|
-
}
|
|
86
|
+
return; // pause() already gave the permit back
|
|
134
87
|
this.inUse -= 1;
|
|
135
88
|
this.drive();
|
|
136
89
|
};
|
|
137
|
-
const rekey = async (to) => {
|
|
138
|
-
if (to === currentKey || closed)
|
|
139
|
-
return;
|
|
140
|
-
if (paused) {
|
|
141
|
-
// Nothing is held while paused: the move only changes the key resume() admits on.
|
|
142
|
-
currentKey = to;
|
|
143
|
-
lease.key = to;
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
// Give the old key back first, waking that key's waiters.
|
|
147
|
-
if (currentKey !== undefined) {
|
|
148
|
-
this.bumpKeyInUse(currentKey, -1);
|
|
149
|
-
}
|
|
150
|
-
currentKey = to;
|
|
151
|
-
lease.key = to;
|
|
152
|
-
this.drive();
|
|
153
|
-
if (to !== undefined && this.keyAtCap(to)) {
|
|
154
|
-
// The new key is full: wait only for its headroom, ahead of every admission waiter.
|
|
155
|
-
const w = { key: to, resolve: () => { }, granted: false };
|
|
156
|
-
parkedRekey = w;
|
|
157
|
-
this.rekeyWaiters.push(w);
|
|
158
|
-
await new Promise((resolve) => {
|
|
159
|
-
w.resolve = resolve;
|
|
160
|
-
});
|
|
161
|
-
parkedRekey = null;
|
|
162
|
-
if (w.granted)
|
|
163
|
-
this.drive();
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (to !== undefined)
|
|
167
|
-
this.bumpKeyInUse(to, 1);
|
|
168
|
-
this.drive();
|
|
169
|
-
};
|
|
170
90
|
const pause = () => {
|
|
171
|
-
if (closed || paused
|
|
91
|
+
if (closed || paused)
|
|
172
92
|
return;
|
|
173
93
|
paused = true;
|
|
174
|
-
if (currentKey !== undefined)
|
|
175
|
-
this.bumpKeyInUse(currentKey, -1);
|
|
176
94
|
this.inUse -= 1;
|
|
177
95
|
this.drive();
|
|
178
96
|
};
|
|
179
97
|
const resume = async () => {
|
|
180
98
|
if (closed || !paused || parkedResume !== null)
|
|
181
99
|
return;
|
|
182
|
-
if (this.canLeaseAtOnce(tier
|
|
100
|
+
if (this.canLeaseAtOnce(tier)) {
|
|
183
101
|
this.inUse += 1;
|
|
184
|
-
if (currentKey !== undefined)
|
|
185
|
-
this.bumpKeyInUse(currentKey, 1);
|
|
186
102
|
paused = false;
|
|
187
103
|
return;
|
|
188
104
|
}
|
|
189
105
|
await new Promise((resolve) => {
|
|
190
|
-
const waiter = { tier,
|
|
106
|
+
const waiter = { tier, resolve };
|
|
191
107
|
parkedResume = waiter;
|
|
192
108
|
this.enqueue(waiter);
|
|
193
109
|
});
|
|
194
110
|
parkedResume = null;
|
|
195
|
-
// drive granted it (the permit
|
|
111
|
+
// drive granted it (the permit is taken), unless release() cancelled it.
|
|
196
112
|
if (!closed)
|
|
197
113
|
paused = false;
|
|
198
114
|
};
|
|
199
|
-
const lease = {
|
|
200
|
-
if (this.inUse < this.capacity - this.reserveFor(tier)
|
|
115
|
+
const lease = { release, pause, resume };
|
|
116
|
+
if (this.inUse < this.capacity - this.reserveFor(tier)) {
|
|
201
117
|
this.inUse += 1;
|
|
202
|
-
if (key !== undefined)
|
|
203
|
-
this.bumpKeyInUse(key, 1);
|
|
204
118
|
return lease;
|
|
205
119
|
}
|
|
206
120
|
await new Promise((resolve) => {
|
|
207
|
-
this.enqueue({ tier,
|
|
121
|
+
this.enqueue({ tier, resolve });
|
|
208
122
|
});
|
|
209
123
|
return lease;
|
|
210
124
|
}
|
|
@@ -221,97 +135,29 @@ export class Semaphore {
|
|
|
221
135
|
}
|
|
222
136
|
this.waiters.splice(i, 0, waiter);
|
|
223
137
|
}
|
|
224
|
-
/** Synchronously report whether acquireLease(tier
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
canLeaseAtOnce(tier
|
|
230
|
-
return this.inUse < this.capacity - this.reserveFor(tier)
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* the
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (currentKey !== undefined) {
|
|
239
|
-
this.bumpKeyInUse(currentKey, 1);
|
|
240
|
-
}
|
|
241
|
-
// True while pause() has given the key's slot back (a forced lease holds no team permit).
|
|
242
|
-
let paused = false;
|
|
243
|
-
const release = () => {
|
|
244
|
-
if (currentKey !== undefined && !paused) {
|
|
245
|
-
this.bumpKeyInUse(currentKey, -1);
|
|
246
|
-
}
|
|
247
|
-
this.drive();
|
|
248
|
-
};
|
|
249
|
-
const rekey = async (to) => {
|
|
250
|
-
if (to === currentKey)
|
|
251
|
-
return;
|
|
252
|
-
if (currentKey !== undefined && !paused) {
|
|
253
|
-
this.bumpKeyInUse(currentKey, -1);
|
|
254
|
-
}
|
|
255
|
-
currentKey = to;
|
|
256
|
-
lease.key = to;
|
|
257
|
-
if (to !== undefined && !paused)
|
|
258
|
-
this.bumpKeyInUse(to, 1);
|
|
259
|
-
this.drive();
|
|
260
|
-
};
|
|
261
|
-
const pause = () => {
|
|
262
|
-
if (paused)
|
|
263
|
-
return;
|
|
264
|
-
paused = true;
|
|
265
|
-
if (currentKey !== undefined)
|
|
266
|
-
this.bumpKeyInUse(currentKey, -1);
|
|
267
|
-
this.drive();
|
|
268
|
-
};
|
|
269
|
-
// Like the forced lease itself, taking the slot back never waits.
|
|
270
|
-
const resume = async () => {
|
|
271
|
-
if (!paused)
|
|
272
|
-
return;
|
|
273
|
-
paused = false;
|
|
274
|
-
if (currentKey !== undefined)
|
|
275
|
-
this.bumpKeyInUse(currentKey, 1);
|
|
276
|
-
};
|
|
277
|
-
const lease = { key: currentKey, release, rekey, pause, resume };
|
|
278
|
-
return lease;
|
|
279
|
-
}
|
|
280
|
-
/** Hand every parked waiter a permit it can take — both team and key headroom — in one
|
|
281
|
-
* pass. The one home of the permit hand-off: release, setCapacity, setReserve and setKeyCaps
|
|
282
|
-
* all drive it, and growth loops it to fill new headroom. It never admits past the team cap
|
|
283
|
-
* or a key cap. Re-key waiters run first (they already hold a team permit, so they wait only
|
|
284
|
-
* on their key). Admission waiters are scanned in queue (tier, then FIFO) order: a waiter
|
|
285
|
-
* whose key is full is skipped and keeps its place, so a later waiter on a free key is never
|
|
286
|
-
* stuck behind it — but the reserve stops the scan at the first tier ≥ 1 waiter it withholds
|
|
287
|
-
* from, since the tier-ascending queue means everything behind is withheld too. */
|
|
138
|
+
/** Synchronously report whether acquireLease(tier) would return at once — the team total
|
|
139
|
+
* has headroom right now and the tier's reserve is satisfied. Mirrors the exact fast-path
|
|
140
|
+
* condition acquireLease's first branch uses, so a caller can decide whether to show a
|
|
141
|
+
* "waiting for a slot" state before awaiting a lease that would actually park
|
|
142
|
+
* (merge-drain.ts's resolver wrapper). */
|
|
143
|
+
canLeaseAtOnce(tier) {
|
|
144
|
+
return this.inUse < this.capacity - this.reserveFor(tier);
|
|
145
|
+
}
|
|
146
|
+
/** Hand every parked waiter a permit it can take in one pass. The one home of the permit
|
|
147
|
+
* hand-off: release, setCapacity, setReserve and a pause/resume all drive it, and growth
|
|
148
|
+
* loops it to fill new headroom. It never admits past the team cap. Admission waiters are
|
|
149
|
+
* scanned in queue (tier, then FIFO) order; the reserve stops the scan at the first tier ≥ 1
|
|
150
|
+
* waiter it withholds from, since the tier-ascending queue means everything behind is
|
|
151
|
+
* withheld too. */
|
|
288
152
|
drive() {
|
|
289
153
|
for (;;) {
|
|
290
|
-
// Re-key waiters first: no team check — they already hold a team permit.
|
|
291
154
|
let granted = false;
|
|
292
|
-
for (let i = 0; i < this.rekeyWaiters.length; i++) {
|
|
293
|
-
const w = this.rekeyWaiters[i];
|
|
294
|
-
if (this.keyAtCap(w.key))
|
|
295
|
-
continue;
|
|
296
|
-
w.granted = true;
|
|
297
|
-
this.rekeyWaiters.splice(i, 1);
|
|
298
|
-
this.bumpKeyInUse(w.key, 1);
|
|
299
|
-
w.resolve();
|
|
300
|
-
granted = true;
|
|
301
|
-
break;
|
|
302
|
-
}
|
|
303
|
-
if (granted)
|
|
304
|
-
continue;
|
|
305
155
|
for (let i = 0; i < this.waiters.length; i++) {
|
|
306
156
|
const w = this.waiters[i];
|
|
307
157
|
if (this.inUse >= this.capacity - this.reserveFor(w.tier))
|
|
308
158
|
break; // reserve/full stops the scan
|
|
309
|
-
if (w.key !== undefined && this.keyAtCap(w.key))
|
|
310
|
-
continue; // skip a full key, keep its place
|
|
311
159
|
this.waiters.splice(i, 1);
|
|
312
160
|
this.inUse += 1;
|
|
313
|
-
if (w.key !== undefined)
|
|
314
|
-
this.bumpKeyInUse(w.key, 1);
|
|
315
161
|
w.resolve();
|
|
316
162
|
granted = true;
|
|
317
163
|
break;
|
|
@@ -320,23 +166,11 @@ export class Semaphore {
|
|
|
320
166
|
break;
|
|
321
167
|
}
|
|
322
168
|
}
|
|
323
|
-
/** The permits withheld from `tier`: `reserve` for tier ≥ 1 (maintenance), 0 for every tier
|
|
324
|
-
* ships work or merges a change (work, VERIFY_TIER, MERGE_TIER). */
|
|
169
|
+
/** The permits withheld from `tier`: `reserve` for tier ≥ 1 (maintenance), 0 for every tier
|
|
170
|
+
* that ships work or merges a change (work, VERIFY_TIER, MERGE_TIER). */
|
|
325
171
|
reserveFor(tier) {
|
|
326
172
|
return tier >= 1 ? this.reserve : 0;
|
|
327
173
|
}
|
|
328
|
-
/** True when `key` is at its cap — a configured cap the in-use already meets or exceeds. An
|
|
329
|
-
* uncapped key (no cap set) is never "full". */
|
|
330
|
-
keyAtCap(key) {
|
|
331
|
-
const cap = this.keyCaps.get(key);
|
|
332
|
-
return cap !== undefined && (this.keyInUse.get(key) ?? 0) >= cap;
|
|
333
|
-
}
|
|
334
|
-
/** Add `delta` to `key`'s held permits, initializing an absent key at zero first — the
|
|
335
|
-
* single home of the get-or-init bump that release, rekey, force and drive all repeated
|
|
336
|
-
* (an absent key implicitly holds zero, so the first bump must not read `undefined`). */
|
|
337
|
-
bumpKeyInUse(key, delta) {
|
|
338
|
-
this.keyInUse.set(key, (this.keyInUse.get(key) ?? 0) + delta);
|
|
339
|
-
}
|
|
340
174
|
/** Release a held permit. The finishing work gives back its permit first, and a queued waiter
|
|
341
175
|
* takes it over only if there is headroom (drive): after a shrink, inUse can sit at or above
|
|
342
176
|
* the cap for a while (the in-flight work admitted before it), and no new grant may proceed
|
|
@@ -360,46 +194,8 @@ export class Semaphore {
|
|
|
360
194
|
this.reserve = Math.max(0, n);
|
|
361
195
|
this.drive();
|
|
362
196
|
}
|
|
363
|
-
/**
|
|
364
|
-
* parked waiters for that key up to the new headroom; shrinking never preempts an in-flight
|
|
365
|
-
* holder — it only withholds future grants on that key until releases drain it under the new
|
|
366
|
-
* cap, the same contract as setCapacity. Non-positive and non-integer entries are dropped. */
|
|
367
|
-
setKeyCaps(caps) {
|
|
368
|
-
this.keyCaps = new Map(enforcedKeyCaps(caps));
|
|
369
|
-
this.drive();
|
|
370
|
-
}
|
|
371
|
-
/** The pool's live state for observability (plans/provider-concurrency.md §8): the team
|
|
372
|
-
* in-use/cap, the reserve, and each referenced key's in-use, cap (undefined when uncapped)
|
|
373
|
-
* and waiting count. */
|
|
197
|
+
/** The pool's live state for observability: the team in-use/cap and the reserve. */
|
|
374
198
|
snapshot() {
|
|
375
|
-
|
|
376
|
-
const addKey = (key) => {
|
|
377
|
-
if (key === undefined)
|
|
378
|
-
return;
|
|
379
|
-
if (byKey[key] === undefined) {
|
|
380
|
-
const cap = this.keyCaps.get(key);
|
|
381
|
-
const entry = {
|
|
382
|
-
inUse: this.keyInUse.get(key) ?? 0,
|
|
383
|
-
waiting: 0,
|
|
384
|
-
};
|
|
385
|
-
if (cap !== undefined)
|
|
386
|
-
entry.limit = cap;
|
|
387
|
-
byKey[key] = entry;
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
for (const k of this.keyCaps.keys())
|
|
391
|
-
addKey(k);
|
|
392
|
-
for (const k of this.keyInUse.keys())
|
|
393
|
-
addKey(k);
|
|
394
|
-
for (const w of this.waiters) {
|
|
395
|
-
addKey(w.key);
|
|
396
|
-
if (w.key !== undefined)
|
|
397
|
-
byKey[w.key].waiting += 1;
|
|
398
|
-
}
|
|
399
|
-
for (const w of this.rekeyWaiters) {
|
|
400
|
-
addKey(w.key);
|
|
401
|
-
byKey[w.key].waiting += 1;
|
|
402
|
-
}
|
|
403
|
-
return { inUse: this.inUse, limit: this.capacity, reserve: this.reserve, byKey };
|
|
199
|
+
return { inUse: this.inUse, limit: this.capacity, reserve: this.reserve };
|
|
404
200
|
}
|
|
405
201
|
}
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* This is config vocabulary, not HTTP vocabulary — it names which tumwater.json keys are
|
|
9
9
|
* browser-editable — so it lives beside the other config-key lists (config-schema.ts's
|
|
10
10
|
* TOP_LEVEL_KEYS, config-write.ts's per-key validators) rather than in the endpoint module
|
|
11
|
-
* that first consumed it. The
|
|
12
|
-
*
|
|
11
|
+
* that first consumed it. The run-concurrency knobs (maxConcurrent,
|
|
12
|
+
* maxConcurrentChecks, worktreeSlots) joined the set because
|
|
13
13
|
* tumwater doctor's own contention checks name them for the operator to raise
|
|
14
|
-
* (checkWorktreePool,
|
|
14
|
+
* (checkWorktreePool, checkCheckContention); the per-role maps and
|
|
15
15
|
* customRoles still stay CLI/director territory. The browser side mirrors the set for its labels
|
|
16
16
|
* (ui/gui/gui-client-settings.ts's SETTINGS_KEYS), a manual copy; the GET /api/config test
|
|
17
17
|
* pins the served key set to this list, and the settings test pins the browser copy's keys
|
|
@@ -23,13 +23,10 @@ export const EDITABLE_CONFIG_KEYS = [
|
|
|
23
23
|
"fallback",
|
|
24
24
|
"maxDailyCostUsd",
|
|
25
25
|
// The run-concurrency knobs tumwater doctor's contention checks name for the operator to
|
|
26
|
-
// raise (
|
|
27
|
-
// shows the advice can apply it.
|
|
26
|
+
// raise (checkCheckContention) — grouped so the Settings card that shows the advice can apply it.
|
|
28
27
|
"maxConcurrent",
|
|
29
|
-
"maxConcurrentPerProvider",
|
|
30
28
|
"maxConcurrentChecks",
|
|
31
29
|
"worktreeSlots",
|
|
32
|
-
"claudeWindowReserve",
|
|
33
30
|
"quietHours",
|
|
34
31
|
"notify",
|
|
35
32
|
// The per-backend section (plans/agent-backends.md, part 4a/7): the whole `agents` object is
|