@dungle-scrubs/harness-cli-normalizer 0.1.3 → 0.3.1
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 +156 -13
- 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 +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +186 -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 +3 -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 +5 -1
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +188 -24
- 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/store.js +1 -1
- 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 +25 -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 +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +33 -9
- 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 +190 -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 +192 -21
- 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/store.ts +1 -1
- 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 +25 -6
- package/src/knowledge/overrides.ts +97 -0
- package/src/knowledge/pi.ts +33 -9
|
@@ -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
|
|
@@ -45,13 +55,18 @@ const SECRETISH = /(sk-[A-Za-z0-9_-]{8,}|(?:token|key|secret|password)=\S+)/i;
|
|
|
45
55
|
|
|
46
56
|
/** Redact by POSITION, not shape: the prompt is a known argv slot and is
|
|
47
57
|
* masked wholesale (content never reaches a log line - v1 D-005); every
|
|
48
|
-
* other token is kept unless it is secret-shaped.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
* other token is kept unless it is secret-shaped. Only the prompt's
|
|
59
|
+
* positional slot is masked, so a one-word prompt that equals a flag
|
|
60
|
+
* value does not cause that flag value to be masked. */
|
|
61
|
+
export const redactArgv = (argv: readonly string[], prompt?: string): string[] => {
|
|
62
|
+
const promptIndex = prompt !== undefined ? argv.lastIndexOf(prompt) : -1;
|
|
63
|
+
const promptLabel = prompt !== undefined ? `[prompt:${prompt.length}ch]` : "";
|
|
64
|
+
return argv.map((token, index) => {
|
|
65
|
+
if (index === promptIndex) return promptLabel;
|
|
52
66
|
if (SECRETISH.test(token)) return "[redacted]";
|
|
53
67
|
return token;
|
|
54
68
|
});
|
|
69
|
+
};
|
|
55
70
|
|
|
56
71
|
/** Bounded tail of unmatched stderr - the crash context a nonzero exit is
|
|
57
72
|
* explained by (v1 kept the turn's output slice for exactly this). Shared
|
|
@@ -79,6 +94,8 @@ export interface TurnRunOptions extends LaunchOptions {
|
|
|
79
94
|
readonly resume?: string;
|
|
80
95
|
/** Working directory for the spawned harness. */
|
|
81
96
|
readonly cwd?: string;
|
|
97
|
+
/** Per-call environment, merged over parent; "" deletes. */
|
|
98
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
export async function* streamTurn(
|
|
@@ -88,18 +105,87 @@ export async function* streamTurn(
|
|
|
88
105
|
): AsyncIterable<HarnessEvent> {
|
|
89
106
|
const turnId = deps.turnId ?? `turn-${++turnCounter}`;
|
|
90
107
|
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
108
|
|
|
109
|
+
// Validate env before building argv so an invalid env is a refusal, not a spawn
|
|
110
|
+
if (opts.env !== undefined) {
|
|
111
|
+
for (const [k, v] of Object.entries(opts.env)) {
|
|
112
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k) || k.includes("\0") || v.includes("\0")) {
|
|
113
|
+
const refusal = new ArgvRefusalError({
|
|
114
|
+
issue: "invalid-env",
|
|
115
|
+
harness: h.name,
|
|
116
|
+
supported: ["env keys must match /^[A-Za-z_][A-Za-z0-9_]*$/ and no NUL"],
|
|
117
|
+
detail: k,
|
|
118
|
+
});
|
|
119
|
+
const failure = failureFromRejected({
|
|
120
|
+
issue: refusal.issue,
|
|
121
|
+
option: undefined,
|
|
122
|
+
supported: refusal.supported,
|
|
123
|
+
detail: k,
|
|
124
|
+
});
|
|
125
|
+
log({
|
|
126
|
+
event: "rejected",
|
|
127
|
+
turnId,
|
|
128
|
+
harness: h.name,
|
|
129
|
+
issue: refusal.issue,
|
|
130
|
+
supported: refusal.supported,
|
|
131
|
+
argv: redactArgv([], opts.prompt),
|
|
132
|
+
});
|
|
133
|
+
yield { kind: "failure", ...failure };
|
|
134
|
+
yield { kind: "done", exitCode: null, cause: "failed", failure };
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let argv: string[];
|
|
141
|
+
let granularity: import("../knowledge/descriptor.js").StreamingGranularity;
|
|
142
|
+
try {
|
|
143
|
+
argv =
|
|
144
|
+
opts.resume === undefined
|
|
145
|
+
? buildLaunchArgv(h, opts)
|
|
146
|
+
: buildResumeArgv(h, { ...opts, sessionId: opts.resume });
|
|
147
|
+
granularity = streamingGranularityOf(h, argv);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
if (e instanceof ArgvRefusalError) {
|
|
150
|
+
const failure = failureFromRejected({
|
|
151
|
+
issue: e.issue,
|
|
152
|
+
option: e.option,
|
|
153
|
+
facet: e.facet,
|
|
154
|
+
supported: e.supported,
|
|
155
|
+
detail: e.message,
|
|
156
|
+
});
|
|
157
|
+
// No process spawned on a refusal - log rejected instead of spawn
|
|
158
|
+
let argvForLog: string[] = [];
|
|
159
|
+
try {
|
|
160
|
+
argvForLog = redactArgv([], opts.prompt);
|
|
161
|
+
} catch {}
|
|
162
|
+
log({
|
|
163
|
+
event: "rejected",
|
|
164
|
+
turnId,
|
|
165
|
+
harness: h.name,
|
|
166
|
+
issue: e.issue,
|
|
167
|
+
option: e.option,
|
|
168
|
+
facet: e.facet,
|
|
169
|
+
supported: e.supported,
|
|
170
|
+
argv: argvForLog,
|
|
171
|
+
});
|
|
172
|
+
yield { kind: "failure", ...failure };
|
|
173
|
+
yield { kind: "done", exitCode: null, cause: "failed", failure };
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
throw e;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const matcherOverrides = matcherOverridesOf.get(h);
|
|
180
|
+
const envKeys = opts.env ? Object.keys(opts.env) : undefined;
|
|
97
181
|
log({
|
|
98
182
|
event: "spawn",
|
|
99
183
|
turnId,
|
|
100
184
|
harness: h.name,
|
|
101
185
|
argv: redactArgv(argv, opts.prompt),
|
|
102
186
|
granularity,
|
|
187
|
+
...(matcherOverrides ? { matcherOverrides } : {}),
|
|
188
|
+
...(envKeys?.length ? { envKeys } : {}),
|
|
103
189
|
});
|
|
104
190
|
|
|
105
191
|
let proc: SpawnedProcess;
|
|
@@ -107,11 +193,12 @@ export async function* streamTurn(
|
|
|
107
193
|
proc = deps.spawn(argv, {
|
|
108
194
|
stdin: stdinPolicyOf(h) === "close-required" ? "close" : "inherit",
|
|
109
195
|
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
196
|
+
...(opts.env !== undefined ? { env: opts.env } : {}),
|
|
110
197
|
});
|
|
111
198
|
} catch (cause) {
|
|
112
|
-
//
|
|
113
|
-
// and a closed exit log - never a throw out of the first next().
|
|
199
|
+
// Spawn failure is a transport failure, not merely a crash
|
|
114
200
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
201
|
+
const failure = failureFromTransport(`spawn failed: ${message}`);
|
|
115
202
|
log({
|
|
116
203
|
event: "exit",
|
|
117
204
|
turnId,
|
|
@@ -121,7 +208,8 @@ export async function* streamTurn(
|
|
|
121
208
|
spawnError: message,
|
|
122
209
|
});
|
|
123
210
|
yield { kind: "error", message: `spawn failed: ${message}` };
|
|
124
|
-
yield { kind: "
|
|
211
|
+
yield { kind: "failure", ...failure };
|
|
212
|
+
yield { kind: "done", exitCode: 127, cause: "failed", failure };
|
|
125
213
|
return;
|
|
126
214
|
}
|
|
127
215
|
|
|
@@ -148,21 +236,62 @@ export async function* streamTurn(
|
|
|
148
236
|
}, KILL_GRACE_MS);
|
|
149
237
|
};
|
|
150
238
|
|
|
239
|
+
const failures: FailureSummary[] = [];
|
|
240
|
+
const pushFailure = async (f: FailureSummary): Promise<void> => {
|
|
241
|
+
failures.push(f);
|
|
242
|
+
await queue.push({ kind: "failure", ...f });
|
|
243
|
+
};
|
|
244
|
+
|
|
151
245
|
let watchdog: number | null = null;
|
|
246
|
+
let turnDeadline: number | null = null;
|
|
247
|
+
let watchdogReason: "inactivity" | "turn-deadline" | null = null;
|
|
152
248
|
const disarm = (): void => {
|
|
153
249
|
if (watchdog !== null) deps.clock.clearTimeout(watchdog);
|
|
154
250
|
watchdog = null;
|
|
251
|
+
if (turnDeadline !== null) deps.clock.clearTimeout(turnDeadline);
|
|
252
|
+
turnDeadline = null;
|
|
155
253
|
};
|
|
156
254
|
const rearm = (): void => {
|
|
157
|
-
if (
|
|
158
|
-
|
|
255
|
+
if (deps.stallMs === undefined) return;
|
|
256
|
+
if (watchdog !== null) deps.clock.clearTimeout(watchdog);
|
|
159
257
|
watchdog = deps.clock.setTimeout(() => {
|
|
160
258
|
killedByWatchdog = true;
|
|
161
|
-
|
|
259
|
+
watchdogReason = "inactivity";
|
|
260
|
+
log({
|
|
261
|
+
event: "stall",
|
|
262
|
+
turnId,
|
|
263
|
+
harness: h.name,
|
|
264
|
+
reason: "inactivity",
|
|
265
|
+
budgetMs: deps.stallMs,
|
|
266
|
+
});
|
|
267
|
+
// Disarm the other budget
|
|
268
|
+
if (turnDeadline !== null) {
|
|
269
|
+
deps.clock.clearTimeout(turnDeadline);
|
|
270
|
+
turnDeadline = null;
|
|
271
|
+
}
|
|
162
272
|
escalate();
|
|
163
273
|
}, deps.stallMs);
|
|
164
274
|
};
|
|
275
|
+
// Arm both budgets
|
|
165
276
|
rearm();
|
|
277
|
+
if (deps.turnTimeoutMs !== undefined) {
|
|
278
|
+
turnDeadline = deps.clock.setTimeout(() => {
|
|
279
|
+
killedByWatchdog = true;
|
|
280
|
+
watchdogReason = "turn-deadline";
|
|
281
|
+
log({
|
|
282
|
+
event: "stall",
|
|
283
|
+
turnId,
|
|
284
|
+
harness: h.name,
|
|
285
|
+
reason: "turn-deadline",
|
|
286
|
+
budgetMs: deps.turnTimeoutMs,
|
|
287
|
+
});
|
|
288
|
+
if (watchdog !== null) {
|
|
289
|
+
deps.clock.clearTimeout(watchdog);
|
|
290
|
+
watchdog = null;
|
|
291
|
+
}
|
|
292
|
+
escalate();
|
|
293
|
+
}, deps.turnTimeoutMs);
|
|
294
|
+
}
|
|
166
295
|
|
|
167
296
|
let pipeGrace: number | null = null;
|
|
168
297
|
void proc.exited.then((code) => {
|
|
@@ -186,16 +315,26 @@ export async function* streamTurn(
|
|
|
186
315
|
const lines = new LineBuffer();
|
|
187
316
|
for await (const chunk of proc.stdout) {
|
|
188
317
|
if (cancelled) break;
|
|
189
|
-
|
|
318
|
+
// Any output chunk rearms the inactivity budget, but not the wall-clock deadline
|
|
319
|
+
if (deps.stallMs !== undefined) rearm();
|
|
190
320
|
for (const line of lines.push(chunk)) {
|
|
191
321
|
for (const event of decodeLine(h, line, state, opts.model ?? "")) {
|
|
322
|
+
if ((event as unknown as { kind: string }).kind === "failure") {
|
|
323
|
+
// Directly from decode's rate_limit_event handling - track for reduction
|
|
324
|
+
failures.push(event as unknown as FailureSummary);
|
|
325
|
+
}
|
|
192
326
|
await queue.push(event);
|
|
193
327
|
}
|
|
194
328
|
}
|
|
195
329
|
}
|
|
196
330
|
const rest = lines.flush();
|
|
197
331
|
if (rest !== null && !cancelled) {
|
|
198
|
-
for (const event of decodeLine(h, rest, state, opts.model ?? ""))
|
|
332
|
+
for (const event of decodeLine(h, rest, state, opts.model ?? "")) {
|
|
333
|
+
if ((event as unknown as { kind: string }).kind === "failure") {
|
|
334
|
+
failures.push(event as unknown as FailureSummary);
|
|
335
|
+
}
|
|
336
|
+
await queue.push(event);
|
|
337
|
+
}
|
|
199
338
|
}
|
|
200
339
|
};
|
|
201
340
|
|
|
@@ -203,20 +342,26 @@ export async function* streamTurn(
|
|
|
203
342
|
const lines = new LineBuffer();
|
|
204
343
|
for await (const chunk of proc.stderr) {
|
|
205
344
|
if (cancelled) break;
|
|
206
|
-
rearm();
|
|
345
|
+
if (deps.stallMs !== undefined) rearm();
|
|
207
346
|
for (const line of lines.push(chunk)) {
|
|
208
347
|
const limit = detectLimitInLine(h, line);
|
|
209
348
|
if (limit !== null) {
|
|
210
349
|
state.limitSeen = true;
|
|
350
|
+
const failure = failureFromLimit(limit);
|
|
351
|
+
// Emit both limit (for 0.1.3 compat) and failure
|
|
211
352
|
await queue.push({
|
|
212
353
|
kind: "limit",
|
|
213
354
|
code: limit,
|
|
214
355
|
message: `limit wall detected (${limit})`,
|
|
215
356
|
});
|
|
357
|
+
await pushFailure(failure);
|
|
216
358
|
continue;
|
|
217
359
|
}
|
|
218
360
|
const auth = detectAuthFailureInLine(h, line);
|
|
219
361
|
if (auth !== null) {
|
|
362
|
+
const failure = failureFromAuth(auth);
|
|
363
|
+
await pushFailure(failure);
|
|
364
|
+
// Emit error alongside failure for 0.1.3 compat
|
|
220
365
|
await queue.push({ kind: "error", message: `auth wall: ${auth}` });
|
|
221
366
|
continue;
|
|
222
367
|
}
|
|
@@ -240,6 +385,7 @@ export async function* streamTurn(
|
|
|
240
385
|
issue: "read-failed",
|
|
241
386
|
});
|
|
242
387
|
await queue.push({ kind: "error", message: pumpFailureMessage(stream, cause) });
|
|
388
|
+
await pushFailure(failureFromTransport(pumpFailureMessage(stream, cause)));
|
|
243
389
|
escalate();
|
|
244
390
|
await proc.exited;
|
|
245
391
|
proc.disposeOutput();
|
|
@@ -254,7 +400,27 @@ export async function* streamTurn(
|
|
|
254
400
|
try {
|
|
255
401
|
for await (const event of queue) yield event;
|
|
256
402
|
|
|
257
|
-
|
|
403
|
+
// Post-queue failure sources: nonzero exit with no other failure is transport
|
|
404
|
+
if (
|
|
405
|
+
failures.length === 0 &&
|
|
406
|
+
exitCode !== 0 &&
|
|
407
|
+
exitCode !== null &&
|
|
408
|
+
!killedByWatchdog &&
|
|
409
|
+
!state.limitSeen
|
|
410
|
+
) {
|
|
411
|
+
const f = failureFromTransport(`nonzero exit ${exitCode}`);
|
|
412
|
+
failures.push(f);
|
|
413
|
+
// Need to emit this failure before done, even though queue is closed
|
|
414
|
+
yield { kind: "failure", ...f };
|
|
415
|
+
}
|
|
416
|
+
// Stall watchdog also implies a transport failure if not already present
|
|
417
|
+
if (killedByWatchdog && failures.length === 0) {
|
|
418
|
+
const f = failureFromTransport(`stalled: ${watchdogReason ?? "inactivity"}`);
|
|
419
|
+
failures.push(f);
|
|
420
|
+
yield { kind: "failure", ...f };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let cause: ExitCause = state.limitSeen
|
|
258
424
|
? "limit"
|
|
259
425
|
: killedByWatchdog && exitCode !== 0
|
|
260
426
|
? "stall"
|
|
@@ -263,6 +429,8 @@ export async function* streamTurn(
|
|
|
263
429
|
: exitCode === null
|
|
264
430
|
? "killed"
|
|
265
431
|
: "crash";
|
|
432
|
+
const reduced = reduceFailures(failures);
|
|
433
|
+
if (reduced && cause === "clean") cause = "failed";
|
|
266
434
|
const tail = stderrTail.snapshot();
|
|
267
435
|
log({
|
|
268
436
|
event: "exit",
|
|
@@ -271,7 +439,10 @@ export async function* streamTurn(
|
|
|
271
439
|
exitCode,
|
|
272
440
|
cause,
|
|
273
441
|
...(pipesOpenAtExit ? { pipesOpenAtExit } : {}),
|
|
274
|
-
...(cause === "crash" || cause === "stall" || cause === "killed"
|
|
442
|
+
...(cause === "crash" || cause === "stall" || cause === "killed" || cause === "failed"
|
|
443
|
+
? { stderrTail: tail }
|
|
444
|
+
: {}),
|
|
445
|
+
...(reduced ? { failure: reduced } : {}),
|
|
275
446
|
});
|
|
276
447
|
// A failure with captured stderr surfaces as a stream-level error, not
|
|
277
448
|
// only in the exit log - so a crash from the real adapter's async spawn
|
|
@@ -280,7 +451,7 @@ export async function* streamTurn(
|
|
|
280
451
|
yield { kind: "error", message: tail.join("\n").slice(0, 4096) };
|
|
281
452
|
}
|
|
282
453
|
terminalEventReached = true;
|
|
283
|
-
yield { kind: "done", exitCode, cause };
|
|
454
|
+
yield { kind: "done", exitCode, cause, ...(reduced ? { failure: reduced } : {}) };
|
|
284
455
|
} finally {
|
|
285
456
|
const abandoned = !terminalEventReached;
|
|
286
457
|
cancelled = true;
|