tumwater 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,17 +1,37 @@
|
|
|
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.
|
|
3
|
+
* coordinates within a single harness process.
|
|
4
|
+
*
|
|
5
|
+
* Since 2026-10-15 it also bounds a model provider (a "key") independently, so a full
|
|
6
|
+
* provider never blocks a waiter on another (plans/provider-concurrency.md §3). The keyless
|
|
7
|
+
* entry points — acquire/tryAcquire/release — are unchanged; only acquireLease, forceLease
|
|
8
|
+
* and setKeyCaps touch the keyed path. */
|
|
4
9
|
export class Semaphore {
|
|
5
10
|
capacity;
|
|
6
|
-
/** A parked waiter: its scheduling tier
|
|
11
|
+
/** A parked WAITING (admission) waiter: its scheduling tier, optional provider key, and the
|
|
12
|
+
* resolver that hands it the permit. Keyless waiters (the legacy path) carry no key and are
|
|
13
|
+
* never held by a key cap. */
|
|
7
14
|
waiters = [];
|
|
15
|
+
/** A parked re-key waiter: it already holds a team permit, so it waits only for its key's
|
|
16
|
+
* headroom, ahead of every admission waiter (which would otherwise queue a team permit uselessly
|
|
17
|
+
* behind another provider's runs). `granted` flips when drive() hands the waiter the target key's
|
|
18
|
+
* slot, so the lease's release can tell a delivered grant (give it back) from a still-parked one
|
|
19
|
+
* (cancel it) when the two interleave. */
|
|
20
|
+
rekeyWaiters = [];
|
|
8
21
|
/** Permits currently held, tracked separately from capacity (rather than as an "available"
|
|
9
22
|
* count): setCapacity can shrink below the current in-use, and a free-permit count cannot
|
|
10
23
|
* represent that state — it would over-admit new work after a shrink. */
|
|
11
24
|
inUse = 0;
|
|
25
|
+
/** The per-key permits currently held: a lease or forced lease charges its key here, and a
|
|
26
|
+
* key at its cap withholds further grants on it. Grows and shrinks only through release and
|
|
27
|
+
* rekey — never preempted. */
|
|
28
|
+
keyInUse = new Map();
|
|
29
|
+
/** The live per-key caps, from `maxConcurrentPerProvider` (plans/provider-concurrency.md
|
|
30
|
+
* §1). An absent key is uncapped — bounded by `capacity` alone. */
|
|
31
|
+
keyCaps = new Map();
|
|
12
32
|
/** Permits withheld from tier ≥ 1 acquirers so a work loop that has work to take can claim
|
|
13
33
|
* one without waiting for a maintenance tick to release (plans/parallel-work-instances.md
|
|
14
|
-
* "Priority headroom", part 7/7). Tiers ≤ 0 (work,
|
|
34
|
+
* "Priority headroom", part 7/7). Tiers ≤ 0 (work, VERIFY_TIER, MERGE_TIER) ignore it. */
|
|
15
35
|
reserve = 0;
|
|
16
36
|
constructor(capacity) {
|
|
17
37
|
this.capacity = capacity;
|
|
@@ -20,12 +40,13 @@ export class Semaphore {
|
|
|
20
40
|
get limit() {
|
|
21
41
|
return this.capacity;
|
|
22
42
|
}
|
|
23
|
-
/** How many acquirers are parked for a permit right now.
|
|
43
|
+
/** How many acquirers are parked for a permit right now, admission and re-key waiters both.
|
|
44
|
+
* With no keys there are never re-key waiters, so this is exactly today's count. */
|
|
24
45
|
get waiting() {
|
|
25
|
-
return this.waiters.length;
|
|
46
|
+
return this.waiters.length + this.rekeyWaiters.length;
|
|
26
47
|
}
|
|
27
48
|
/** Take a free permit without parking: true when one was granted (the same fast path
|
|
28
|
-
* acquire takes), false when acquire would have to wait. */
|
|
49
|
+
* acquire takes), false when acquire would have to wait. Unkeyed, as before. */
|
|
29
50
|
tryAcquire() {
|
|
30
51
|
if (this.inUse >= this.capacity)
|
|
31
52
|
return false;
|
|
@@ -36,7 +57,7 @@ export class Semaphore {
|
|
|
36
57
|
* requests only: on arrival a waiter inserts ahead of every parked waiter with a strictly
|
|
37
58
|
* greater tier (the orchestrator passes roleTier so work roles beat maintenance across
|
|
38
59
|
* polls), keeping stable FIFO within a tier. In-flight holders are never preempted — they
|
|
39
|
-
* run to completion and their release is what hands out the permit. */
|
|
60
|
+
* run to completion and their release is what hands out the permit. Unkeyed, as before. */
|
|
40
61
|
async acquire(tier) {
|
|
41
62
|
if (this.inUse < this.capacity - this.reserveFor(tier)) {
|
|
42
63
|
this.inUse += 1;
|
|
@@ -54,54 +75,255 @@ export class Semaphore {
|
|
|
54
75
|
this.waiters.splice(i, 0, waiter);
|
|
55
76
|
});
|
|
56
77
|
}
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
/** Admit by both the team total and an optional provider key at once, waiting until both
|
|
79
|
+
* hold, and return a lease handle. `tier` orders the wait exactly as acquire does; `key`
|
|
80
|
+
* bounds the permit by its provider cap: granted when `inUse < capacity − reserveFor(tier)`
|
|
81
|
+
* and the key is under its cap (an absent or uncapped key always passes). FIFO holds within
|
|
82
|
+
* a key, and a full key never delays a waiter on another. */
|
|
83
|
+
async acquireLease(tier, key) {
|
|
84
|
+
let currentKey = key;
|
|
85
|
+
// True once release() has run: a released lease owns no slot and must not undo one again —
|
|
86
|
+
// a second release (or a parked rekey's late grant) must be a no-op, never a negative
|
|
87
|
+
// count. The orchestrator releases each lease exactly once, but the rekey cancel path
|
|
88
|
+
// relies on this guard so a cancelled lease never re-decrements its target key.
|
|
89
|
+
let closed = false;
|
|
90
|
+
// The lease's parked re-key, while it waits on a full target key. At that point the lease
|
|
91
|
+
// holds NO key slot (the old one was returned at the rekey's start), so release() must
|
|
92
|
+
// CANCEL the pending grant rather than undo a slot this lease never took. Null when no
|
|
93
|
+
// rekey is parked.
|
|
94
|
+
let parkedRekey = null;
|
|
95
|
+
const release = () => {
|
|
96
|
+
if (closed)
|
|
97
|
+
return;
|
|
98
|
+
closed = true;
|
|
99
|
+
if (parkedRekey !== null) {
|
|
100
|
+
// A re-key is parked waiting on its new key. Cancel it: pull it off the queue (so drive
|
|
101
|
+
// can't grant it) and, if the grant already arrived this microtask, give the new key's slot
|
|
102
|
+
// back. The lease holds no slot either way, so the team permit is the only thing to free.
|
|
103
|
+
const w = parkedRekey;
|
|
104
|
+
const idx = this.rekeyWaiters.indexOf(w);
|
|
105
|
+
if (idx >= 0)
|
|
106
|
+
this.rekeyWaiters.splice(idx, 1);
|
|
107
|
+
if (w.granted) {
|
|
108
|
+
this.bumpKeyInUse(w.key, -1);
|
|
109
|
+
}
|
|
110
|
+
w.resolve();
|
|
111
|
+
parkedRekey = null;
|
|
112
|
+
}
|
|
113
|
+
else if (currentKey !== undefined) {
|
|
114
|
+
this.bumpKeyInUse(currentKey, -1);
|
|
115
|
+
}
|
|
116
|
+
this.inUse -= 1;
|
|
117
|
+
this.drive();
|
|
118
|
+
};
|
|
119
|
+
const rekey = async (to) => {
|
|
120
|
+
if (to === currentKey || closed)
|
|
121
|
+
return;
|
|
122
|
+
// Give the old key back first, waking that key's waiters.
|
|
123
|
+
if (currentKey !== undefined) {
|
|
124
|
+
this.bumpKeyInUse(currentKey, -1);
|
|
125
|
+
}
|
|
126
|
+
currentKey = to;
|
|
127
|
+
lease.key = to;
|
|
128
|
+
this.drive();
|
|
129
|
+
if (to !== undefined && this.keyAtCap(to)) {
|
|
130
|
+
// The new key is full: wait only for its headroom, ahead of every admission waiter.
|
|
131
|
+
const w = { key: to, resolve: () => { }, granted: false };
|
|
132
|
+
parkedRekey = w;
|
|
133
|
+
this.rekeyWaiters.push(w);
|
|
134
|
+
await new Promise((resolve) => {
|
|
135
|
+
w.resolve = resolve;
|
|
136
|
+
});
|
|
137
|
+
parkedRekey = null;
|
|
138
|
+
if (w.granted)
|
|
139
|
+
this.drive();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (to !== undefined)
|
|
143
|
+
this.bumpKeyInUse(to, 1);
|
|
144
|
+
this.drive();
|
|
145
|
+
};
|
|
146
|
+
const lease = { key: currentKey, release, rekey };
|
|
147
|
+
if (this.inUse < this.capacity - this.reserveFor(tier) && (key === undefined || !this.keyAtCap(key))) {
|
|
148
|
+
this.inUse += 1;
|
|
149
|
+
if (key !== undefined)
|
|
150
|
+
this.bumpKeyInUse(key, 1);
|
|
151
|
+
return lease;
|
|
152
|
+
}
|
|
153
|
+
await new Promise((resolve) => {
|
|
154
|
+
const waiter = { tier, key, resolve };
|
|
155
|
+
let i = this.waiters.length;
|
|
156
|
+
while (i > 0) {
|
|
157
|
+
const prev = this.waiters[i - 1];
|
|
158
|
+
if (!prev || prev.tier <= tier)
|
|
159
|
+
break;
|
|
160
|
+
i -= 1;
|
|
161
|
+
}
|
|
162
|
+
this.waiters.splice(i, 0, waiter);
|
|
163
|
+
});
|
|
164
|
+
return lease;
|
|
165
|
+
}
|
|
166
|
+
/** Synchronously report whether acquireLease(tier, key) would return at once — both the
|
|
167
|
+
* team total and the key (when set) have headroom right now, and the tier's reserve is
|
|
168
|
+
* satisfied. Mirrors the exact fast-path condition acquireLease's first branch uses, so a
|
|
169
|
+
* caller can decide whether to show a "waiting for a slot" state before awaiting a lease
|
|
170
|
+
* that would actually park (merge-drain.ts's resolver wrapper). */
|
|
171
|
+
canLeaseAtOnce(tier, key) {
|
|
172
|
+
return this.inUse < this.capacity - this.reserveFor(tier) && (key === undefined || !this.keyAtCap(key));
|
|
74
173
|
}
|
|
75
|
-
/** The
|
|
76
|
-
*
|
|
174
|
+
/** The director's bypass: take a key's slot at once, never waiting, and never counting toward
|
|
175
|
+
* the team cap. It DOES count toward the key's in-use, so a forced lease can overshoot a
|
|
176
|
+
* full key by one holder; no grant on that key happens until a release drains it back under
|
|
177
|
+
* the cap. Its re-key also never waits. */
|
|
178
|
+
forceLease(key) {
|
|
179
|
+
let currentKey = key;
|
|
180
|
+
if (currentKey !== undefined) {
|
|
181
|
+
this.bumpKeyInUse(currentKey, 1);
|
|
182
|
+
}
|
|
183
|
+
const release = () => {
|
|
184
|
+
if (currentKey !== undefined) {
|
|
185
|
+
this.bumpKeyInUse(currentKey, -1);
|
|
186
|
+
}
|
|
187
|
+
this.drive();
|
|
188
|
+
};
|
|
189
|
+
const rekey = async (to) => {
|
|
190
|
+
if (to === currentKey)
|
|
191
|
+
return;
|
|
192
|
+
if (currentKey !== undefined) {
|
|
193
|
+
this.bumpKeyInUse(currentKey, -1);
|
|
194
|
+
}
|
|
195
|
+
currentKey = to;
|
|
196
|
+
lease.key = to;
|
|
197
|
+
if (to !== undefined)
|
|
198
|
+
this.bumpKeyInUse(to, 1);
|
|
199
|
+
this.drive();
|
|
200
|
+
};
|
|
201
|
+
const lease = { key: currentKey, release, rekey };
|
|
202
|
+
return lease;
|
|
203
|
+
}
|
|
204
|
+
/** Hand every parked waiter a permit it can take — both team and key headroom — in one
|
|
205
|
+
* pass. The one home of the permit hand-off: release, setCapacity, setReserve and setKeyCaps
|
|
206
|
+
* all drive it, and growth loops it to fill new headroom. It never admits past the team cap
|
|
207
|
+
* or a key cap. Re-key waiters run first (they already hold a team permit, so they wait only
|
|
208
|
+
* on their key). Admission waiters are scanned in queue (tier, then FIFO) order: a waiter
|
|
209
|
+
* whose key is full is skipped and keeps its place, so a later waiter on a free key is never
|
|
210
|
+
* stuck behind it — but the reserve stops the scan at the first tier ≥ 1 waiter it withholds
|
|
211
|
+
* from, since the tier-ascending queue means everything behind is withheld too. */
|
|
212
|
+
drive() {
|
|
213
|
+
for (;;) {
|
|
214
|
+
// Re-key waiters first: no team check — they already hold a team permit.
|
|
215
|
+
let granted = false;
|
|
216
|
+
for (let i = 0; i < this.rekeyWaiters.length; i++) {
|
|
217
|
+
const w = this.rekeyWaiters[i];
|
|
218
|
+
if (this.keyAtCap(w.key))
|
|
219
|
+
continue;
|
|
220
|
+
w.granted = true;
|
|
221
|
+
this.rekeyWaiters.splice(i, 1);
|
|
222
|
+
this.bumpKeyInUse(w.key, 1);
|
|
223
|
+
w.resolve();
|
|
224
|
+
granted = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
if (granted)
|
|
228
|
+
continue;
|
|
229
|
+
for (let i = 0; i < this.waiters.length; i++) {
|
|
230
|
+
const w = this.waiters[i];
|
|
231
|
+
if (this.inUse >= this.capacity - this.reserveFor(w.tier))
|
|
232
|
+
break; // reserve/full stops the scan
|
|
233
|
+
if (w.key !== undefined && this.keyAtCap(w.key))
|
|
234
|
+
continue; // skip a full key, keep its place
|
|
235
|
+
this.waiters.splice(i, 1);
|
|
236
|
+
this.inUse += 1;
|
|
237
|
+
if (w.key !== undefined)
|
|
238
|
+
this.bumpKeyInUse(w.key, 1);
|
|
239
|
+
w.resolve();
|
|
240
|
+
granted = true;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
if (!granted)
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** The permits withheld from `tier`: `reserve` for tier ≥ 1 (maintenance), 0 for every tier that
|
|
248
|
+
* ships work or merges a change (work, VERIFY_TIER, MERGE_TIER). */
|
|
77
249
|
reserveFor(tier) {
|
|
78
250
|
return tier >= 1 ? this.reserve : 0;
|
|
79
251
|
}
|
|
252
|
+
/** True when `key` is at its cap — a configured cap the in-use already meets or exceeds. An
|
|
253
|
+
* uncapped key (no cap set) is never "full". */
|
|
254
|
+
keyAtCap(key) {
|
|
255
|
+
const cap = this.keyCaps.get(key);
|
|
256
|
+
return cap !== undefined && (this.keyInUse.get(key) ?? 0) >= cap;
|
|
257
|
+
}
|
|
258
|
+
/** Add `delta` to `key`'s held permits, initializing an absent key at zero first — the
|
|
259
|
+
* single home of the get-or-init bump that release, rekey, force and drive all repeated
|
|
260
|
+
* (an absent key implicitly holds zero, so the first bump must not read `undefined`). */
|
|
261
|
+
bumpKeyInUse(key, delta) {
|
|
262
|
+
this.keyInUse.set(key, (this.keyInUse.get(key) ?? 0) + delta);
|
|
263
|
+
}
|
|
80
264
|
/** Release a held permit. The finishing work gives back its permit first, and a queued waiter
|
|
81
|
-
* takes it over only if there is headroom (
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
265
|
+
* takes it over only if there is headroom (drive): after a shrink, inUse can sit at or above
|
|
266
|
+
* the cap for a while (the in-flight work admitted before it), and no new grant may proceed
|
|
267
|
+
* until releases bring in-use under the cap. In the normal case this is exactly the classic
|
|
268
|
+
* hand-off — one release wakes one waiter, net in-use unchanged. */
|
|
85
269
|
release() {
|
|
86
270
|
this.inUse -= 1;
|
|
87
|
-
this.
|
|
271
|
+
this.drive();
|
|
88
272
|
}
|
|
89
273
|
/** Live-resize the cap (a mid-run tumwater.json edit). Growing admits queued waiters up to
|
|
90
274
|
* the new headroom — one permit per woken waiter. Shrinking never preempts in-flight work:
|
|
91
275
|
* it only caps future grants until releases bring in-use under the new cap. */
|
|
92
276
|
setCapacity(n) {
|
|
93
277
|
this.capacity = n;
|
|
94
|
-
|
|
95
|
-
// One permit per woken waiter, up to the new headroom.
|
|
96
|
-
}
|
|
278
|
+
this.drive();
|
|
97
279
|
}
|
|
98
280
|
/** Live-resize the permits withheld from tier ≥ 1 acquirers. Lowering it re-runs the hand-off
|
|
99
281
|
* so a parked maintenance waiter can take a permit the reserve had held; raising it never
|
|
100
282
|
* preempts an in-flight holder — it only withholds future grants. */
|
|
101
283
|
setReserve(n) {
|
|
102
284
|
this.reserve = Math.max(0, n);
|
|
103
|
-
|
|
104
|
-
|
|
285
|
+
this.drive();
|
|
286
|
+
}
|
|
287
|
+
/** Live-resize the per-key caps (a mid-run `maxConcurrentPerProvider` edit). Growing admits
|
|
288
|
+
* parked waiters for that key up to the new headroom; shrinking never preempts an in-flight
|
|
289
|
+
* holder — it only withholds future grants on that key until releases drain it under the new
|
|
290
|
+
* cap, the same contract as setCapacity. Non-positive and non-integer entries are dropped. */
|
|
291
|
+
setKeyCaps(caps) {
|
|
292
|
+
this.keyCaps = new Map(Object.entries(caps).filter(([, v]) => Number.isInteger(v) && v > 0));
|
|
293
|
+
this.drive();
|
|
294
|
+
}
|
|
295
|
+
/** The pool's live state for observability (plans/provider-concurrency.md §8): the team
|
|
296
|
+
* in-use/cap, the reserve, and each referenced key's in-use, cap (undefined when uncapped)
|
|
297
|
+
* and waiting count. */
|
|
298
|
+
snapshot() {
|
|
299
|
+
const byKey = {};
|
|
300
|
+
const addKey = (key) => {
|
|
301
|
+
if (key === undefined)
|
|
302
|
+
return;
|
|
303
|
+
if (byKey[key] === undefined) {
|
|
304
|
+
const cap = this.keyCaps.get(key);
|
|
305
|
+
const entry = {
|
|
306
|
+
inUse: this.keyInUse.get(key) ?? 0,
|
|
307
|
+
waiting: 0,
|
|
308
|
+
};
|
|
309
|
+
if (cap !== undefined)
|
|
310
|
+
entry.limit = cap;
|
|
311
|
+
byKey[key] = entry;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
for (const k of this.keyCaps.keys())
|
|
315
|
+
addKey(k);
|
|
316
|
+
for (const k of this.keyInUse.keys())
|
|
317
|
+
addKey(k);
|
|
318
|
+
for (const w of this.waiters) {
|
|
319
|
+
addKey(w.key);
|
|
320
|
+
if (w.key !== undefined)
|
|
321
|
+
byKey[w.key].waiting += 1;
|
|
322
|
+
}
|
|
323
|
+
for (const w of this.rekeyWaiters) {
|
|
324
|
+
addKey(w.key);
|
|
325
|
+
byKey[w.key].waiting += 1;
|
|
105
326
|
}
|
|
327
|
+
return { inUse: this.inUse, limit: this.capacity, reserve: this.reserve, byKey };
|
|
106
328
|
}
|
|
107
329
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { isJsonObject } from "../files/json-object.js";
|
|
2
|
+
import { LEGACY_ROLE_IDS } from "../roles/legacy-role-ids.js";
|
|
3
|
+
/** The config maps keyed by role id, whose old-id keys move to the new id. */
|
|
4
|
+
const ROLE_KEYED_MAPS = ["roles", "maxDailyCostUsdPerRole", "quietHoursPerRole"];
|
|
5
|
+
/** Move each retired role id keyed in `map` to its new id. Returns `map` itself when none
|
|
6
|
+
* moved. */
|
|
7
|
+
function renameRoleIds(map, name, legacy, renamed, problems) {
|
|
8
|
+
let out = map;
|
|
9
|
+
for (const [old, next] of Object.entries(legacy)) {
|
|
10
|
+
if (!Object.hasOwn(map, old))
|
|
11
|
+
continue;
|
|
12
|
+
if (Object.hasOwn(map, next)) {
|
|
13
|
+
problems.push(`sets both ${name}.${old} and ${name}.${next} — keep ${name}.${next} (${old} is its old id)`);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (out === map)
|
|
17
|
+
out = { ...map };
|
|
18
|
+
out[next] = out[old];
|
|
19
|
+
delete out[old];
|
|
20
|
+
renamed.push(`${name}.${old} → ${name}.${next}`);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/** The one table of renamed config keys: tumwater.json may spell either name, load reads both,
|
|
25
|
+
* and every writer writes only `to`. */
|
|
26
|
+
const CONFIG_KEY_RENAMES = [
|
|
27
|
+
{ from: "landBatchMax", to: "mergeBatchMax", scope: "top" },
|
|
28
|
+
{ from: "maintenancePerWorkLanding", to: "maintenancePerWorkMerge", scope: "top" },
|
|
29
|
+
{ from: "tickTimeoutSeconds", to: "runTimeoutSeconds", scope: "top" },
|
|
30
|
+
{ from: "minTickIntervalSeconds", to: "minRunIntervalSeconds", scope: "top" },
|
|
31
|
+
{ from: "minTickIntervalSeconds", to: "minRunIntervalSeconds", scope: "role" },
|
|
32
|
+
{ from: "customLoops", to: "customRoles", scope: "top" },
|
|
33
|
+
{ from: "agent", to: "backend", scope: "top" },
|
|
34
|
+
{ from: "agents", to: "backends", scope: "top" },
|
|
35
|
+
];
|
|
36
|
+
/** Move every old key of one object to its new name. Returns a copy when anything moved. */
|
|
37
|
+
function renameIn(obj, scope, prefix, renamed, problems) {
|
|
38
|
+
let out = obj;
|
|
39
|
+
for (const r of CONFIG_KEY_RENAMES) {
|
|
40
|
+
if (r.scope !== scope || !Object.hasOwn(obj, r.from))
|
|
41
|
+
continue;
|
|
42
|
+
if (Object.hasOwn(obj, r.to)) {
|
|
43
|
+
problems.push(`${prefix}${r.from} and ${prefix}${r.to} are both set — keep ${prefix}${r.to} (${r.from} is its old name)`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (out === obj)
|
|
47
|
+
out = { ...obj };
|
|
48
|
+
out[r.to] = out[r.from];
|
|
49
|
+
delete out[r.from];
|
|
50
|
+
renamed.push(`${prefix}${r.from} → ${prefix}${r.to}`);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/** Rewrite a parsed config (or director request) so it carries only current key names. Pure:
|
|
55
|
+
* the input is never mutated, and a non-object passes through unchanged. `renamed` lists each
|
|
56
|
+
* `<from> → <to>` applied; `problems` names each pair the file set both ways. */
|
|
57
|
+
export function normalizeConfigKeys(raw, legacy = LEGACY_ROLE_IDS) {
|
|
58
|
+
const renamed = [];
|
|
59
|
+
const problems = [];
|
|
60
|
+
if (!isJsonObject(raw))
|
|
61
|
+
return { raw, renamed, problems };
|
|
62
|
+
let out = renameIn(raw, "top", "", renamed, problems);
|
|
63
|
+
for (const name of ROLE_KEYED_MAPS) {
|
|
64
|
+
const map = out[name];
|
|
65
|
+
if (!isJsonObject(map))
|
|
66
|
+
continue;
|
|
67
|
+
const next = renameRoleIds(map, name, legacy, renamed, problems);
|
|
68
|
+
if (next !== map)
|
|
69
|
+
out = { ...out, [name]: next };
|
|
70
|
+
}
|
|
71
|
+
if (isJsonObject(out.roles)) {
|
|
72
|
+
let roles = out.roles;
|
|
73
|
+
for (const [id, entry] of Object.entries(out.roles)) {
|
|
74
|
+
if (!isJsonObject(entry))
|
|
75
|
+
continue;
|
|
76
|
+
const next = renameIn(entry, "role", `roles.${id}.`, renamed, problems);
|
|
77
|
+
if (next !== entry) {
|
|
78
|
+
if (roles === out.roles)
|
|
79
|
+
roles = { ...out.roles };
|
|
80
|
+
roles[id] = next;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (roles !== out.roles)
|
|
84
|
+
out = { ...out, roles };
|
|
85
|
+
}
|
|
86
|
+
return { raw: out, renamed, problems };
|
|
87
|
+
}
|
|
88
|
+
/** Map a key a person typed to its current spelling: a top-level old name, a dotted key whose
|
|
89
|
+
* first segment is an old top-level name, or `roles.<id>.<old field>`. Anything else passes
|
|
90
|
+
* through unchanged. */
|
|
91
|
+
export function canonicalConfigKey(key, legacy = LEGACY_ROLE_IDS) {
|
|
92
|
+
const parts = key.split(".");
|
|
93
|
+
const first = parts[0] ?? "";
|
|
94
|
+
const top = CONFIG_KEY_RENAMES.find((r) => r.scope === "top" && r.from === first);
|
|
95
|
+
if (top)
|
|
96
|
+
return [top.to, ...parts.slice(1)].join(".");
|
|
97
|
+
if (parts.length >= 2 && ROLE_KEYED_MAPS.includes(first) && Object.hasOwn(legacy, parts[1])) {
|
|
98
|
+
return canonicalConfigKey([first, legacy[parts[1]], ...parts.slice(2)].join("."), legacy);
|
|
99
|
+
}
|
|
100
|
+
if (parts.length === 3 && first === "roles") {
|
|
101
|
+
const role = CONFIG_KEY_RENAMES.find((r) => r.scope === "role" && r.from === parts[2]);
|
|
102
|
+
if (role)
|
|
103
|
+
return `roles.${parts[1]}.${role.to}`;
|
|
104
|
+
}
|
|
105
|
+
return key;
|
|
106
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The config keys the dashboard's Settings view may show and edit: the top-level settings
|
|
3
|
-
* an operator changes often from the browser. Everything else —
|
|
3
|
+
* an operator changes often from the browser. Everything else — customRoles, the per-role
|
|
4
4
|
* maps — stays CLI/director territory (the plan's curation decision, 2026-10-02). One
|
|
5
5
|
* constant so handleConfig (GET /api/config) and handleConfigSet (POST /api/config-set)
|
|
6
6
|
* cannot drift apart on what is editable.
|
|
@@ -21,6 +21,6 @@ export const EDITABLE_CONFIG_KEYS = [
|
|
|
21
21
|
"quietHours",
|
|
22
22
|
"notify",
|
|
23
23
|
// The per-backend section (plans/agent-backends.md, part 4a/7): the whole `agents` object is
|
|
24
|
-
// edited here as JSON. The CLI's dotted `
|
|
25
|
-
"
|
|
24
|
+
// edited here as JSON. The CLI's dotted `backends.<id>.bin`/`.args` merge one field at a time.
|
|
25
|
+
"backends",
|
|
26
26
|
];
|