@dungle-scrubs/harness-cli-normalizer 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -7
- package/dist/cli/args.d.ts +74 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +339 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/check.d.ts +2 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +110 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +7 -0
- package/dist/cli/exit-codes.d.ts.map +1 -0
- package/dist/cli/exit-codes.js +11 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/help.d.ts +7 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +110 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +170 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/inspect.d.ts +3 -0
- package/dist/cli/inspect.d.ts.map +1 -0
- package/dist/cli/inspect.js +170 -0
- package/dist/cli/inspect.js.map +1 -0
- package/dist/cli/ls.d.ts +2 -0
- package/dist/cli/ls.d.ts.map +1 -0
- package/dist/cli/ls.js +10 -0
- package/dist/cli/ls.js.map +1 -0
- package/dist/cli/render.d.ts +8 -0
- package/dist/cli/render.d.ts.map +1 -0
- package/dist/cli/render.js +55 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/cli/resolve-harness.d.ts +4 -0
- package/dist/cli/resolve-harness.d.ts.map +1 -0
- package/dist/cli/resolve-harness.js +31 -0
- package/dist/cli/resolve-harness.js.map +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +280 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/session.d.ts +2 -0
- package/dist/cli/session.d.ts.map +1 -0
- package/dist/cli/session.js +128 -0
- package/dist/cli/session.js.map +1 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +34 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/execution/decode.d.ts.map +1 -1
- package/dist/execution/decode.js +32 -0
- package/dist/execution/decode.js.map +1 -1
- package/dist/execution/deps.d.ts +7 -1
- package/dist/execution/deps.d.ts.map +1 -1
- package/dist/execution/events.d.ts +6 -2
- package/dist/execution/events.d.ts.map +1 -1
- package/dist/execution/events.js.map +1 -1
- package/dist/execution/failure.d.ts +45 -0
- package/dist/execution/failure.d.ts.map +1 -0
- package/dist/execution/failure.js +121 -0
- package/dist/execution/failure.js.map +1 -0
- package/dist/execution/node-deps.d.ts.map +1 -1
- package/dist/execution/node-deps.js +19 -0
- package/dist/execution/node-deps.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +2 -0
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +181 -16
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +16 -4
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +68 -19
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/dimensions.d.ts +0 -2
- package/dist/interpretation/dimensions.d.ts.map +1 -1
- package/dist/interpretation/dimensions.js +0 -2
- package/dist/interpretation/dimensions.js.map +1 -1
- package/dist/interpretation/limits.d.ts +10 -1
- package/dist/interpretation/limits.d.ts.map +1 -1
- package/dist/interpretation/limits.js +84 -5
- package/dist/interpretation/limits.js.map +1 -1
- package/dist/interpretation/refusal.d.ts +31 -0
- package/dist/interpretation/refusal.d.ts.map +1 -0
- package/dist/interpretation/refusal.js +87 -0
- package/dist/interpretation/refusal.js.map +1 -0
- package/dist/interpretation/turn-options.d.ts +18 -0
- package/dist/interpretation/turn-options.d.ts.map +1 -0
- package/dist/interpretation/turn-options.js +417 -0
- package/dist/interpretation/turn-options.js.map +1 -0
- package/dist/interpretation/vocabulary.d.ts +5 -0
- package/dist/interpretation/vocabulary.d.ts.map +1 -1
- package/dist/interpretation/vocabulary.js +1 -1
- package/dist/interpretation/vocabulary.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts +13 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +35 -13
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +25 -14
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +118 -14
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +58 -0
- package/dist/knowledge/descriptor.js.map +1 -1
- package/dist/knowledge/matchers.d.ts +3 -3
- package/dist/knowledge/matchers.d.ts.map +1 -1
- package/dist/knowledge/matchers.js +19 -6
- package/dist/knowledge/matchers.js.map +1 -1
- package/dist/knowledge/muse.d.ts +2 -2
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +22 -6
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/overrides.d.ts +7 -0
- package/dist/knowledge/overrides.d.ts.map +1 -1
- package/dist/knowledge/overrides.js +76 -0
- package/dist/knowledge/overrides.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +25 -3
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +5 -2
- package/src/cli/args.ts +381 -0
- package/src/cli/check.ts +136 -0
- package/src/cli/exit-codes.ts +13 -0
- package/src/cli/help.ts +114 -0
- package/src/cli/index.ts +177 -0
- package/src/cli/inspect.ts +174 -0
- package/src/cli/ls.ts +14 -0
- package/src/cli/render.ts +62 -0
- package/src/cli/resolve-harness.ts +33 -0
- package/src/cli/run.ts +280 -0
- package/src/cli/session.ts +137 -0
- package/src/cli/version.ts +32 -0
- package/src/execution/decode.ts +32 -0
- package/src/execution/deps.ts +7 -1
- package/src/execution/events.ts +9 -2
- package/src/execution/failure.ts +159 -0
- package/src/execution/node-deps.ts +17 -0
- package/src/execution/stream-turn.ts +189 -17
- package/src/interpretation/argv.ts +85 -27
- package/src/interpretation/dimensions.ts +0 -6
- package/src/interpretation/limits.ts +97 -6
- package/src/interpretation/refusal.ts +116 -0
- package/src/interpretation/turn-options.ts +424 -0
- package/src/interpretation/vocabulary.ts +1 -1
- package/src/knowledge/claude-code.ts +35 -13
- package/src/knowledge/codex.ts +25 -14
- package/src/knowledge/descriptor.ts +138 -12
- package/src/knowledge/matchers.ts +22 -9
- package/src/knowledge/muse.ts +22 -6
- package/src/knowledge/overrides.ts +97 -0
- package/src/knowledge/pi.ts +25 -3
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* failure: typed taxonomy for every way a turn can fail, and the reduction
|
|
3
|
+
* that collapses many observations into one self-sufficient summary on `done`.
|
|
4
|
+
*
|
|
5
|
+
* Provider-unavailable vs work-verdict is the load-bearing split: a provider-
|
|
6
|
+
* unavailable failure (rate-limit, usage-limit, quota, auth, transport) reached
|
|
7
|
+
* no verdict on the work, so routing the same work elsewhere is safe. A
|
|
8
|
+
* work-verdict failure (task, budget) means the model did the work and the
|
|
9
|
+
* work is wrong or incomplete - the router must NOT auto-route it elsewhere,
|
|
10
|
+
* or it would hide a model error behind a retry. `rejected` is the library
|
|
11
|
+
* refusing to build the call at all (unexpressible option, bad value); it is
|
|
12
|
+
* non-retryable across the whole model chain because the remedy is different
|
|
13
|
+
* options or a different harness, not a different model.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { RefusalIssue } from "../interpretation/refusal.js";
|
|
17
|
+
import type {
|
|
18
|
+
AuthFailureKind,
|
|
19
|
+
DiscoveryFacet,
|
|
20
|
+
LimitCode,
|
|
21
|
+
TurnOptionKey,
|
|
22
|
+
} from "../knowledge/descriptor.js";
|
|
23
|
+
|
|
24
|
+
export const FAILURE_CLASSES = Object.freeze([
|
|
25
|
+
"rate-limit",
|
|
26
|
+
"usage-limit",
|
|
27
|
+
"quota",
|
|
28
|
+
"auth",
|
|
29
|
+
"budget",
|
|
30
|
+
"task",
|
|
31
|
+
"transport",
|
|
32
|
+
"rejected",
|
|
33
|
+
] as const);
|
|
34
|
+
export type FailureClass = (typeof FAILURE_CLASSES)[number];
|
|
35
|
+
|
|
36
|
+
export interface FailureSummary {
|
|
37
|
+
readonly class: FailureClass;
|
|
38
|
+
readonly retryable: boolean;
|
|
39
|
+
readonly message: string;
|
|
40
|
+
readonly code?: LimitCode;
|
|
41
|
+
readonly authKind?: AuthFailureKind;
|
|
42
|
+
readonly resetsAt?: number;
|
|
43
|
+
readonly issue?: RefusalIssue;
|
|
44
|
+
readonly option?: TurnOptionKey;
|
|
45
|
+
readonly facet?: DiscoveryFacet;
|
|
46
|
+
readonly supported?: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const retryableOf = (cls: FailureClass): boolean =>
|
|
50
|
+
cls !== "task" && cls !== "budget" && cls !== "rejected";
|
|
51
|
+
|
|
52
|
+
const messageFor = (cls: FailureClass, detail?: string): string => {
|
|
53
|
+
switch (cls) {
|
|
54
|
+
case "rate-limit":
|
|
55
|
+
return `Rate limit hit${detail ? ` (${detail})` : ""} - retry after backoff or route to another provider`;
|
|
56
|
+
case "usage-limit":
|
|
57
|
+
return `Usage limit reached${detail ? ` (${detail})` : ""} - wait for reset or route to another provider`;
|
|
58
|
+
case "quota":
|
|
59
|
+
return `Quota exceeded${detail ? ` (${detail})` : ""} - replenish quota or route to another provider`;
|
|
60
|
+
case "auth":
|
|
61
|
+
return `Authentication failed${detail ? ` (${detail})` : ""} - re-authenticate this provider`;
|
|
62
|
+
case "budget":
|
|
63
|
+
return `Step budget exhausted${detail ? ` (${detail})` : ""} - raise the step cap and retry on the same model`;
|
|
64
|
+
case "task":
|
|
65
|
+
return `Task failed${detail ? ` (${detail})` : ""} - surface to caller, do not auto-route`;
|
|
66
|
+
case "transport":
|
|
67
|
+
return `Transport failure${detail ? ` (${detail})` : ""} - retry or route to another provider`;
|
|
68
|
+
case "rejected":
|
|
69
|
+
return `Request rejected${detail ? ` (${detail})` : ""} - change options or harness`;
|
|
70
|
+
default:
|
|
71
|
+
return `Failure${detail ? ` (${detail})` : ""}`;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const failureFromLimit = (code: LimitCode): FailureSummary => {
|
|
76
|
+
const cls: FailureClass =
|
|
77
|
+
code === "rate-limit"
|
|
78
|
+
? "rate-limit"
|
|
79
|
+
: code === "credits" || code === "quota"
|
|
80
|
+
? "quota"
|
|
81
|
+
: "usage-limit";
|
|
82
|
+
return {
|
|
83
|
+
class: cls,
|
|
84
|
+
retryable: retryableOf(cls),
|
|
85
|
+
message: messageFor(cls, code),
|
|
86
|
+
code: code as LimitCode,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const failureFromAuth = (kind: AuthFailureKind): FailureSummary => ({
|
|
91
|
+
class: "auth",
|
|
92
|
+
retryable: true,
|
|
93
|
+
message: messageFor("auth", kind),
|
|
94
|
+
authKind: kind,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const failureFromTask = (detail?: string): FailureSummary => ({
|
|
98
|
+
class: "task",
|
|
99
|
+
retryable: false,
|
|
100
|
+
message: messageFor("task", detail),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const failureFromBudget = (detail?: string): FailureSummary => ({
|
|
104
|
+
class: "budget",
|
|
105
|
+
retryable: false,
|
|
106
|
+
message: messageFor("budget", detail),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const failureFromTransport = (detail?: string): FailureSummary => ({
|
|
110
|
+
class: "transport",
|
|
111
|
+
retryable: true,
|
|
112
|
+
message: messageFor("transport", detail),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const failureFromRejected = (opts: {
|
|
116
|
+
issue: RefusalIssue;
|
|
117
|
+
option?: TurnOptionKey;
|
|
118
|
+
facet?: DiscoveryFacet;
|
|
119
|
+
supported?: readonly string[];
|
|
120
|
+
detail?: string;
|
|
121
|
+
}): FailureSummary => ({
|
|
122
|
+
class: "rejected",
|
|
123
|
+
retryable: false,
|
|
124
|
+
message: messageFor("rejected", opts.detail ?? opts.issue),
|
|
125
|
+
issue: opts.issue,
|
|
126
|
+
option: opts.option,
|
|
127
|
+
facet: opts.facet,
|
|
128
|
+
supported: opts.supported,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** Precedence for reduction: lower number = higher priority (wins). */
|
|
132
|
+
const PRECEDENCE: Record<FailureClass, number> = {
|
|
133
|
+
auth: 1,
|
|
134
|
+
"rate-limit": 2,
|
|
135
|
+
"usage-limit": 2,
|
|
136
|
+
quota: 2,
|
|
137
|
+
budget: 3,
|
|
138
|
+
task: 3,
|
|
139
|
+
transport: 4,
|
|
140
|
+
rejected: 0, // rejected is separate, not reduced with others? But per spec, rejected is non-retryable and stands alone
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const reduceFailures = (failures: readonly FailureSummary[]): FailureSummary | undefined => {
|
|
144
|
+
if (failures.length === 0) return undefined;
|
|
145
|
+
if (failures.length === 1) return failures[0];
|
|
146
|
+
// Sort by precedence, then by earliest (stable). Lower precedence number wins.
|
|
147
|
+
let best = failures[0]!;
|
|
148
|
+
let bestPrec = PRECEDENCE[best.class] ?? 99;
|
|
149
|
+
for (let i = 1; i < failures.length; i++) {
|
|
150
|
+
const cur = failures[i]!;
|
|
151
|
+
const curPrec = PRECEDENCE[cur.class] ?? 99;
|
|
152
|
+
if (curPrec < bestPrec) {
|
|
153
|
+
best = cur;
|
|
154
|
+
bestPrec = curPrec;
|
|
155
|
+
}
|
|
156
|
+
// ties keep earliest (do nothing)
|
|
157
|
+
}
|
|
158
|
+
return best;
|
|
159
|
+
};
|
|
@@ -91,8 +91,25 @@ export const disposableOutputStream = (
|
|
|
91
91
|
const realSpawn = (argv: readonly string[], opts: SpawnOptions): SpawnedProcess => {
|
|
92
92
|
const [bin, ...args] = argv;
|
|
93
93
|
if (bin === undefined) throw new Error("empty argv");
|
|
94
|
+
const env =
|
|
95
|
+
opts.env === undefined
|
|
96
|
+
? undefined
|
|
97
|
+
: (() => {
|
|
98
|
+
const merged: Record<string, string | undefined> = { ...process.env };
|
|
99
|
+
for (const [k, v] of Object.entries(opts.env)) {
|
|
100
|
+
if (v === "") delete merged[k];
|
|
101
|
+
else merged[k] = v;
|
|
102
|
+
}
|
|
103
|
+
// Filter out undefined to satisfy Node's env type (string -> string)
|
|
104
|
+
const out: Record<string, string> = {};
|
|
105
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
106
|
+
if (v !== undefined) out[k] = v;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
})();
|
|
94
110
|
const child = nodeSpawn(bin, args, {
|
|
95
111
|
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
112
|
+
...(env !== undefined ? { env } : {}),
|
|
96
113
|
stdio: [
|
|
97
114
|
opts.stdin === "pipe" ? "pipe" : opts.stdin === "close" ? "ignore" : "inherit",
|
|
98
115
|
"pipe",
|
|
@@ -17,11 +17,21 @@ import {
|
|
|
17
17
|
} from "../interpretation/argv.js";
|
|
18
18
|
import { stdinPolicyOf } from "../interpretation/dimensions.js";
|
|
19
19
|
import { detectAuthFailureInLine, detectLimitInLine } from "../interpretation/limits.js";
|
|
20
|
+
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
20
21
|
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
22
|
+
import { matcherOverridesOf } from "../knowledge/overrides.js";
|
|
21
23
|
import { AsyncChannel } from "./channel.js";
|
|
22
24
|
import { decodeLine, freshDecodeState } from "./decode.js";
|
|
23
25
|
import type { RunnerDeps, SpawnedProcess } from "./deps.js";
|
|
24
26
|
import type { ExitCause, HarnessEvent } from "./events.js";
|
|
27
|
+
import type { FailureSummary } from "./failure.js";
|
|
28
|
+
import {
|
|
29
|
+
failureFromAuth,
|
|
30
|
+
failureFromLimit,
|
|
31
|
+
failureFromRejected,
|
|
32
|
+
failureFromTransport,
|
|
33
|
+
reduceFailures,
|
|
34
|
+
} from "./failure.js";
|
|
25
35
|
import { LineBuffer } from "./lines.js";
|
|
26
36
|
|
|
27
37
|
/** Fallback correlation when the host does not mint turn ids: monotonic per
|
|
@@ -79,6 +89,8 @@ export interface TurnRunOptions extends LaunchOptions {
|
|
|
79
89
|
readonly resume?: string;
|
|
80
90
|
/** Working directory for the spawned harness. */
|
|
81
91
|
readonly cwd?: string;
|
|
92
|
+
/** Per-call environment, merged over parent; "" deletes. */
|
|
93
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
export async function* streamTurn(
|
|
@@ -88,18 +100,87 @@ export async function* streamTurn(
|
|
|
88
100
|
): AsyncIterable<HarnessEvent> {
|
|
89
101
|
const turnId = deps.turnId ?? `turn-${++turnCounter}`;
|
|
90
102
|
const log = deps.log ?? (() => {});
|
|
91
|
-
const argv =
|
|
92
|
-
opts.resume === undefined
|
|
93
|
-
? buildLaunchArgv(h, opts)
|
|
94
|
-
: buildResumeArgv(h, { ...opts, sessionId: opts.resume });
|
|
95
|
-
const granularity = streamingGranularityOf(h, argv);
|
|
96
103
|
|
|
104
|
+
// Validate env before building argv so an invalid env is a refusal, not a spawn
|
|
105
|
+
if (opts.env !== undefined) {
|
|
106
|
+
for (const [k, v] of Object.entries(opts.env)) {
|
|
107
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k) || k.includes("\0") || v.includes("\0")) {
|
|
108
|
+
const refusal = new ArgvRefusalError({
|
|
109
|
+
issue: "invalid-env",
|
|
110
|
+
harness: h.name,
|
|
111
|
+
supported: ["env keys must match /^[A-Za-z_][A-Za-z0-9_]*$/ and no NUL"],
|
|
112
|
+
detail: k,
|
|
113
|
+
});
|
|
114
|
+
const failure = failureFromRejected({
|
|
115
|
+
issue: refusal.issue,
|
|
116
|
+
option: undefined,
|
|
117
|
+
supported: refusal.supported,
|
|
118
|
+
detail: k,
|
|
119
|
+
});
|
|
120
|
+
log({
|
|
121
|
+
event: "rejected",
|
|
122
|
+
turnId,
|
|
123
|
+
harness: h.name,
|
|
124
|
+
issue: refusal.issue,
|
|
125
|
+
supported: refusal.supported,
|
|
126
|
+
argv: redactArgv([], opts.prompt),
|
|
127
|
+
});
|
|
128
|
+
yield { kind: "failure", ...failure };
|
|
129
|
+
yield { kind: "done", exitCode: null, cause: "failed", failure };
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let argv: string[];
|
|
136
|
+
let granularity: import("../knowledge/descriptor.js").StreamingGranularity;
|
|
137
|
+
try {
|
|
138
|
+
argv =
|
|
139
|
+
opts.resume === undefined
|
|
140
|
+
? buildLaunchArgv(h, opts)
|
|
141
|
+
: buildResumeArgv(h, { ...opts, sessionId: opts.resume });
|
|
142
|
+
granularity = streamingGranularityOf(h, argv);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
if (e instanceof ArgvRefusalError) {
|
|
145
|
+
const failure = failureFromRejected({
|
|
146
|
+
issue: e.issue,
|
|
147
|
+
option: e.option,
|
|
148
|
+
facet: e.facet,
|
|
149
|
+
supported: e.supported,
|
|
150
|
+
detail: e.message,
|
|
151
|
+
});
|
|
152
|
+
// No process spawned on a refusal - log rejected instead of spawn
|
|
153
|
+
let argvForLog: string[] = [];
|
|
154
|
+
try {
|
|
155
|
+
argvForLog = redactArgv([], opts.prompt);
|
|
156
|
+
} catch {}
|
|
157
|
+
log({
|
|
158
|
+
event: "rejected",
|
|
159
|
+
turnId,
|
|
160
|
+
harness: h.name,
|
|
161
|
+
issue: e.issue,
|
|
162
|
+
option: e.option,
|
|
163
|
+
facet: e.facet,
|
|
164
|
+
supported: e.supported,
|
|
165
|
+
argv: argvForLog,
|
|
166
|
+
});
|
|
167
|
+
yield { kind: "failure", ...failure };
|
|
168
|
+
yield { kind: "done", exitCode: null, cause: "failed", failure };
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const matcherOverrides = matcherOverridesOf.get(h);
|
|
175
|
+
const envKeys = opts.env ? Object.keys(opts.env) : undefined;
|
|
97
176
|
log({
|
|
98
177
|
event: "spawn",
|
|
99
178
|
turnId,
|
|
100
179
|
harness: h.name,
|
|
101
180
|
argv: redactArgv(argv, opts.prompt),
|
|
102
181
|
granularity,
|
|
182
|
+
...(matcherOverrides ? { matcherOverrides } : {}),
|
|
183
|
+
...(envKeys?.length ? { envKeys } : {}),
|
|
103
184
|
});
|
|
104
185
|
|
|
105
186
|
let proc: SpawnedProcess;
|
|
@@ -107,11 +188,12 @@ export async function* streamTurn(
|
|
|
107
188
|
proc = deps.spawn(argv, {
|
|
108
189
|
stdin: stdinPolicyOf(h) === "close-required" ? "close" : "inherit",
|
|
109
190
|
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
191
|
+
...(opts.env !== undefined ? { env: opts.env } : {}),
|
|
110
192
|
});
|
|
111
193
|
} catch (cause) {
|
|
112
|
-
//
|
|
113
|
-
// and a closed exit log - never a throw out of the first next().
|
|
194
|
+
// Spawn failure is a transport failure, not merely a crash
|
|
114
195
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
196
|
+
const failure = failureFromTransport(`spawn failed: ${message}`);
|
|
115
197
|
log({
|
|
116
198
|
event: "exit",
|
|
117
199
|
turnId,
|
|
@@ -121,7 +203,8 @@ export async function* streamTurn(
|
|
|
121
203
|
spawnError: message,
|
|
122
204
|
});
|
|
123
205
|
yield { kind: "error", message: `spawn failed: ${message}` };
|
|
124
|
-
yield { kind: "
|
|
206
|
+
yield { kind: "failure", ...failure };
|
|
207
|
+
yield { kind: "done", exitCode: 127, cause: "failed", failure };
|
|
125
208
|
return;
|
|
126
209
|
}
|
|
127
210
|
|
|
@@ -148,21 +231,62 @@ export async function* streamTurn(
|
|
|
148
231
|
}, KILL_GRACE_MS);
|
|
149
232
|
};
|
|
150
233
|
|
|
234
|
+
const failures: FailureSummary[] = [];
|
|
235
|
+
const pushFailure = async (f: FailureSummary): Promise<void> => {
|
|
236
|
+
failures.push(f);
|
|
237
|
+
await queue.push({ kind: "failure", ...f });
|
|
238
|
+
};
|
|
239
|
+
|
|
151
240
|
let watchdog: number | null = null;
|
|
241
|
+
let turnDeadline: number | null = null;
|
|
242
|
+
let watchdogReason: "inactivity" | "turn-deadline" | null = null;
|
|
152
243
|
const disarm = (): void => {
|
|
153
244
|
if (watchdog !== null) deps.clock.clearTimeout(watchdog);
|
|
154
245
|
watchdog = null;
|
|
246
|
+
if (turnDeadline !== null) deps.clock.clearTimeout(turnDeadline);
|
|
247
|
+
turnDeadline = null;
|
|
155
248
|
};
|
|
156
249
|
const rearm = (): void => {
|
|
157
|
-
if (
|
|
158
|
-
|
|
250
|
+
if (deps.stallMs === undefined) return;
|
|
251
|
+
if (watchdog !== null) deps.clock.clearTimeout(watchdog);
|
|
159
252
|
watchdog = deps.clock.setTimeout(() => {
|
|
160
253
|
killedByWatchdog = true;
|
|
161
|
-
|
|
254
|
+
watchdogReason = "inactivity";
|
|
255
|
+
log({
|
|
256
|
+
event: "stall",
|
|
257
|
+
turnId,
|
|
258
|
+
harness: h.name,
|
|
259
|
+
reason: "inactivity",
|
|
260
|
+
budgetMs: deps.stallMs,
|
|
261
|
+
});
|
|
262
|
+
// Disarm the other budget
|
|
263
|
+
if (turnDeadline !== null) {
|
|
264
|
+
deps.clock.clearTimeout(turnDeadline);
|
|
265
|
+
turnDeadline = null;
|
|
266
|
+
}
|
|
162
267
|
escalate();
|
|
163
268
|
}, deps.stallMs);
|
|
164
269
|
};
|
|
270
|
+
// Arm both budgets
|
|
165
271
|
rearm();
|
|
272
|
+
if (deps.turnTimeoutMs !== undefined) {
|
|
273
|
+
turnDeadline = deps.clock.setTimeout(() => {
|
|
274
|
+
killedByWatchdog = true;
|
|
275
|
+
watchdogReason = "turn-deadline";
|
|
276
|
+
log({
|
|
277
|
+
event: "stall",
|
|
278
|
+
turnId,
|
|
279
|
+
harness: h.name,
|
|
280
|
+
reason: "turn-deadline",
|
|
281
|
+
budgetMs: deps.turnTimeoutMs,
|
|
282
|
+
});
|
|
283
|
+
if (watchdog !== null) {
|
|
284
|
+
deps.clock.clearTimeout(watchdog);
|
|
285
|
+
watchdog = null;
|
|
286
|
+
}
|
|
287
|
+
escalate();
|
|
288
|
+
}, deps.turnTimeoutMs);
|
|
289
|
+
}
|
|
166
290
|
|
|
167
291
|
let pipeGrace: number | null = null;
|
|
168
292
|
void proc.exited.then((code) => {
|
|
@@ -186,16 +310,26 @@ export async function* streamTurn(
|
|
|
186
310
|
const lines = new LineBuffer();
|
|
187
311
|
for await (const chunk of proc.stdout) {
|
|
188
312
|
if (cancelled) break;
|
|
189
|
-
|
|
313
|
+
// Any output chunk rearms the inactivity budget, but not the wall-clock deadline
|
|
314
|
+
if (deps.stallMs !== undefined) rearm();
|
|
190
315
|
for (const line of lines.push(chunk)) {
|
|
191
316
|
for (const event of decodeLine(h, line, state, opts.model ?? "")) {
|
|
317
|
+
if ((event as unknown as { kind: string }).kind === "failure") {
|
|
318
|
+
// Directly from decode's rate_limit_event handling - track for reduction
|
|
319
|
+
failures.push(event as unknown as FailureSummary);
|
|
320
|
+
}
|
|
192
321
|
await queue.push(event);
|
|
193
322
|
}
|
|
194
323
|
}
|
|
195
324
|
}
|
|
196
325
|
const rest = lines.flush();
|
|
197
326
|
if (rest !== null && !cancelled) {
|
|
198
|
-
for (const event of decodeLine(h, rest, state, opts.model ?? ""))
|
|
327
|
+
for (const event of decodeLine(h, rest, state, opts.model ?? "")) {
|
|
328
|
+
if ((event as unknown as { kind: string }).kind === "failure") {
|
|
329
|
+
failures.push(event as unknown as FailureSummary);
|
|
330
|
+
}
|
|
331
|
+
await queue.push(event);
|
|
332
|
+
}
|
|
199
333
|
}
|
|
200
334
|
};
|
|
201
335
|
|
|
@@ -203,20 +337,27 @@ export async function* streamTurn(
|
|
|
203
337
|
const lines = new LineBuffer();
|
|
204
338
|
for await (const chunk of proc.stderr) {
|
|
205
339
|
if (cancelled) break;
|
|
206
|
-
rearm();
|
|
340
|
+
if (deps.stallMs !== undefined) rearm();
|
|
207
341
|
for (const line of lines.push(chunk)) {
|
|
208
342
|
const limit = detectLimitInLine(h, line);
|
|
209
343
|
if (limit !== null) {
|
|
210
344
|
state.limitSeen = true;
|
|
345
|
+
const failure = failureFromLimit(limit);
|
|
346
|
+
// Emit both limit (for 0.1.3 compat) and failure
|
|
211
347
|
await queue.push({
|
|
212
348
|
kind: "limit",
|
|
213
349
|
code: limit,
|
|
214
350
|
message: `limit wall detected (${limit})`,
|
|
215
351
|
});
|
|
352
|
+
await pushFailure(failure);
|
|
216
353
|
continue;
|
|
217
354
|
}
|
|
218
355
|
const auth = detectAuthFailureInLine(h, line);
|
|
219
356
|
if (auth !== null) {
|
|
357
|
+
const failure = failureFromAuth(auth);
|
|
358
|
+
await pushFailure(failure);
|
|
359
|
+
// Also emit error for compat? The old code emitted error, but now we emit failure.
|
|
360
|
+
// Keep error for 0.1.3 compat as well
|
|
220
361
|
await queue.push({ kind: "error", message: `auth wall: ${auth}` });
|
|
221
362
|
continue;
|
|
222
363
|
}
|
|
@@ -240,6 +381,7 @@ export async function* streamTurn(
|
|
|
240
381
|
issue: "read-failed",
|
|
241
382
|
});
|
|
242
383
|
await queue.push({ kind: "error", message: pumpFailureMessage(stream, cause) });
|
|
384
|
+
await pushFailure(failureFromTransport(pumpFailureMessage(stream, cause)));
|
|
243
385
|
escalate();
|
|
244
386
|
await proc.exited;
|
|
245
387
|
proc.disposeOutput();
|
|
@@ -254,7 +396,27 @@ export async function* streamTurn(
|
|
|
254
396
|
try {
|
|
255
397
|
for await (const event of queue) yield event;
|
|
256
398
|
|
|
257
|
-
|
|
399
|
+
// Post-queue failure sources: nonzero exit with no other failure is transport
|
|
400
|
+
if (
|
|
401
|
+
failures.length === 0 &&
|
|
402
|
+
exitCode !== 0 &&
|
|
403
|
+
exitCode !== null &&
|
|
404
|
+
!killedByWatchdog &&
|
|
405
|
+
!state.limitSeen
|
|
406
|
+
) {
|
|
407
|
+
const f = failureFromTransport(`nonzero exit ${exitCode}`);
|
|
408
|
+
failures.push(f);
|
|
409
|
+
// Need to emit this failure before done, even though queue is closed
|
|
410
|
+
yield { kind: "failure", ...f };
|
|
411
|
+
}
|
|
412
|
+
// Stall watchdog also implies a transport failure if not already present
|
|
413
|
+
if (killedByWatchdog && failures.length === 0) {
|
|
414
|
+
const f = failureFromTransport(`stalled: ${watchdogReason ?? "inactivity"}`);
|
|
415
|
+
failures.push(f);
|
|
416
|
+
yield { kind: "failure", ...f };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let cause: ExitCause = state.limitSeen
|
|
258
420
|
? "limit"
|
|
259
421
|
: killedByWatchdog && exitCode !== 0
|
|
260
422
|
? "stall"
|
|
@@ -263,6 +425,13 @@ export async function* streamTurn(
|
|
|
263
425
|
: exitCode === null
|
|
264
426
|
? "killed"
|
|
265
427
|
: "crash";
|
|
428
|
+
const reduced = reduceFailures(failures);
|
|
429
|
+
if (reduced && cause === "clean") cause = "failed";
|
|
430
|
+
// Also handle pi stopReason error at exit 0 yielding failed - if we have a failure and exit 0, ensure cause is failed
|
|
431
|
+
if (reduced && exitCode === 0 && cause !== "limit" && cause !== "stall") {
|
|
432
|
+
// If we have any failure at exit 0, the cause should be failed, not clean
|
|
433
|
+
if (cause === "clean") cause = "failed";
|
|
434
|
+
}
|
|
266
435
|
const tail = stderrTail.snapshot();
|
|
267
436
|
log({
|
|
268
437
|
event: "exit",
|
|
@@ -271,7 +440,10 @@ export async function* streamTurn(
|
|
|
271
440
|
exitCode,
|
|
272
441
|
cause,
|
|
273
442
|
...(pipesOpenAtExit ? { pipesOpenAtExit } : {}),
|
|
274
|
-
...(cause === "crash" || cause === "stall" || cause === "killed"
|
|
443
|
+
...(cause === "crash" || cause === "stall" || cause === "killed" || cause === "failed"
|
|
444
|
+
? { stderrTail: tail }
|
|
445
|
+
: {}),
|
|
446
|
+
...(reduced ? { failure: reduced } : {}),
|
|
275
447
|
});
|
|
276
448
|
// A failure with captured stderr surfaces as a stream-level error, not
|
|
277
449
|
// only in the exit log - so a crash from the real adapter's async spawn
|
|
@@ -280,7 +452,7 @@ export async function* streamTurn(
|
|
|
280
452
|
yield { kind: "error", message: tail.join("\n").slice(0, 4096) };
|
|
281
453
|
}
|
|
282
454
|
terminalEventReached = true;
|
|
283
|
-
yield { kind: "done", exitCode, cause };
|
|
455
|
+
yield { kind: "done", exitCode, cause, ...(reduced ? { failure: reduced } : {}) };
|
|
284
456
|
} finally {
|
|
285
457
|
const abandoned = !terminalEventReached;
|
|
286
458
|
cancelled = true;
|