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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** The backend-failure classifier for the pi seam: given provider/pi error text, decide
|
|
2
|
+
* whether it names a permanent configuration error or a transient backend failure, and which
|
|
3
|
+
* `BackendFailureKind` it is. This vocabulary is cross-cutting — the settle/team hold groups a
|
|
4
|
+
* storm by kind (src/team/team-hold.ts), the merge path pins a permanent config error, the pi
|
|
5
|
+
* adapter's stderr path (src/pi/pi-backend.ts) and the stream parser both classify — so it
|
|
6
|
+
* lives in its own module instead of hiding inside the stream parser (which used to be its
|
|
7
|
+
* only home: pi-backend.ts had to import a parser to get a classifier). Keep the per-kind
|
|
8
|
+
* regexes here so the transient gate and the kind classifier cannot disagree: they compose one
|
|
9
|
+
* `TRANSIENT_BACKEND` alternation and `backendKind` orders its checks the same way.
|
|
10
|
+
*
|
|
11
|
+
* The parser-local error heuristics that feed PiStreamParser's own flags — the context-overflow,
|
|
12
|
+
* predict-stream-timeout, and rate-limit+Retry-After patterns — stay in src/pi/pi-stream.ts:
|
|
13
|
+
* they are stream-parsing concerns (they drive `contextExceeded`/`transientServerTimeout`/
|
|
14
|
+
* `transientRateLimit`), not the cross-module "what kind of failure is this" vocabulary. */
|
|
15
|
+
const PERMANENT_CONFIG_ERROR = /\b(?:400|401|403|404)\b|model_not_supported|invalid_request_error/i;
|
|
16
|
+
/** True when an error text names a permanent provider configuration error (see
|
|
17
|
+
* PERMANENT_CONFIG_ERROR). Pure, exported so the merge path and the stderr path classify
|
|
18
|
+
* with one rule. */
|
|
19
|
+
export function isPermanentConfigError(text) {
|
|
20
|
+
return PERMANENT_CONFIG_ERROR.test(text);
|
|
21
|
+
}
|
|
22
|
+
/** undici's spelling of a severed HTTP stream: the provider (or its own idle timeout) cuts
|
|
23
|
+
* the request while pi is still reading it, and undici's fetch rejects with the bare word
|
|
24
|
+
* "terminated" — the 2026-08-22 idle-timeout diagnosis. Anchored to the END of the error text
|
|
25
|
+
* on purpose: the real message is the bare word, optionally behind a prefix and closing
|
|
26
|
+
* punctuation, while an unrelated error that merely contains the word ("worker terminated
|
|
27
|
+
* with exit code 1") must not read as a backend failure. Module-private: matchBackendFailure
|
|
28
|
+
* below, the parser, and backendKind all share the one instance. */
|
|
29
|
+
const STREAM_SEVERED = /(^|[^a-z])terminated[.!\s]*$/i;
|
|
30
|
+
/** The connection-class backend-failure phrases: fetch-level "Connection error.", the
|
|
31
|
+
* refusals and resets Node surfaces. One home shared by the TRANSIENT_BACKEND gate and the
|
|
32
|
+
* backendKind classifier, so a spelling added to one cannot go missing from the other. */
|
|
33
|
+
const CONNECTION_ERROR = /connection error|connection refused|connection reset|econn(refused|reset)/i;
|
|
34
|
+
/** The timeout-class backend-failure phrase (OpenAI-style "Request timed out"). Shared by the
|
|
35
|
+
* TRANSIENT_BACKEND gate and the backendKind classifier. */
|
|
36
|
+
const TIMEOUT_ERROR = /request timed out/i;
|
|
37
|
+
/** The server-class backend-failure phrases: 5xx status texts, oMLX's model-server pause, and
|
|
38
|
+
* a provider pausing itself team-wide with a 503 whose body carries prose rather than any
|
|
39
|
+
* status phrase ("Provider '<name>' is currently failing for model '<id>' and has been paused
|
|
40
|
+
* by the provider"). Shared by the TRANSIENT_BACKEND gate and the backendKind classifier. */
|
|
41
|
+
const SERVER_ERROR = /internal server error|bad gateway|service unavailable|gateway timeout|currently failing for model|paused by the provider/i;
|
|
42
|
+
/** The model-load spelling (oMLX's "Failed to load model"): its backendKind is the
|
|
43
|
+
* "model-load" default, so only the gate names the pattern. */
|
|
44
|
+
const MODEL_LOAD_ERROR = /failed to load model/i;
|
|
45
|
+
/** The backend-failure error texts pi actually surfaces, matched against every error text
|
|
46
|
+
* feedLine sees — the same shapes as the rate-limit regex's various renderings.
|
|
47
|
+
* Deliberately does NOT match the 429 texts: feedLine checks TRANSIENT_RATE_LIMIT first and
|
|
48
|
+
* only falls through here, so a rate limit stays a rate limit (with its Retry-After hint)
|
|
49
|
+
* and never counts as a backend failure. Composed from the per-kind patterns above so each
|
|
50
|
+
* kind's spellings — including the stream-severed one (STREAM_SEVERED) — live in exactly one
|
|
51
|
+
* place. Only matchBackendFailure and PiStreamParser.feedLine match it; backendKind below
|
|
52
|
+
* classifies which phrase matched. */
|
|
53
|
+
export const TRANSIENT_BACKEND = new RegExp(`${CONNECTION_ERROR.source}|${TIMEOUT_ERROR.source}|${SERVER_ERROR.source}|${MODEL_LOAD_ERROR.source}|${STREAM_SEVERED.source}`, "i");
|
|
54
|
+
/** Which backend-failure kind an error text belongs to — pure, exported so the parser's
|
|
55
|
+
* classification is unit-testable without a stream (pi-parser.test.ts feeds the texts
|
|
56
|
+
* directly). Each check is the same per-kind pattern the TRANSIENT_BACKEND gate composes, so
|
|
57
|
+
* the gate and the classifier cannot disagree; the order matches that alternation. */
|
|
58
|
+
export function backendKind(text) {
|
|
59
|
+
if (CONNECTION_ERROR.test(text))
|
|
60
|
+
return "connection";
|
|
61
|
+
if (TIMEOUT_ERROR.test(text))
|
|
62
|
+
return "timeout";
|
|
63
|
+
if (SERVER_ERROR.test(text))
|
|
64
|
+
return "server";
|
|
65
|
+
if (STREAM_SEVERED.test(text))
|
|
66
|
+
return "stream-severed";
|
|
67
|
+
return "model-load";
|
|
68
|
+
}
|
|
69
|
+
/** The backend-failure kind `text` names, or undefined when it names none — the stderr
|
|
70
|
+
* classifier (src/pi/pi-backend.ts's classifyExit) needs "no match" distinct from
|
|
71
|
+
* backendKind's "model-load" default. Gated by TRANSIENT_BACKEND, so a text the event path
|
|
72
|
+
* ignores is ignored on stderr too. Pure; exported for pi-backend.ts's classifyExit. */
|
|
73
|
+
export function matchBackendFailure(text) {
|
|
74
|
+
return TRANSIENT_BACKEND.test(text) ? backendKind(text) : undefined;
|
|
75
|
+
}
|
|
76
|
+
/** The valid `backendKind` values, for validating a translator-supplied string at the wire
|
|
77
|
+
* boundary (the same untrusted-JSON policy every other field read here applies). */
|
|
78
|
+
const BACKEND_FAILURE_KINDS = [
|
|
79
|
+
"connection",
|
|
80
|
+
"timeout",
|
|
81
|
+
"server",
|
|
82
|
+
"model-load",
|
|
83
|
+
"stream-severed",
|
|
84
|
+
];
|
|
85
|
+
export function isBackendFailureKind(value) {
|
|
86
|
+
return typeof value === "string" && BACKEND_FAILURE_KINDS.includes(value);
|
|
87
|
+
}
|
package/dist/src/pi/pi-models.js
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { enabledRoleIds } from "../config/config.js";
|
|
4
|
-
import { configForRole,
|
|
5
|
-
import {
|
|
4
|
+
import { configForRole, resolverConfig, reviewConfig } from "../config/config-views.js";
|
|
5
|
+
import { fallbackPair } from "../config/config-fallbacks.js";
|
|
6
|
+
import { seamAgent } from "../config/model-selector.js";
|
|
6
7
|
import { cachedByStat } from "../files/stat-cache.js";
|
|
7
8
|
import { isJsonObject } from "../files/json-object.js";
|
|
8
9
|
import { readJsonFile } from "../files/json-files.js";
|
|
10
|
+
import { expandHomeTilde } from "../files/files.js";
|
|
11
|
+
import { declaredWindow } from "../pi-extension/context-window.js";
|
|
12
|
+
import { piSettingsPath, readPiDefaultModel } from "./pi-settings.js";
|
|
13
|
+
import { opencodeModelFree, opencodeModelKey, readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
|
|
9
14
|
/** pi's model definitions — the custom providers and models they serve, with each model's
|
|
10
|
-
* declared cost. This is where a local (free)
|
|
15
|
+
* declared cost. This is where a local (free) team differs from an API one: unpriced or
|
|
11
16
|
* zero-cost models never accumulate spend against the daily budget cap, so the badge can
|
|
12
17
|
* read n/a instead of a dollar figure that can never move. */
|
|
13
|
-
export function piModelsPath() {
|
|
14
|
-
return path.join(
|
|
18
|
+
export function piModelsPath(env = process.env, homeDir = os.homedir()) {
|
|
19
|
+
return path.join(piAgentDir(env, homeDir), "models.json");
|
|
20
|
+
}
|
|
21
|
+
/** pi's config directory: `$PI_CODING_AGENT_DIR` when non-empty (a leading `~` expands against
|
|
22
|
+
* the home dir), else `~/.pi/agent`. This mirrors pi's `getAgentDir`. */
|
|
23
|
+
export function piAgentDir(env = process.env, homeDir = os.homedir()) {
|
|
24
|
+
const dir = env.PI_CODING_AGENT_DIR;
|
|
25
|
+
if (!dir)
|
|
26
|
+
return path.join(homeDir, ".pi", "agent");
|
|
27
|
+
return expandHomeTilde(dir, homeDir);
|
|
15
28
|
}
|
|
16
29
|
/** The cost components pi's models.json may declare. */
|
|
17
30
|
const COST_KEYS = ["input", "output", "cacheRead", "cacheWrite"];
|
|
@@ -33,7 +46,7 @@ function costIsFree(cost) {
|
|
|
33
46
|
});
|
|
34
47
|
}
|
|
35
48
|
/** Per-poll cache of the parsed definitions, keyed by models path: both dashboards poll
|
|
36
|
-
*
|
|
49
|
+
* teamModelsFree every second while models.json changes only when a user edits it — and it grows
|
|
37
50
|
* with the model catalog (every added provider/model entry), so an unchanged file costs one stat
|
|
38
51
|
* per poll instead of a re-read plus JSON.parse of the whole catalog. Any write invalidates via
|
|
39
52
|
* dev/ino/mtime/size (stat-cache.cachedByStat, same freshness check as the other polled files in
|
|
@@ -45,6 +58,17 @@ const providersCache = new Map();
|
|
|
45
58
|
* Exported for budget-gates.ts's per-tier resolution, which prices several candidate pairs
|
|
46
59
|
* against the same snapshot instead of re-entering this cache per pair.
|
|
47
60
|
*/
|
|
61
|
+
/** A provider/model pair's claimed image input from pi's definitions: `true` when the model
|
|
62
|
+
* declares `"image"` in its `input`, `false` when the pair resolves but declares no image
|
|
63
|
+
* support (input absent, or present without `"image"` — pi's own text-only default for the
|
|
64
|
+
* custom model), and `null` when the pair cannot be resolved at all (see resolveModelDef). The
|
|
65
|
+
* null case is left to the doctor's `tier models` check, which already fails such pairs. */
|
|
66
|
+
export function piModelImageInput(providers, provider, model) {
|
|
67
|
+
const def = resolveModelDef(providers, provider, model);
|
|
68
|
+
if (!def)
|
|
69
|
+
return null;
|
|
70
|
+
return Array.isArray(def.input) ? def.input.includes("image") : false;
|
|
71
|
+
}
|
|
48
72
|
export function readPiProviders(modelsPath) {
|
|
49
73
|
return cachedByStat(providersCache, modelsPath, // Keyed by path so distinct roots and test files never collide.
|
|
50
74
|
modelsPath, () => {
|
|
@@ -70,6 +94,47 @@ export function readPiProviders(modelsPath) {
|
|
|
70
94
|
// A copy: callers may treat the result as their own.
|
|
71
95
|
(providers) => new Map([...providers].map(([name, models]) => [name, [...models]])));
|
|
72
96
|
}
|
|
97
|
+
/** The context window a run's model declares, or null when it cannot be known: an unresolvable
|
|
98
|
+
* pair, a missing or unusable window, or a backend that is neither pi nor opencode (its catalog
|
|
99
|
+
* is elsewhere). A pi seam names its window from pi's models.json, running on pi's saved default
|
|
100
|
+
* (readPiDefaultModel) when it names no provider/model — exactly the model pi will start on. An
|
|
101
|
+
* opencode seam names its window from the models.dev catalog (part 6e/7): the same cache entry
|
|
102
|
+
* and key the free-ness check reads, so the window and the price always come from one model.
|
|
103
|
+
* A missing cache, an unknown model, or an unusable `context` reads null, as an unknown pi
|
|
104
|
+
* model does. */
|
|
105
|
+
export function seamContextWindow(seam, projectRoot, modelsPath = piModelsPath()) {
|
|
106
|
+
const agent = seamAgent(seam);
|
|
107
|
+
switch (agent) {
|
|
108
|
+
case "pi":
|
|
109
|
+
let { provider, model } = seam;
|
|
110
|
+
if (!provider || !model) {
|
|
111
|
+
const piDefault = readPiDefaultModel(piSettingsPath(modelsPath), projectRoot);
|
|
112
|
+
provider = piDefault?.provider;
|
|
113
|
+
model = piDefault?.model;
|
|
114
|
+
}
|
|
115
|
+
return declaredWindow(resolveModelDef(readPiProviders(modelsPath), provider, model)?.contextWindow);
|
|
116
|
+
case "opencode":
|
|
117
|
+
// The models.dev cache (readOpencodeCache, an empty cache when the file is missing) holds
|
|
118
|
+
// the window part 6a/7 parsed from `limit.context`, keyed by the gateway-qualified id the
|
|
119
|
+
// run actually invokes (opencodeModelKey) — the same key and entry the free-ness check
|
|
120
|
+
// prices by, so a seam's window never disagrees with the model it runs.
|
|
121
|
+
if (seam.model === undefined)
|
|
122
|
+
return null;
|
|
123
|
+
return declaredWindow(readOpencodeCache(projectRoot).models[opencodeModelKey(seam.model)]?.context);
|
|
124
|
+
default:
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** A provider's `baseUrl` from pi's models.json, or null when the file, the provider or the
|
|
129
|
+
* field is missing — what doctor probes to tell a local server that is down from one that is up. */
|
|
130
|
+
export function piProviderBaseUrl(modelsPath, provider) {
|
|
131
|
+
const doc = readJsonFile(modelsPath);
|
|
132
|
+
const providers = doc?.providers;
|
|
133
|
+
if (!isJsonObject(providers) || !Object.hasOwn(providers, provider))
|
|
134
|
+
return null;
|
|
135
|
+
const pdef = providers[provider];
|
|
136
|
+
return isJsonObject(pdef) && typeof pdef.baseUrl === "string" ? pdef.baseUrl : null;
|
|
137
|
+
}
|
|
73
138
|
/** Resolve a `provider`/`model` pair against pi's definitions — the shared first step of
|
|
74
139
|
* pairFree and cacheReadUnpriced. Undefined when the pair cannot be resolved at all: an
|
|
75
140
|
* omitted provider or model (pi's own default), a missing or malformed definitions file, an
|
|
@@ -118,50 +183,72 @@ function accountUsesOverage(account) {
|
|
|
118
183
|
return account !== null && Object.values(account.windows).some((w) => w.isUsingOverage === true);
|
|
119
184
|
}
|
|
120
185
|
/** Whether a seam may be called free: a subscription claude seam is free money (its marginal
|
|
121
|
-
* cost is zero, plans/agent-backends.md "Free-ness"),
|
|
122
|
-
* models.json prices it at zero
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
186
|
+
* cost is zero, plans/agent-backends.md "Free-ness"), a pi seam is free when pi's own
|
|
187
|
+
* models.json prices it at zero, and an opencode seam is free when the models.dev catalog
|
|
188
|
+
* prices its model at zero and no run has billed it (part 6b/7). A subscription that is
|
|
189
|
+
* spending into overage is NOT free: that overflow is API-priced real money, exactly as
|
|
190
|
+
* `runCostIsReal` treats a run that saw `isUsingOverage`. Everything else is NOT free — an
|
|
191
|
+
* API-billed claude seam, an overage subscription claude seam, an unresolvable or priced pi
|
|
192
|
+
* pair, an opencode model the catalog does not price at zero, and any other backend — so the
|
|
126
193
|
* badge never reads n/a while spend it cannot see could still reach the cap. The one central
|
|
127
194
|
* rule: the budget gate, the fallback view, and the status free-ness read all judge seams
|
|
128
|
-
* through it (plans/agent-backends.md "Free-ness" and "Every seam, checked").
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
195
|
+
* through it (plans/agent-backends.md "Free-ness" and "Every seam, checked"). The opencode
|
|
196
|
+
* `catalog` is threaded the way the claude `account` is: an absent catalog means "no cache to
|
|
197
|
+
* read", so an opencode seam reads NOT free, the safe direction for a budget gate. */
|
|
198
|
+
export function seamFree(providers, seam, account = null, catalog = null) {
|
|
199
|
+
const agent = seamAgent(seam);
|
|
200
|
+
// One free-ness rule per backend: pi's pair price, claude's subscription, opencode's catalog,
|
|
201
|
+
// and nothing for any other backend (its spend could still reach the cap, so the badge must
|
|
202
|
+
// not read n/a). The opencode arm reads the gateway-qualified id (part 6b/7) and changes the
|
|
203
|
+
// pi and claude arms nothing.
|
|
204
|
+
switch (agent) {
|
|
205
|
+
case "pi":
|
|
206
|
+
return pairFree(providers, seam.provider, seam.model);
|
|
207
|
+
case "claude":
|
|
208
|
+
return account?.billing === "subscription" && !accountUsesOverage(account);
|
|
209
|
+
case "opencode":
|
|
210
|
+
// A null catalog — no cache to read — and an unknown or billed model must all read NOT
|
|
211
|
+
// free (opencodeModelFree's safe direction): the badge never reads n/a while spend it
|
|
212
|
+
// cannot see could still reach the cap (plans/opencode-backend.md §7 "Free-ness").
|
|
213
|
+
// opencodeModelKey is the models.dev cache key for the run's resolved selector — the
|
|
214
|
+
// same `<provider>/<model>` join opencodeArgs invokes — so the seam's free-ness lookup
|
|
215
|
+
// never disagrees with the model it actually runs, and the free-ness key is the one
|
|
216
|
+
// billing key (part 6b/7).
|
|
217
|
+
return catalog !== null && seam.model !== undefined && opencodeModelFree(catalog, opencodeModelKey(seam.model));
|
|
218
|
+
default:
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
136
221
|
}
|
|
137
222
|
/** Whether a fallback-config pair may be called free: a `claude/<id>` pair is free while the
|
|
138
|
-
* account bills a subscription and is not using overage,
|
|
139
|
-
* models.json prices it at zero
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* (src/
|
|
143
|
-
*
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
223
|
+
* account bills a subscription and is not using overage, a pi provider/model is free when
|
|
224
|
+
* models.json prices it at zero, and an opencode pair is free when the catalog prices it at
|
|
225
|
+
* zero (part 6b/7). Any other agent namespace, or an unresolvable pair, is refused — the badge
|
|
226
|
+
* must not read n/a while a backend whose billing it cannot see runs. Exported so the budget
|
|
227
|
+
* gate (src/gates/budget-gates.ts), the status pause set (src/status/status-data.ts) and the
|
|
228
|
+
* free-ness view judge fallback pairs with the same predicate — neither may engage a backend
|
|
229
|
+
* their own badge would not call free. `catalog` is the opencode cache, threaded like `account`;
|
|
230
|
+
* absent, an opencode pair reads NOT free. */
|
|
231
|
+
export function piPairFree(providers, provider, model, account = null, catalog = null) {
|
|
232
|
+
return seamFree(providers, { provider, model }, account, catalog);
|
|
233
|
+
}
|
|
234
|
+
/** The free fallback pair the budget gate would engage for the team, or null when none is
|
|
148
235
|
* usable: the price-aware resolution of the default tier (fallbackPair over
|
|
149
236
|
* resolveTierFallbacks' borrow order), so a map whose `default` entry is absent or priced
|
|
150
237
|
* still names the free pair the tier borrows. Null for a missing, unresolvable, or priced
|
|
151
238
|
* pair — the gate's refusal, and the pair the badge/doctor must not advertise. */
|
|
152
|
-
export function fallbackModelPair(config, modelsPath = piModelsPath(), account = null) {
|
|
239
|
+
export function fallbackModelPair(config, modelsPath = piModelsPath(), account = null, catalog = null) {
|
|
153
240
|
const providers = readPiProviders(modelsPath);
|
|
154
|
-
return fallbackPair(config, (p) => piPairFree(providers, p.provider, p.model, account));
|
|
241
|
+
return fallbackPair(config, (p) => piPairFree(providers, p.provider, p.model, account, catalog));
|
|
155
242
|
}
|
|
156
243
|
/** True when the configured fallback model (plans/fallback-model.md) is one the budget gate may
|
|
157
244
|
* actually engage: configured at all, resolvable to a provider/model pair, and priced at zero in
|
|
158
245
|
* pi's definitions. False for every other case — no fallback, a half-named pair that would fall
|
|
159
246
|
* through to pi's own default, an unknown id, or a priced one — and the gate then pauses role
|
|
160
247
|
* loops exactly as it did before this feature. */
|
|
161
|
-
export function fallbackModelFree(config, modelsPath = piModelsPath(), account = null) {
|
|
162
|
-
return fallbackModelPair(config, modelsPath, account) !== null;
|
|
248
|
+
export function fallbackModelFree(config, modelsPath = piModelsPath(), account = null, catalog = null) {
|
|
249
|
+
return fallbackModelPair(config, modelsPath, account, catalog) !== null;
|
|
163
250
|
}
|
|
164
|
-
/** True when every model the
|
|
251
|
+
/** True when every model the team could use is free: each enabled role's effective
|
|
165
252
|
* provider/model (configForRole — the director included, it is a catalog role, resolved at
|
|
166
253
|
* its tier per plans/model-tiers.md) plus the reviewer's while review is on, plus the
|
|
167
254
|
* `strong` tier's own model whenever any role is enabled — the conflict resolver (part 4/8)
|
|
@@ -169,10 +256,11 @@ export function fallbackModelFree(config, modelsPath = piModelsPath(), account =
|
|
|
169
256
|
* Every pair is judged by `seamFree` above: a subscription claude seam is free, a pi seam is
|
|
170
257
|
* free when models.json prices it at zero, and everything else counts as NOT free — the badge
|
|
171
258
|
* must never read n/a while spend it is tracking could still reach the cap. With no enabled
|
|
172
|
-
* roles and review off there are no pairs at all, so the
|
|
259
|
+
* roles and review off there are no pairs at all, so the team cannot spend and the answer is
|
|
173
260
|
* true. `account` is the persisted claude billing answer (6a/7's reader), absent meaning "no
|
|
174
|
-
* subscription"
|
|
175
|
-
|
|
261
|
+
* subscription", and `catalog` the opencode models cache (absent, an opencode seam reads not
|
|
262
|
+
* free). */
|
|
263
|
+
export function teamModelsFree(config, modelsPath = piModelsPath(), account = null, catalog = null) {
|
|
176
264
|
const pairs = [];
|
|
177
265
|
for (const role of enabledRoleIds(config)) {
|
|
178
266
|
const rc = configForRole(config, role);
|
|
@@ -193,5 +281,5 @@ export function fleetModelsFree(config, modelsPath = piModelsPath(), account = n
|
|
|
193
281
|
if (pairs.length === 0)
|
|
194
282
|
return true;
|
|
195
283
|
const providers = readPiProviders(modelsPath);
|
|
196
|
-
return pairs.every((seam) => seamFree(providers, seam, account));
|
|
284
|
+
return pairs.every((seam) => seamFree(providers, seam, account, catalog));
|
|
197
285
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Whether a run's cost is real money that charges the daily cap (plans/agent-backends.md
|
|
2
2
|
* part 6a): an API-billed run, or a subscription run that saw overage. A plain subscription
|
|
3
3
|
* run's cost is notional — API-equivalent spend that never moves the cap. The one predicate
|
|
4
|
-
* both the tick-usage fold and the
|
|
4
|
+
* both the tick-usage fold and the merge accumulator split on, so the two cannot drift. */
|
|
5
5
|
export function runCostIsReal(run) {
|
|
6
6
|
return run.billing !== "subscription" || run.usedOverage === true;
|
|
7
7
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { cachedByStat } from "../files/stat-cache.js";
|
|
3
|
+
import { isJsonObject } from "../files/json-object.js";
|
|
4
|
+
import { readJsonFile } from "../files/json-files.js";
|
|
5
|
+
/** pi's own saved-default model, read from pi's `settings.json` beside the models catalog —
|
|
6
|
+
* the name a loop row shows when `tumwater.json` resolves no model and pi starts on its own
|
|
7
|
+
* default. This mirrors step 3 of pi 1.2.0's start-up model choice, "saved default from
|
|
8
|
+
* settings" (`findInitialModel` in pi's `dist/core/model-resolver.js`), for a run tumwater
|
|
9
|
+
* starts without `--provider`/`--model`. The reader does NOT model pi's last step: when the
|
|
10
|
+
* default pair is missing from pi's catalog or has no auth, pi picks the first available
|
|
11
|
+
* model — tumwater cannot resolve that pattern (see `enabledModels` below), so it shows
|
|
12
|
+
* nothing rather than a name pi may not run. */
|
|
13
|
+
/** pi's `settings.json` path, derived from the models catalog path so the two files share one
|
|
14
|
+
* seam: `snapshot` and `rolePayload` already take `modelsPath`, and the default
|
|
15
|
+
* `piModelsPath()` follows `$PI_CODING_AGENT_DIR` through `piAgentDir`. The secret-leak gate
|
|
16
|
+
* derives `auth.json` the same way. */
|
|
17
|
+
export function piSettingsPath(modelsPath) {
|
|
18
|
+
return path.join(path.dirname(modelsPath), "settings.json");
|
|
19
|
+
}
|
|
20
|
+
/** Per-poll cache of the parsed default, keyed by settings path: the snapshot runs on every
|
|
21
|
+
* poll while settings.json changes only when a user edits it, so an unchanged file costs one
|
|
22
|
+
* stat per poll instead of a re-read plus JSON.parse. Any write invalidates via
|
|
23
|
+
* dev/ino/mtime/size (stat-cache.cachedByStat, same freshness check as the other polled files);
|
|
24
|
+
* a missing or malformed file yields null and is not cached, so a mid-edit broken file recovers
|
|
25
|
+
* on the next poll exactly like before. */
|
|
26
|
+
const settingsCache = new Map();
|
|
27
|
+
/** The keys of pi's settings.json that decide the saved default (findInitialModel's inputs):
|
|
28
|
+
* the default pair, its preferred thinking, and the two scope patterns — `enabledModels`
|
|
29
|
+
* (pi starts on the first scoped model instead) and the project-local override file's own
|
|
30
|
+
* settings. */
|
|
31
|
+
const DEFAULT_KEYS = [
|
|
32
|
+
"defaultProvider",
|
|
33
|
+
"defaultModel",
|
|
34
|
+
"defaultThinkingLevel",
|
|
35
|
+
"enabledModels",
|
|
36
|
+
"modelThinkingLevels",
|
|
37
|
+
];
|
|
38
|
+
/** The model pi starts on when tumwater names none, or null when it cannot be known. Mirrors
|
|
39
|
+
* step 3 of pi 1.2.0's start-up model choice ("saved default from settings") for a run started
|
|
40
|
+
* without `--provider`/`--model`. Null when the file is missing, unreadable, or not a JSON
|
|
41
|
+
* object; when `defaultProvider` and `defaultModel` are not both non-empty strings (pi uses a
|
|
42
|
+
* saved default only as a pair); when `enabledModels` is a non-empty array (pi then starts on
|
|
43
|
+
* the first scoped model, a pattern tumwater does not resolve); or when
|
|
44
|
+
* `<projectRoot>/.pi/settings.json` exists and names any of the default keys — pi merges that
|
|
45
|
+
* file over the global one only for a project it trusts, and tumwater cannot tell whether it
|
|
46
|
+
* does, so showing nothing is better than naming a model pi may not run. Otherwise returns
|
|
47
|
+
* `{ provider, model }` with `thinking` from `modelThinkingLevels["<provider>/<model>"]` when
|
|
48
|
+
* that is a string, else from `defaultThinkingLevel` when that is a string — pi's precedence. */
|
|
49
|
+
export function readPiDefaultModel(settingsPath, projectRoot) {
|
|
50
|
+
return cachedByStat(settingsCache, settingsPath, // Keyed by path so distinct roots and test files never collide.
|
|
51
|
+
settingsPath, () => readPiDefaultModelUncached(settingsPath, projectRoot), (defaultModel) => ({ ...defaultModel }));
|
|
52
|
+
}
|
|
53
|
+
function readPiDefaultModelUncached(settingsPath, projectRoot) {
|
|
54
|
+
// readJsonFile is the shared "missing or torn reads as no data" policy for a JSON object
|
|
55
|
+
// file; the nested shapes get the same isJsonObject guard.
|
|
56
|
+
const doc = readJsonFile(settingsPath);
|
|
57
|
+
if (!doc)
|
|
58
|
+
return null;
|
|
59
|
+
const { defaultProvider, defaultModel } = doc;
|
|
60
|
+
if (typeof defaultProvider !== "string" || defaultProvider === "")
|
|
61
|
+
return null;
|
|
62
|
+
if (typeof defaultModel !== "string" || defaultModel === "")
|
|
63
|
+
return null;
|
|
64
|
+
if (Array.isArray(doc.enabledModels) && doc.enabledModels.length > 0)
|
|
65
|
+
return null;
|
|
66
|
+
// A project-local override that names any default key means pi's final choice depends on
|
|
67
|
+
// whether it trusts the project — unknowable to tumwater, so show nothing.
|
|
68
|
+
const proj = readJsonFile(path.join(projectRoot, ".pi", "settings.json"));
|
|
69
|
+
if (proj) {
|
|
70
|
+
for (const key of DEFAULT_KEYS) {
|
|
71
|
+
if (proj[key] !== undefined)
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const key = `${defaultProvider}/${defaultModel}`;
|
|
76
|
+
const levels = doc.modelThinkingLevels;
|
|
77
|
+
const level = isJsonObject(levels) ? levels[key] : undefined;
|
|
78
|
+
let thinking;
|
|
79
|
+
if (typeof level === "string")
|
|
80
|
+
thinking = level;
|
|
81
|
+
else if (typeof doc.defaultThinkingLevel === "string")
|
|
82
|
+
thinking = doc.defaultThinkingLevel;
|
|
83
|
+
return thinking !== undefined
|
|
84
|
+
? { provider: defaultProvider, model: defaultModel, thinking }
|
|
85
|
+
: { provider: defaultProvider, model: defaultModel };
|
|
86
|
+
}
|