tumwater 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/** The seam's answer to "who in this team can read images" (plans/image-input.md
|
|
2
|
-
* "Capability"). One resolver, `seamImageInput`, is the only place that rule lives
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* seam
|
|
6
|
-
* declares a hook. */
|
|
7
|
-
import { configForRole } from "../config/config-views.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
2
|
+
* "Capability"). One resolver, `seamImageInput`, is the only place that rule lives; the reroute
|
|
3
|
+
* target (imageReroute) and the per-role surfaces (roleImageInfo) both read it, so the answer
|
|
4
|
+
* is never reached two ways. It lives at runtime level because it needs the backend registry:
|
|
5
|
+
* a claude seam is always capable through the AgentBackend.imageInput hook, a pi seam reads the
|
|
6
|
+
* model's `input` in models.json, and any other backend answers null unless it declares a hook. */
|
|
7
|
+
import { configForRole, roleSeamTier, withModelOverrides } from "../config/config-views.js";
|
|
8
|
+
import { BORROW_ORDER, resolveTierFallbacks } from "../config/config-fallbacks.js";
|
|
9
|
+
import { seamAgent } from "../config/config-model-selector.js";
|
|
10
|
+
import { modelLabel } from "./backend-traits.js";
|
|
11
|
+
import { piModelImageInput, piPairFree, piModelsPath, readPiProviders } from "../pi/pi-models.js";
|
|
10
12
|
import { piListedImageInput } from "../pi/pi-listed-models.js";
|
|
11
13
|
import { backendById } from "./agent-backend.js";
|
|
12
14
|
/** One seam's image-input capability (plans/image-input.md "Capability"): `true` when the
|
|
@@ -32,12 +34,67 @@ export function seamImageInput(seam, providers, listed = null) {
|
|
|
32
34
|
return null;
|
|
33
35
|
return piModelImageInput(providers, seam.provider, seam.model) ?? piListedImageInput(listed, seam.provider, seam.model);
|
|
34
36
|
}
|
|
37
|
+
/** The image-capable reroute target for `role`, or null when no peer tier can read images
|
|
38
|
+
* (plans/image-input.md "Reroute target"). Walks the role's seam tier through BORROW_ORDER,
|
|
39
|
+
* skipping the tier itself — small → default → strong, default → strong → small, strong →
|
|
40
|
+
* default (a strong-tier seam never drops to small, the same rule as the budget fallback) —
|
|
41
|
+
* and takes the first tier whose model reads images. `regime` picks the pair set (see
|
|
42
|
+
* RerouteRegime); `modelsPath` and `account` are injectable so tests need no real pi install
|
|
43
|
+
* or account. */
|
|
44
|
+
export function imageReroute(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, providers = null, listed = null) {
|
|
45
|
+
const tier = roleSeamTier(config, role);
|
|
46
|
+
const candidates = BORROW_ORDER[tier].filter((t) => t !== tier);
|
|
47
|
+
if (candidates.length === 0)
|
|
48
|
+
return null;
|
|
49
|
+
const providerMap = providers ?? readPiProviders(modelsPath);
|
|
50
|
+
const capable = (rc) => seamImageInput(rc, providerMap, listed) === true;
|
|
51
|
+
if (regime === "primary") {
|
|
52
|
+
for (const t of candidates) {
|
|
53
|
+
const rc = withModelOverrides(config, t, {});
|
|
54
|
+
if (capable(rc))
|
|
55
|
+
return { tier: t, config: rc };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// Fallback regime: only the tiers' free fallback pairs qualify (resolveTierFallbacks over the
|
|
60
|
+
// price check), never a priced primary model — a reroute must not spend past the cap.
|
|
61
|
+
const resolved = resolveTierFallbacks(config, (p) =>
|
|
62
|
+
// The free-ness lookup reads the one claude account held here (part 2a/9).
|
|
63
|
+
piPairFree(providerMap, p, (b) => (b === "claude" ? account : null)));
|
|
64
|
+
for (const t of candidates) {
|
|
65
|
+
const pair = resolved[t].pair;
|
|
66
|
+
if (!pair)
|
|
67
|
+
continue;
|
|
68
|
+
const rc = withModelOverrides(config, t, {
|
|
69
|
+
provider: pair.provider,
|
|
70
|
+
model: pair.model,
|
|
71
|
+
thinking: pair.thinking,
|
|
72
|
+
});
|
|
73
|
+
if (capable(rc))
|
|
74
|
+
return { tier: t, config: rc };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** The config of imageReroute, for callers that want only the target model. */
|
|
79
|
+
export function imageRerouteTarget(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, providers = null, listed = null) {
|
|
80
|
+
return imageReroute(config, role, regime, modelsPath, account, providers, listed)?.config ?? null;
|
|
81
|
+
}
|
|
82
|
+
/** A seam's model as one readable token, the same label the doctor's `image input` check used
|
|
83
|
+
* (`claude/<id>` for a claude seam, `provider/model` for pi), delegated to backend-traits'
|
|
84
|
+
* modelLabel. Used by roleImageInfo alone. */
|
|
85
|
+
function seamImageLabel(backend, provider, model) {
|
|
86
|
+
return modelLabel(backend, provider, model);
|
|
87
|
+
}
|
|
35
88
|
/** Compute RoleImageInfo for one role. `modelsPath` and `listed` are injectable; the providers
|
|
36
|
-
* catalog is read (stat-cached) here
|
|
37
|
-
*
|
|
89
|
+
* catalog is read (stat-cached) here and also handed to imageReroute so a poll that asks many
|
|
90
|
+
* roles still parses the catalog once. */
|
|
38
91
|
export function roleImageInfo(config, role, modelsPath = piModelsPath(), listed = null) {
|
|
39
92
|
const providers = readPiProviders(modelsPath);
|
|
40
93
|
const rc = configForRole(config, role);
|
|
41
94
|
const imageInput = seamImageInput(rc, providers, listed);
|
|
42
|
-
|
|
95
|
+
const rr = imageReroute(config, role, "primary", modelsPath, null, providers, listed);
|
|
96
|
+
const imageRerouteStr = rr
|
|
97
|
+
? `${rr.tier} (${seamImageLabel(rr.config.agent, rr.config.provider, rr.config.model)})`
|
|
98
|
+
: null;
|
|
99
|
+
return { imageInput, imageReroute: imageRerouteStr };
|
|
43
100
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Installing the per-team `tumwater-notes` shim every backend's run finds on its PATH
|
|
2
|
-
* (plans/notes-command.md "Reaching it from a run"):
|
|
2
|
+
* (plans/notes-command.md "Reaching it from a run"): agent-spawn calls `ensureNotesShim(root)`
|
|
3
3
|
* before building each invocation, and the backend prepends the returned dir to the run's
|
|
4
4
|
* PATH (backendBaseEnv's `binDir`, src/process/run-marker.ts). The shim is a two-line `sh`
|
|
5
5
|
* script that execs the compiled notes CLI, so a model's shell — pi's, Claude Code's or
|
|
@@ -20,7 +20,7 @@ export const NOTES_SHIM_NAME = "tumwater-notes";
|
|
|
20
20
|
/** Install the `tumwater-notes` shim under `<root>/.tumwater/bin` and return that directory.
|
|
21
21
|
* The shim is written atomically with mode 0755, only when its content differs from what is
|
|
22
22
|
* already there (a redeploy that changes `<dist>` rewrites it). Never throws: a failure to
|
|
23
|
-
* write is the caller's to absorb —
|
|
23
|
+
* write is the caller's to absorb — agent-spawn logs one warning and runs without the shim. */
|
|
24
24
|
export function ensureNotesShim(root, execPath = process.execPath) {
|
|
25
25
|
const dir = notesShimDir(root);
|
|
26
26
|
const target = path.join(dir, NOTES_SHIM_NAME);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The opencode adapter for the agent seam (plans/opencode-backend.md "The invocation",
|
|
2
2
|
* "Sessions", §6). It shapes one run's argv/stdin/env, translates opencode's `run --format
|
|
3
3
|
* json` into the canonical stream, runs the authoritative session export after the process
|
|
4
|
-
* exits, and classifies the exit's stderr — everything backend-specific. src/agent/
|
|
4
|
+
* exits, and classifies the exit's stderr — everything backend-specific. src/agent/agent-spawn.ts
|
|
5
5
|
* spawns the run and records this run in the session ledger; opencode mints its own session id
|
|
6
6
|
* (a `ses_…` id, or the resumed record's id), which `invocation` returns so the ledger can
|
|
7
7
|
* carry it.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* the only thing that stops a follow-up running against an empty session. Anything else mints a
|
|
14
14
|
* fresh id (design §4).
|
|
15
15
|
*
|
|
16
|
-
* Registration lives in src/agent/run
|
|
16
|
+
* Registration lives in src/agent/agent-run.ts beside the pi and claude adapters; a test selects
|
|
17
17
|
* opencode by setting `config.agent = "opencode"` on a run. A run carrying a note gets the
|
|
18
18
|
* notes path in its environment (the `tumwater-notes` command on PATH reads it), as the pi and
|
|
19
19
|
* claude adapters do. */
|
|
@@ -79,42 +79,44 @@ export function opencodeSpillDir(root) {
|
|
|
79
79
|
function catalogModelKey(model) {
|
|
80
80
|
return opencodeModelKey(model);
|
|
81
81
|
}
|
|
82
|
-
/** Run `opencode session export <id> --standalone`
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
|
|
82
|
+
/** Run the bounded `opencode session export <id> --standalone` spawn that both the resume
|
|
83
|
+
* existence check and the reconciling export issue, under the same env/cwd and the shared
|
|
84
|
+
* 10 s bound. `capture` is false for the existence check, which reads only the exit status:
|
|
85
|
+
* `stdio: "ignore"` keeps the export — which carries every message of the session — out of
|
|
86
|
+
* memory and below the default 1 MiB `maxBuffer`, whose `ENOBUFS` kill would otherwise make a
|
|
87
|
+
* large but real session read as absent and mint a fresh one. Returns the spawn result, or null
|
|
88
|
+
* when the spawn itself threw — a missing binary or an invalid option, resolved the same way
|
|
89
|
+
* both callers treated a throw. The command line, env, cwd and timeout live here once, so the
|
|
90
|
+
* existence check and the reconciling export cannot drift on what they run. */
|
|
91
|
+
function runSessionExport(id, env, bin, cwd, capture = true) {
|
|
87
92
|
try {
|
|
88
|
-
|
|
93
|
+
return spawnSync(bin, ["session", "export", id, "--standalone"], {
|
|
89
94
|
env,
|
|
90
95
|
cwd,
|
|
91
96
|
timeout: EXPORT_TIMEOUT_MS,
|
|
92
|
-
|
|
97
|
+
encoding: "utf8",
|
|
98
|
+
stdio: capture ? "pipe" : "ignore",
|
|
93
99
|
});
|
|
94
|
-
return res.status === 0;
|
|
95
100
|
}
|
|
96
101
|
catch {
|
|
97
|
-
return
|
|
102
|
+
return null;
|
|
98
103
|
}
|
|
99
104
|
}
|
|
105
|
+
/** Run `opencode session export <id> --standalone` (the same database the run used, bounded at
|
|
106
|
+
* 10 s) and return whether it reported success — the existence check that decides a resume.
|
|
107
|
+
* Nonzero exit, a timeout, or a spawn failure all read as "no such session", which is the safe
|
|
108
|
+
* direction: `--session` would otherwise silently create a missing session. */
|
|
109
|
+
function opencodeSessionExists(id, env, bin, cwd) {
|
|
110
|
+
const res = runSessionExport(id, env, bin, cwd, false);
|
|
111
|
+
return res?.status === 0;
|
|
112
|
+
}
|
|
100
113
|
/** Run the reconciling export after a successful run and return its two canonical lines
|
|
101
114
|
* (`run_cost`, `run_usage`) from the session's `info`. Anything that goes wrong — a spawn or
|
|
102
115
|
* timeout, a nonzero exit, unparseable or malformed output — returns `[]`, and the caller
|
|
103
116
|
* (afterRun) reports `opencode_usage_partial` so the run keeps its streamed numbers. */
|
|
104
117
|
function opencodeExport(result) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
res = spawnSync(result.bin, ["session", "export", result.id, "--standalone"], {
|
|
108
|
-
env: result.env,
|
|
109
|
-
cwd: result.cwd,
|
|
110
|
-
timeout: EXPORT_TIMEOUT_MS,
|
|
111
|
-
encoding: "utf8",
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return [];
|
|
116
|
-
}
|
|
117
|
-
if (res.status !== 0 || !res.stdout)
|
|
118
|
+
const res = runSessionExport(result.id, result.env, result.bin, result.cwd);
|
|
119
|
+
if (res?.status !== 0 || !res.stdout)
|
|
118
120
|
return [];
|
|
119
121
|
let parsed;
|
|
120
122
|
try {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
10
|
import { isJsonObject, validEntries } from "../../files/json-object.js";
|
|
11
11
|
import { readJsonFile, writeJsonAtomic } from "../../files/json-files.js";
|
|
12
|
+
import { cachedByStat } from "../../files/stat-cache.js";
|
|
12
13
|
import { errorMessage } from "../../text/text.js";
|
|
13
14
|
import { DAY_MS } from "../../text/datetime.js";
|
|
14
15
|
import { opencodeModelsPath } from "../../paths.js";
|
|
@@ -21,6 +22,25 @@ const CATALOG_URL = "https://models.dev/api.json";
|
|
|
21
22
|
const CATALOG_FETCH_TIMEOUT_MS = 30_000;
|
|
22
23
|
/** How long a fetched cache is trusted before it is refreshed. */
|
|
23
24
|
const CATALOG_CACHE_TTL_MS = DAY_MS;
|
|
25
|
+
/** Per-catalog cache of the parsed catalog, keyed by the models.json path: the status
|
|
26
|
+
* snapshot and the budget gate both poll it every second while the file changes only when a
|
|
27
|
+
* refresh re-fetches models.dev — and it grows with the models.dev catalog — so an
|
|
28
|
+
* unchanged file costs one stat per poll instead of a re-read plus a JSON.parse and a
|
|
29
|
+
* validation of the whole file (the same stat-keyed contract readOpencodeCache's siblings
|
|
30
|
+
* readCodexCatalog, readPiProviders and readMergeQueue already use). Any write invalidates
|
|
31
|
+
* via dev/ino/mtime/size (stat-cache.cachedByStat's freshness check). */
|
|
32
|
+
const catalogCache = new Map();
|
|
33
|
+
/** A structural copy of a parsed catalog, so a caller that mutates its own result (e.g.
|
|
34
|
+
* observeOpencodeRun sets `billedAt` on an entry) never reaches the shared cache entry; the
|
|
35
|
+
* next poll re-derives from the file. The entries are plain data (`efforts` arrays included)
|
|
36
|
+
* so a shallow model-object spread suffices. */
|
|
37
|
+
function cloneCache(cache) {
|
|
38
|
+
const models = {};
|
|
39
|
+
for (const [key, entry] of Object.entries(cache.models)) {
|
|
40
|
+
models[key] = entry.efforts !== undefined ? { ...entry, efforts: [...entry.efforts] } : { ...entry };
|
|
41
|
+
}
|
|
42
|
+
return { fetchedAt: cache.fetchedAt, cliVersion: cache.cliVersion, models };
|
|
43
|
+
}
|
|
24
44
|
const defaultFetch = async (url, timeoutMs) => {
|
|
25
45
|
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
26
46
|
if (!res.ok)
|
|
@@ -33,6 +53,53 @@ const defaultFetch = async (url, timeoutMs) => {
|
|
|
33
53
|
export async function fetchOpencodeCatalog(fetchFn = defaultFetch) {
|
|
34
54
|
return fetchFn(CATALOG_URL, CATALOG_FETCH_TIMEOUT_MS);
|
|
35
55
|
}
|
|
56
|
+
/** The catalog root `parseOpencodeCatalog` iterates: models.dev's live api.json puts the provider
|
|
57
|
+
* map at the top level, OpenCode's cached copy wraps that same map under a `body` key. */
|
|
58
|
+
function catalogRoot(body) {
|
|
59
|
+
const root = isJsonObject(body) ? body : {};
|
|
60
|
+
// models.dev's api.json IS the provider map; OpenCode's cached copy wraps that map under `body`.
|
|
61
|
+
const wrapped = root.body;
|
|
62
|
+
return isJsonObject(wrapped) ? wrapped : root;
|
|
63
|
+
}
|
|
64
|
+
/** The free/priced reads from a per-model cost block: whether it is present and yields two zero
|
|
65
|
+
* per-M prices. A missing cost block is unknown price data, and treating it as $0 is the unsafe
|
|
66
|
+
* direction for a budget gate. */
|
|
67
|
+
function modelPricing(m) {
|
|
68
|
+
const cost = isJsonObject(m.cost) ? m.cost : undefined;
|
|
69
|
+
const input = cost?.input ?? 0;
|
|
70
|
+
const output = cost?.output ?? 0;
|
|
71
|
+
return {
|
|
72
|
+
costed: cost !== undefined,
|
|
73
|
+
free: cost !== undefined && input === 0 && output === 0,
|
|
74
|
+
inputPerM: input,
|
|
75
|
+
outputPerM: output,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** The effort levels from a per-model object: the `values` of the `reasoning_options` entries
|
|
79
|
+
* whose `type` is `"effort"`. */
|
|
80
|
+
function modelEfforts(m) {
|
|
81
|
+
if (!Array.isArray(m.reasoning_options))
|
|
82
|
+
return [];
|
|
83
|
+
return m.reasoning_options.flatMap((r) => isJsonObject(r) && r.type === "effort" && Array.isArray(r.values) ? r.values : []);
|
|
84
|
+
}
|
|
85
|
+
/** Build one catalog model from an untrusted per-model object, null when it is malformed. */
|
|
86
|
+
function catalogModelFrom(m) {
|
|
87
|
+
const pricing = modelPricing(m);
|
|
88
|
+
const model = { free: pricing.free };
|
|
89
|
+
if (pricing.costed) {
|
|
90
|
+
model.inputPerM = pricing.inputPerM;
|
|
91
|
+
model.outputPerM = pricing.outputPerM;
|
|
92
|
+
}
|
|
93
|
+
if (isJsonObject(m.limit) && m.limit.context !== undefined)
|
|
94
|
+
model.context = m.limit.context;
|
|
95
|
+
if (isJsonObject(m.modalities) && Array.isArray(m.modalities.input)) {
|
|
96
|
+
model.images = m.modalities.input.includes("image");
|
|
97
|
+
}
|
|
98
|
+
const efforts = modelEfforts(m);
|
|
99
|
+
if (efforts.length > 0)
|
|
100
|
+
model.efforts = efforts;
|
|
101
|
+
return isOpencodeCatalogModel(model) ? model : null;
|
|
102
|
+
}
|
|
36
103
|
/** Parse models.dev's `api.json` into the models the config routes through OpenCode: always the
|
|
37
104
|
* `opencode` provider, plus every provider named in `providers` (the config's
|
|
38
105
|
* `opencode/<provider>/…` routes — `opencode-go` and so on). The live `GET /api.json` returns
|
|
@@ -43,11 +110,7 @@ export async function fetchOpencodeCatalog(fetchFn = defaultFetch) {
|
|
|
43
110
|
* provider or model is skipped, never thrown on. */
|
|
44
111
|
export function parseOpencodeCatalog(body, providers) {
|
|
45
112
|
const out = {};
|
|
46
|
-
const
|
|
47
|
-
// models.dev's api.json IS the provider map; OpenCode's cached copy wraps that map under `body`.
|
|
48
|
-
const wrapped = root.body;
|
|
49
|
-
const catalog = isJsonObject(wrapped) ? wrapped : root;
|
|
50
|
-
for (const [provider, providerInfo] of Object.entries(catalog)) {
|
|
113
|
+
for (const [provider, providerInfo] of Object.entries(catalogRoot(body))) {
|
|
51
114
|
if (provider !== "opencode" && !providers.includes(provider))
|
|
52
115
|
continue;
|
|
53
116
|
if (!isJsonObject(providerInfo) || !isJsonObject(providerInfo.models))
|
|
@@ -55,28 +118,8 @@ export function parseOpencodeCatalog(body, providers) {
|
|
|
55
118
|
for (const [id, m] of Object.entries(providerInfo.models)) {
|
|
56
119
|
if (!isJsonObject(m))
|
|
57
120
|
continue;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const cost = isJsonObject(m.cost) ? m.cost : undefined;
|
|
61
|
-
const input = cost?.input ?? 0;
|
|
62
|
-
const output = cost?.output ?? 0;
|
|
63
|
-
const model = { free: cost !== undefined && input === 0 && output === 0 };
|
|
64
|
-
if (cost !== undefined) {
|
|
65
|
-
model.inputPerM = input;
|
|
66
|
-
model.outputPerM = output;
|
|
67
|
-
}
|
|
68
|
-
if (isJsonObject(m.limit) && m.limit.context !== undefined)
|
|
69
|
-
model.context = m.limit.context;
|
|
70
|
-
if (isJsonObject(m.modalities) && Array.isArray(m.modalities.input)) {
|
|
71
|
-
model.images = m.modalities.input.includes("image");
|
|
72
|
-
}
|
|
73
|
-
if (Array.isArray(m.reasoning_options)) {
|
|
74
|
-
const efforts = m.reasoning_options
|
|
75
|
-
.flatMap((r) => isJsonObject(r) && r.type === "effort" && Array.isArray(r.values) ? r.values : []);
|
|
76
|
-
if (efforts.length > 0)
|
|
77
|
-
model.efforts = efforts;
|
|
78
|
-
}
|
|
79
|
-
if (isOpencodeCatalogModel(model))
|
|
121
|
+
const model = catalogModelFrom(m);
|
|
122
|
+
if (model !== null)
|
|
80
123
|
out[`${provider}/${id}`] = model;
|
|
81
124
|
}
|
|
82
125
|
}
|
|
@@ -137,15 +180,21 @@ function isOpencodeCatalogModel(value) {
|
|
|
137
180
|
* fills the file in. Entries that are not well-formed catalog models are dropped, so a foreign
|
|
138
181
|
* entry reads as a model missing from the cache — not free. */
|
|
139
182
|
export function readOpencodeCache(root) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
183
|
+
const file = opencodeModelsPath(root);
|
|
184
|
+
const cached = cachedByStat(catalogCache, file, file, () => {
|
|
185
|
+
// readJsonFile is the shared "missing or torn reads as no data" policy; a torn write
|
|
186
|
+
// or a malformed file reads as an empty cache (fetchedAt 0), exactly as before.
|
|
187
|
+
const existing = readJsonFile(file);
|
|
188
|
+
if (existing !== null && typeof existing.fetchedAt === "number" && isJsonObject(existing.models)) {
|
|
189
|
+
return {
|
|
190
|
+
fetchedAt: existing.fetchedAt,
|
|
191
|
+
cliVersion: typeof existing.cliVersion === "string" ? existing.cliVersion : undefined,
|
|
192
|
+
models: validEntries(existing.models, isOpencodeCatalogModel),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return { fetchedAt: 0, models: {} };
|
|
196
|
+
}, cloneCache);
|
|
197
|
+
return cached ?? { fetchedAt: 0, models: {} };
|
|
149
198
|
}
|
|
150
199
|
/** Persist the catalog cache atomically under `.tumwater/state/`. */
|
|
151
200
|
export function writeOpencodeCache(root, cache) {
|
|
@@ -38,29 +38,47 @@ const DENIED_PERMISSIONS = [
|
|
|
38
38
|
function isCommentStart(src, i) {
|
|
39
39
|
return src[i] === "/" && (src[i + 1] === "/" || src[i + 1] === "*");
|
|
40
40
|
}
|
|
41
|
+
/** True when `c` is a JSONC whitespace character. */
|
|
42
|
+
function isWhitespace(c) {
|
|
43
|
+
return c === " " || c === "\t" || c === "\n" || c === "\r";
|
|
44
|
+
}
|
|
45
|
+
/** Advance `j` past a `//` line comment (a caller-supplied position where `src[j] === "/"`
|
|
46
|
+
* and `src[j + 1] === "/"`), not past the terminating newline. */
|
|
47
|
+
function skipLineComment(src, j) {
|
|
48
|
+
while (j < src.length && src[j] !== "\n")
|
|
49
|
+
j += 1;
|
|
50
|
+
return j;
|
|
51
|
+
}
|
|
52
|
+
/** Advance `j` past a `/*`-style block comment (a caller-supplied position where `src[j] ===
|
|
53
|
+
* "/"` and `src[j + 1] === "*"`), ending after its closing two characters. An unterminated
|
|
54
|
+
* comment leaves `j` at `src.length + 2`, overshooting past the end — today's behaviour, kept. */
|
|
55
|
+
function skipBlockComment(src, j) {
|
|
56
|
+
j += 2;
|
|
57
|
+
while (j < src.length && !(src[j] === "*" && src[j + 1] === "/"))
|
|
58
|
+
j += 1;
|
|
59
|
+
j += 2;
|
|
60
|
+
return j;
|
|
61
|
+
}
|
|
62
|
+
/** Advance `j` past one comment, dispatching on the second character: `//` to the line-comment
|
|
63
|
+
* scan, `/*` to the block-comment scan (a caller-supplied position where `isCommentStart`
|
|
64
|
+
* holds). */
|
|
65
|
+
function skipComment(src, j) {
|
|
66
|
+
return src[j + 1] === "/" ? skipLineComment(src, j) : skipBlockComment(src, j);
|
|
67
|
+
}
|
|
41
68
|
/** Advance `j` past any whitespace and comments, for a position that is never inside a string.
|
|
42
69
|
* Returns the index of the next significant (non-comment, non-whitespace) character, or
|
|
43
70
|
* `src.length` when the input ends in comments/whitespace. */
|
|
44
71
|
function skipWhitespaceAndComments(src, j) {
|
|
45
72
|
while (j < src.length) {
|
|
46
|
-
|
|
47
|
-
if (c === " " || c === "\t" || c === "\n" || c === "\r") {
|
|
73
|
+
if (isWhitespace(src[j])) {
|
|
48
74
|
j += 1;
|
|
49
|
-
continue;
|
|
50
75
|
}
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
j += 1;
|
|
54
|
-
continue;
|
|
76
|
+
else if (isCommentStart(src, j)) {
|
|
77
|
+
j = skipComment(src, j);
|
|
55
78
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
while (j < src.length && !(src[j] === "*" && src[j + 1] === "/"))
|
|
59
|
-
j += 1;
|
|
60
|
-
j += 2;
|
|
61
|
-
continue;
|
|
79
|
+
else {
|
|
80
|
+
break;
|
|
62
81
|
}
|
|
63
|
-
break;
|
|
64
82
|
}
|
|
65
83
|
return j;
|
|
66
84
|
}
|
|
@@ -14,22 +14,21 @@
|
|
|
14
14
|
* the model to pass `timeout` for the check (§5). `XDG_CONFIG_HOME` and its siblings are NOT
|
|
15
15
|
* set: OpenCode's shell-tool children inherit the environment, and pointing XDG at a private
|
|
16
16
|
* dir would stop `git` reading `~/.config/git/config`, losing commits' author identity (§3). */
|
|
17
|
+
import { scrubEnv } from "../env-scrub.js";
|
|
17
18
|
/** Build the opencode process environment from `base`. Never mutates `base`. */
|
|
18
19
|
export function opencodeEnv(base, opts) {
|
|
19
|
-
|
|
20
|
-
for (const [key, value] of Object.entries(base)) {
|
|
20
|
+
return scrubEnv(base, {
|
|
21
21
|
// Everything an inherited OpenCode session set is scrubbed — server, config, database,
|
|
22
22
|
// client and session id — except the operator's API key, which is operator intent and must
|
|
23
23
|
// survive. `AI_AGENT` goes too, so the shell tool sets its own (finding 16).
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return env;
|
|
24
|
+
keep: (key) => !(key === "OPENCODE" || (key.startsWith("OPENCODE_") && key !== "OPENCODE_API_KEY") || key === "AI_AGENT"),
|
|
25
|
+
then: (env) => {
|
|
26
|
+
env.OPENCODE_DISABLE_CLAUDE_CODE = "1";
|
|
27
|
+
env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = "1";
|
|
28
|
+
env.OPENCODE_DISABLE_AUTOUPDATE = "1";
|
|
29
|
+
env.OPENCODE_DISABLE_FILEWATCHER = "1";
|
|
30
|
+
env.OPENCODE_DB = opts.dbPath;
|
|
31
|
+
env.OPENCODE_CONFIG_CONTENT = opts.configContent;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
35
34
|
}
|
|
@@ -28,49 +28,71 @@ const TOOL_NAMES = {
|
|
|
28
28
|
/** A Retry-After hint spelled out in a rate-limit error's message text ("retry after 30s" /
|
|
29
29
|
* "retry in 30 seconds"). */
|
|
30
30
|
const RETRY_AFTER = /retry (?:after|in) (\d+)\s*s(?:econds?)?\b/i;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const type = typeof err.type === "string" ? err.type : "";
|
|
39
|
-
const status = err.status;
|
|
40
|
-
// The free tier refuses intermittently (a provider failure, not a config error): three
|
|
41
|
-
// refusals in a row move the tier to its fallback through the existing model-failure
|
|
42
|
-
// episode. Typed `provider.auth` WITH status 403 — the free tier's own refusal — or the
|
|
43
|
-
// text naming the free tier. A `provider.auth` with any other status (a 401 bad
|
|
44
|
-
// credential) is not a refusal and falls through to the config-error row below.
|
|
31
|
+
;
|
|
32
|
+
/** Is this the free tier's intermittent refusal (a provider failure, not a config error)?
|
|
33
|
+
* Three refusals in a row move the tier to its fallback through the existing model-failure
|
|
34
|
+
* episode. Typed `provider.auth` WITH status 403 — the free tier's own refusal — or the
|
|
35
|
+
* text naming the free tier. A `provider.auth` with any other status (a 401 bad
|
|
36
|
+
* credential) is not a refusal and falls through to the config-error row. */
|
|
37
|
+
function freeTierClassification(type, status, message) {
|
|
45
38
|
if ((type === "provider.auth" && status === 403) || /free\s?tier|FreeTierError/i.test(message))
|
|
46
39
|
return { kind: "transientBackend", backendKind: "server", freeTier: true };
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Is this a rate limit (429 or rate-limit text) — the world saying "later", inherently
|
|
43
|
+
* retryable? Carry the echoed Retry-After hint when the message spells it. */
|
|
44
|
+
function rateLimitClassification(status, message) {
|
|
49
45
|
if (status === 429 || /\b429\b|rate limit|too many requests/i.test(message)) {
|
|
50
46
|
const retry = RETRY_AFTER.exec(message);
|
|
51
47
|
return { kind: "transientRateLimit", ...(retry ? { retryAfterSeconds: Number(retry[1]) } : {}) };
|
|
52
48
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Is this a permanent config error — an unknown model or variant, or the malformed `ses_`
|
|
52
|
+
* id (a misconfiguration, not a transient outage): typed `provider.no-route` / `unknown`,
|
|
53
|
+
* or the model-not-found / 401 text? */
|
|
54
|
+
function configErrorClassification(type, status, message) {
|
|
56
55
|
if (type === "provider.no-route" ||
|
|
57
56
|
type === "unknown" ||
|
|
58
57
|
status === 401 ||
|
|
59
58
|
/unknown model|unknown provider|model not found|provider not found|unauthorized|status\s*401|\b401\b/i.test(message))
|
|
60
59
|
return { kind: "configError" };
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Did context overflow in a way compaction could not fix? Matched STRICTLY (the same shape
|
|
63
|
+
* pi's own CONTEXT_ERROR matches), so a message that merely mentions "context" or "too long"
|
|
64
|
+
* in passing — a plugin error, say — is never misread as an overflow that would then be
|
|
65
|
+
* trusted by PiStreamParser over its own tighter regex. */
|
|
66
|
+
function contextOverflowClassification(message) {
|
|
65
67
|
if (/context (size|length|window)?\s*(has been |was )?exceeded|exceeds? (the )?context|too (long|large) for .*context/i.test(message))
|
|
66
68
|
return { kind: "contextExceeded" };
|
|
67
|
-
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Is this a transient server, connection or timeout failure? The server statuses and text
|
|
72
|
+
* first, then the shared connection/timeout prose vocabulary (see `transientProseKind`). */
|
|
73
|
+
function transientClassification(message) {
|
|
68
74
|
if (/\b(500|501|502|503|504|529)\b|internal server error|bad gateway|service unavailable|gateway timeout|overloaded/i.test(message))
|
|
69
75
|
return { kind: "transientBackend", backendKind: "server" };
|
|
70
76
|
const prose = transientProseKind(message);
|
|
71
77
|
if (prose)
|
|
72
78
|
return prose;
|
|
73
|
-
return
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/** How an `error` line classifies into the canonical `tumwaterKind` (and, for a transient
|
|
82
|
+
* backend, its kind), trusted by `PiStreamParser` before its text regexes. The error's
|
|
83
|
+
* `type` — the provider signal the plan's table maps (provider.auth, provider.no-route,
|
|
84
|
+
* unknown) — is consulted FIRST, the message text second (plans/opencode-backend.md §8 and
|
|
85
|
+
* its classification table). */
|
|
86
|
+
function classifyError(err) {
|
|
87
|
+
const message = typeof err.message === "string" ? err.message : "";
|
|
88
|
+
const type = typeof err.type === "string" ? err.type : "";
|
|
89
|
+
const status = err.status;
|
|
90
|
+
return (freeTierClassification(type, status, message) ??
|
|
91
|
+
rateLimitClassification(status, message) ??
|
|
92
|
+
configErrorClassification(type, status, message) ??
|
|
93
|
+
contextOverflowClassification(message) ??
|
|
94
|
+
transientClassification(message) ??
|
|
95
|
+
{});
|
|
74
96
|
}
|
|
75
97
|
/** The step's token counts and cost → pi's canonical usage block: the four counts, their sum
|
|
76
98
|
* as `totalTokens`, and `cost.total` = the step's USD cost (plans/opencode-backend.md §6).
|
|
@@ -39,7 +39,7 @@ export function recordSession(dir, record) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
/** The one run's ledger record, minus the id the two recording sites mint differently — the
|
|
42
|
-
* pre-spawn id in
|
|
42
|
+
* pre-spawn id in agent-spawn.ts, or the stream-minted session id in agent-stream.ts. One place
|
|
43
43
|
* for the record's non-id shape (agent, kind, name, cwd, the optional model and the stamp), so
|
|
44
44
|
* the two appends cannot drift on the model spread or the required fields. `agent` is the
|
|
45
45
|
* owning backend's id; the run options supply the rest. */
|
|
@@ -191,6 +191,24 @@ export function pruneSessionLedgers(sessionsRoot, days) {
|
|
|
191
191
|
/** How long one `opencode session delete <id> --standalone` may take before it gives up: the
|
|
192
192
|
* session export measured 0.18 s live, so 10 s is a generous bound on a slow or wedged CLI. */
|
|
193
193
|
const SESSION_DELETE_TIMEOUT_MS = 10_000;
|
|
194
|
+
/** Run one bounded headless CLI session delete (`bin args…`) under `env`, best-effort: a
|
|
195
|
+
* spawn that throws (a missing binary, an unwritable env) returns false without throwing, so
|
|
196
|
+
* a failed delete never fails the prune. Shared by the OpenCode and Codex session purges below
|
|
197
|
+
* so neither drift on the 10 s bound, the status-0 success rule, or the swallow policy. */
|
|
198
|
+
function sessionDeleteSucceeds(bin, args, env) {
|
|
199
|
+
try {
|
|
200
|
+
const res = spawnSync(bin, args, {
|
|
201
|
+
env,
|
|
202
|
+
timeout: SESSION_DELETE_TIMEOUT_MS,
|
|
203
|
+
stdio: "ignore",
|
|
204
|
+
});
|
|
205
|
+
return res.status === 0;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Best effort: an unwritable env or a missing binary must not fail the prune.
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
194
212
|
/** Delete each OpenCode session the ledger records as expired, with one
|
|
195
213
|
* `opencode session delete <id> --standalone` each (bounded), plus the spill files older than
|
|
196
214
|
* the window in the team's shell spill dir. The session's spill files and server state live
|
|
@@ -209,18 +227,8 @@ export function pruneOpencodeSessions(sessionsRoot, days, opts) {
|
|
|
209
227
|
for (const record of readSessions(path.dirname(file))) {
|
|
210
228
|
if (record.agent !== "opencode" || record.at >= cutoff)
|
|
211
229
|
continue;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
env: opts.env,
|
|
215
|
-
timeout: SESSION_DELETE_TIMEOUT_MS,
|
|
216
|
-
stdio: "ignore",
|
|
217
|
-
});
|
|
218
|
-
if (res.status === 0)
|
|
219
|
-
deleted += 1;
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
// Best effort: an unwritable env or a missing binary must not fail the prune.
|
|
223
|
-
}
|
|
230
|
+
if (sessionDeleteSucceeds(opts.bin, ["session", "delete", record.id, "--standalone"], opts.env))
|
|
231
|
+
deleted += 1;
|
|
224
232
|
}
|
|
225
233
|
});
|
|
226
234
|
}
|
|
@@ -276,20 +284,8 @@ export function pruneCodexSessions(sessionsRoot, days, opts) {
|
|
|
276
284
|
for (const id of expired) {
|
|
277
285
|
if (live.has(id))
|
|
278
286
|
continue;
|
|
279
|
-
if (opts.bin !== undefined)
|
|
280
|
-
|
|
281
|
-
const res = spawnSync(opts.bin, ["delete", "--force", id], {
|
|
282
|
-
env: opts.env,
|
|
283
|
-
timeout: SESSION_DELETE_TIMEOUT_MS,
|
|
284
|
-
stdio: "ignore",
|
|
285
|
-
});
|
|
286
|
-
if (res.status === 0)
|
|
287
|
-
deleted += 1;
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
// Best effort: an unwritable env or a missing binary must not fail the prune.
|
|
291
|
-
}
|
|
292
|
-
}
|
|
287
|
+
if (opts.bin !== undefined && sessionDeleteSucceeds(opts.bin, ["delete", "--force", id], opts.env))
|
|
288
|
+
deleted += 1;
|
|
293
289
|
// Fallback: unlink the home's rollout for this session so neither the ledger line nor
|
|
294
290
|
// its file outlives the window. A real `codex delete` removes the rollout itself, so a
|
|
295
291
|
// successful delete has nothing left to unlink; a failed or skipped delete does, and
|