tumwater 0.4.1 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/backend-traits.js +14 -0
- package/dist/src/agent/claude/claude-stream.js +27 -31
- package/dist/src/agent/codex/codex-rollout.js +38 -24
- package/dist/src/agent/codex/codex-stream.js +71 -38
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +4 -4
- package/dist/src/agent/run-agent.js +11 -2
- package/dist/src/agent/run-settle.js +22 -13
- package/dist/src/agent/run-spawn.js +3 -12
- package/dist/src/agent/run-stream.js +2 -10
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/backlog/backlog-archive.js +64 -165
- package/dist/src/backlog/backlog-eligibility.js +50 -58
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +2 -2
- package/dist/src/backlog/backlog-md.js +38 -27
- package/dist/src/backlog/backlog-release.js +11 -19
- package/dist/src/backlog/backlog-structure.js +14 -1
- package/dist/src/backlog/backlog-write.js +1 -1
- package/dist/src/backlog/backlog.js +5 -4
- package/dist/src/backlog/question-answer.js +30 -14
- package/dist/src/baseline/main-baseline.js +30 -20
- package/dist/src/baseline/main-guardrails.js +54 -17
- package/dist/src/baseline/main-watch.js +1 -1
- package/dist/src/budget/budget-wiring.js +0 -5
- package/dist/src/budget/budget.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +1 -4
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +27 -50
- package/dist/src/cli/cli-start.js +62 -12
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +6 -7
- package/dist/src/cli/log-commands.js +3 -1
- package/dist/src/cli.js +3 -7
- package/dist/src/collections.js +21 -7
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +19 -26
- package/dist/src/doctor/doctor-backlog-bugs.js +17 -115
- package/dist/src/doctor/doctor-backlog-plans.js +8 -8
- package/dist/src/doctor/doctor-backlog.js +22 -36
- package/dist/src/doctor/doctor-check-names.js +2 -2
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-contention-checks.js +1 -1
- package/dist/src/doctor/doctor-holds.js +13 -11
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +85 -48
- package/dist/src/doctor/doctor-project-checks.js +66 -17
- package/dist/src/doctor/doctor-role-notes.js +6 -24
- package/dist/src/doctor/doctor.js +6 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +5 -15
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/failure/failure-state-change.js +183 -68
- package/dist/src/failure/time-spend.js +1 -1
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/git/slot-salvage.js +5 -4
- package/dist/src/gui/gui-admin-endpoints.js +2 -3
- package/dist/src/gui/gui-backlog-commands.js +1 -1
- package/dist/src/gui/gui-backlog-endpoints.js +1 -7
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-operator-commands.js +0 -12
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +2 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/history/history.js +102 -67
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/prompt-commands.js +24 -19
- package/dist/src/init/init.js +68 -15
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-post-review-drain.js +1 -1
- package/dist/src/operator/operator-commands.js +68 -47
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +0 -6
- package/dist/src/pi/pi-watchdogs.js +20 -1
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/process/process.js +23 -0
- package/dist/src/process/supervisor.js +17 -1
- package/dist/src/prompt/gate-notes.js +9 -9
- package/dist/src/prompt/notes-backlog.js +11 -24
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +4 -5
- package/dist/src/report/report-data.js +10 -225
- package/dist/src/report/report-fold.js +20 -295
- package/dist/src/report/report-main-red.js +45 -42
- package/dist/src/report/report-render.js +66 -39
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +3 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +9 -9
- package/dist/src/review/quarantine-render.js +2 -1
- package/dist/src/roles/role-catalog.js +18 -30
- package/dist/src/roles/role-guidance.js +18 -43
- package/dist/src/roles/role-view.js +1 -2
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-detail-data.js +23 -7
- package/dist/src/run/run-detail-render.js +6 -0
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-prompt.js +100 -56
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +4 -21
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +8 -73
- package/dist/src/run/stage-backlog-checks.js +2 -29
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-run.js +2 -79
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +3 -10
- package/dist/src/status/status-data.js +5 -9
- package/dist/src/status/status-role-notes.js +18 -14
- package/dist/src/status/status-roles.js +2 -35
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +7 -212
- package/dist/src/ui/badges.js +2 -3
- package/dist/src/ui/gui/gui-client-boot.js +19 -4
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-report-chart.js +10 -8
- package/dist/src/ui/gui/gui-client-report.js +1 -2
- package/dist/src/ui/gui/gui-client-settings.js +0 -23
- package/dist/src/ui/gui/gui-client-team.js +26 -0
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/status-model.js +2 -2
- package/dist/src/ui/status-payload.js +3 -3
- package/dist/src/ui/status-render.js +3 -3
- package/dist/src/ui/team-alert-backlog.js +23 -54
- package/dist/src/ui/team-alert-shared.js +23 -10
- package/dist/src/ui/team-alert-status.js +8 -3
- package/dist/src/ui/team-alert-tail.js +1 -14
- package/dist/src/ui/team-alerts.js +6 -17
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +105 -93
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-pause.js +209 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +44 -34
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +121 -108
- package/dist/src/ui/tui/tui-editor-release.js +50 -42
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +58 -46
- package/dist/src/ui/tui/tui-editor-role-key.js +49 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-frame.js +1 -1
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-health.js +3 -24
- package/dist/src/ui/tui/tui-pane-history.js +20 -24
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +3 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +151 -128
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +2 -3
- package/dist/src/ui/tui/tui.js +3 -3
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +30 -6
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- package/dist/src/ui/team-alert-backlog-feed.js +0 -181
- package/dist/src/ui/team-alert-merge.js +0 -183
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ and is also outside `npm test`.
|
|
|
42
42
|
## Status
|
|
43
43
|
|
|
44
44
|
<!-- tumwater:status:start -->
|
|
45
|
-
**v0.4.
|
|
45
|
+
**v0.4.2**: working tumwater. The director and 15 of the 17 roles are enabled by default; `selfcheck`,
|
|
46
46
|
which files tumwater bugs from tumwater's own event log, and `feedback`, which files them as GitHub
|
|
47
47
|
issues on znation/tumwater, are opt-in (`roles.selfcheck.enabled`, `roles.feedback.enabled`).
|
|
48
48
|
|
|
@@ -75,11 +75,9 @@ and `tumwater pause`, `resume`, `wake`, and `stop` control the roles. `tumwater
|
|
|
75
75
|
so pair it with `--token <secret>`.
|
|
76
76
|
|
|
77
77
|
`tumwater prompt --attach <image>` gives a role an image to read at its next run. When a role's
|
|
78
|
-
model cannot read images, the command refuses the attachment up front (naming the fix)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
force) and the dashboards and `tumwater role <id>` say which roles read images and where a blind
|
|
82
|
-
run reroutes.
|
|
78
|
+
model cannot read images, the command refuses the attachment up front (naming the fix); the
|
|
79
|
+
dashboards and `tumwater role <id>` say which roles read images. Nothing reroutes a blind read
|
|
80
|
+
to another tier — a run that reads an image its model cannot see fails with `lastError`.
|
|
83
81
|
|
|
84
82
|
Every command, flag, and TUI key is in [docs/cli.md](docs/cli.md); `tumwater help` prints the
|
|
85
83
|
same list, and `tumwater completion bash|zsh|fish` prints shell completion. Settings live in
|
package/dist/build-info.json
CHANGED
|
@@ -95,3 +95,17 @@ export function backendDefaultLabel(backend) {
|
|
|
95
95
|
* for one cannot go missing from the other (opencode classifies the same failure from message
|
|
96
96
|
* text, not a status code, so it is not in the set). */
|
|
97
97
|
export const SERVER_TRANSIENT_STATUS = new Set([500, 502, 503, 504, 529]);
|
|
98
|
+
/** Classify a failure message's prose into the transient-backend failure it names, when the
|
|
99
|
+
* wire carries no structured signal: a dropped connection or a timeout, read from the message
|
|
100
|
+
* text by claude's and opencode's stream translators, which both fall back to prose for the
|
|
101
|
+
* statuses their native events leave to the message (claude's classifyResult and opencode's
|
|
102
|
+
* classifyError end with this same connection/then-timeout tail). The single home of the
|
|
103
|
+
* connection/timeout vocabulary, so the two backends' tail regexes cannot drift. undefined
|
|
104
|
+
* when the text names neither. */
|
|
105
|
+
export function transientProseKind(text) {
|
|
106
|
+
if (/connection|econn|refused|reset|terminated/i.test(text))
|
|
107
|
+
return { kind: "transientBackend", backendKind: "connection" };
|
|
108
|
+
if (/timed out|timeout/i.test(text))
|
|
109
|
+
return { kind: "transientBackend", backendKind: "timeout" };
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
@@ -15,9 +15,9 @@
|
|
|
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";
|
|
18
|
-
import {
|
|
19
|
-
import { canonicalUsageBlock, isJsonObject, objectOrEmpty,
|
|
20
|
-
import { SERVER_TRANSIENT_STATUS } from "../backend-traits.js";
|
|
18
|
+
import { secondsUntilUnixStamp } from "../../text/datetime.js";
|
|
19
|
+
import { canonicalUsageBlock, isJsonObject, nonNegativeNumber, objectOrEmpty, optionalString, parseJsonObject } from "../../files/json-object.js";
|
|
20
|
+
import { SERVER_TRANSIENT_STATUS, transientProseKind } from "../backend-traits.js";
|
|
21
21
|
/** claude's tool names → the canonical pi names the readers already know. Glob/Grep are kept
|
|
22
22
|
* lower-cased; MultiEdit collapses onto edit (pi has one edit tool). Anything else is
|
|
23
23
|
* lower-cased so a future tool still reads as a name rather than as `undefined`. */
|
|
@@ -32,9 +32,6 @@ const TOOL_NAMES = {
|
|
|
32
32
|
};
|
|
33
33
|
/** claude's HTTP status classes that mean a permanent configuration error. */
|
|
34
34
|
const CONFIG_STATUS = new Set([400, 401, 403, 404]);
|
|
35
|
-
function str(value) {
|
|
36
|
-
return typeof value === "string" ? value : undefined;
|
|
37
|
-
}
|
|
38
35
|
/** claude's usage block → pi's canonical usage block, with `cost.total` 0 because the run's
|
|
39
36
|
* authoritative cost rides the final `run_cost` event (a split-block message repeats its usage
|
|
40
37
|
* on every event, so summing per event would double-count — the fold takes it once). The
|
|
@@ -49,15 +46,15 @@ function canonicalUsage(usage) {
|
|
|
49
46
|
function canonicalBlock(block) {
|
|
50
47
|
switch (block.type) {
|
|
51
48
|
case "text":
|
|
52
|
-
return { type: "text", text:
|
|
49
|
+
return { type: "text", text: optionalString(block.text) ?? "" };
|
|
53
50
|
case "thinking":
|
|
54
|
-
return { type: "thinking", thinking:
|
|
51
|
+
return { type: "thinking", thinking: optionalString(block.thinking) ?? optionalString(block.text) ?? "" };
|
|
55
52
|
case "tool_use": {
|
|
56
53
|
const input = objectOrEmpty(block.input);
|
|
57
54
|
const args = { ...input };
|
|
58
55
|
if (typeof input.file_path === "string")
|
|
59
56
|
args.path = input.file_path;
|
|
60
|
-
const name =
|
|
57
|
+
const name = optionalString(block.name) ?? "";
|
|
61
58
|
// Own-property test, not a bare index: a tool name like `constructor`, `toString`, or
|
|
62
59
|
// `__proto__` would otherwise inherit a function (or Object.prototype) from the table's
|
|
63
60
|
// prototype chain and bypass the lower-cased fallback — JSON.stringify then drops the
|
|
@@ -65,7 +62,7 @@ function canonicalBlock(block) {
|
|
|
65
62
|
// applies to EFFORT_BY_THINKING.
|
|
66
63
|
return {
|
|
67
64
|
type: "toolCall",
|
|
68
|
-
id:
|
|
65
|
+
id: optionalString(block.id) ?? "",
|
|
69
66
|
name: Object.hasOwn(TOOL_NAMES, name) ? (TOOL_NAMES[name]) : name.toLowerCase(),
|
|
70
67
|
arguments: args,
|
|
71
68
|
};
|
|
@@ -81,12 +78,12 @@ function canonicalBlock(block) {
|
|
|
81
78
|
* provider classification applies. `status` is `api_error_status`, which is null on the
|
|
82
79
|
* not-logged-in result, so the text checks carry that case. */
|
|
83
80
|
function classifyResult(result) {
|
|
84
|
-
const terminal =
|
|
81
|
+
const terminal = optionalString(result.terminal_reason);
|
|
85
82
|
if (terminal === "aborted_tools")
|
|
86
83
|
return {};
|
|
87
84
|
if (terminal === "error_max_budget_usd")
|
|
88
85
|
return { kind: "budgetCapped" };
|
|
89
|
-
const text = joinParts([
|
|
86
|
+
const text = joinParts([optionalString(result.result), optionalString(result.error)], " ");
|
|
90
87
|
if (/error_max_budget_usd/i.test(text))
|
|
91
88
|
return { kind: "budgetCapped" };
|
|
92
89
|
const status = nonNegativeNumber(result.api_error_status, undefined);
|
|
@@ -109,10 +106,9 @@ function classifyResult(result) {
|
|
|
109
106
|
return { kind: "transientRateLimit" };
|
|
110
107
|
if (/overloaded|internal server error|bad gateway|service unavailable|gateway timeout|\b5\d\d\b/i.test(text))
|
|
111
108
|
return { kind: "transientBackend", backendKind: "server" };
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return { kind: "transientBackend", backendKind: "timeout" };
|
|
109
|
+
const prose = transientProseKind(text);
|
|
110
|
+
if (prose)
|
|
111
|
+
return prose;
|
|
116
112
|
if (/\b(400|401|403|404)\b/i.test(text))
|
|
117
113
|
return { kind: "configError" };
|
|
118
114
|
return {};
|
|
@@ -169,14 +165,14 @@ export function claudeTranslator(opts = {}) {
|
|
|
169
165
|
warnings.add(kind);
|
|
170
166
|
};
|
|
171
167
|
const handleSystem = (event) => {
|
|
172
|
-
const subtype =
|
|
168
|
+
const subtype = optionalString(event.subtype);
|
|
173
169
|
if (subtype === "init") {
|
|
174
170
|
const out = [];
|
|
175
|
-
const cwd =
|
|
176
|
-
const id =
|
|
171
|
+
const cwd = optionalString(event.cwd);
|
|
172
|
+
const id = optionalString(event.session_id);
|
|
177
173
|
out.push(JSON.stringify({ type: "session", ...(cwd !== undefined ? { cwd } : {}), ...(id !== undefined ? { id } : {}) }));
|
|
178
174
|
out.push(JSON.stringify({ type: "agent_start" }));
|
|
179
|
-
const billing = billingOf(
|
|
175
|
+
const billing = billingOf(optionalString(event.apiKeySource), opts.thirdPartyProvider === true);
|
|
180
176
|
if (billing)
|
|
181
177
|
out.push(JSON.stringify({ type: "billing", billing }));
|
|
182
178
|
return out;
|
|
@@ -185,7 +181,7 @@ export function claudeTranslator(opts = {}) {
|
|
|
185
181
|
const attempt = nonNegativeNumber(event.attempt, undefined);
|
|
186
182
|
const maxAttempts = nonNegativeNumber(event.max_retries, undefined);
|
|
187
183
|
const status = nonNegativeNumber(event.error_status, undefined);
|
|
188
|
-
const error =
|
|
184
|
+
const error = optionalString(event.error) ?? "";
|
|
189
185
|
const errorMessage = joinParts([status !== undefined ? `status ${status}` : "", error], ": ");
|
|
190
186
|
return [
|
|
191
187
|
JSON.stringify({
|
|
@@ -207,7 +203,7 @@ export function claudeTranslator(opts = {}) {
|
|
|
207
203
|
const handleAssistant = (event) => {
|
|
208
204
|
sawActivity = true;
|
|
209
205
|
const message = objectOrEmpty(event.message);
|
|
210
|
-
const id =
|
|
206
|
+
const id = optionalString(message.id) ?? `anon-${anonymous++}`;
|
|
211
207
|
// A new id flushes the open message; a continuing one flushes nothing. Either way the
|
|
212
208
|
// fresh or open pending then folds this event's content, usage, and stop reason the
|
|
213
209
|
// same way — that shared fold is why both branches live on one path.
|
|
@@ -230,7 +226,7 @@ export function claudeTranslator(opts = {}) {
|
|
|
230
226
|
// Usage repeats on every event of the same message; the last read wins and is counted
|
|
231
227
|
// once because the fold emits one message_end per id.
|
|
232
228
|
pending.usage = canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined);
|
|
233
|
-
const stopReason =
|
|
229
|
+
const stopReason = optionalString(message.stop_reason);
|
|
234
230
|
if (stopReason !== undefined)
|
|
235
231
|
pending.stopReason = stopReason;
|
|
236
232
|
return out;
|
|
@@ -244,7 +240,7 @@ export function claudeTranslator(opts = {}) {
|
|
|
244
240
|
continue;
|
|
245
241
|
out.push(JSON.stringify({
|
|
246
242
|
type: "tool_execution_end",
|
|
247
|
-
toolCallId:
|
|
243
|
+
toolCallId: optionalString(raw.tool_use_id) ?? "",
|
|
248
244
|
isError: raw.is_error === true,
|
|
249
245
|
}));
|
|
250
246
|
}
|
|
@@ -252,8 +248,8 @@ export function claudeTranslator(opts = {}) {
|
|
|
252
248
|
};
|
|
253
249
|
const handleRateLimitEvent = (event) => {
|
|
254
250
|
const info = objectOrEmpty(event.rate_limit_info);
|
|
255
|
-
const rateLimitType =
|
|
256
|
-
const status =
|
|
251
|
+
const rateLimitType = optionalString(info.rateLimitType);
|
|
252
|
+
const status = optionalString(info.status);
|
|
257
253
|
// claude 2.1.295 reports every window's utilization and reset under `unifiedWindows`,
|
|
258
254
|
// keyed by window type, and only the binding window's status at the top level.
|
|
259
255
|
const windows = [];
|
|
@@ -281,14 +277,14 @@ export function claudeTranslator(opts = {}) {
|
|
|
281
277
|
const utilization = nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
|
|
282
278
|
if (utilization !== undefined)
|
|
283
279
|
out.utilization = utilization;
|
|
284
|
-
const overageStatus =
|
|
280
|
+
const overageStatus = optionalString(info.overageStatus);
|
|
285
281
|
if (overageStatus !== undefined)
|
|
286
282
|
out.overageStatus = overageStatus;
|
|
287
283
|
if (info.isUsingOverage === true)
|
|
288
284
|
out.isUsingOverage = true;
|
|
289
285
|
if (status === "rejected" && resetsAt !== undefined) {
|
|
290
|
-
// resetsAt is a unix-seconds stamp;
|
|
291
|
-
out.retryAfterSeconds =
|
|
286
|
+
// resetsAt is a unix-seconds stamp; secondsUntilUnixStamp folds it to whole seconds.
|
|
287
|
+
out.retryAfterSeconds = secondsUntilUnixStamp(resetsAt, Date.now());
|
|
292
288
|
}
|
|
293
289
|
return [JSON.stringify(out), ...windows];
|
|
294
290
|
};
|
|
@@ -315,7 +311,7 @@ export function claudeTranslator(opts = {}) {
|
|
|
315
311
|
content: [],
|
|
316
312
|
stopReason: "error",
|
|
317
313
|
};
|
|
318
|
-
const errorMessage =
|
|
314
|
+
const errorMessage = optionalString(event.result) ?? optionalString(event.error);
|
|
319
315
|
if (errorMessage !== undefined)
|
|
320
316
|
message.errorMessage = errorMessage;
|
|
321
317
|
// tumwaterKind/backendKind ride the canonical EVENT (not its message): the parser
|
|
@@ -334,7 +330,7 @@ export function claudeTranslator(opts = {}) {
|
|
|
334
330
|
if (!parsed)
|
|
335
331
|
return []; // Non-JSON noise on stdout; ignore, as pi's parser does.
|
|
336
332
|
const event = parsed;
|
|
337
|
-
const type =
|
|
333
|
+
const type = optionalString(event.type);
|
|
338
334
|
switch (type) {
|
|
339
335
|
case "system": return handleSystem(event);
|
|
340
336
|
case "assistant": return handleAssistant(event);
|
|
@@ -13,11 +13,8 @@
|
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { walkFiles } from "../../files/files.js";
|
|
16
|
-
import { isJsonObject, nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
|
|
16
|
+
import { isJsonObject, nonNegativeNumber, objectOrEmpty, optionalString, parseJsonObject } from "../../files/json-object.js";
|
|
17
17
|
import { windowTypeName } from "./codex-stream.js";
|
|
18
|
-
function str(value) {
|
|
19
|
-
return typeof value === "string" ? value : undefined;
|
|
20
|
-
}
|
|
21
18
|
function finite(value) {
|
|
22
19
|
return nonNegativeNumber(value, undefined);
|
|
23
20
|
}
|
|
@@ -32,15 +29,11 @@ function windowStatus(windowType, rateLimitReachedType, usedPercent) {
|
|
|
32
29
|
return "allowed_warning";
|
|
33
30
|
return "allowed";
|
|
34
31
|
}
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
* (finding 4). `requestedModel` is the `-m` tumwater passed; a turn whose `turn_context.model`
|
|
41
|
-
* differs reports the switch. */
|
|
42
|
-
export function rolloutLines(text, opts) {
|
|
43
|
-
const { sinceTurnStart, requestedModel } = opts;
|
|
32
|
+
/** Scan ONE turn's records out of a rollout's text, tracking `task_started` blocks and
|
|
33
|
+
* collecting the read turn's `turn_context.model`, `token_usage_record` usage, `token_count`
|
|
34
|
+
* rate limits and `plan_type`. `rolloutLines` decides emptiness from `sawTurn`/`lastUsage`;
|
|
35
|
+
* this scanner never decides it. */
|
|
36
|
+
function scanTurnRecords(text, sinceTurnStart) {
|
|
44
37
|
let currentTurn;
|
|
45
38
|
let sawTurn = false;
|
|
46
39
|
let lastUsage;
|
|
@@ -57,9 +50,9 @@ export function rolloutLines(text, opts) {
|
|
|
57
50
|
// `task_started`, `task_complete` and `token_count` come wrapped in an `event_msg` whose
|
|
58
51
|
// `payload.type` discriminates; `turn_context` and `token_usage_record` name themselves in
|
|
59
52
|
// the outer `type`. Read either.
|
|
60
|
-
const ptype =
|
|
53
|
+
const ptype = optionalString(parsed.type) === "event_msg" ? optionalString(payload.type) : optionalString(parsed.type);
|
|
61
54
|
if (ptype === "task_started") {
|
|
62
|
-
currentTurn =
|
|
55
|
+
currentTurn = optionalString(payload.turn_id);
|
|
63
56
|
// Each new task_started begins a fresh turn (a resume appends a second run to the same
|
|
64
57
|
// file), so the collectors restart at the block we are reading. With no turn id (the
|
|
65
58
|
// adapter's read) every task_started restarts them and the last block wins; with one,
|
|
@@ -78,7 +71,7 @@ export function rolloutLines(text, opts) {
|
|
|
78
71
|
switch (ptype) {
|
|
79
72
|
case "turn_context":
|
|
80
73
|
if (model === undefined)
|
|
81
|
-
model =
|
|
74
|
+
model = optionalString(payload.model);
|
|
82
75
|
break;
|
|
83
76
|
case "token_usage_record": {
|
|
84
77
|
const turnUsage = objectOrEmpty(payload.turn_token_usage);
|
|
@@ -90,7 +83,7 @@ export function rolloutLines(text, opts) {
|
|
|
90
83
|
case "token_count": {
|
|
91
84
|
const rateLimits = objectOrEmpty(payload.rate_limits);
|
|
92
85
|
lastRateLimits = rateLimits;
|
|
93
|
-
const plan =
|
|
86
|
+
const plan = optionalString(rateLimits.plan_type);
|
|
94
87
|
if (plan !== undefined)
|
|
95
88
|
planType = plan;
|
|
96
89
|
break;
|
|
@@ -99,12 +92,15 @@ export function rolloutLines(text, opts) {
|
|
|
99
92
|
break;
|
|
100
93
|
}
|
|
101
94
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
return { sawTurn, lastUsage, lastRateLimits, model, planType };
|
|
96
|
+
}
|
|
97
|
+
/** Assemble the reading's canonical `run_usage`, `billing` and up-to-two `rate_limit` lines
|
|
98
|
+
* (in that order), plus the `switchedTo` model comparison, from scanned records. */
|
|
99
|
+
function buildRolloutLines(records, requestedModel) {
|
|
100
|
+
// `rolloutLines` guarantees `lastUsage` is a Json object before calling (its presence check
|
|
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;
|
|
108
104
|
const lines = [];
|
|
109
105
|
const input = finite(lastUsage.input_tokens) ?? 0;
|
|
110
106
|
const cacheRead = finite(lastUsage.cached_input_tokens) ?? 0;
|
|
@@ -124,7 +120,7 @@ export function rolloutLines(text, opts) {
|
|
|
124
120
|
// rollout has a token_usage_record but no token_count, so it yields run_usage with no
|
|
125
121
|
// rate_limit (finding 23).
|
|
126
122
|
if (lastRateLimits !== undefined) {
|
|
127
|
-
const reached =
|
|
123
|
+
const reached = optionalString(lastRateLimits.rate_limit_reached_type);
|
|
128
124
|
for (const key of ["primary", "secondary"]) {
|
|
129
125
|
const windowRaw = lastRateLimits[key];
|
|
130
126
|
if (!isJsonObject(windowRaw))
|
|
@@ -146,6 +142,24 @@ export function rolloutLines(text, opts) {
|
|
|
146
142
|
const switchedTo = model !== undefined && requestedModel !== undefined && model !== requestedModel ? model : undefined;
|
|
147
143
|
return { lines, ...(switchedTo !== undefined ? { switchedTo } : {}) };
|
|
148
144
|
}
|
|
145
|
+
/** Read the records of ONE turn out of a rollout's text. A rollout is one file per thread that
|
|
146
|
+
* appends a new `task_started` block per run, so the adapter's `afterRun` read (which knows
|
|
147
|
+
* only the thread id that names the file, never a turn id) reads the LAST block — everything
|
|
148
|
+
* after the last `task_started`. `sinceTurnStart` (optional) selects a specific turn's block
|
|
149
|
+
* instead, which pins per-turn isolation: the thread total hides a resume's own figures
|
|
150
|
+
* (finding 4). `requestedModel` is the `-m` tumwater passed; a turn whose `turn_context.model`
|
|
151
|
+
* differs reports the switch. */
|
|
152
|
+
export function rolloutLines(text, opts) {
|
|
153
|
+
const { sinceTurnStart, requestedModel } = opts;
|
|
154
|
+
const records = scanTurnRecords(text, sinceTurnStart);
|
|
155
|
+
// No matching task_started, or the turn's usage record is missing or malformed (a truncated
|
|
156
|
+
// or torn file): there is no reading. (currentTurn will have moved on to a later run's
|
|
157
|
+
// task_started in a resume-shared file; sawTurn — not currentTurn — is the real signal.)
|
|
158
|
+
if (!records.sawTurn || !isJsonObject(records.lastUsage)) {
|
|
159
|
+
return { lines: [] };
|
|
160
|
+
}
|
|
161
|
+
return buildRolloutLines(records, requestedModel);
|
|
162
|
+
}
|
|
149
163
|
/** Locate the rollout file of `$CODEX_HOME` that names the session `id`: the first
|
|
150
164
|
* `sessions/…/rollout-*-<id>.jsonl` under `home`, or null. It takes no start date and does no
|
|
151
165
|
* date math — the id match alone decides (finding 5: Codex's rollout name carries local time,
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
|
|
17
17
|
import { boundText, outputLimits } from "../../pi-extension/bounded-output.js";
|
|
18
|
-
import {
|
|
18
|
+
import { secondsUntilUnixStamp } from "../../text/datetime.js";
|
|
19
19
|
import { SERVER_TRANSIENT_STATUS } from "../backend-traits.js";
|
|
20
20
|
/** How codex wraps every shell command — the prefix to strip (with its quoting) so the
|
|
21
21
|
* canonical `bash` call carries the model's own command, which suite-rerun detection matches. */
|
|
@@ -43,31 +43,31 @@ export function windowTypeName(windowMinutes) {
|
|
|
43
43
|
return "seven_day";
|
|
44
44
|
return `${windowMinutes}_minute`;
|
|
45
45
|
}
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
* transient backend, its kind), trusted by `PiStreamParser` before its text regexes. The inner
|
|
50
|
-
* `message` is a JSON string carrying `status` and `error.type` / `error.code` (finding 11).
|
|
51
|
-
* Returns the canonical lines to emit beside the closing `message_end`: a rejected rate-limit
|
|
52
|
-
* reading when the error names a used-up window. */
|
|
53
|
-
function classifyTurnFailed(parsed) {
|
|
54
|
-
const kind = {};
|
|
46
|
+
/** Read the `turn.failed` inner JSON's four deciding fields. The inner `message` is a JSON
|
|
47
|
+
* string carrying `status` and `error.type` / `error.code` (finding 11). */
|
|
48
|
+
function readTurnFailedFields(parsed) {
|
|
55
49
|
const error = objectOrEmpty(parsed.error);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
return {
|
|
51
|
+
code: typeof error.code === "string" ? error.code : undefined,
|
|
52
|
+
errType: typeof error.type === "string" ? error.type : undefined,
|
|
53
|
+
message: typeof error.message === "string" ? error.message : typeof parsed.message === "string" ? parsed.message : undefined,
|
|
54
|
+
status: nonNegativeNumber(parsed.status, undefined),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Owns the context-window arm: a `context_window_exceeded` code, or a 400 whose message names
|
|
58
|
+
* the context window, classifies as `contextExceeded`. */
|
|
59
|
+
function isContextExceeded({ code, errType, message, status }) {
|
|
61
60
|
// The context-window signal is tested BEFORE the generic 400 `invalid_request_error`
|
|
62
61
|
// branch (as claude's classifyResult tests "prompt is too long" before CONFIG_STATUS): a
|
|
63
62
|
// `context_window_exceeded` code, or a 400 whose message names the context window, is
|
|
64
63
|
// `contextExceeded`, never `configError`.
|
|
65
64
|
const namesContextWindow = (t) => t !== undefined &&
|
|
66
65
|
/context window|context length|too long|exceeds? (the )?(model'?s )?context|prompt is too long/i.test(t);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
return code === "context_window_exceeded" || (status === 400 && errType === "invalid_request_error" && namesContextWindow(message));
|
|
67
|
+
}
|
|
68
|
+
/** Owns the transient rate-limit arm: a `usage_limit_reached` / `rate_limit_exceeded` code or a
|
|
69
|
+
* 429 returns the rejected reading naming the used-up window, else undefined. */
|
|
70
|
+
function rateLimitReading(parsed, { code, status }) {
|
|
71
71
|
if (code === "usage_limit_reached" || code === "rate_limit_exceeded" || status === 429) {
|
|
72
72
|
// Which window the account hit, from the error's own `rate_limit_reached_type` (else the
|
|
73
73
|
// five-hour window, the one Plus exhausts first). The rejected reading's wait comes from
|
|
@@ -75,42 +75,75 @@ function classifyTurnFailed(parsed) {
|
|
|
75
75
|
const minutes = nonNegativeNumber(parsed.rate_limit_reached_type, undefined);
|
|
76
76
|
const named = typeof parsed.rate_limit_reached_type === "string" ? parsed.rate_limit_reached_type : undefined;
|
|
77
77
|
const resetsAt = nonNegativeNumber(parsed.resets_at, undefined);
|
|
78
|
-
|
|
79
|
-
const retryAfter = resetsAt === undefined ? undefined : secondsUntil(resetsAt * 1000, Date.now());
|
|
80
|
-
kind.kind = "transientRateLimit";
|
|
81
|
-
kind.rateLimit = {
|
|
78
|
+
return {
|
|
82
79
|
type: "rate_limit",
|
|
83
80
|
rateLimitType: typeof named === "string" && /_minute$|_hour$|_day$/.test(named)
|
|
84
81
|
? named
|
|
85
82
|
: windowTypeName(minutes ?? 300),
|
|
86
83
|
status: "rejected",
|
|
87
84
|
utilization: 1,
|
|
88
|
-
|
|
89
|
-
...(
|
|
85
|
+
// secondsUntilUnixStamp folds the unix-seconds resets stamp to epoch-ms, clamped at 0.
|
|
86
|
+
...(resetsAt === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: { resetsAt, retryAfterSeconds: secondsUntilUnixStamp(resetsAt, Date.now()) }),
|
|
90
89
|
};
|
|
91
|
-
return kind;
|
|
92
90
|
}
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
/** Owns the used-up-account arm: `usage_not_included` is a permanent config error, never a
|
|
94
|
+
* transient retry. */
|
|
95
|
+
function isUsageNotIncluded({ code }) {
|
|
96
|
+
return code === "usage_not_included";
|
|
97
|
+
}
|
|
98
|
+
/** Owns the server-status arm: any `SERVER_TRANSIENT_STATUS` is a transient backend. */
|
|
99
|
+
function isTransientServer({ status }) {
|
|
100
|
+
return status !== undefined && SERVER_TRANSIENT_STATUS.has(status);
|
|
101
|
+
}
|
|
102
|
+
/** Owns the permanent-login arms: a reused/invalidated refresh token, a 401, and a 400
|
|
103
|
+
* `invalid_request_error` are permanent config errors. */
|
|
104
|
+
function isPermanentLogin({ message, status, errType }) {
|
|
105
|
+
// A refresh-token failure and an absent/invalid login are permanent config errors, which
|
|
106
|
+
// the doctor names with the login command (design §5).
|
|
107
|
+
if (/refresh_token_reused|invalidated refresh token/i.test(message ?? ""))
|
|
108
|
+
return true;
|
|
109
|
+
if (status === 401)
|
|
110
|
+
return true;
|
|
111
|
+
if (status === 400 && errType === "invalid_request_error") {
|
|
112
|
+
// A 400 `invalid_request_error` names the model as unsupported on a ChatGPT account.
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
/** How a `turn.failed` error message classifies into the canonical `tumwaterKind` (and, for a
|
|
118
|
+
* transient backend, its kind), trusted by `PiStreamParser` before its text regexes. The inner
|
|
119
|
+
* `message` is a JSON string carrying `status` and `error.type` / `error.code` (finding 11).
|
|
120
|
+
* Returns the canonical lines to emit beside the closing `message_end`: a rejected rate-limit
|
|
121
|
+
* reading when the error names a used-up window. The body is an ordered composition; each arm
|
|
122
|
+
* lives in its named helper: `readTurnFailedFields` (the field reads), `isContextExceeded`,
|
|
123
|
+
* `rateLimitReading`, `isUsageNotIncluded`, `isTransientServer` and `isPermanentLogin`. */
|
|
124
|
+
function classifyTurnFailed(parsed) {
|
|
125
|
+
const fields = readTurnFailedFields(parsed);
|
|
126
|
+
const kind = { errorMessage: fields.message };
|
|
127
|
+
if (isContextExceeded(fields)) {
|
|
128
|
+
kind.kind = "contextExceeded";
|
|
95
129
|
return kind;
|
|
96
130
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
kind.
|
|
131
|
+
const rateLimit = rateLimitReading(parsed, fields);
|
|
132
|
+
if (rateLimit !== undefined) {
|
|
133
|
+
kind.kind = "transientRateLimit";
|
|
134
|
+
kind.rateLimit = rateLimit;
|
|
100
135
|
return kind;
|
|
101
136
|
}
|
|
102
|
-
|
|
103
|
-
// the doctor names with the login command (design §5).
|
|
104
|
-
if (/refresh_token_reused|invalidated refresh token/i.test(message ?? "")) {
|
|
137
|
+
if (isUsageNotIncluded(fields)) {
|
|
105
138
|
kind.kind = "configError";
|
|
106
139
|
return kind;
|
|
107
140
|
}
|
|
108
|
-
if (
|
|
109
|
-
kind.kind = "
|
|
141
|
+
if (isTransientServer(fields)) {
|
|
142
|
+
kind.kind = "transientBackend";
|
|
143
|
+
kind.backendKind = "server";
|
|
110
144
|
return kind;
|
|
111
145
|
}
|
|
112
|
-
if (
|
|
113
|
-
// A 400 `invalid_request_error` names the model as unsupported on a ChatGPT account.
|
|
146
|
+
if (isPermanentLogin(fields)) {
|
|
114
147
|
kind.kind = "configError";
|
|
115
148
|
return kind;
|
|
116
149
|
}
|