tumwater 0.2.0 → 0.4.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 +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +55 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +6 -4
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +404 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +281 -55
- package/dist/src/backlog/backlog-cli-view.js +255 -0
- package/dist/src/backlog/backlog-cli.js +91 -335
- package/dist/src/backlog/backlog-commit.js +20 -13
- package/dist/src/backlog/backlog-eligibility.js +193 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +264 -0
- package/dist/src/backlog/backlog-release.js +134 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-guardrails.js +136 -0
- package/dist/src/baseline/main-red.js +153 -36
- package/dist/src/baseline/main-watch.js +232 -0
- package/dist/src/brief/brief-edit.js +12 -12
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +31 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +65 -3
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +181 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +183 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +291 -598
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +525 -0
- package/dist/src/cli/cli-start-summary.js +167 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
- package/dist/src/cli/completion.js +169 -22
- package/dist/src/cli/config-commands.js +58 -26
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +258 -82
- package/dist/src/cli/log-commands.js +164 -85
- package/dist/src/cli/question-commands.js +211 -10
- package/dist/src/cli.js +173 -45
- package/dist/src/collections.js +56 -13
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +16 -1
- package/dist/src/config/config-fallbacks.js +94 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +169 -222
- package/dist/src/config/config-views.js +136 -54
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +164 -265
- package/dist/src/config/config.js +53 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
- package/dist/src/doctor/doctor-backlog-plans.js +398 -0
- package/dist/src/doctor/doctor-backlog.js +249 -512
- package/dist/src/doctor/doctor-check-names.js +85 -0
- package/dist/src/doctor/doctor-checks.js +61 -593
- package/dist/src/doctor/doctor-claude-checks.js +142 -0
- package/dist/src/doctor/doctor-codex-checks.js +276 -0
- package/dist/src/doctor/doctor-contention-checks.js +506 -0
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +80 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +216 -537
- package/dist/src/doctor/doctor-notify.js +9 -5
- package/dist/src/doctor/doctor-opencode-checks.js +164 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +397 -0
- package/dist/src/doctor/doctor.js +126 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +21 -12
- package/dist/src/events/event-format.js +185 -89
- package/dist/src/events/event-labels.js +11 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +52 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +80 -26
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +65 -60
- package/dist/src/failure/failure-render.js +29 -21
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +35 -35
- package/dist/src/files/file-queue.js +17 -0
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +88 -225
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +101 -143
- package/dist/src/gates/maintenance-quota.js +37 -30
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +89 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +54 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +43 -19
- package/dist/src/gui/gui-backlog-endpoints.js +156 -0
- package/dist/src/gui/gui-command.js +26 -13
- package/dist/src/gui/gui-config-commands.js +168 -80
- package/dist/src/gui/gui-endpoint-commands.js +5 -4
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +235 -13
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +97 -0
- package/dist/src/gui/gui-server.js +26 -5
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +155 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +190 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +28 -17
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +58 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +45 -19
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +214 -0
- package/dist/src/merge/merge-queue-render.js +48 -11
- package/dist/src/merge/merge-queue.js +52 -15
- package/dist/src/merge/merge-requeue.js +103 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +27 -11
- package/dist/src/merge/merge-to-main.js +77 -26
- package/dist/src/merge/merge-top.js +83 -0
- package/dist/src/merge/merge-verify.js +75 -18
- package/dist/src/notes-cli.js +35 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +352 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
- package/dist/src/orchestrator/orchestrator.js +129 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +245 -0
- package/dist/src/orchestrator/scheduling-skip.js +195 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +126 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +26 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +150 -217
- package/dist/src/prompt/notes-backlog.js +184 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +131 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +188 -124
- package/dist/src/report/report-fold.js +130 -54
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +136 -65
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +23 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +112 -1
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +239 -0
- package/dist/src/review/protected-edits.js +24 -10
- package/dist/src/review/quarantine-render.js +65 -0
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +180 -401
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +127 -12
- package/dist/src/roles/role-guidance.js +65 -20
- package/dist/src/roles/role-labels.js +7 -5
- package/dist/src/roles/role-render.js +108 -56
- package/dist/src/roles/role-view.js +95 -56
- package/dist/src/roles/roles.js +33 -33
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/run/qa-coverage.js +125 -0
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +175 -0
- package/dist/src/run/run-detail-render.js +91 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +178 -0
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +231 -0
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +216 -0
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +191 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +109 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +103 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claim-entries.js +49 -0
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +49 -17
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +43 -28
- package/dist/src/status/status-roles.js +50 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +7 -8
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +65 -18
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +60 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +78 -13
- package/dist/src/ui/badges.js +99 -38
- package/dist/src/ui/gui/gui-client-backlog.js +314 -0
- package/dist/src/ui/gui/gui-client-boot.js +89 -61
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
- package/dist/src/ui/gui/gui-client-drawer.js +155 -289
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +437 -68
- package/dist/src/ui/gui/gui-client-labels.js +6 -3
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +60 -52
- package/dist/src/ui/gui/gui-client-pending.js +405 -24
- package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
- package/dist/src/ui/gui/gui-client-report.js +105 -430
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +163 -5
- package/dist/src/ui/gui/gui-client-team.js +36 -465
- package/dist/src/ui/gui/gui-client.js +38 -26
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +10 -7
- package/dist/src/ui/gui/gui-styles.js +38 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +364 -49
- package/dist/src/ui/status-render.js +109 -82
- package/dist/src/ui/team-alerts.js +728 -62
- package/dist/src/ui/tone.js +12 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +53 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +4 -12
- package/dist/src/ui/tui/tui-editor-backends.js +127 -0
- package/dist/src/ui/tui/tui-editor-brief.js +14 -36
- package/dist/src/ui/tui/tui-editor-budget.js +36 -39
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
- package/dist/src/ui/tui/tui-editor-config.js +99 -0
- package/dist/src/ui/tui/tui-editor-entry.js +39 -64
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +46 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
- package/dist/src/ui/tui/tui-editor-interval.js +29 -16
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +9 -29
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
- package/dist/src/ui/tui/tui-editor-retire.js +5 -13
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +101 -34
- package/dist/src/ui/tui/tui-editor-slots.js +24 -0
- package/dist/src/ui/tui/tui-editors.js +44 -2
- package/dist/src/ui/tui/tui-frame.js +68 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +33 -9
- package/dist/src/ui/tui/tui-pane-activity.js +35 -19
- package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +168 -8
- package/dist/src/ui/tui/tui-pane-history.js +446 -82
- package/dist/src/ui/tui/tui-pane-pending.js +539 -15
- package/dist/src/ui/tui/tui-pane-queued.js +78 -57
- package/dist/src/ui/tui/tui-pane-report.js +54 -16
- package/dist/src/ui/tui/tui-pane-session.js +34 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +91 -28
- package/dist/src/ui/tui/tui-panes.js +153 -34
- package/dist/src/ui/tui/tui.js +18 -15
- package/dist/src/verdict/fix-claim.js +129 -33
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +77 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/leftover.js +0 -130
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/qa-coverage.js +0 -108
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/** The settlement half of a backend agent run (the split of src/agent/run-agent.ts along the
|
|
2
|
+
* same seam the runner-run → runner-outcome split used): everything that happens after the
|
|
3
|
+
* run's live feed — the spawn-`error` result, the `exit` orphan sweep, and the `close`-time
|
|
4
|
+
* flush/reconcile/classify that builds the `PiRunResult`. run-agent.ts keeps the lifecycle
|
|
5
|
+
* (pre-spawn setup, `prepare`, spawn, decoders, stream pipeline, watchdogs, the feed) and
|
|
6
|
+
* delegates its `error`/`exit`/`close` listeners here, so the streaming half stays readable.
|
|
7
|
+
*
|
|
8
|
+
* Each handler takes the run's live state (`RunSettleState`, defined in run-agent.ts and
|
|
9
|
+
* imported here only as a type) and reads what it closes over at the moment its event fires —
|
|
10
|
+
* the live flags it needs are getters on that state. There is no runtime import cycle:
|
|
11
|
+
* run-agent imports these functions at runtime, and this module imports only run-agent's type.
|
|
12
|
+
*/
|
|
13
|
+
import { backendBinConfigKey, backendBinSourceLabel } from "../pi/pi-bin.js";
|
|
14
|
+
import { terminateChild } from "../process/process.js";
|
|
15
|
+
import { leftoversSweptMessage, sweepRunMarker } from "../process/run-marker.js";
|
|
16
|
+
/** Prefix of PiRunResult.errorMessage when the agent process never started (no session file
|
|
17
|
+
* is created for such a run). The resolved binary follows it, so a configured agentBin
|
|
18
|
+
* reads "failed to spawn <bin>" rather than blaming an ambient "pi". */
|
|
19
|
+
const SPAWN_ERROR_PREFIX = "failed to spawn";
|
|
20
|
+
/** The spawn-error message for a resolved binary: the default source keeps today's text
|
|
21
|
+
* ("failed to spawn pi: …"); a configured source names the binary and where it came from —
|
|
22
|
+
* the backend's own env var and config key (backendBinConfigKey) — so a wrong
|
|
23
|
+
* `backends.claude.bin` does not read as pi's legacy `agentBin`. */
|
|
24
|
+
function spawnErrorMessage(resolved, errMessage, backend) {
|
|
25
|
+
const source = resolved.source === "default"
|
|
26
|
+
? ""
|
|
27
|
+
: ` (resolved from ${backendBinSourceLabel(resolved.source, backend.binEnv, backendBinConfigKey(backend.id))})`;
|
|
28
|
+
return `${SPAWN_ERROR_PREFIX} ${resolved.bin}${source}: ${errMessage}`;
|
|
29
|
+
}
|
|
30
|
+
/** The failed result of a run that never started: all counters zero, all flags false. */
|
|
31
|
+
export function setupFailureResult(agent, message) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
finalText: "",
|
|
35
|
+
nothingToDo: false,
|
|
36
|
+
refused: false,
|
|
37
|
+
agent,
|
|
38
|
+
outputTokens: 0,
|
|
39
|
+
promptTokens: 0,
|
|
40
|
+
cacheReadTokens: 0,
|
|
41
|
+
preEditPromptTokens: 0,
|
|
42
|
+
peakContextTokens: 0,
|
|
43
|
+
turns: 0,
|
|
44
|
+
costUsd: 0,
|
|
45
|
+
errorMessage: message,
|
|
46
|
+
timedOut: false,
|
|
47
|
+
timedOutProgressing: false,
|
|
48
|
+
aborted: false,
|
|
49
|
+
quietKilled: false,
|
|
50
|
+
contextExceeded: false,
|
|
51
|
+
transientServerTimeout: false,
|
|
52
|
+
transientPiCrash: false,
|
|
53
|
+
transientRateLimit: false,
|
|
54
|
+
transientBackend: false,
|
|
55
|
+
configError: false,
|
|
56
|
+
finalMessageContentless: false,
|
|
57
|
+
producedAssistantContent: false,
|
|
58
|
+
compacted: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Every PiRunResult is built here from what the parser has seen plus how the run ended, so
|
|
62
|
+
* adding a field to PiRunResult touches this single place. The spawn-error path passes only
|
|
63
|
+
* an errorMessage override: at that point no output ever arrived, so the parser holds exactly
|
|
64
|
+
* its initial values — which is precisely what a failed spawn reports. */
|
|
65
|
+
function resultFromParser(state, overrides) {
|
|
66
|
+
const parser = state.parser;
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
finalText: parser.finalText,
|
|
70
|
+
nothingToDo: parser.declaredNothingToDo,
|
|
71
|
+
refused: parser.refused,
|
|
72
|
+
refusedReason: parser.refusedReason || undefined,
|
|
73
|
+
verdictText: parser.verdictText || undefined,
|
|
74
|
+
agent: state.backend.id,
|
|
75
|
+
outputTokens: parser.outputTokens,
|
|
76
|
+
promptTokens: parser.promptTokens,
|
|
77
|
+
cacheReadTokens: parser.cacheReadTokens,
|
|
78
|
+
preEditPromptTokens: parser.preEditPromptTokens,
|
|
79
|
+
firstEditTurn: parser.firstEditTurn,
|
|
80
|
+
peakContextTokens: parser.peakContextTokens,
|
|
81
|
+
turns: parser.turns,
|
|
82
|
+
costUsd: parser.costUsd,
|
|
83
|
+
stopReason: parser.stopReason,
|
|
84
|
+
errorMessage: undefined,
|
|
85
|
+
timedOut: false,
|
|
86
|
+
timedOutProgressing: false,
|
|
87
|
+
quietKilled: false,
|
|
88
|
+
aborted: state.aborted(),
|
|
89
|
+
contextExceeded: parser.contextExceeded,
|
|
90
|
+
transientServerTimeout: parser.transientServerTimeout,
|
|
91
|
+
transientRateLimit: parser.transientRateLimit,
|
|
92
|
+
transientBackend: parser.transientBackend,
|
|
93
|
+
backendKind: parser.backendFailureKind,
|
|
94
|
+
configError: parser.configError,
|
|
95
|
+
retryAfterSeconds: parser.retryAfterSeconds,
|
|
96
|
+
transientPiCrash: false,
|
|
97
|
+
fallbackClone: state.fallbackClone(),
|
|
98
|
+
finalMessageContentless: parser.finalMessageContentless,
|
|
99
|
+
producedAssistantContent: parser.producedAssistantContent,
|
|
100
|
+
compacted: parser.compacted,
|
|
101
|
+
budgetCapped: parser.budgetCapped,
|
|
102
|
+
resumeMissing: parser.resumeMissing,
|
|
103
|
+
billing: parser.billing,
|
|
104
|
+
rateWindows: parser.rateWindows.length > 0 ? parser.rateWindows : undefined,
|
|
105
|
+
usedOverage: parser.usedOverage,
|
|
106
|
+
...overrides,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** The run's `error` event: the process never spawned. Only the spawn-error cause is set; no
|
|
110
|
+
* output ever arrived, so the parser still holds its initial values (what a failed spawn
|
|
111
|
+
* reports), and `finish` settles the run so the `close` event no-ops. */
|
|
112
|
+
export function settleError(state, err) {
|
|
113
|
+
state.finish(resultFromParser(state, {
|
|
114
|
+
errorMessage: spawnErrorMessage(state.resolved, err.message, state.backend),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
/** The run's `exit` event: sweep whatever the run (or a tool call it started) left running,
|
|
118
|
+
* however the run ended, a normal exit included, in two passes:
|
|
119
|
+
*
|
|
120
|
+
* 1. The group pi led: pi exits before its orphans — a tool call's `(server &)` or
|
|
121
|
+
* `cd … && server &` is reparented to PID 1 but stays in pi's group — and the harness
|
|
122
|
+
* cannot rely on the model's own cleanup. On 'exit', not 'close': pi has just been reaped,
|
|
123
|
+
* so the pgid still names only its group (never reissued while a member lives), and an
|
|
124
|
+
* orphan holding pi's stdio open dies now instead of holding 'close' — and the run — open.
|
|
125
|
+
* Synchronous, so it adds nothing to the run's resolution; on the kill paths it re-sends a
|
|
126
|
+
* SIGTERM the group already had.
|
|
127
|
+
*
|
|
128
|
+
* 2. The marker sweep: pi 0.87.1's bash tool starts every command detached, in its own
|
|
129
|
+
* process group, so whatever a tool call backgrounds sits in a group whose leader is not
|
|
130
|
+
* pi and pass 1 never reaches it. Every process the run started carries the run's
|
|
131
|
+
* TUMWATER_RUN mark in its environment, so the same-user scan finds them wherever they
|
|
132
|
+
* were reparented. Fire-and-forget: one process-table scan, never awaited — the run
|
|
133
|
+
* resolves on pi's output, and the sweep's own SIGKILL escalation (10 s) is unref'd like
|
|
134
|
+
* signalTree's.
|
|
135
|
+
*/
|
|
136
|
+
export function settleExit(state) {
|
|
137
|
+
terminateChild(state.child);
|
|
138
|
+
void sweepRunMarker(state.runMarker)
|
|
139
|
+
.then((n) => {
|
|
140
|
+
const msg = leftoversSweptMessage(state.kind, n, state.wasKilled());
|
|
141
|
+
if (msg)
|
|
142
|
+
state.onLeftoversSwept?.(msg);
|
|
143
|
+
})
|
|
144
|
+
.catch(() => { });
|
|
145
|
+
}
|
|
146
|
+
/** The run's `close` event: the process died and all its streams closed. Flush what the
|
|
147
|
+
* decoders held back, reconcile the backend's post-run work, classify the exit cause, and
|
|
148
|
+
* settle the run's result. */
|
|
149
|
+
export function settleClose(state, code) {
|
|
150
|
+
const { backend, runSpec, invocation, translator, onLine } = state;
|
|
151
|
+
// Flush any bytes the decoder held back at stream end and any unterminated final line so the
|
|
152
|
+
// tail is not lost, then let the translator flush its own pending canonical line(s) — a
|
|
153
|
+
// batching backend's unclosed last assistant message, which a killed run never closes with a
|
|
154
|
+
// later event.
|
|
155
|
+
state.feedCanonical(state.decoder.end(), true);
|
|
156
|
+
for (const canonical of translator.finalize?.() ?? [])
|
|
157
|
+
state.parser.feed(canonical + "\n", onLine);
|
|
158
|
+
// A blind image read on an authoring run deliberately killed it, so its exit is a kill like
|
|
159
|
+
// the watchdogs' — the half-done work is discarded.
|
|
160
|
+
const killed = state.wasKilled();
|
|
161
|
+
// The backend's post-run reconciliation (opencode's authoritative session export), only when
|
|
162
|
+
// the run is not already settled (a failed spawn settles in the 'error' handler before
|
|
163
|
+
// 'close') and was not killed — a killed run's session is half-done and no export should
|
|
164
|
+
// spend its 10 s on it. Its lines are written and parsed like streamed ones, so they reach
|
|
165
|
+
// the raw log and the parser's authoritative fold (run_cost/run_usage).
|
|
166
|
+
if (!state.settled() && !killed) {
|
|
167
|
+
for (const canonical of backend.afterRun?.(runSpec, invocation, translator) ?? []) {
|
|
168
|
+
state.parser.feed(canonical + "\n", onLine);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const failed = killed ||
|
|
172
|
+
state.parser.stopReason === "error" ||
|
|
173
|
+
(code !== 0 && !state.parser.finalText.trim());
|
|
174
|
+
// The backend classifies the exit's stderr (pi: the transient JSON-parse crash, a
|
|
175
|
+
// provider-wide backend failure, a permanent config error). The runner merges that with the
|
|
176
|
+
// parser's own event-derived flags — one exit has one cause.
|
|
177
|
+
const stderr = state.stderr();
|
|
178
|
+
const cls = backend.classifyExit({ code, stderr, killed });
|
|
179
|
+
const stderrBackend = cls.backendKind;
|
|
180
|
+
const configError = !killed &&
|
|
181
|
+
cls.transientPiCrash !== true &&
|
|
182
|
+
stderrBackend === undefined &&
|
|
183
|
+
(state.parser.configError || cls.configError === true);
|
|
184
|
+
// The blind-read failure names both the file and the model (plans/image-input.md
|
|
185
|
+
// "Failing the run"): take it over any generic exit text so lastError is actionable.
|
|
186
|
+
const blindRead = state.blindRead();
|
|
187
|
+
const errorMessage = blindRead !== undefined
|
|
188
|
+
? `read ${blindRead}, but ${state.config.model ?? "the model"} cannot read images`
|
|
189
|
+
: state.aborted()
|
|
190
|
+
? "stopped by tumwater shutdown"
|
|
191
|
+
: state.wd.quietKilled
|
|
192
|
+
? `killed as hung: no agent progress for over ${state.config.quietTimeoutSeconds}s`
|
|
193
|
+
: state.wd.timedOut
|
|
194
|
+
? state.wd.timedOutProgressing
|
|
195
|
+
? `timed out after ${state.wd.timeoutBudgetMs / 1000}s while still making progress — session and worktree edits preserved for resume`
|
|
196
|
+
: `timed out after ${state.wd.timeoutBudgetMs / 1000}s`
|
|
197
|
+
: (state.parser.errorMessage ?? (failed ? stderr.trim().slice(-500) || `the agent exited ${code}` : undefined));
|
|
198
|
+
state.finish(resultFromParser(state, {
|
|
199
|
+
ok: !failed,
|
|
200
|
+
transientPiCrash: cls.transientPiCrash === true,
|
|
201
|
+
transientBackend: state.parser.transientBackend || cls.transientBackend === true,
|
|
202
|
+
backendKind: state.parser.backendFailureKind ?? stderrBackend,
|
|
203
|
+
configError,
|
|
204
|
+
// Kept distinct on purpose: a hung tool call leaves its session and worktree edits
|
|
205
|
+
// intact, so the runner resumes them (quiet_killed) instead of discarding them as an
|
|
206
|
+
// unfulfilled timeout does. A deadline that fired on a run still making progress gets
|
|
207
|
+
// the same treatment (BUGS.md 2026-09-29).
|
|
208
|
+
timedOut: state.wd.timedOut,
|
|
209
|
+
timedOutProgressing: state.wd.timedOut && state.wd.timedOutProgressing,
|
|
210
|
+
quietKilled: state.wd.quietKilled,
|
|
211
|
+
imageBlind: state.parser.imageBlind,
|
|
212
|
+
errorMessage,
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import fs from "node:fs";
|
|
13
13
|
import path from "node:path";
|
|
14
|
-
import { ensureParentDir, readTextOrNull, statOrNull, terminateTornTail, walkFiles, writeTextAtomic } from "../files/files.js";
|
|
14
|
+
import { daysAgoMs, dirEntryNames, ensureParentDir, readTextOrNull, statOrNull, terminateTornTail, walkFiles, writeTextAtomic } from "../files/files.js";
|
|
15
15
|
import { isJsonObject } from "../files/json-object.js";
|
|
16
|
+
import { findRollout } from "./codex/codex-rollout.js";
|
|
16
17
|
/** The ledger file name every session dir holds. Deliberately a `.jsonl` name so it sits beside
|
|
17
18
|
* the backend's own session files; readers that count session files must exclude it. */
|
|
18
19
|
const SESSION_LEDGER_FILE = "sessions.jsonl";
|
|
@@ -21,7 +22,7 @@ export function sessionLedgerPath(dir) {
|
|
|
21
22
|
return path.join(dir, SESSION_LEDGER_FILE);
|
|
22
23
|
}
|
|
23
24
|
/** Append one run's record to `dir`'s ledger, before its spawn. Best-effort: an unwritable
|
|
24
|
-
* session dir or a full disk must degrade to "no resumable session" on a later
|
|
25
|
+
* session dir or a full disk must degrade to "no resumable session" on a later run, never
|
|
25
26
|
* fail the run it was trying to describe. A crash or power loss mid-append leaves the previous
|
|
26
27
|
* record without its newline; terminating that torn tail first keeps the next record on its
|
|
27
28
|
* own line instead of gluing the two into one unparseable line that loses both runs' records
|
|
@@ -42,14 +43,8 @@ export function recordSession(dir, record) {
|
|
|
42
43
|
* unregistered backend. The ledger file shares the dir and is a `.jsonl` too, so it is
|
|
43
44
|
* excluded — it is the harness's record of runs, not a session. */
|
|
44
45
|
export function hasResumableSession(dir) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.readdirSync(dir)
|
|
48
|
-
.some((f) => f.endsWith(".jsonl") && f !== SESSION_LEDGER_FILE);
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
return false; // Missing directory — nothing to resume.
|
|
52
|
-
}
|
|
46
|
+
// A missing directory reads empty — nothing to resume.
|
|
47
|
+
return dirEntryNames(dir).some((f) => f.endsWith(".jsonl") && f !== SESSION_LEDGER_FILE);
|
|
53
48
|
}
|
|
54
49
|
/** Every valid record in `dir`'s ledger, oldest first. A missing ledger reads as no records;
|
|
55
50
|
* a torn, malformed, or shape-invalid line is skipped rather than poisoning the rest (a crash
|
|
@@ -136,9 +131,11 @@ export function sessionResumable(dir, opts, sessionFileExists) {
|
|
|
136
131
|
/** Drop ledger lines older than `days` from every session dir under `sessionsRoot`. The ledger
|
|
137
132
|
* file's mtime keeps it young while its role runs, so pruneOldFiles never ages it out; trimming
|
|
138
133
|
* its lines is what actually bounds it, under the same window the session files prune by.
|
|
139
|
-
* Returns the number of lines removed; a missing root or unreadable dir reads as nothing to do.
|
|
134
|
+
* Returns the number of lines removed; a missing root or unreadable dir reads as nothing to do.
|
|
135
|
+
* A ledger that cannot be rewritten (a full disk, a read-only dir) is left as it was and not
|
|
136
|
+
* counted, because this pass runs in the orchestrator's poll loop, where a throw ends the team. */
|
|
140
137
|
export function pruneSessionLedgers(sessionsRoot, days) {
|
|
141
|
-
const cutoff =
|
|
138
|
+
const cutoff = daysAgoMs(days);
|
|
142
139
|
let pruned = 0;
|
|
143
140
|
walkFiles(sessionsRoot, (file, entry) => {
|
|
144
141
|
if (!entry.isFile() || entry.name !== SESSION_LEDGER_FILE)
|
|
@@ -148,22 +145,29 @@ export function pruneSessionLedgers(sessionsRoot, days) {
|
|
|
148
145
|
return;
|
|
149
146
|
const lines = text.split("\n").filter((line) => line !== "");
|
|
150
147
|
const kept = [];
|
|
148
|
+
let dropped = 0;
|
|
151
149
|
for (const line of lines) {
|
|
152
150
|
let at;
|
|
153
151
|
try {
|
|
154
152
|
at = JSON.parse(line).at;
|
|
155
153
|
}
|
|
156
154
|
catch {
|
|
157
|
-
|
|
155
|
+
dropped += 1; // Malformed line: no timestamp to keep, so it ages out.
|
|
158
156
|
continue;
|
|
159
157
|
}
|
|
160
158
|
if (typeof at === "number" && at >= cutoff)
|
|
161
159
|
kept.push(line);
|
|
162
160
|
else
|
|
163
|
-
|
|
161
|
+
dropped += 1;
|
|
164
162
|
}
|
|
165
163
|
if (kept.length !== lines.length) {
|
|
166
|
-
|
|
164
|
+
try {
|
|
165
|
+
writeTextAtomic(file, kept.length > 0 ? kept.join("\n") + "\n" : "");
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
pruned += dropped;
|
|
167
171
|
}
|
|
168
172
|
});
|
|
169
173
|
return pruned;
|
|
@@ -180,7 +184,7 @@ const SESSION_DELETE_TIMEOUT_MS = 10_000;
|
|
|
180
184
|
* reported success, and prunes spill files with the same window. The caller (retention) runs
|
|
181
185
|
* this BEFORE pruneSessionLedgers trims the lines, so the full ledger is still in view. */
|
|
182
186
|
export function pruneOpencodeSessions(sessionsRoot, days, opts) {
|
|
183
|
-
const cutoff =
|
|
187
|
+
const cutoff = daysAgoMs(days);
|
|
184
188
|
let deleted = 0;
|
|
185
189
|
if (opts.bin !== undefined) {
|
|
186
190
|
walkFiles(sessionsRoot, (file) => {
|
|
@@ -223,3 +227,69 @@ export function pruneOpencodeSessions(sessionsRoot, days, opts) {
|
|
|
223
227
|
}
|
|
224
228
|
return deleted;
|
|
225
229
|
}
|
|
230
|
+
/** Delete each Codex session the ledger records as expired, with one
|
|
231
|
+
* `codex delete --force <id>` each (bounded), plus the rollout-file fallback. Codex's sessions
|
|
232
|
+
* live under `CODEX_HOME` (part 8/9), OUTSIDE `.tumwater/` and outside the session dirs this
|
|
233
|
+
* pass walks, so trimming the ledger line alone would orphan them; the CLI's `delete --force`
|
|
234
|
+
* is the one way to reach them headless (design finding 21: without `--force` Codex refuses to
|
|
235
|
+
* delete without a terminal). Best-effort and backend-specific, kept out of the generic
|
|
236
|
+
* `pruneSessionLedgers` line pass: it returns the deletes that reported success. Whether or not
|
|
237
|
+
* a delete succeeded the session's own rollout file is removed as the fallback (the OpenCode
|
|
238
|
+
* spill-file fallback's twin), so the ledger line and rollout do not outlive the window. The
|
|
239
|
+
* caller (retention) runs this BEFORE pruneSessionLedgers trims the lines, so the full ledger
|
|
240
|
+
* is still in view. */
|
|
241
|
+
export function pruneCodexSessions(sessionsRoot, days, opts) {
|
|
242
|
+
const cutoff = daysAgoMs(days);
|
|
243
|
+
let deleted = 0;
|
|
244
|
+
// Codex reuses a resumed session's UUID, so one id can carry several ledger records — each
|
|
245
|
+
// resume appends a fresh line with the same id (design §4 resumes by id). Only an id whose
|
|
246
|
+
// EVERY record is expired is a session safe to trim: an older expired record beside a
|
|
247
|
+
// still-fresh resume must not `delete` the live session or unlink its rollout, so the live
|
|
248
|
+
// ids are gathered first and every candidate id is checked against them.
|
|
249
|
+
const live = new Set();
|
|
250
|
+
const expired = new Set();
|
|
251
|
+
walkFiles(sessionsRoot, (file) => {
|
|
252
|
+
if (!file.endsWith(SESSION_LEDGER_FILE))
|
|
253
|
+
return;
|
|
254
|
+
for (const record of readSessions(path.dirname(file))) {
|
|
255
|
+
if (record.agent !== "codex")
|
|
256
|
+
continue;
|
|
257
|
+
(record.at >= cutoff ? live : expired).add(record.id);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
for (const id of expired) {
|
|
261
|
+
if (live.has(id))
|
|
262
|
+
continue;
|
|
263
|
+
if (opts.bin !== undefined) {
|
|
264
|
+
try {
|
|
265
|
+
const res = spawnSync(opts.bin, ["delete", "--force", id], {
|
|
266
|
+
env: opts.env,
|
|
267
|
+
timeout: SESSION_DELETE_TIMEOUT_MS,
|
|
268
|
+
stdio: "ignore",
|
|
269
|
+
});
|
|
270
|
+
if (res.status === 0)
|
|
271
|
+
deleted += 1;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// Best effort: an unwritable env or a missing binary must not fail the prune.
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Fallback: unlink the home's rollout for this session so neither the ledger line nor
|
|
278
|
+
// its file outlives the window. A real `codex delete` removes the rollout itself, so a
|
|
279
|
+
// successful delete has nothing left to unlink; a failed or skipped delete does, and
|
|
280
|
+
// this is what trims it.
|
|
281
|
+
const home = opts.env?.CODEX_HOME;
|
|
282
|
+
if (home !== undefined) {
|
|
283
|
+
const rollout = findRollout(home, id);
|
|
284
|
+
if (rollout !== null) {
|
|
285
|
+
try {
|
|
286
|
+
fs.unlinkSync(rollout);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// Best effort: a vanished or unwritable rollout is not a failure.
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return deleted;
|
|
295
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/** The per-backend subscription account state: which billing pays for a backend's runs, and
|
|
2
|
+
* the newest subscription usage-window reading each run reported (plans/agent-backends.md §7
|
|
3
|
+
* "Billing" and §8 "The subscription usage window"). Persisted at
|
|
4
|
+
* `.tumwater/state/<backend>-account.json` (plan design grounds/codex-backend.md "Groundwork:
|
|
5
|
+
* the subscription account is per backend", item 1), written only by the orchestrator process,
|
|
6
|
+
* so the sibling parts that throttle or display the subscription window read one answer instead
|
|
7
|
+
* of re-deriving it per surface.
|
|
8
|
+
*
|
|
9
|
+
* The file is a cache, never a source of truth: a run that reports billing overwrites it, and a
|
|
10
|
+
* missing or unreadable file reads as "no subscription". `resetsAt` is stored as epoch
|
|
11
|
+
* milliseconds (the run parser reports unix seconds; observeAccount converts once here), so
|
|
12
|
+
* consumers compare it to `Date.now()` directly. The claude account (src/agent/claude/
|
|
13
|
+
* claude-account.ts) is this module specialized to the "claude" backend: for its file the
|
|
14
|
+
* logical `plan` field is stored under the historical key `subscriptionType`, so claude's file
|
|
15
|
+
* keeps today's shape. */
|
|
16
|
+
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
17
|
+
import { finiteNumber, isJsonObject, setOwn } from "../files/json-object.js";
|
|
18
|
+
import { accountPath } from "../paths.js";
|
|
19
|
+
/** True when a failed run ended on a subscription usage window the account reported as
|
|
20
|
+
* `rejected` without overage — the signal the team hold throttles on (plans/agent-backends.md
|
|
21
|
+
* §8). Such a run is a transient of the world, not a failure of the session: its session and
|
|
22
|
+
* worktree edits are kept and resumed after `resetsAt` (part 6e) rather than committed as
|
|
23
|
+
* partial work or reset. API-billed runs and overage are budget/fallback matters, not a
|
|
24
|
+
* window hold, so they read false; a backend that reports no window also reads false (a pi
|
|
25
|
+
* 429, for example). */
|
|
26
|
+
export function subscriptionWindowRejected(run) {
|
|
27
|
+
return (run.transientRateLimit &&
|
|
28
|
+
run.billing === "subscription" &&
|
|
29
|
+
(run.rateWindows ?? []).some((w) => w.status === "rejected" && w.isUsingOverage !== true));
|
|
30
|
+
}
|
|
31
|
+
/** The subscription account's used-up usage window, or null: a window reading `rejected` with
|
|
32
|
+
* no overage, or (with `includeOverage`, the default) a window whose current reading shows
|
|
33
|
+
* `isUsingOverage`, and a `resetsAt` still ahead of `now`; among several, the one that resets
|
|
34
|
+
* last. With a `reserve` (config `backends.<id>.windowReserve`), a window whose utilization
|
|
35
|
+
* has reached its type's fraction counts too, and comes back with `reserve: true`; a rejected
|
|
36
|
+
* or overage window outranks a reserve one, so the flag is set only when no window is used up
|
|
37
|
+
* for real. The budget gate's window move (pollBudgetGate) reads the default; the team hold
|
|
38
|
+
* (windowObservation) passes `includeOverage: false`, because overage never holds a
|
|
39
|
+
* seam. Only subscription billing produces one; a missing account reads null. */
|
|
40
|
+
export function windowUsedUp(account, now, includeOverage = true, reserve) {
|
|
41
|
+
if (account?.billing !== "subscription")
|
|
42
|
+
return null;
|
|
43
|
+
let furthest = null;
|
|
44
|
+
let reserved = null;
|
|
45
|
+
for (const [type, w] of Object.entries(account.windows)) {
|
|
46
|
+
if (typeof w.resetsAt !== "number" || w.resetsAt <= now)
|
|
47
|
+
continue;
|
|
48
|
+
const used = w.isUsingOverage === true ? includeOverage : w.status === "rejected";
|
|
49
|
+
if (used) {
|
|
50
|
+
if (furthest === null || w.resetsAt > furthest.resetsAt)
|
|
51
|
+
furthest = { type, resetsAt: w.resetsAt };
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (windowReserved(w, reserve?.[type]) && (reserved === null || w.resetsAt > reserved.resetsAt))
|
|
55
|
+
reserved = { type, resetsAt: w.resetsAt, reserve: true };
|
|
56
|
+
}
|
|
57
|
+
return furthest ?? reserved;
|
|
58
|
+
}
|
|
59
|
+
/** Has this window's utilization reached its reserve fraction? False without a fraction or a
|
|
60
|
+
* reading. */
|
|
61
|
+
export function windowReserved(w, fraction) {
|
|
62
|
+
return fraction !== undefined && typeof w.utilization === "number" && w.utilization >= fraction;
|
|
63
|
+
}
|
|
64
|
+
/** Status severity for ordering: a rejected window limits, an allowed_warning nears its limit,
|
|
65
|
+
* every other reading (allowed/ok) rides at the bottom, and a reset one no longer limits. */
|
|
66
|
+
const WINDOW_RANK = { rejected: 3, allowed_warning: 2, reset: 0 };
|
|
67
|
+
/** The window type as a compact label: `five_hour` → `5h`, `seven_day` → `7d`; an unknown
|
|
68
|
+
* type falls back to its words with underscores as spaces. The one home of the window spelling
|
|
69
|
+
* the status badge and the role view share. */
|
|
70
|
+
export function windowLabel(type) {
|
|
71
|
+
const m = /^(\w+)_(hour|day|week)$/.exec(type);
|
|
72
|
+
if (!m)
|
|
73
|
+
return type.replace(/_/g, " ");
|
|
74
|
+
const words = { one: "1", two: "2", three: "3", four: "4", five: "5", six: "6", seven: "7", eight: "8", nine: "9", ten: "10" };
|
|
75
|
+
const unit = m[2] === "hour" ? "h" : m[2] === "day" ? "d" : "w";
|
|
76
|
+
const word = m[1] ?? "";
|
|
77
|
+
return `${words[word] ?? word}${unit}`;
|
|
78
|
+
}
|
|
79
|
+
/** The fraction of a subscription usage window consumed (0–1) as a whole-number percent: a
|
|
80
|
+
* `0.64` utilization reads `64`. The one home of the fraction→percent conversion, shared by
|
|
81
|
+
* the status badge, the doctor's hold reason and the event formatter so they render a window's
|
|
82
|
+
* fill identically (the browser dashboard's own window line is embedded client-side and keeps
|
|
83
|
+
* its inline copy). */
|
|
84
|
+
export function utilizationPct(utilization) {
|
|
85
|
+
return Math.round(utilization * 100);
|
|
86
|
+
}
|
|
87
|
+
/** Order two window entries by the windowEntries rule: status severity, then the reported
|
|
88
|
+
* utilization (a missing reading sorts below any number), then the earlier reset, then the
|
|
89
|
+
* type name. Exported so a list of per-backend blocks can be ordered by their primary entry
|
|
90
|
+
* the same way windowEntries orders one account's windows (part 3a/9). */
|
|
91
|
+
export function compareWindowEntries(a, b) {
|
|
92
|
+
const rank = (WINDOW_RANK[b.status] ?? 1) - (WINDOW_RANK[a.status] ?? 1);
|
|
93
|
+
if (rank !== 0)
|
|
94
|
+
return rank;
|
|
95
|
+
const util = (b.utilization ?? -1) - (a.utilization ?? -1);
|
|
96
|
+
if (util !== 0)
|
|
97
|
+
return util;
|
|
98
|
+
const reset = (a.resetsAt ?? Number.POSITIVE_INFINITY) - (b.resetsAt ?? Number.POSITIVE_INFINITY);
|
|
99
|
+
if (reset !== 0)
|
|
100
|
+
return reset;
|
|
101
|
+
return a.type.localeCompare(b.type);
|
|
102
|
+
}
|
|
103
|
+
/** The account's windows as a list, most constrained first (by compareWindowEntries). Empty
|
|
104
|
+
* when the account reported none. */
|
|
105
|
+
export function windowEntries(account) {
|
|
106
|
+
return Object.entries(account.windows)
|
|
107
|
+
.map(([type, w]) => ({ type, ...w }))
|
|
108
|
+
.sort((a, b) => compareWindowEntries(a, b));
|
|
109
|
+
}
|
|
110
|
+
/** Read the persisted account state for a backend, or null when it is absent or unreadable. A
|
|
111
|
+
* file without a usable top-level `observedAt` reads as absent (the plan's torn-file rule),
|
|
112
|
+
* and a window whose `resetsAt` is at or before `now` reads back with `status: "reset"` — the
|
|
113
|
+
* reader, not every consumer, decides that the window no longer limits. */
|
|
114
|
+
export function readAccount(root, backend, now = Date.now()) {
|
|
115
|
+
const doc = readJsonFile(accountPath(root, backend));
|
|
116
|
+
if (doc === null)
|
|
117
|
+
return null;
|
|
118
|
+
const billing = doc.billing === "subscription" || doc.billing === "api" ? (doc.billing) : undefined;
|
|
119
|
+
const observedAt = finiteNumber(doc.observedAt, Number.NaN);
|
|
120
|
+
if (billing === undefined || !Number.isFinite(observedAt))
|
|
121
|
+
return null;
|
|
122
|
+
const windows = {};
|
|
123
|
+
if (isJsonObject(doc.windows)) {
|
|
124
|
+
for (const [type, value] of Object.entries(doc.windows)) {
|
|
125
|
+
const window = readWindow(value, observedAt, now);
|
|
126
|
+
if (window !== undefined)
|
|
127
|
+
setWindow(windows, type, window);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
billing,
|
|
132
|
+
...(typeof doc.subscriptionType === "string" ? { plan: doc.subscriptionType } : {}),
|
|
133
|
+
observedAt,
|
|
134
|
+
windows,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** One window entry from the file, healed: a non-object or a status-less entry is dropped, a
|
|
138
|
+
* missing per-window `observedAt` falls back to the file's, and a past `resetsAt` flips the
|
|
139
|
+
* status to `reset` and drops the overage flags. */
|
|
140
|
+
function readWindow(value, fileObservedAt, now) {
|
|
141
|
+
if (!isJsonObject(value) || typeof value.status !== "string")
|
|
142
|
+
return undefined;
|
|
143
|
+
const observedAt = finiteNumber(value.observedAt, fileObservedAt);
|
|
144
|
+
const resetsAt = finiteNumber(value.resetsAt, Number.NaN);
|
|
145
|
+
const resets = Number.isFinite(resetsAt);
|
|
146
|
+
// utilization heals like the two stamps above: JSON's `1e999` parses to Infinity, and a
|
|
147
|
+
// non-finite fraction would render as `Infinity%` in the badges and poison the
|
|
148
|
+
// windowEntries comparator (Infinity - Infinity is NaN).
|
|
149
|
+
const utilization = finiteNumber(value.utilization, undefined);
|
|
150
|
+
const reset = resets && resetsAt <= now;
|
|
151
|
+
const window = {
|
|
152
|
+
status: reset ? "reset" : value.status,
|
|
153
|
+
...(resets ? { resetsAt } : {}),
|
|
154
|
+
...(utilization !== undefined ? { utilization } : {}),
|
|
155
|
+
// a reset window is no longer in overage: dropping both keeps every reader in agreement
|
|
156
|
+
...(!reset && typeof value.overageStatus === "string" ? { overageStatus: value.overageStatus } : {}),
|
|
157
|
+
...(!reset && value.isUsingOverage === true ? { isUsingOverage: true } : {}),
|
|
158
|
+
observedAt,
|
|
159
|
+
};
|
|
160
|
+
return window;
|
|
161
|
+
}
|
|
162
|
+
/** Store one window under its `rateLimitType`. A plain `windows[type] = window` loses a type
|
|
163
|
+
* of `__proto__`: the assignment hits Object.prototype's setter and rewrites the map's
|
|
164
|
+
* prototype instead of adding an own entry, so the window vanishes from every
|
|
165
|
+
* Object.entries/values reader. Defining the own property keeps even that key — the same
|
|
166
|
+
* inherited-key guard claude-stream's TOOL_NAMES lookup uses. */
|
|
167
|
+
function setWindow(windows, type, window) {
|
|
168
|
+
setOwn(windows, type, window);
|
|
169
|
+
}
|
|
170
|
+
/** Overwrite a backend's account file with `account`, creating the state directory. The
|
|
171
|
+
* orchestrator's one writer; a failure to write is the caller's to handle (best-effort at the
|
|
172
|
+
* run boundary). Written through writeJsonAtomic (tmp + rename): a crash or ENOSPC mid-write
|
|
173
|
+
* leaves the previous file whole, not a torn one the reader heals to "no subscription" — which
|
|
174
|
+
* would silently drop the billing answer and the window hold until the next run re-observed
|
|
175
|
+
* them. The claude file's `plan` is serialized under its historical `subscriptionType` key. */
|
|
176
|
+
export function writeAccount(root, backend, account) {
|
|
177
|
+
writeJsonAtomic(accountPath(root, backend), {
|
|
178
|
+
billing: account.billing,
|
|
179
|
+
...(account.plan !== undefined ? { subscriptionType: account.plan } : {}),
|
|
180
|
+
observedAt: account.observedAt,
|
|
181
|
+
windows: account.windows,
|
|
182
|
+
}, true);
|
|
183
|
+
}
|
|
184
|
+
/** Fold one run's billing and rate-window readings into a backend's account file: the run's
|
|
185
|
+
* `billing` overwrites the stored answer, and each reported window replaces the stored reading
|
|
186
|
+
* for its `type`, stamped with `observedAt`. A run that reported neither billing nor windows
|
|
187
|
+
* leaves the file untouched (and returns whatever was already there). */
|
|
188
|
+
export function observeAccount(root, backend, run, now = Date.now()) {
|
|
189
|
+
const existing = readAccount(root, backend, now);
|
|
190
|
+
const billing = run.billing ?? existing?.billing;
|
|
191
|
+
const reported = run.rateWindows ?? [];
|
|
192
|
+
if (billing === undefined || (run.billing === undefined && reported.length === 0))
|
|
193
|
+
return existing;
|
|
194
|
+
const windows = { ...(existing?.windows ?? {}) };
|
|
195
|
+
for (const w of reported)
|
|
196
|
+
setWindow(windows, w.type, windowFromRun(w, now));
|
|
197
|
+
const account = {
|
|
198
|
+
billing,
|
|
199
|
+
...(existing?.plan !== undefined ? { plan: existing.plan } : {}),
|
|
200
|
+
observedAt: now,
|
|
201
|
+
windows,
|
|
202
|
+
};
|
|
203
|
+
writeAccount(root, backend, account);
|
|
204
|
+
return account;
|
|
205
|
+
}
|
|
206
|
+
/** One run's rate-window reading in the file's shape: unix-seconds `resetsAt` becomes epoch ms,
|
|
207
|
+
* and absent optional fields stay absent. */
|
|
208
|
+
function windowFromRun(w, now) {
|
|
209
|
+
// The write choke point heals utilization like readWindow does, so a non-finite value
|
|
210
|
+
// handed straight to observeAccount can never re-enter the file through a save.
|
|
211
|
+
const utilization = finiteNumber(w.utilization, undefined);
|
|
212
|
+
return {
|
|
213
|
+
status: w.status,
|
|
214
|
+
...(typeof w.resetsAt === "number" ? { resetsAt: w.resetsAt * 1000 } : {}),
|
|
215
|
+
...(utilization !== undefined ? { utilization } : {}),
|
|
216
|
+
...(typeof w.overageStatus === "string" ? { overageStatus: w.overageStatus } : {}),
|
|
217
|
+
...(w.isUsingOverage === true ? { isUsingOverage: true } : {}),
|
|
218
|
+
observedAt: now,
|
|
219
|
+
};
|
|
220
|
+
}
|