tumwater 0.4.0 → 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/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.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-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- 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 +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- 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/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- 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 +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- 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 +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- 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-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- 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 +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -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-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -10,12 +10,13 @@
|
|
|
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/run-
|
|
13
|
+
* `warnings()` — src/agent/run-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";
|
|
17
17
|
import { boundText, outputLimits } from "../../pi-extension/bounded-output.js";
|
|
18
|
-
import {
|
|
18
|
+
import { secondsUntilUnixStamp } from "../../text/datetime.js";
|
|
19
|
+
import { SERVER_TRANSIENT_STATUS } from "../backend-traits.js";
|
|
19
20
|
/** How codex wraps every shell command — the prefix to strip (with its quoting) so the
|
|
20
21
|
* canonical `bash` call carries the model's own command, which suite-rerun detection matches. */
|
|
21
22
|
const SHELL_WRAPPER = /^\/(?:bin\/)?(?:zsh|bash) -lc '(.*)'$/s;
|
|
@@ -25,9 +26,6 @@ const RECONNECT = /^Reconnecting\.\.\. (\d+)\/(\d+) /;
|
|
|
25
26
|
/** The item types dropped silently, with no canonical event and no `warnings()` entry
|
|
26
27
|
* (design §5's `item.*` row): `todo_list` and `web_search`. */
|
|
27
28
|
const DROPPED_ITEM_TYPES = new Set(["todo_list", "web_search"]);
|
|
28
|
-
/** HTTP status classes that mean a transient backend failure (shared with the claude and
|
|
29
|
-
* codex classifiers' reading of `codex exec`'s JSON error). */
|
|
30
|
-
const SERVER_STATUS = new Set([500, 502, 503, 504, 529]);
|
|
31
29
|
/** Strip codex's `/bin/zsh -lc '<cmd>'` (or `bash -lc`) wrapper and its quoting, returning
|
|
32
30
|
* the model's own command — the argument suite-rerun detection matches. Passes a command the
|
|
33
31
|
* capture did not wrap through unchanged. */
|
|
@@ -45,31 +43,31 @@ export function windowTypeName(windowMinutes) {
|
|
|
45
43
|
return "seven_day";
|
|
46
44
|
return `${windowMinutes}_minute`;
|
|
47
45
|
}
|
|
48
|
-
/**
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
* transient backend, its kind), trusted by `PiStreamParser` before its text regexes. The inner
|
|
52
|
-
* `message` is a JSON string carrying `status` and `error.type` / `error.code` (finding 11).
|
|
53
|
-
* Returns the canonical lines to emit beside the closing `message_end`: a rejected rate-limit
|
|
54
|
-
* reading when the error names a used-up window. */
|
|
55
|
-
function classifyTurnFailed(parsed) {
|
|
56
|
-
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) {
|
|
57
49
|
const error = objectOrEmpty(parsed.error);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 }) {
|
|
63
60
|
// The context-window signal is tested BEFORE the generic 400 `invalid_request_error`
|
|
64
61
|
// branch (as claude's classifyResult tests "prompt is too long" before CONFIG_STATUS): a
|
|
65
62
|
// `context_window_exceeded` code, or a 400 whose message names the context window, is
|
|
66
63
|
// `contextExceeded`, never `configError`.
|
|
67
64
|
const namesContextWindow = (t) => t !== undefined &&
|
|
68
65
|
/context window|context length|too long|exceeds? (the )?(model'?s )?context|prompt is too long/i.test(t);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 }) {
|
|
73
71
|
if (code === "usage_limit_reached" || code === "rate_limit_exceeded" || status === 429) {
|
|
74
72
|
// Which window the account hit, from the error's own `rate_limit_reached_type` (else the
|
|
75
73
|
// five-hour window, the one Plus exhausts first). The rejected reading's wait comes from
|
|
@@ -77,42 +75,75 @@ function classifyTurnFailed(parsed) {
|
|
|
77
75
|
const minutes = nonNegativeNumber(parsed.rate_limit_reached_type, undefined);
|
|
78
76
|
const named = typeof parsed.rate_limit_reached_type === "string" ? parsed.rate_limit_reached_type : undefined;
|
|
79
77
|
const resetsAt = nonNegativeNumber(parsed.resets_at, undefined);
|
|
80
|
-
|
|
81
|
-
const retryAfter = resetsAt === undefined ? undefined : secondsUntil(resetsAt * 1000, Date.now());
|
|
82
|
-
kind.kind = "transientRateLimit";
|
|
83
|
-
kind.rateLimit = {
|
|
78
|
+
return {
|
|
84
79
|
type: "rate_limit",
|
|
85
80
|
rateLimitType: typeof named === "string" && /_minute$|_hour$|_day$/.test(named)
|
|
86
81
|
? named
|
|
87
82
|
: windowTypeName(minutes ?? 300),
|
|
88
83
|
status: "rejected",
|
|
89
84
|
utilization: 1,
|
|
90
|
-
|
|
91
|
-
...(
|
|
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()) }),
|
|
92
89
|
};
|
|
93
|
-
return kind;
|
|
94
90
|
}
|
|
95
|
-
|
|
96
|
-
|
|
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";
|
|
97
129
|
return kind;
|
|
98
130
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
kind.
|
|
131
|
+
const rateLimit = rateLimitReading(parsed, fields);
|
|
132
|
+
if (rateLimit !== undefined) {
|
|
133
|
+
kind.kind = "transientRateLimit";
|
|
134
|
+
kind.rateLimit = rateLimit;
|
|
102
135
|
return kind;
|
|
103
136
|
}
|
|
104
|
-
|
|
105
|
-
// the doctor names with the login command (design §5).
|
|
106
|
-
if (/refresh_token_reused|invalidated refresh token/i.test(message ?? "")) {
|
|
137
|
+
if (isUsageNotIncluded(fields)) {
|
|
107
138
|
kind.kind = "configError";
|
|
108
139
|
return kind;
|
|
109
140
|
}
|
|
110
|
-
if (
|
|
111
|
-
kind.kind = "
|
|
141
|
+
if (isTransientServer(fields)) {
|
|
142
|
+
kind.kind = "transientBackend";
|
|
143
|
+
kind.backendKind = "server";
|
|
112
144
|
return kind;
|
|
113
145
|
}
|
|
114
|
-
if (
|
|
115
|
-
// A 400 `invalid_request_error` names the model as unsupported on a ChatGPT account.
|
|
146
|
+
if (isPermanentLogin(fields)) {
|
|
116
147
|
kind.kind = "configError";
|
|
117
148
|
return kind;
|
|
118
149
|
}
|
|
@@ -185,187 +216,210 @@ export function codexTranslator(opts = {}) {
|
|
|
185
216
|
}
|
|
186
217
|
return out;
|
|
187
218
|
};
|
|
219
|
+
/** The `thread.started` arm: announces the session and the agent start, or records the
|
|
220
|
+
* unmapped event once when the thread id is missing. */
|
|
221
|
+
const handleThreadStarted = (event) => {
|
|
222
|
+
const id = typeof event.thread_id === "string" ? event.thread_id : undefined;
|
|
223
|
+
if (id === undefined) {
|
|
224
|
+
recordEvent("thread.started");
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
sessionIdValue = id;
|
|
228
|
+
const out = [];
|
|
229
|
+
out.push(JSON.stringify({
|
|
230
|
+
type: "session",
|
|
231
|
+
id,
|
|
232
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
233
|
+
}));
|
|
234
|
+
out.push(JSON.stringify({ type: "agent_start" }));
|
|
235
|
+
return out;
|
|
236
|
+
};
|
|
237
|
+
/** The `turn.completed` arm: folds the usage block and abandons the open calls. */
|
|
238
|
+
const handleTurnCompleted = (event) => {
|
|
239
|
+
sawTurnCompleted = true;
|
|
240
|
+
const usage = objectOrEmpty(event.usage);
|
|
241
|
+
turnUsage =
|
|
242
|
+
usage.input_tokens !== undefined || usage.output_tokens !== undefined
|
|
243
|
+
? {
|
|
244
|
+
input: nonNegativeNumber(usage.input_tokens, 0),
|
|
245
|
+
cacheRead: nonNegativeNumber(usage.cached_input_tokens, 0),
|
|
246
|
+
cacheWrite: nonNegativeNumber(usage.cache_write_input_tokens, 0),
|
|
247
|
+
output: nonNegativeNumber(usage.output_tokens, 0),
|
|
248
|
+
}
|
|
249
|
+
: undefined;
|
|
250
|
+
return abandonOpenCalls();
|
|
251
|
+
};
|
|
252
|
+
/** The `turn.failed` arm: classifies the inner-JSON error and closes the turn. */
|
|
253
|
+
const handleTurnFailed = (event) => {
|
|
254
|
+
sawTurnFailed = true;
|
|
255
|
+
// The error message is itself a JSON string (finding 11); fall back to the held
|
|
256
|
+
// top-level error text when it is not.
|
|
257
|
+
const raw = typeof event.error === "object" && event.error !== null && !Array.isArray(event.error)
|
|
258
|
+
? event.error.message
|
|
259
|
+
: undefined;
|
|
260
|
+
const inner = typeof raw === "string" ? parseJsonObject(raw) : undefined;
|
|
261
|
+
const cls = inner ? classifyTurnFailed(inner) : {};
|
|
262
|
+
const errorMessage = cls.errorMessage ?? (typeof raw === "string" ? raw : pendingFailureText);
|
|
263
|
+
const closes = abandonOpenCalls();
|
|
264
|
+
const message = { role: "assistant", content: [], stopReason: "error" };
|
|
265
|
+
if (errorMessage !== undefined)
|
|
266
|
+
message.errorMessage = errorMessage;
|
|
267
|
+
const end = {
|
|
268
|
+
type: "message_end",
|
|
269
|
+
message,
|
|
270
|
+
...(cls.kind !== undefined ? { tumwaterKind: cls.kind } : {}),
|
|
271
|
+
...(cls.backendKind !== undefined ? { backendKind: cls.backendKind } : {}),
|
|
272
|
+
};
|
|
273
|
+
if (cls.kind === "transientRateLimit" && typeof cls.rateLimit?.retryAfterSeconds === "number")
|
|
274
|
+
end.retryAfterSeconds = cls.rateLimit.retryAfterSeconds;
|
|
275
|
+
return [...(cls.rateLimit !== undefined ? [JSON.stringify(cls.rateLimit)] : []), ...closes, JSON.stringify(end)];
|
|
276
|
+
};
|
|
277
|
+
/** The top-level `error` arm: a reconnect text emits an `auto_retry_start`; any other
|
|
278
|
+
* message is held as the next `turn.failed`'s fallback text. */
|
|
279
|
+
const handleErrorLine = (event) => {
|
|
280
|
+
const message = typeof event.message === "string" ? event.message : undefined;
|
|
281
|
+
const reconnect = message === undefined ? undefined : RECONNECT.exec(message);
|
|
282
|
+
if (reconnect) {
|
|
283
|
+
return [
|
|
284
|
+
JSON.stringify({
|
|
285
|
+
type: "auto_retry_start",
|
|
286
|
+
attempt: Number(reconnect[1]),
|
|
287
|
+
maxAttempts: Number(reconnect[2]),
|
|
288
|
+
errorMessage: message,
|
|
289
|
+
}),
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
// Other top-level `error` lines are not fatal (design §5): held as the fallback text
|
|
293
|
+
// for a following `turn.failed`.
|
|
294
|
+
if (message !== undefined)
|
|
295
|
+
pendingFailureText = message;
|
|
296
|
+
return [];
|
|
297
|
+
};
|
|
298
|
+
/** The `item.started`/`item.completed` arm: reads the item and delegates by type to the
|
|
299
|
+
* item handlers, dropping the silent types and warning once on the unknown tail. */
|
|
300
|
+
const handleItem = (event, started) => {
|
|
301
|
+
const item = objectOrEmpty(event.item);
|
|
302
|
+
const itemType = typeof item.type === "string" ? item.type : "unknown";
|
|
303
|
+
if (itemType === "command_execution")
|
|
304
|
+
return handleCommandItem(item, started);
|
|
305
|
+
if (itemType === "file_change")
|
|
306
|
+
return handleFileChangeItem(item, started);
|
|
307
|
+
if (itemType === "error")
|
|
308
|
+
return handleErrorItem(item);
|
|
309
|
+
if (itemType === "agent_message")
|
|
310
|
+
return handleAgentMessageItem(item);
|
|
311
|
+
if (DROPPED_ITEM_TYPES.has(itemType))
|
|
312
|
+
return [];
|
|
313
|
+
// mcp_tool_call / reasoning and any unknown item shape map to nothing canonical; a
|
|
314
|
+
// once-per-type warning only (design §5).
|
|
315
|
+
recordItem(item);
|
|
316
|
+
return [];
|
|
317
|
+
};
|
|
318
|
+
/** The `command_execution` item arm: opens or closes the bash call, capping the result. */
|
|
319
|
+
const handleCommandItem = (item, started) => {
|
|
320
|
+
const command = typeof item.command === "string" ? item.command : "";
|
|
321
|
+
const out = [];
|
|
322
|
+
if (started) {
|
|
323
|
+
const id = typeof item.id === "string" ? item.id : `cmd-${openCalls.size}`;
|
|
324
|
+
openCalls.set(id, "command_execution");
|
|
325
|
+
const inner = innerCommand(command);
|
|
326
|
+
out.push(JSON.stringify({
|
|
327
|
+
type: "message_end",
|
|
328
|
+
message: {
|
|
329
|
+
role: "assistant",
|
|
330
|
+
content: [{ type: "toolCall", id, name: "bash", arguments: { command: inner } }],
|
|
331
|
+
},
|
|
332
|
+
}));
|
|
333
|
+
out.push(JSON.stringify({
|
|
334
|
+
type: "tool_execution_start",
|
|
335
|
+
toolCallId: id,
|
|
336
|
+
toolName: "bash",
|
|
337
|
+
args: { command: inner },
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
342
|
+
openCalls.delete(id);
|
|
343
|
+
const aggregated = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
|
|
344
|
+
const exitCode = nonNegativeNumber(item.exit_code, undefined);
|
|
345
|
+
out.push(JSON.stringify({
|
|
346
|
+
type: "tool_execution_end",
|
|
347
|
+
toolCallId: id,
|
|
348
|
+
isError: exitCode !== undefined ? exitCode !== 0 : item.status !== "completed",
|
|
349
|
+
result: boundText(aggregated, bashLimit),
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
};
|
|
354
|
+
/** The `file_change` item arm: mirrors each change as an `edit` tool call. */
|
|
355
|
+
const handleFileChangeItem = (item, started) => {
|
|
356
|
+
const changes = Array.isArray(item.changes) ? item.changes.filter((c) => typeof c === "object" && c !== null) : [];
|
|
357
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
358
|
+
const out = [];
|
|
359
|
+
if (!started) {
|
|
360
|
+
const isError = typeof item.status === "string" && item.status !== "completed";
|
|
361
|
+
for (let i = 0; i < changes.length; i++) {
|
|
362
|
+
const callId = `${id}-${i}`;
|
|
363
|
+
openCalls.delete(callId);
|
|
364
|
+
out.push(JSON.stringify({ type: "tool_execution_end", toolCallId: callId, isError }));
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
const content = changes.map((c, i) => ({
|
|
369
|
+
type: "toolCall",
|
|
370
|
+
id: `${id}-${i}`,
|
|
371
|
+
name: "edit",
|
|
372
|
+
arguments: {
|
|
373
|
+
path: typeof c.path === "string" ? c.path : undefined,
|
|
374
|
+
kind: typeof c.kind === "string" ? c.kind : undefined,
|
|
375
|
+
},
|
|
376
|
+
}));
|
|
377
|
+
if (content.length === 0)
|
|
378
|
+
return out; // no changes to mirror, nothing else to emit
|
|
379
|
+
out.push(JSON.stringify({ type: "message_end", message: { role: "assistant", content } }));
|
|
380
|
+
for (let i = 0; i < content.length; i++) {
|
|
381
|
+
const callId = `${id}-${i}`;
|
|
382
|
+
openCalls.set(callId, "file_change");
|
|
383
|
+
out.push(JSON.stringify({
|
|
384
|
+
type: "tool_execution_start",
|
|
385
|
+
toolCallId: callId,
|
|
386
|
+
toolName: "edit",
|
|
387
|
+
args: content[i].arguments,
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
};
|
|
392
|
+
/** The `item.type === "error"` arm: a non-fatal notice warned once per distinct message. */
|
|
393
|
+
const handleErrorItem = (item) => {
|
|
394
|
+
const message = typeof item.message === "string" ? item.message : undefined;
|
|
395
|
+
if (message !== undefined)
|
|
396
|
+
recordErrorItem(message);
|
|
397
|
+
return [];
|
|
398
|
+
};
|
|
399
|
+
/** The `agent_message` arm: the model's reply as one canonical `message_end`. */
|
|
400
|
+
const handleAgentMessageItem = (item) => {
|
|
401
|
+
const text = typeof item.text === "string" ? item.text : "";
|
|
402
|
+
return [
|
|
403
|
+
JSON.stringify({
|
|
404
|
+
type: "message_end",
|
|
405
|
+
message: { role: "assistant", content: [{ type: "text", text }] },
|
|
406
|
+
}),
|
|
407
|
+
];
|
|
408
|
+
};
|
|
188
409
|
const translate = (line) => {
|
|
189
410
|
const parsed = parseJsonObject(line);
|
|
190
411
|
if (!parsed)
|
|
191
412
|
return []; // Non-JSON noise on stdout; ignore, as pi's parser does.
|
|
192
413
|
const event = parsed;
|
|
193
414
|
const type = typeof event.type === "string" ? event.type : "unknown";
|
|
194
|
-
const out = [];
|
|
195
415
|
switch (type) {
|
|
196
|
-
case "thread.started":
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
sessionIdValue = id;
|
|
203
|
-
out.push(JSON.stringify({
|
|
204
|
-
type: "session",
|
|
205
|
-
id,
|
|
206
|
-
...(cwd !== undefined ? { cwd } : {}),
|
|
207
|
-
}));
|
|
208
|
-
out.push(JSON.stringify({ type: "agent_start" }));
|
|
209
|
-
return out;
|
|
210
|
-
}
|
|
211
|
-
case "turn.started":
|
|
212
|
-
return [];
|
|
213
|
-
case "turn.completed": {
|
|
214
|
-
sawTurnCompleted = true;
|
|
215
|
-
const usage = objectOrEmpty(event.usage);
|
|
216
|
-
turnUsage =
|
|
217
|
-
usage.input_tokens !== undefined || usage.output_tokens !== undefined
|
|
218
|
-
? {
|
|
219
|
-
input: nonNegativeNumber(usage.input_tokens, 0),
|
|
220
|
-
cacheRead: nonNegativeNumber(usage.cached_input_tokens, 0),
|
|
221
|
-
cacheWrite: nonNegativeNumber(usage.cache_write_input_tokens, 0),
|
|
222
|
-
output: nonNegativeNumber(usage.output_tokens, 0),
|
|
223
|
-
}
|
|
224
|
-
: undefined;
|
|
225
|
-
return abandonOpenCalls();
|
|
226
|
-
}
|
|
227
|
-
case "turn.failed": {
|
|
228
|
-
sawTurnFailed = true;
|
|
229
|
-
// The error message is itself a JSON string (finding 11); fall back to the held
|
|
230
|
-
// top-level error text when it is not.
|
|
231
|
-
const raw = typeof event.error === "object" && event.error !== null && !Array.isArray(event.error)
|
|
232
|
-
? event.error.message
|
|
233
|
-
: undefined;
|
|
234
|
-
const inner = typeof raw === "string" ? parseJsonObject(raw) : undefined;
|
|
235
|
-
const cls = inner ? classifyTurnFailed(inner) : {};
|
|
236
|
-
const errorMessage = cls.errorMessage ?? (typeof raw === "string" ? raw : pendingFailureText);
|
|
237
|
-
const closes = abandonOpenCalls();
|
|
238
|
-
const message = { role: "assistant", content: [], stopReason: "error" };
|
|
239
|
-
if (errorMessage !== undefined)
|
|
240
|
-
message.errorMessage = errorMessage;
|
|
241
|
-
const end = {
|
|
242
|
-
type: "message_end",
|
|
243
|
-
message,
|
|
244
|
-
...(cls.kind !== undefined ? { tumwaterKind: cls.kind } : {}),
|
|
245
|
-
...(cls.backendKind !== undefined ? { backendKind: cls.backendKind } : {}),
|
|
246
|
-
};
|
|
247
|
-
if (cls.kind === "transientRateLimit" && typeof cls.rateLimit?.retryAfterSeconds === "number")
|
|
248
|
-
end.retryAfterSeconds = cls.rateLimit.retryAfterSeconds;
|
|
249
|
-
return [...(cls.rateLimit !== undefined ? [JSON.stringify(cls.rateLimit)] : []), ...closes, JSON.stringify(end)];
|
|
250
|
-
}
|
|
251
|
-
case "error": {
|
|
252
|
-
const message = typeof event.message === "string" ? event.message : undefined;
|
|
253
|
-
const reconnect = message === undefined ? undefined : RECONNECT.exec(message);
|
|
254
|
-
if (reconnect) {
|
|
255
|
-
return [
|
|
256
|
-
JSON.stringify({
|
|
257
|
-
type: "auto_retry_start",
|
|
258
|
-
attempt: Number(reconnect[1]),
|
|
259
|
-
maxAttempts: Number(reconnect[2]),
|
|
260
|
-
errorMessage: message,
|
|
261
|
-
}),
|
|
262
|
-
];
|
|
263
|
-
}
|
|
264
|
-
// Other top-level `error` lines are not fatal (design §5): held as the fallback text
|
|
265
|
-
// for a following `turn.failed`.
|
|
266
|
-
if (message !== undefined)
|
|
267
|
-
pendingFailureText = message;
|
|
268
|
-
return [];
|
|
269
|
-
}
|
|
416
|
+
case "thread.started": return handleThreadStarted(event);
|
|
417
|
+
case "turn.started": return [];
|
|
418
|
+
case "turn.completed": return handleTurnCompleted(event);
|
|
419
|
+
case "turn.failed": return handleTurnFailed(event);
|
|
420
|
+
case "error": return handleErrorLine(event);
|
|
270
421
|
case "item.started":
|
|
271
|
-
case "item.completed":
|
|
272
|
-
const item = objectOrEmpty(event.item);
|
|
273
|
-
const itemType = typeof item.type === "string" ? item.type : "unknown";
|
|
274
|
-
const started = type === "item.started";
|
|
275
|
-
if (itemType === "command_execution") {
|
|
276
|
-
const command = typeof item.command === "string" ? item.command : "";
|
|
277
|
-
if (started) {
|
|
278
|
-
const id = typeof item.id === "string" ? item.id : `cmd-${openCalls.size}`;
|
|
279
|
-
openCalls.set(id, "command_execution");
|
|
280
|
-
const inner = innerCommand(command);
|
|
281
|
-
out.push(JSON.stringify({
|
|
282
|
-
type: "message_end",
|
|
283
|
-
message: {
|
|
284
|
-
role: "assistant",
|
|
285
|
-
content: [{ type: "toolCall", id, name: "bash", arguments: { command: inner } }],
|
|
286
|
-
},
|
|
287
|
-
}));
|
|
288
|
-
out.push(JSON.stringify({
|
|
289
|
-
type: "tool_execution_start",
|
|
290
|
-
toolCallId: id,
|
|
291
|
-
toolName: "bash",
|
|
292
|
-
args: { command: inner },
|
|
293
|
-
}));
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
const id = typeof item.id === "string" ? item.id : "";
|
|
297
|
-
openCalls.delete(id);
|
|
298
|
-
const aggregated = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
|
|
299
|
-
const exitCode = nonNegativeNumber(item.exit_code, undefined);
|
|
300
|
-
out.push(JSON.stringify({
|
|
301
|
-
type: "tool_execution_end",
|
|
302
|
-
toolCallId: id,
|
|
303
|
-
isError: exitCode !== undefined ? exitCode !== 0 : item.status !== "completed",
|
|
304
|
-
result: boundText(aggregated, bashLimit),
|
|
305
|
-
}));
|
|
306
|
-
}
|
|
307
|
-
return out;
|
|
308
|
-
}
|
|
309
|
-
if (itemType === "file_change") {
|
|
310
|
-
const changes = Array.isArray(item.changes) ? item.changes.filter((c) => typeof c === "object" && c !== null) : [];
|
|
311
|
-
const id = typeof item.id === "string" ? item.id : "";
|
|
312
|
-
if (!started) {
|
|
313
|
-
const isError = typeof item.status === "string" && item.status !== "completed";
|
|
314
|
-
for (let i = 0; i < changes.length; i++) {
|
|
315
|
-
const callId = `${id}-${i}`;
|
|
316
|
-
openCalls.delete(callId);
|
|
317
|
-
out.push(JSON.stringify({ type: "tool_execution_end", toolCallId: callId, isError }));
|
|
318
|
-
}
|
|
319
|
-
return out;
|
|
320
|
-
}
|
|
321
|
-
const content = changes.map((c, i) => ({
|
|
322
|
-
type: "toolCall",
|
|
323
|
-
id: `${id}-${i}`,
|
|
324
|
-
name: "edit",
|
|
325
|
-
arguments: {
|
|
326
|
-
path: typeof c.path === "string" ? c.path : undefined,
|
|
327
|
-
kind: typeof c.kind === "string" ? c.kind : undefined,
|
|
328
|
-
},
|
|
329
|
-
}));
|
|
330
|
-
if (content.length === 0)
|
|
331
|
-
return out; // no changes to mirror, nothing else to emit
|
|
332
|
-
out.push(JSON.stringify({ type: "message_end", message: { role: "assistant", content } }));
|
|
333
|
-
for (let i = 0; i < content.length; i++) {
|
|
334
|
-
const callId = `${id}-${i}`;
|
|
335
|
-
openCalls.set(callId, "file_change");
|
|
336
|
-
out.push(JSON.stringify({
|
|
337
|
-
type: "tool_execution_start",
|
|
338
|
-
toolCallId: callId,
|
|
339
|
-
toolName: "edit",
|
|
340
|
-
args: content[i].arguments,
|
|
341
|
-
}));
|
|
342
|
-
}
|
|
343
|
-
return out;
|
|
344
|
-
}
|
|
345
|
-
if (itemType === "error") {
|
|
346
|
-
// An error item is a non-fatal notice (finding 11); warned once per distinct message.
|
|
347
|
-
const message = typeof item.message === "string" ? item.message : undefined;
|
|
348
|
-
if (message !== undefined)
|
|
349
|
-
recordErrorItem(message);
|
|
350
|
-
return [];
|
|
351
|
-
}
|
|
352
|
-
if (itemType === "agent_message") {
|
|
353
|
-
// The model's reply: one canonical `message_end` with one text block.
|
|
354
|
-
const text = typeof item.text === "string" ? item.text : "";
|
|
355
|
-
return [
|
|
356
|
-
JSON.stringify({
|
|
357
|
-
type: "message_end",
|
|
358
|
-
message: { role: "assistant", content: [{ type: "text", text }] },
|
|
359
|
-
}),
|
|
360
|
-
];
|
|
361
|
-
}
|
|
362
|
-
if (DROPPED_ITEM_TYPES.has(itemType))
|
|
363
|
-
return [];
|
|
364
|
-
// mcp_tool_call / reasoning and any unknown item shape map to nothing canonical; a
|
|
365
|
-
// once-per-type warning only (design §5).
|
|
366
|
-
recordItem(item);
|
|
367
|
-
return [];
|
|
368
|
-
}
|
|
422
|
+
case "item.completed": return handleItem(event, type === "item.started");
|
|
369
423
|
default:
|
|
370
424
|
recordEvent(type);
|
|
371
425
|
return [];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** The PreToolUse hook codex runs before each `apply_patch` in a team run (part 6/9 installs it
|
|
2
|
-
* through `-c`): `node write-guard-hook.js` with codex's hook payload on stdin. To block
|
|
2
|
+
* through `-c`): `node codex-write-guard-hook.js` with codex's hook payload on stdin. To block
|
|
3
|
+
* the patch
|
|
3
4
|
* it prints a `deny` decision with the refusal as its reason, in the shape finding 5 showed codex
|
|
4
5
|
* accepts: the model reads only the reason, so a denial by exit code would not carry it. It prints
|
|
5
6
|
* nothing to let the patch run, and exits 0 either way. Any failure of its own (bad JSON, a throw)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** codex's half of the write guard (src/agent/write-guard.ts holds the rule): a PreToolUse hook on
|
|
2
2
|
* `apply_patch`, installed by part 6/9, refuses an `apply_patch` that writes under the team root
|
|
3
|
-
* but outside the run's worktree. The entry point is write-guard-hook.ts beside this file.
|
|
3
|
+
* but outside the run's worktree. The entry point is codex-write-guard-hook.ts beside this file.
|
|
4
4
|
*
|
|
5
5
|
* Codex's hook takes no argument naming the worktree, unlike claude's: codex trusts a hook by a
|
|
6
6
|
* hash of its whole definition, so a command constant per build needs one trust lookup, while a
|
|
@@ -63,11 +63,11 @@ export function tomlString(s) {
|
|
|
63
63
|
return out + '"';
|
|
64
64
|
}
|
|
65
65
|
/** The codex write guard's definition: the PreToolUse hook on `apply_patch` that runs the
|
|
66
|
-
* compiled write-guard-hook.js with this process's node, falling back to `node` on PATH when
|
|
66
|
+
* compiled codex-write-guard-hook.js with this process's node, falling back to `node` on PATH when
|
|
67
67
|
* that binary has moved (as claude's and the notes shim do). The hook path resolves from this
|
|
68
68
|
* module, so a staged build (.tumwater/build/<sha>) runs its own copy and gets its own hash. */
|
|
69
69
|
export function codexWriteGuardDefinition(nodeBin = process.execPath) {
|
|
70
|
-
const hook = fileURLToPath(new URL("./write-guard-hook.js", import.meta.url));
|
|
70
|
+
const hook = fileURLToPath(new URL("./codex-write-guard-hook.js", import.meta.url));
|
|
71
71
|
const command = `n=${shellQuote(nodeBin)}; [ -x "$n" ] || n=node; exec "$n" ${shellQuote(hook)}`;
|
|
72
72
|
return { matcher: HOOK_MATCHER, command, timeout: HOOK_TIMEOUT_SECONDS };
|
|
73
73
|
}
|