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
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Pure translation: no spawn, no backend registration, no config. The factory keeps per-run
|
|
12
12
|
* state (the pending split-block assistant message, the unmapped native types seen, whether a
|
|
13
13
|
* `run_cost` arrived), so call it once per run. Unmapped native types are recorded once each
|
|
14
|
-
* on the seam's optional `warnings()` — src/agent/
|
|
14
|
+
* on the seam's optional `warnings()` — src/agent/agent-stream.ts forwards each as an
|
|
15
15
|
* `agent_unknown_event` warning after the run. `cost_unknown` is recorded the same way when a
|
|
16
16
|
* run produced no authoritative `run_cost`. */
|
|
17
17
|
import { joinParts } from "../../text/text.js";
|
|
@@ -71,47 +71,80 @@ function canonicalBlock(block) {
|
|
|
71
71
|
return undefined;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
* provider classification applies. `status` is `api_error_status`, which is null on the
|
|
79
|
-
* not-logged-in result, so the text checks carry that case. */
|
|
80
|
-
function classifyResult(result) {
|
|
81
|
-
const terminal = optionalString(result.terminal_reason);
|
|
74
|
+
/** `terminal === "aborted_tools"` is deliberate: the harness killed the run, so no provider
|
|
75
|
+
* classification applies — the chain stops there. `error_max_budget_usd`, as a terminal
|
|
76
|
+
* reason or in the text, caps the budget. */
|
|
77
|
+
function terminalAndBudgetClassification(terminal, text) {
|
|
82
78
|
if (terminal === "aborted_tools")
|
|
83
79
|
return {};
|
|
84
80
|
if (terminal === "error_max_budget_usd")
|
|
85
81
|
return { kind: "budgetCapped" };
|
|
86
|
-
const text = joinParts([optionalString(result.result), optionalString(result.error)], " ");
|
|
87
82
|
if (/error_max_budget_usd/i.test(text))
|
|
88
83
|
return { kind: "budgetCapped" };
|
|
89
|
-
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
function statusClassification(status) {
|
|
90
87
|
if (status === 429)
|
|
91
88
|
return { kind: "transientRateLimit" };
|
|
92
89
|
if (status !== undefined && SERVER_TRANSIENT_STATUS.has(status))
|
|
93
90
|
return { kind: "transientBackend", backendKind: "server" };
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
/** The context-overflow text wins over a configuration status: claude reports an oversized
|
|
94
|
+
* prompt as HTTP 400 ("prompt is too long"), whose status alone would read as configError. */
|
|
95
|
+
function contextOverflowClassification(text) {
|
|
96
96
|
if (/prompt is too long/i.test(text))
|
|
97
97
|
return { kind: "contextExceeded" };
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
function configStatusClassification(status) {
|
|
98
101
|
if (status !== undefined && CONFIG_STATUS.has(status))
|
|
99
102
|
return { kind: "configError" };
|
|
100
|
-
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
function resumeMissingClassification(text) {
|
|
101
106
|
if (/No conversation found with session ID/i.test(text))
|
|
102
107
|
return { kind: "resumeMissing" };
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
function configTextClassification(text) {
|
|
103
111
|
if (/Not logged in|issue with the selected model|does not support this model/i.test(text))
|
|
104
112
|
return { kind: "configError" };
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
function transientTextClassification(text) {
|
|
105
116
|
if (/\b429\b|rate limit/i.test(text))
|
|
106
117
|
return { kind: "transientRateLimit" };
|
|
107
118
|
if (/overloaded|internal server error|bad gateway|service unavailable|gateway timeout|\b5\d\d\b/i.test(text))
|
|
108
119
|
return { kind: "transientBackend", backendKind: "server" };
|
|
109
120
|
const prose = transientProseKind(text);
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
return prose;
|
|
122
|
+
}
|
|
123
|
+
/** Text fallbacks, for the statuses claude leaves null or omits. */
|
|
124
|
+
function httpStatusTextClassification(text) {
|
|
112
125
|
if (/\b(400|401|403|404)\b/i.test(text))
|
|
113
126
|
return { kind: "configError" };
|
|
114
|
-
return
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
/** How a `result` event's error signals classify into the canonical `tumwaterKind` (and, for
|
|
130
|
+
* a transient backend, its kind), trusted by `PiStreamParser` before its text regexes. Empty
|
|
131
|
+
* when the result is not an error or carries no signal the vocabulary names — a SIGTERM
|
|
132
|
+
* (`terminal_reason: "aborted_tools"`) is deliberate: the harness killed the run, so no
|
|
133
|
+
* provider classification applies. `status` is `api_error_status`, which is null on the
|
|
134
|
+
* not-logged-in result, so the text checks carry that case. */
|
|
135
|
+
function classifyResult(result) {
|
|
136
|
+
const terminal = optionalString(result.terminal_reason);
|
|
137
|
+
const text = joinParts([optionalString(result.result), optionalString(result.error)], " ");
|
|
138
|
+
const status = nonNegativeNumber(result.api_error_status, undefined);
|
|
139
|
+
return (terminalAndBudgetClassification(terminal, text) ??
|
|
140
|
+
statusClassification(status) ??
|
|
141
|
+
contextOverflowClassification(text) ??
|
|
142
|
+
configStatusClassification(status) ??
|
|
143
|
+
resumeMissingClassification(text) ??
|
|
144
|
+
configTextClassification(text) ??
|
|
145
|
+
transientTextClassification(text) ??
|
|
146
|
+
httpStatusTextClassification(text) ??
|
|
147
|
+
{});
|
|
115
148
|
}
|
|
116
149
|
/** Build a fresh per-run claude translator. */
|
|
117
150
|
export function claudeTranslator(opts = {}) {
|
|
@@ -250,43 +283,10 @@ export function claudeTranslator(opts = {}) {
|
|
|
250
283
|
const info = objectOrEmpty(event.rate_limit_info);
|
|
251
284
|
const rateLimitType = optionalString(info.rateLimitType);
|
|
252
285
|
const status = optionalString(info.status);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (type === rateLimitType || !isJsonObject(raw))
|
|
258
|
-
continue;
|
|
259
|
-
const reading = { type: "rate_limit", rateLimitType: type };
|
|
260
|
-
const windowResets = nonNegativeNumber(raw.resetsAt, undefined);
|
|
261
|
-
if (windowResets !== undefined)
|
|
262
|
-
reading.resetsAt = windowResets;
|
|
263
|
-
const windowUtil = nonNegativeNumber(raw.utilization, undefined);
|
|
264
|
-
if (windowUtil !== undefined)
|
|
265
|
-
reading.utilization = windowUtil;
|
|
266
|
-
windows.push(JSON.stringify(reading));
|
|
267
|
-
}
|
|
268
|
-
const own = rateLimitType === undefined ? undefined : objectOrEmpty(objectOrEmpty(info.unifiedWindows)[rateLimitType]);
|
|
269
|
-
const resetsAt = nonNegativeNumber(info.resetsAt, undefined) ?? nonNegativeNumber(own?.resetsAt, undefined);
|
|
270
|
-
const out = { type: "rate_limit" };
|
|
271
|
-
if (rateLimitType !== undefined)
|
|
272
|
-
out.rateLimitType = rateLimitType;
|
|
273
|
-
if (status !== undefined)
|
|
274
|
-
out.status = status;
|
|
275
|
-
if (resetsAt !== undefined)
|
|
276
|
-
out.resetsAt = resetsAt;
|
|
277
|
-
const utilization = nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
|
|
278
|
-
if (utilization !== undefined)
|
|
279
|
-
out.utilization = utilization;
|
|
280
|
-
const overageStatus = optionalString(info.overageStatus);
|
|
281
|
-
if (overageStatus !== undefined)
|
|
282
|
-
out.overageStatus = overageStatus;
|
|
283
|
-
if (info.isUsingOverage === true)
|
|
284
|
-
out.isUsingOverage = true;
|
|
285
|
-
if (status === "rejected" && resetsAt !== undefined) {
|
|
286
|
-
// resetsAt is a unix-seconds stamp; secondsUntilUnixStamp folds it to whole seconds.
|
|
287
|
-
out.retryAfterSeconds = secondsUntilUnixStamp(resetsAt, Date.now());
|
|
288
|
-
}
|
|
289
|
-
return [JSON.stringify(out), ...windows];
|
|
286
|
+
return [
|
|
287
|
+
JSON.stringify(claudeRateLimitReading(info, rateLimitType, status)),
|
|
288
|
+
...claudeRateLimitWindowReadings(info, rateLimitType),
|
|
289
|
+
];
|
|
290
290
|
};
|
|
291
291
|
const handleResult = (event) => {
|
|
292
292
|
sawActivity = true;
|
|
@@ -363,6 +363,63 @@ export function claudeTranslator(opts = {}) {
|
|
|
363
363
|
},
|
|
364
364
|
};
|
|
365
365
|
}
|
|
366
|
+
/** One reading per sibling `unifiedWindows` window other than the binding window. */
|
|
367
|
+
function claudeRateLimitWindowReadings(info, rateLimitType) {
|
|
368
|
+
// claude 2.1.295 reports every window's utilization and reset under `unifiedWindows`,
|
|
369
|
+
// keyed by window type, and only the binding window's status at the top level.
|
|
370
|
+
const windows = [];
|
|
371
|
+
for (const [type, raw] of Object.entries(objectOrEmpty(info.unifiedWindows))) {
|
|
372
|
+
if (type === rateLimitType || !isJsonObject(raw))
|
|
373
|
+
continue;
|
|
374
|
+
const reading = { type: "rate_limit", rateLimitType: type };
|
|
375
|
+
const windowResets = nonNegativeNumber(raw.resetsAt, undefined);
|
|
376
|
+
if (windowResets !== undefined)
|
|
377
|
+
reading.resetsAt = windowResets;
|
|
378
|
+
const windowUtil = nonNegativeNumber(raw.utilization, undefined);
|
|
379
|
+
if (windowUtil !== undefined)
|
|
380
|
+
reading.utilization = windowUtil;
|
|
381
|
+
windows.push(JSON.stringify(reading));
|
|
382
|
+
}
|
|
383
|
+
return windows;
|
|
384
|
+
}
|
|
385
|
+
/** The binding window's own `unifiedWindows` record, if a rate-limit type was named. */
|
|
386
|
+
function claudeRateLimitOwnWindow(info, rateLimitType) {
|
|
387
|
+
return rateLimitType === undefined ? undefined : objectOrEmpty(objectOrEmpty(info.unifiedWindows)[rateLimitType]);
|
|
388
|
+
}
|
|
389
|
+
/** The binding window's reset time: the top-level `resetsAt` falling back to its own record. */
|
|
390
|
+
function claudeRateLimitResetsAt(info, own) {
|
|
391
|
+
return nonNegativeNumber(info.resetsAt, undefined) ?? nonNegativeNumber(own?.resetsAt, undefined);
|
|
392
|
+
}
|
|
393
|
+
/** The binding window's utilization: the top-level `utilization` falling back to its own record. */
|
|
394
|
+
function claudeRateLimitUtilization(info, own) {
|
|
395
|
+
return nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
|
|
396
|
+
}
|
|
397
|
+
/** The binding window's canonical reading: its optional fields, plus a rejected window's
|
|
398
|
+
* retry-after. */
|
|
399
|
+
function claudeRateLimitReading(info, rateLimitType, status) {
|
|
400
|
+
const own = claudeRateLimitOwnWindow(info, rateLimitType);
|
|
401
|
+
const resetsAt = claudeRateLimitResetsAt(info, own);
|
|
402
|
+
const out = { type: "rate_limit" };
|
|
403
|
+
if (rateLimitType !== undefined)
|
|
404
|
+
out.rateLimitType = rateLimitType;
|
|
405
|
+
if (status !== undefined)
|
|
406
|
+
out.status = status;
|
|
407
|
+
if (resetsAt !== undefined)
|
|
408
|
+
out.resetsAt = resetsAt;
|
|
409
|
+
const utilization = claudeRateLimitUtilization(info, own);
|
|
410
|
+
if (utilization !== undefined)
|
|
411
|
+
out.utilization = utilization;
|
|
412
|
+
const overageStatus = optionalString(info.overageStatus);
|
|
413
|
+
if (overageStatus !== undefined)
|
|
414
|
+
out.overageStatus = overageStatus;
|
|
415
|
+
if (info.isUsingOverage === true)
|
|
416
|
+
out.isUsingOverage = true;
|
|
417
|
+
if (status === "rejected" && resetsAt !== undefined) {
|
|
418
|
+
// resetsAt is a unix-seconds stamp; secondsUntilUnixStamp folds it to whole seconds.
|
|
419
|
+
out.retryAfterSeconds = secondsUntilUnixStamp(resetsAt, Date.now());
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
366
423
|
/** The run's billing, from `system/init.apiKeySource`: claude.ai OAuth reports `"none"`, an
|
|
367
424
|
* environment key reports `ANTHROPIC_API_KEY`, a helper reports `apiKeyHelper`. A
|
|
368
425
|
* third-party provider overrides both — it is API billing even when the init omitted a
|
|
@@ -84,6 +84,14 @@ export function parseCodexLoginStatus(stdout) {
|
|
|
84
84
|
return "api";
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
|
+
/** The refusal message for a codex logged in with an API key, shared by the startup gate, the
|
|
88
|
+
* live-config warn and the doctor's codex auth check (decision 5): Codex reports no cost, so
|
|
89
|
+
* per-token billing would spend money the budget cap cannot see. `home` names the codex home
|
|
90
|
+
* the operator should re-login under; the gate and the live warn pass the literal "<home>"
|
|
91
|
+
* placeholder because they do not resolve one, while the doctor passes the resolved home. */
|
|
92
|
+
export function codexApiKeyRefusalMessage(home) {
|
|
93
|
+
return `Codex is logged in with an API key; tumwater runs Codex on a ChatGPT subscription only — run \`CODEX_HOME=${home} codex login --device-auth\``;
|
|
94
|
+
}
|
|
87
95
|
/** The billing the login probe names, or null when it cannot be decided. */
|
|
88
96
|
function seedBilling(config) {
|
|
89
97
|
const status = probeLoginStatus(config);
|
|
@@ -106,11 +106,11 @@ export const EFFORT_BY_THINKING = {
|
|
|
106
106
|
* spawn on a model that lists it. */
|
|
107
107
|
const KNOWN_EFFORTS = new Set(Object.values(EFFORT_BY_THINKING));
|
|
108
108
|
/** Map a thinking level to the Codex `model_reasoning_effort` it means, gated by the model's own
|
|
109
|
-
* catalog. `levels` is the model's `supported_reasoning_levels` (design finding 14), which
|
|
110
|
-
*
|
|
111
|
-
* result is returned unclamped. An unknown thinking level returns `undefined` (no effort
|
|
112
|
-
* the level simply has no Codex effort, unlike a thinking level that maps but the model
|
|
113
|
-
* support, which is also `undefined`. */
|
|
109
|
+
* catalog. `levels` is the model's `supported_reasoning_levels` (design finding 14), which the
|
|
110
|
+
* codex catalog (part 8/9) supplies; when the caller has no catalog it passes `undefined`, and
|
|
111
|
+
* the result is returned unclamped. An unknown thinking level returns `undefined` (no effort
|
|
112
|
+
* flag) — the level simply has no Codex effort, unlike a thinking level that maps but the model
|
|
113
|
+
* does not support, which is also `undefined`. */
|
|
114
114
|
export function codexEffort(thinking, levels) {
|
|
115
115
|
if (!Object.hasOwn(EFFORT_BY_THINKING, thinking))
|
|
116
116
|
return undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The codex adapter for the agent seam (plans/codex-backend.md §4, §5, §6): it shapes one
|
|
2
2
|
* run's argv/stdin/env, translates codex's `exec --json` stream, reads the authoritative
|
|
3
3
|
* rollout after the process exits, and classifies the exit's stderr — everything
|
|
4
|
-
* backend-specific. src/agent/
|
|
4
|
+
* backend-specific. src/agent/agent-spawn.ts spawns the run and records this run in the session
|
|
5
5
|
* ledger; codex mints its own session id (a thread id, or the resumed record's id) in the
|
|
6
6
|
* stream's `thread.started`, which the runner's stream-mint pass appends to the ledger and
|
|
7
7
|
* `invocation` returns so a later `exec resume` can reach it.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* thread and the existing `handoff` path takes over (design §4). Together with the `resumable`
|
|
13
13
|
* hook, that is every resume point's one rule.
|
|
14
14
|
*
|
|
15
|
-
* Registration lives in src/agent/run
|
|
15
|
+
* Registration lives in src/agent/agent-run.ts beside the pi, claude and opencode adapters; a
|
|
16
16
|
* test selects codex by setting `config.agent = "codex"` on a run. A run carrying a note gets
|
|
17
17
|
* the notes path in its environment (the `tumwater-notes` command on PATH reads it), as the
|
|
18
18
|
* pi and claude adapters do. */
|
|
@@ -1,18 +1,31 @@
|
|
|
1
|
+
/** Building the environment for one codex run (plans/codex-backend.md "The invocation" §3,
|
|
2
|
+
* findings 10, 13) — pure scrub-and-set over a base environment, with no subprocess. The caller
|
|
3
|
+
* layers the run-marker, the macOS LaunchServices preload and the notes shim on first, then hands
|
|
4
|
+
* the result here.
|
|
5
|
+
*
|
|
6
|
+
* Inherited state from a host Codex or Claude Code session breaks the team run: a Codex child
|
|
7
|
+
* keeps every `CODEX_*` it was given (finding 13), and `--ignore-user-config` still reads a host
|
|
8
|
+
* home whose `config.toml` a run writes into (finding 10). So every `CODEX_*`, the host Claude
|
|
9
|
+
* session's `CLAUDECODE` / `CLAUDE_CODE_*`, and both API keys an operator might carry
|
|
10
|
+
* (`OPENAI_API_KEY`, `CODEX_API_KEY`) are scrubbed — pi's OpenAI key must never switch Codex to
|
|
11
|
+
* per-token billing behind tumwater's back (decision 5). Everything else survives, including the
|
|
12
|
+
* run marker and the PATH that carries the notes shim.
|
|
13
|
+
*
|
|
14
|
+
* The set half points Codex at a home of the team's own (`CODEX_HOME`) when one is configured, so
|
|
15
|
+
* team sessions and trust entries stay out of the operator's `~/.codex` (finding 10). Unset, runs
|
|
16
|
+
* keep the operator's home. */
|
|
17
|
+
import { scrubEnv } from "../env-scrub.js";
|
|
1
18
|
/** Build the codex process environment from `base`. Never mutates `base`. */
|
|
2
19
|
export function codexEnv(base, opts = {}) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
if (opts.home)
|
|
16
|
-
env.CODEX_HOME = opts.home;
|
|
17
|
-
return env;
|
|
20
|
+
return scrubEnv(base, {
|
|
21
|
+
keep: (key) => key !== "CLAUDECODE" &&
|
|
22
|
+
!key.startsWith("CODEX_") &&
|
|
23
|
+
!key.startsWith("CLAUDE_CODE_") &&
|
|
24
|
+
key !== "OPENAI_API_KEY" &&
|
|
25
|
+
key !== "CODEX_API_KEY",
|
|
26
|
+
then: (env) => {
|
|
27
|
+
if (opts.home)
|
|
28
|
+
env.CODEX_HOME = opts.home;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
18
31
|
}
|
|
@@ -37,29 +37,48 @@ function hookListMessages(cwd) {
|
|
|
37
37
|
* `eventName: "preToolUse"` and `command` exactly the guard definition's (finding 3), or null
|
|
38
38
|
* when none matches. The key and hash are returned as codex reports them. */
|
|
39
39
|
function findTrust(result, definition) {
|
|
40
|
+
for (const group of hookGroups(result)) {
|
|
41
|
+
for (const hook of groupHooks(group)) {
|
|
42
|
+
const trust = matchHookTrust(hook, definition);
|
|
43
|
+
if (trust !== null)
|
|
44
|
+
return trust;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/** The top-level group list from `hooks/list`'s `result.data`, or `[]` when the result is not
|
|
50
|
+
* an object or has no array `data`. */
|
|
51
|
+
function hookGroups(result) {
|
|
40
52
|
if (typeof result !== "object" || result === null)
|
|
41
|
-
return
|
|
53
|
+
return [];
|
|
42
54
|
const data = result.data;
|
|
43
|
-
if (
|
|
55
|
+
if (Array.isArray(data))
|
|
56
|
+
return data;
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
/** One group's hook list from its `hooks` field, or `[]` when the group is not an object or
|
|
60
|
+
* has no array `hooks`. */
|
|
61
|
+
function groupHooks(group) {
|
|
62
|
+
if (typeof group !== "object" || group === null)
|
|
63
|
+
return [];
|
|
64
|
+
const hooks = group.hooks;
|
|
65
|
+
if (Array.isArray(hooks))
|
|
66
|
+
return hooks;
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
/** The trust of one hook when it is the guard's — `source` `sessionFlags`, `eventName`
|
|
70
|
+
* `preToolUse`, `command` exactly the definition's, with string `key` and `currentHash` — or
|
|
71
|
+
* null. */
|
|
72
|
+
function matchHookTrust(hook, definition) {
|
|
73
|
+
if (typeof hook !== "object" || hook === null)
|
|
44
74
|
return null;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (typeof hook !== "object" || hook === null)
|
|
53
|
-
continue;
|
|
54
|
-
const h = hook;
|
|
55
|
-
if (h.source !== "sessionFlags" || h.eventName !== "preToolUse")
|
|
56
|
-
continue;
|
|
57
|
-
if (h.command !== definition.command)
|
|
58
|
-
continue;
|
|
59
|
-
if (typeof h.key === "string" && typeof h.currentHash === "string") {
|
|
60
|
-
return { key: h.key, hash: h.currentHash };
|
|
61
|
-
}
|
|
62
|
-
}
|
|
75
|
+
const h = hook;
|
|
76
|
+
if (h.source !== "sessionFlags" || h.eventName !== "preToolUse")
|
|
77
|
+
return null;
|
|
78
|
+
if (h.command !== definition.command)
|
|
79
|
+
return null;
|
|
80
|
+
if (typeof h.key === "string" && typeof h.currentHash === "string") {
|
|
81
|
+
return { key: h.key, hash: h.currentHash };
|
|
63
82
|
}
|
|
64
83
|
return null;
|
|
65
84
|
}
|
|
@@ -29,6 +29,33 @@ function windowStatus(windowType, rateLimitReachedType, usedPercent) {
|
|
|
29
29
|
return "allowed_warning";
|
|
30
30
|
return "allowed";
|
|
31
31
|
}
|
|
32
|
+
/** The carrier type of one rollout line: an `event_msg` wrapper's `payload.type` when the
|
|
33
|
+
* outer `type` is `event_msg`, else the outer `type` itself. `task_started`, `task_complete`
|
|
34
|
+
* and `token_count` come wrapped in an `event_msg` whose `payload.type` discriminates;
|
|
35
|
+
* `turn_context` and `token_usage_record` name themselves in the outer `type`. Read either. */
|
|
36
|
+
function lineCarrierType(parsed, payload) {
|
|
37
|
+
return optionalString(parsed.type) === "event_msg" ? optionalString(payload.type) : optionalString(parsed.type);
|
|
38
|
+
}
|
|
39
|
+
/** Whether a `task_started` line begins the read turn and, when it does, its turn id. Each new
|
|
40
|
+
* task_started begins a fresh turn (a resume appends a second run to the same file), so the
|
|
41
|
+
* collectors restart at the block we are reading. With no turn id (the adapter's read) every
|
|
42
|
+
* task_started restarts them and the last block wins; with one, only the requested turn's does. */
|
|
43
|
+
function turnStartReading(payload, sinceTurnStart) {
|
|
44
|
+
const turnId = optionalString(payload.turn_id);
|
|
45
|
+
return { turnId, readsTurn: sinceTurnStart === undefined || turnId === sinceTurnStart };
|
|
46
|
+
}
|
|
47
|
+
/** The usage figure a `token_usage_record` line contributes — its `turn_token_usage` when it
|
|
48
|
+
* carries input or output tokens, else undefined. */
|
|
49
|
+
function usageFromRecord(payload) {
|
|
50
|
+
const turnUsage = objectOrEmpty(payload.turn_token_usage);
|
|
51
|
+
return isJsonObject(turnUsage) && (turnUsage.input_tokens !== undefined || turnUsage.output_tokens !== undefined) ? turnUsage : undefined;
|
|
52
|
+
}
|
|
53
|
+
/** The rate limits and `plan_type` a `token_count` line contributes, from its `rate_limits`
|
|
54
|
+
* object (assigned even when empty). */
|
|
55
|
+
function rateLimitsFromCount(payload) {
|
|
56
|
+
const rateLimits = objectOrEmpty(payload.rate_limits);
|
|
57
|
+
return { rateLimits, planType: optionalString(rateLimits.plan_type) };
|
|
58
|
+
}
|
|
32
59
|
/** Scan ONE turn's records out of a rollout's text, tracking `task_started` blocks and
|
|
33
60
|
* collecting the read turn's `turn_context.model`, `token_usage_record` usage, `token_count`
|
|
34
61
|
* rate limits and `plan_type`. `rolloutLines` decides emptiness from `sawTurn`/`lastUsage`;
|
|
@@ -47,17 +74,11 @@ function scanTurnRecords(text, sinceTurnStart) {
|
|
|
47
74
|
if (!parsed)
|
|
48
75
|
continue; // a torn or foreign line belongs to no turn
|
|
49
76
|
const payload = objectOrEmpty(parsed.payload);
|
|
50
|
-
|
|
51
|
-
// `payload.type` discriminates; `turn_context` and `token_usage_record` name themselves in
|
|
52
|
-
// the outer `type`. Read either.
|
|
53
|
-
const ptype = optionalString(parsed.type) === "event_msg" ? optionalString(payload.type) : optionalString(parsed.type);
|
|
77
|
+
const ptype = lineCarrierType(parsed, payload);
|
|
54
78
|
if (ptype === "task_started") {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// adapter's read) every task_started restarts them and the last block wins; with one,
|
|
59
|
-
// only the requested turn's does.
|
|
60
|
-
if (sinceTurnStart === undefined || currentTurn === sinceTurnStart) {
|
|
79
|
+
const start = turnStartReading(payload, sinceTurnStart);
|
|
80
|
+
currentTurn = start.turnId;
|
|
81
|
+
if (start.readsTurn) {
|
|
61
82
|
sawTurn = true;
|
|
62
83
|
lastUsage = undefined;
|
|
63
84
|
lastRateLimits = undefined;
|
|
@@ -73,19 +94,14 @@ function scanTurnRecords(text, sinceTurnStart) {
|
|
|
73
94
|
if (model === undefined)
|
|
74
95
|
model = optionalString(payload.model);
|
|
75
96
|
break;
|
|
76
|
-
case "token_usage_record":
|
|
77
|
-
|
|
78
|
-
if (isJsonObject(turnUsage) && (turnUsage.input_tokens !== undefined || turnUsage.output_tokens !== undefined)) {
|
|
79
|
-
lastUsage = turnUsage;
|
|
80
|
-
}
|
|
97
|
+
case "token_usage_record":
|
|
98
|
+
lastUsage = usageFromRecord(payload) ?? lastUsage;
|
|
81
99
|
break;
|
|
82
|
-
}
|
|
83
100
|
case "token_count": {
|
|
84
|
-
const
|
|
85
|
-
lastRateLimits = rateLimits;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
planType = plan;
|
|
101
|
+
const reading = rateLimitsFromCount(payload);
|
|
102
|
+
lastRateLimits = reading.rateLimits;
|
|
103
|
+
if (reading.planType !== undefined)
|
|
104
|
+
planType = reading.planType;
|
|
89
105
|
break;
|
|
90
106
|
}
|
|
91
107
|
default:
|
|
@@ -94,52 +110,73 @@ function scanTurnRecords(text, sinceTurnStart) {
|
|
|
94
110
|
}
|
|
95
111
|
return { sawTurn, lastUsage, lastRateLimits, model, planType };
|
|
96
112
|
}
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// stays there, so this builder never reads `sawTurn`); narrow the shared interface here.
|
|
102
|
-
const lastUsage = records.lastUsage;
|
|
103
|
-
const { lastRateLimits, model, planType } = records;
|
|
104
|
-
const lines = [];
|
|
113
|
+
/** The `run_usage` line of a turn's scanned usage: this run's own input (net of what the
|
|
114
|
+
* cache already held), output and cache figures (finding 4: the stream reports only the
|
|
115
|
+
* whole-thread total; the rollout records this run's own). */
|
|
116
|
+
function runUsageLine(lastUsage) {
|
|
105
117
|
const input = finite(lastUsage.input_tokens) ?? 0;
|
|
106
118
|
const cacheRead = finite(lastUsage.cached_input_tokens) ?? 0;
|
|
107
|
-
|
|
119
|
+
return JSON.stringify({
|
|
108
120
|
type: "run_usage",
|
|
109
121
|
input: Math.max(0, input - cacheRead),
|
|
110
122
|
output: finite(lastUsage.output_tokens) ?? 0,
|
|
111
123
|
cacheRead,
|
|
112
124
|
cacheWrite: finite(lastUsage.cache_write_input_tokens) ?? 0,
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** The `billing` line of a turn with a plan_type: a reading with a plan_type bills on the
|
|
128
|
+
* subscription (finding 5; API-key login is refused, decision 5). */
|
|
129
|
+
function billingLine() {
|
|
130
|
+
// A reading with a plan_type bills on the subscription (finding 5; API-key login is
|
|
131
|
+
// refused, decision 5).
|
|
132
|
+
return JSON.stringify({ type: "billing", billing: "subscription" });
|
|
133
|
+
}
|
|
134
|
+
/** The up-to-two `rate_limit` lines of a turn's scanned rate limits: the primary (5 h) and
|
|
135
|
+
* the secondary (weekly) windows when both are present and well-formed. */
|
|
136
|
+
function rateLimitLines(lastRateLimits) {
|
|
119
137
|
// Two windows when present: the primary (5 h) and the secondary (weekly). A killed run's
|
|
120
138
|
// rollout has a token_usage_record but no token_count, so it yields run_usage with no
|
|
121
139
|
// rate_limit (finding 23).
|
|
122
|
-
if (lastRateLimits
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
})
|
|
140
|
-
|
|
140
|
+
if (lastRateLimits === undefined)
|
|
141
|
+
return [];
|
|
142
|
+
const reached = optionalString(lastRateLimits.rate_limit_reached_type);
|
|
143
|
+
const lines = [];
|
|
144
|
+
for (const key of ["primary", "secondary"]) {
|
|
145
|
+
const windowRaw = lastRateLimits[key];
|
|
146
|
+
if (!isJsonObject(windowRaw))
|
|
147
|
+
continue;
|
|
148
|
+
const minutes = finite(windowRaw.window_minutes);
|
|
149
|
+
if (minutes === undefined)
|
|
150
|
+
continue;
|
|
151
|
+
const typeName = windowTypeName(minutes);
|
|
152
|
+
const usedPercent = finite(windowRaw.used_percent);
|
|
153
|
+
lines.push(JSON.stringify({
|
|
154
|
+
type: "rate_limit",
|
|
155
|
+
rateLimitType: typeName,
|
|
156
|
+
status: windowStatus(typeName, reached, usedPercent),
|
|
157
|
+
...(usedPercent !== undefined ? { utilization: usedPercent / 100 } : {}),
|
|
158
|
+
...(finite(windowRaw.resets_at) !== undefined ? { resetsAt: finite(windowRaw.resets_at) } : {}),
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
163
|
+
/** The model this turn switched to when its `turn_context.model` differs from what the run
|
|
164
|
+
* requested (finding 24): the model itself, else undefined for no switch. */
|
|
165
|
+
function switchedToModel(model, requestedModel) {
|
|
166
|
+
return model !== undefined && model !== requestedModel ? model : undefined;
|
|
167
|
+
}
|
|
168
|
+
/** Assemble the reading's canonical `run_usage`, `billing` and up-to-two `rate_limit` lines
|
|
169
|
+
* (in that order), plus the `switchedTo` model comparison, from scanned records. */
|
|
170
|
+
function buildRolloutLines(records, requestedModel) {
|
|
171
|
+
// `rolloutLines` guarantees `lastUsage` is a Json object before calling (its presence check
|
|
172
|
+
// stays there, so this builder never reads `sawTurn`); narrow the shared interface here.
|
|
173
|
+
const lastUsage = records.lastUsage;
|
|
174
|
+
const lines = [runUsageLine(lastUsage)];
|
|
175
|
+
if (records.planType !== undefined) {
|
|
176
|
+
lines.push(billingLine());
|
|
141
177
|
}
|
|
142
|
-
|
|
178
|
+
lines.push(...rateLimitLines(records.lastRateLimits));
|
|
179
|
+
const switchedTo = switchedToModel(records.model, requestedModel);
|
|
143
180
|
return { lines, ...(switchedTo !== undefined ? { switchedTo } : {}) };
|
|
144
181
|
}
|
|
145
182
|
/** Read the records of ONE turn out of a rollout's text. A rollout is one file per thread that
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* state (the open tool calls, the turn's usage fallback, the unmapped native types seen), so
|
|
11
11
|
* call it once per run. Unmapped native types, the distinct text of `error` items, and a
|
|
12
12
|
* command abandoned at the end of its turn are recorded once each on the seam's optional
|
|
13
|
-
* `warnings()` — src/agent/
|
|
13
|
+
* `warnings()` — src/agent/agent-stream.ts forwards each as an `agent_unknown_event` warning
|
|
14
14
|
* after the run. `sessionId()` reports the thread id once `thread.started` has named it.
|
|
15
15
|
*/
|
|
16
16
|
import { nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Shared environment-building skeleton for the three agent backend env builders
|
|
2
|
+
* (claude-env.ts, codex-env.ts, opencode-env.ts). Each backend isolates one run the same shape:
|
|
3
|
+
* copy the keys of `base` that survive the scrub, then let the backend set its own run vars.
|
|
4
|
+
* The copy half never writes anything but `keep`-accepted keys of `base`, so a caller's base
|
|
5
|
+
* (the process env, or a layered run-marker/notes/preload env) is never mutated — the invariant
|
|
6
|
+
* the per-backend tests pin. */
|
|
7
|
+
/** Build a process environment from `base` under `scrub` without mutating `base`: copy the
|
|
8
|
+
* `keep`-accepted base entries into a fresh object, then hand it to `then` for the backend's
|
|
9
|
+
* own additions. */
|
|
10
|
+
export function scrubEnv(base, scrub) {
|
|
11
|
+
const env = {};
|
|
12
|
+
for (const [key, value] of Object.entries(base)) {
|
|
13
|
+
if (scrub.keep(key))
|
|
14
|
+
env[key] = value;
|
|
15
|
+
}
|
|
16
|
+
scrub.then(env);
|
|
17
|
+
return env;
|
|
18
|
+
}
|