@dungle-scrubs/harness-cli-normalizer 0.5.7 → 0.6.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 +25 -8
- package/dist/cli/args.d.ts +9 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +31 -26
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/check.d.ts +14 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +10 -3
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +6 -6
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/exit-codes.d.ts +0 -1
- package/dist/cli/exit-codes.d.ts.map +1 -1
- package/dist/cli/exit-codes.js +0 -1
- package/dist/cli/exit-codes.js.map +1 -1
- package/dist/cli/help.d.ts +3 -3
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +9 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +84 -265
- package/dist/cli/inspect.js.map +1 -1
- package/dist/cli/plan-turn.d.ts +68 -0
- package/dist/cli/plan-turn.d.ts.map +1 -0
- package/dist/cli/plan-turn.js +266 -0
- package/dist/cli/plan-turn.js.map +1 -0
- package/dist/cli/refuse.d.ts.map +1 -1
- package/dist/cli/refuse.js +2 -1
- package/dist/cli/refuse.js.map +1 -1
- package/dist/cli/resolve-harness.d.ts.map +1 -1
- package/dist/cli/resolve-harness.js +2 -3
- package/dist/cli/resolve-harness.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +33 -367
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/session-json.d.ts +1 -1
- package/dist/cli/session-json.d.ts.map +1 -1
- package/dist/cli/session-json.js +1 -3
- package/dist/cli/session-json.js.map +1 -1
- package/dist/cli/session.d.ts.map +1 -1
- package/dist/cli/session.js +27 -45
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/skills-root.d.ts +1 -1
- package/dist/cli/skills-root.d.ts.map +1 -1
- package/dist/cli/skills-root.js +1 -1
- package/dist/cli/skills-root.js.map +1 -1
- package/dist/execution/decode.d.ts.map +1 -1
- package/dist/execution/decode.js +11 -36
- package/dist/execution/decode.js.map +1 -1
- package/dist/execution/events.d.ts +4 -1
- package/dist/execution/events.d.ts.map +1 -1
- package/dist/execution/events.js.map +1 -1
- package/dist/execution/failure.d.ts +3 -1
- package/dist/execution/failure.d.ts.map +1 -1
- package/dist/execution/failure.js +14 -11
- package/dist/execution/failure.js.map +1 -1
- package/dist/execution/open-session.d.ts +6 -1
- package/dist/execution/open-session.d.ts.map +1 -1
- package/dist/execution/open-session.js +151 -223
- package/dist/execution/open-session.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +9 -14
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +85 -188
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/execution/supervisor.d.ts +71 -0
- package/dist/execution/supervisor.d.ts.map +1 -0
- package/dist/execution/supervisor.js +127 -0
- package/dist/execution/supervisor.js.map +1 -0
- package/dist/interpretation/argv.d.ts +44 -12
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +38 -24
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/content.d.ts +10 -1
- package/dist/interpretation/content.d.ts.map +1 -1
- package/dist/interpretation/content.js +20 -11
- package/dist/interpretation/content.js.map +1 -1
- package/dist/interpretation/hints.d.ts +1 -1
- package/dist/interpretation/hints.d.ts.map +1 -1
- package/dist/interpretation/hints.js +10 -11
- package/dist/interpretation/hints.js.map +1 -1
- package/dist/interpretation/index.d.ts +0 -1
- package/dist/interpretation/index.d.ts.map +1 -1
- package/dist/interpretation/index.js +0 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/limits.d.ts +2 -5
- package/dist/interpretation/limits.d.ts.map +1 -1
- package/dist/interpretation/limits.js +8 -62
- package/dist/interpretation/limits.js.map +1 -1
- package/dist/interpretation/question.d.ts +27 -0
- package/dist/interpretation/question.d.ts.map +1 -1
- package/dist/interpretation/question.js +28 -0
- package/dist/interpretation/question.js.map +1 -1
- package/dist/interpretation/refusal.d.ts +10 -5
- package/dist/interpretation/refusal.d.ts.map +1 -1
- package/dist/interpretation/refusal.js +24 -15
- package/dist/interpretation/refusal.js.map +1 -1
- package/dist/interpretation/resolve-options.d.ts +36 -2
- package/dist/interpretation/resolve-options.d.ts.map +1 -1
- package/dist/interpretation/resolve-options.js +38 -21
- package/dist/interpretation/resolve-options.js.map +1 -1
- package/dist/interpretation/session-input.d.ts +44 -1
- package/dist/interpretation/session-input.d.ts.map +1 -1
- package/dist/interpretation/session-input.js +59 -4
- package/dist/interpretation/session-input.js.map +1 -1
- package/dist/interpretation/shape.d.ts +1 -0
- package/dist/interpretation/shape.d.ts.map +1 -1
- package/dist/interpretation/shape.js +10 -0
- package/dist/interpretation/shape.js.map +1 -1
- package/dist/interpretation/skills-selection.d.ts +25 -33
- package/dist/interpretation/skills-selection.d.ts.map +1 -1
- package/dist/interpretation/skills-selection.js +41 -48
- package/dist/interpretation/skills-selection.js.map +1 -1
- package/dist/interpretation/support.d.ts.map +1 -1
- package/dist/interpretation/support.js +23 -8
- package/dist/interpretation/support.js.map +1 -1
- package/dist/interpretation/tool-selection.d.ts +7 -6
- package/dist/interpretation/tool-selection.d.ts.map +1 -1
- package/dist/interpretation/tool-selection.js +54 -116
- package/dist/interpretation/tool-selection.js.map +1 -1
- package/dist/interpretation/tool-vocabulary.d.ts +31 -12
- package/dist/interpretation/tool-vocabulary.d.ts.map +1 -1
- package/dist/interpretation/tool-vocabulary.js +19 -41
- package/dist/interpretation/tool-vocabulary.js.map +1 -1
- package/dist/interpretation/turn-options.d.ts +4 -0
- package/dist/interpretation/turn-options.d.ts.map +1 -1
- package/dist/interpretation/turn-options.js +105 -241
- package/dist/interpretation/turn-options.js.map +1 -1
- package/dist/interpretation/vocabulary.d.ts +8 -6
- package/dist/interpretation/vocabulary.d.ts.map +1 -1
- package/dist/interpretation/vocabulary.js +22 -9
- package/dist/interpretation/vocabulary.js.map +1 -1
- package/dist/knowledge/claude-code.js +3 -3
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +31 -7
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +67 -36
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +49 -15
- package/dist/knowledge/descriptor.js.map +1 -1
- package/dist/knowledge/matchers.d.ts +12 -0
- package/dist/knowledge/matchers.d.ts.map +1 -1
- package/dist/knowledge/matchers.js +34 -0
- package/dist/knowledge/matchers.js.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +7 -5
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/overrides.d.ts.map +1 -1
- package/dist/knowledge/overrides.js +9 -20
- package/dist/knowledge/overrides.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +8 -6
- package/dist/knowledge/pi.js.map +1 -1
- package/dist/knowledge/profile.d.ts +1 -0
- package/dist/knowledge/profile.d.ts.map +1 -1
- package/dist/knowledge/profile.js +1 -0
- package/dist/knowledge/profile.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/args.ts +35 -27
- package/src/cli/check.ts +10 -3
- package/src/cli/config.ts +12 -11
- package/src/cli/exit-codes.ts +0 -2
- package/src/cli/help.ts +9 -2
- package/src/cli/index.ts +3 -1
- package/src/cli/inspect.ts +88 -269
- package/src/cli/plan-turn.ts +346 -0
- package/src/cli/refuse.ts +2 -1
- package/src/cli/resolve-harness.ts +4 -5
- package/src/cli/run.ts +36 -399
- package/src/cli/session-json.ts +1 -6
- package/src/cli/session.ts +36 -45
- package/src/cli/skills-root.ts +1 -4
- package/src/execution/decode.ts +10 -36
- package/src/execution/events.ts +4 -1
- package/src/execution/failure.ts +18 -11
- package/src/execution/open-session.ts +160 -228
- package/src/execution/stream-turn.ts +90 -193
- package/src/execution/supervisor.ts +199 -0
- package/src/interpretation/argv.ts +86 -38
- package/src/interpretation/content.ts +30 -13
- package/src/interpretation/hints.ts +2 -12
- package/src/interpretation/index.ts +0 -1
- package/src/interpretation/limits.ts +14 -75
- package/src/interpretation/question.ts +46 -0
- package/src/interpretation/refusal.ts +28 -18
- package/src/interpretation/resolve-options.ts +85 -32
- package/src/interpretation/session-input.ts +88 -5
- package/src/interpretation/shape.ts +10 -0
- package/src/interpretation/skills-selection.ts +70 -74
- package/src/interpretation/support.ts +19 -6
- package/src/interpretation/tool-selection.ts +67 -122
- package/src/interpretation/tool-vocabulary.ts +48 -53
- package/src/interpretation/turn-options.ts +123 -220
- package/src/interpretation/vocabulary.ts +15 -10
- package/src/knowledge/claude-code.ts +3 -3
- package/src/knowledge/codex.ts +31 -7
- package/src/knowledge/descriptor.ts +105 -49
- package/src/knowledge/matchers.ts +35 -0
- package/src/knowledge/muse.ts +7 -5
- package/src/knowledge/overrides.ts +17 -32
- package/src/knowledge/pi.ts +8 -6
- package/src/knowledge/profile.ts +1 -0
- package/dist/interpretation/dimensions.d.ts +0 -11
- package/dist/interpretation/dimensions.d.ts.map +0 -1
- package/dist/interpretation/dimensions.js +0 -4
- package/dist/interpretation/dimensions.js.map +0 -1
- package/src/interpretation/dimensions.ts +0 -14
|
@@ -8,39 +8,39 @@
|
|
|
8
8
|
* evidence, not diagnostics. An abandoned turn (consumer breaks early)
|
|
9
9
|
* closes backpressure, disposes output, stops the child, and awaits both
|
|
10
10
|
* pumps before it returns.
|
|
11
|
+
*
|
|
12
|
+
* The supervising policies - stall clock, signal escalation, stderr
|
|
13
|
+
* classification, question detection at close - are the turn supervisor's
|
|
14
|
+
* (RFC-02 change 5); this runner composes it and owns only what is
|
|
15
|
+
* particular to one-shot turns: the wall-clock deadline, the exit
|
|
16
|
+
* classification, and the identity-first event buffer.
|
|
11
17
|
*/
|
|
12
18
|
import {
|
|
13
|
-
|
|
14
|
-
buildResumeArgv,
|
|
19
|
+
buildSpawnArgv,
|
|
15
20
|
type LaunchOptions,
|
|
21
|
+
promptTextOf,
|
|
16
22
|
streamingGranularityOf,
|
|
23
|
+
withPromptText,
|
|
17
24
|
} from "../interpretation/argv.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
detectAuthFailureInLine,
|
|
21
|
-
detectLimitInLine,
|
|
22
|
-
detectTransportInLine,
|
|
23
|
-
detectUnavailableInLine,
|
|
24
|
-
} from "../interpretation/limits.js";
|
|
25
|
-
import {
|
|
26
|
-
composeEscalatedPrompt,
|
|
27
|
-
detectQuestionBlock,
|
|
28
|
-
type QuestionMode,
|
|
29
|
-
} from "../interpretation/question.js";
|
|
25
|
+
import { detectTransportInLine, detectUnavailableInLine } from "../interpretation/limits.js";
|
|
26
|
+
import { composeEscalatedPrompt, type QuestionMode } from "../interpretation/question.js";
|
|
30
27
|
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
31
28
|
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
32
29
|
import { matcherOverridesOf } from "../knowledge/overrides.js";
|
|
33
30
|
import { AsyncChannel } from "./channel.js";
|
|
34
31
|
import { decodeLine, freshDecodeState } from "./decode.js";
|
|
35
32
|
import type { RunnerDeps, SpawnedProcess } from "./deps.js";
|
|
36
|
-
import
|
|
33
|
+
import {
|
|
34
|
+
DROPPABLE_KINDS,
|
|
35
|
+
type EscalationDetection,
|
|
36
|
+
type ExitCause,
|
|
37
|
+
type HarnessEvent,
|
|
38
|
+
} from "./events.js";
|
|
37
39
|
import type { FailureSummary } from "./failure.js";
|
|
38
40
|
import {
|
|
39
|
-
failureFromAuth,
|
|
40
41
|
failureFromLimit,
|
|
41
42
|
failureFromNative,
|
|
42
43
|
failureFromRejected,
|
|
43
|
-
failureFromTask,
|
|
44
44
|
failureFromTerminalError,
|
|
45
45
|
failureFromTimeout,
|
|
46
46
|
failureFromTransport,
|
|
@@ -48,15 +48,14 @@ import {
|
|
|
48
48
|
reduceFailures,
|
|
49
49
|
} from "./failure.js";
|
|
50
50
|
import { LineBuffer } from "./lines.js";
|
|
51
|
+
import { StderrTail, superviseTurn } from "./supervisor.js";
|
|
51
52
|
|
|
52
53
|
/** Fallback correlation when the host does not mint turn ids: monotonic per
|
|
53
54
|
* process. Hosts that need cross-process uniqueness pass deps.turnId. */
|
|
54
55
|
let turnCounter = 0;
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
-
*
|
|
58
|
-
* after the process itself exited. */
|
|
59
|
-
export const KILL_GRACE_MS = 5_000;
|
|
57
|
+
/** The grace allowed for pipes still held open (by a grandchild) after the
|
|
58
|
+
* process itself exited. */
|
|
60
59
|
export const PIPE_GRACE_MS = 2_000;
|
|
61
60
|
|
|
62
61
|
const OUTPUT_STREAMS = ["stdout", "stderr"] as const;
|
|
@@ -83,25 +82,6 @@ export const redactArgv = (argv: readonly string[], prompt?: string): string[] =
|
|
|
83
82
|
});
|
|
84
83
|
};
|
|
85
84
|
|
|
86
|
-
/** Bounded tail of unmatched stderr - the crash context a nonzero exit is
|
|
87
|
-
* explained by (v1 kept the turn's output slice for exactly this). Shared
|
|
88
|
-
* with the session runner. */
|
|
89
|
-
export class StderrTail {
|
|
90
|
-
private readonly lines: string[] = [];
|
|
91
|
-
private bytes = 0;
|
|
92
|
-
push(line: string): void {
|
|
93
|
-
this.lines.push(line);
|
|
94
|
-
this.bytes += line.length;
|
|
95
|
-
while (this.lines.length > 20 || (this.bytes > 4096 && this.lines.length > 1)) {
|
|
96
|
-
const dropped = this.lines.shift();
|
|
97
|
-
this.bytes -= dropped?.length ?? 0;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
snapshot(): readonly string[] {
|
|
101
|
-
return [...this.lines];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
85
|
export interface TurnRunOptions extends LaunchOptions {
|
|
106
86
|
/** Resume this session id instead of launching fresh - the turn spawns
|
|
107
87
|
* with the descriptor's resume grammar, and identity decoding treats a
|
|
@@ -136,11 +116,9 @@ export async function* streamTurn(
|
|
|
136
116
|
const questionMode: QuestionMode = opts.questions ?? "ask";
|
|
137
117
|
const effective: TurnRunOptions = {
|
|
138
118
|
...opts,
|
|
139
|
-
prompt:
|
|
119
|
+
prompt: withPromptText(opts.prompt, composeEscalatedPrompt(promptTextOf(opts), questionMode)),
|
|
140
120
|
};
|
|
141
|
-
|
|
142
|
-
// hcn-question block lives. Tracked only when detection is armed.
|
|
143
|
-
let lastAssistantText: string | null = null;
|
|
121
|
+
const promptText = promptTextOf(effective);
|
|
144
122
|
let asked = false;
|
|
145
123
|
let escalationDetection: EscalationDetection = "none";
|
|
146
124
|
|
|
@@ -166,7 +144,7 @@ export async function* streamTurn(
|
|
|
166
144
|
harness: h.name,
|
|
167
145
|
issue: refusal.issue,
|
|
168
146
|
supported: refusal.supported,
|
|
169
|
-
argv: redactArgv([],
|
|
147
|
+
argv: redactArgv([], promptText),
|
|
170
148
|
});
|
|
171
149
|
yield { kind: "failure", ...failure };
|
|
172
150
|
yield {
|
|
@@ -184,19 +162,7 @@ export async function* streamTurn(
|
|
|
184
162
|
let argv: string[];
|
|
185
163
|
let granularity: import("../knowledge/descriptor.js").StreamingGranularity;
|
|
186
164
|
try {
|
|
187
|
-
argv =
|
|
188
|
-
effective.resume === undefined
|
|
189
|
-
? buildLaunchArgv(h, effective)
|
|
190
|
-
: buildResumeArgv(h, { ...effective, sessionId: effective.resume });
|
|
191
|
-
if (effective.passthrough !== undefined && effective.passthrough.length > 0) {
|
|
192
|
-
argv = [...argv, "--", ...effective.passthrough];
|
|
193
|
-
}
|
|
194
|
-
// issue #38: claude/codex render the skills allowlist as complement-off
|
|
195
|
-
// tokens at the argv tail (claude: settings JSON; codex: -c skills.config).
|
|
196
|
-
const skillTokens = (opts as unknown as { __skillTokens?: string[] }).__skillTokens;
|
|
197
|
-
if (skillTokens !== undefined && skillTokens.length > 0) {
|
|
198
|
-
argv = [...argv, ...skillTokens];
|
|
199
|
-
}
|
|
165
|
+
argv = buildSpawnArgv(h, effective);
|
|
200
166
|
granularity = streamingGranularityOf(h, argv);
|
|
201
167
|
} catch (e) {
|
|
202
168
|
if (e instanceof ArgvRefusalError) {
|
|
@@ -212,7 +178,7 @@ export async function* streamTurn(
|
|
|
212
178
|
// No process spawned on a refusal - log rejected instead of spawn
|
|
213
179
|
let argvForLog: string[] = [];
|
|
214
180
|
try {
|
|
215
|
-
argvForLog = redactArgv([],
|
|
181
|
+
argvForLog = redactArgv([], promptText);
|
|
216
182
|
} catch {}
|
|
217
183
|
log({
|
|
218
184
|
event: "rejected",
|
|
@@ -243,7 +209,7 @@ export async function* streamTurn(
|
|
|
243
209
|
event: "spawn",
|
|
244
210
|
turnId,
|
|
245
211
|
harness: h.name,
|
|
246
|
-
argv: redactArgv(argv,
|
|
212
|
+
argv: redactArgv(argv, promptText),
|
|
247
213
|
granularity,
|
|
248
214
|
...(matcherOverrides ? { matcherOverrides } : {}),
|
|
249
215
|
...(envKeys?.length ? { envKeys } : {}),
|
|
@@ -260,7 +226,7 @@ export async function* streamTurn(
|
|
|
260
226
|
let proc: SpawnedProcess;
|
|
261
227
|
try {
|
|
262
228
|
proc = deps.spawn(argv, {
|
|
263
|
-
stdin:
|
|
229
|
+
stdin: h.stdin === "close-required" ? "close" : "inherit",
|
|
264
230
|
...(effective.cwd !== undefined ? { cwd: effective.cwd } : {}),
|
|
265
231
|
...(effective.env !== undefined ? { env: effective.env } : {}),
|
|
266
232
|
});
|
|
@@ -303,34 +269,6 @@ export async function* streamTurn(
|
|
|
303
269
|
let cancelled = false;
|
|
304
270
|
let terminalEventReached = false;
|
|
305
271
|
|
|
306
|
-
const safeSignal = (sig: "SIGTERM" | "SIGKILL"): void => {
|
|
307
|
-
if (!exited) deps.signal(proc, sig);
|
|
308
|
-
};
|
|
309
|
-
let escalationTimer: number | null = null;
|
|
310
|
-
const escalate = (): void => {
|
|
311
|
-
if (exited || escalationTimer !== null) return;
|
|
312
|
-
safeSignal("SIGTERM");
|
|
313
|
-
escalationTimer = deps.clock.setTimeout(() => {
|
|
314
|
-
escalationTimer = null;
|
|
315
|
-
safeSignal("SIGKILL");
|
|
316
|
-
}, KILL_GRACE_MS);
|
|
317
|
-
};
|
|
318
|
-
let abortHandler: (() => void) | null = null;
|
|
319
|
-
if (opts.signal) {
|
|
320
|
-
const onAbort = (): void => {
|
|
321
|
-
if (killedByAbort) return;
|
|
322
|
-
killedByAbort = true;
|
|
323
|
-
escalate();
|
|
324
|
-
};
|
|
325
|
-
if (opts.signal.aborted) {
|
|
326
|
-
killedByAbort = true;
|
|
327
|
-
escalate();
|
|
328
|
-
} else {
|
|
329
|
-
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
330
|
-
abortHandler = onAbort;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
272
|
const failures: FailureSummary[] = [];
|
|
335
273
|
const pushFailure = async (f: FailureSummary): Promise<void> => {
|
|
336
274
|
// Suppress a failure identical in class and message to the previous one
|
|
@@ -340,19 +278,26 @@ export async function* streamTurn(
|
|
|
340
278
|
await queue.push({ kind: "failure", ...f });
|
|
341
279
|
};
|
|
342
280
|
|
|
343
|
-
|
|
281
|
+
// The wall-clock deadline is this runner's own: a session has none.
|
|
344
282
|
let turnDeadline: number | null = null;
|
|
345
283
|
let watchdogReason: "inactivity" | "turn-deadline" | null = null;
|
|
346
|
-
const
|
|
347
|
-
if (watchdog !== null) deps.clock.clearTimeout(watchdog);
|
|
348
|
-
watchdog = null;
|
|
284
|
+
const disarmDeadline = (): void => {
|
|
349
285
|
if (turnDeadline !== null) deps.clock.clearTimeout(turnDeadline);
|
|
350
286
|
turnDeadline = null;
|
|
351
287
|
};
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
288
|
+
|
|
289
|
+
const sup = superviseTurn(h, questionMode, {
|
|
290
|
+
clock: deps.clock,
|
|
291
|
+
stallMs: deps.stallMs,
|
|
292
|
+
signal: (sig) => deps.signal(proc, sig),
|
|
293
|
+
emit: async (event) => {
|
|
294
|
+
// A wall read on stderr counts like one decoded from stdout.
|
|
295
|
+
if (event.kind === "limit") state.limitSeen = true;
|
|
296
|
+
await queue.push(event);
|
|
297
|
+
},
|
|
298
|
+
fail: pushFailure,
|
|
299
|
+
tail: stderrTail,
|
|
300
|
+
onStall: () => {
|
|
356
301
|
killedByWatchdog = true;
|
|
357
302
|
watchdogReason = "inactivity";
|
|
358
303
|
log({
|
|
@@ -363,15 +308,31 @@ export async function* streamTurn(
|
|
|
363
308
|
budgetMs: deps.stallMs,
|
|
364
309
|
});
|
|
365
310
|
// Disarm the other budget
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
311
|
+
disarmDeadline();
|
|
312
|
+
},
|
|
313
|
+
onQuestion: (options) => {
|
|
314
|
+
log({ event: "question", turnId, harness: h.name, options });
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
let abortHandler: (() => void) | null = null;
|
|
319
|
+
if (opts.signal) {
|
|
320
|
+
const onAbort = (): void => {
|
|
321
|
+
if (killedByAbort) return;
|
|
322
|
+
killedByAbort = true;
|
|
323
|
+
sup.escalate();
|
|
324
|
+
};
|
|
325
|
+
if (opts.signal.aborted) {
|
|
326
|
+
killedByAbort = true;
|
|
327
|
+
sup.escalate();
|
|
328
|
+
} else {
|
|
329
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
330
|
+
abortHandler = onAbort;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
373
334
|
// Arm both budgets
|
|
374
|
-
|
|
335
|
+
sup.beginTurn();
|
|
375
336
|
if (deps.turnTimeoutMs !== undefined) {
|
|
376
337
|
turnDeadline = deps.clock.setTimeout(() => {
|
|
377
338
|
killedByWatchdog = true;
|
|
@@ -383,11 +344,8 @@ export async function* streamTurn(
|
|
|
383
344
|
reason: "turn-deadline",
|
|
384
345
|
budgetMs: deps.turnTimeoutMs,
|
|
385
346
|
});
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
watchdog = null;
|
|
389
|
-
}
|
|
390
|
-
escalate();
|
|
347
|
+
sup.disarm();
|
|
348
|
+
sup.escalate();
|
|
391
349
|
}, deps.turnTimeoutMs);
|
|
392
350
|
}
|
|
393
351
|
|
|
@@ -395,11 +353,11 @@ export async function* streamTurn(
|
|
|
395
353
|
void proc.exited.then((code) => {
|
|
396
354
|
exited = true;
|
|
397
355
|
exitCode = code;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
|
|
356
|
+
// The process is gone: no signal, no pending escalation, no stall
|
|
357
|
+
// clock - a fire after this point would flip a completed turn to
|
|
358
|
+
// "stall".
|
|
359
|
+
sup.settle();
|
|
360
|
+
disarmDeadline();
|
|
403
361
|
// Pipes held open past exit (a grandchild inherited the fd) must not
|
|
404
362
|
// hang the turn forever - close out with the exit code in hand.
|
|
405
363
|
if (cancelled) return;
|
|
@@ -414,8 +372,6 @@ export async function* streamTurn(
|
|
|
414
372
|
let identitySeen = false;
|
|
415
373
|
const droppableBuffer: HarnessEvent[] = [];
|
|
416
374
|
const BUFFER_CAP = 256;
|
|
417
|
-
const isDroppable = (kind: string): boolean =>
|
|
418
|
-
kind === "progress" || kind === "token" || kind === "context";
|
|
419
375
|
const flushDroppable = async (): Promise<void> => {
|
|
420
376
|
for (const e of droppableBuffer) await queue.push(e);
|
|
421
377
|
droppableBuffer.length = 0;
|
|
@@ -428,7 +384,7 @@ export async function* streamTurn(
|
|
|
428
384
|
await flushDroppable();
|
|
429
385
|
return;
|
|
430
386
|
}
|
|
431
|
-
if (
|
|
387
|
+
if (DROPPABLE_KINDS.has(event.kind)) {
|
|
432
388
|
if (droppableBuffer.length >= BUFFER_CAP) droppableBuffer.shift();
|
|
433
389
|
droppableBuffer.push(event);
|
|
434
390
|
return;
|
|
@@ -453,15 +409,13 @@ export async function* streamTurn(
|
|
|
453
409
|
if (event.terminal === true) await pushFailure(failureFromTerminalError(h, event.message));
|
|
454
410
|
return;
|
|
455
411
|
}
|
|
456
|
-
|
|
457
|
-
lastAssistantText = event.text;
|
|
458
|
-
}
|
|
412
|
+
sup.noteEvent(event);
|
|
459
413
|
await queue.push(event);
|
|
460
414
|
};
|
|
461
415
|
for await (const chunk of proc.stdout) {
|
|
462
416
|
if (cancelled) break;
|
|
463
417
|
// Any output chunk rearms the inactivity budget, but not the wall-clock deadline
|
|
464
|
-
|
|
418
|
+
sup.rearm();
|
|
465
419
|
for (const line of lines.push(chunk)) {
|
|
466
420
|
for (const event of decodeLine(h, line, state, opts.model ?? "", granularity)) {
|
|
467
421
|
await handleEvent(event);
|
|
@@ -480,75 +434,12 @@ export async function* streamTurn(
|
|
|
480
434
|
}
|
|
481
435
|
};
|
|
482
436
|
|
|
483
|
-
/** issue #41: scan the last assistant message for the hcn-question
|
|
484
|
-
* block. Structured-first - the block's fields become the event; no
|
|
485
|
-
* prose parsing. Runs after the pumps settle (the last message is only
|
|
486
|
-
* last then) and only when detection is armed (questions ask).
|
|
487
|
-
* A malformed block surfaces as an error event, never a silent
|
|
488
|
-
* no-op. */
|
|
489
|
-
const emitQuestionIfAsked = async (): Promise<void> => {
|
|
490
|
-
if (questionMode !== "ask" || lastAssistantText === null) {
|
|
491
|
-
escalationDetection = "none";
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
const detection = detectQuestionBlock(lastAssistantText);
|
|
495
|
-
if (detection === null) {
|
|
496
|
-
escalationDetection = "none";
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
if ("malformed" in detection) {
|
|
500
|
-
escalationDetection = "malformed";
|
|
501
|
-
await queue.push({ kind: "error", message: detection.malformed });
|
|
502
|
-
await pushFailure(failureFromTask(`malformed hcn-question block: ${detection.malformed}`));
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
escalationDetection = "block";
|
|
506
|
-
log({
|
|
507
|
-
event: "question",
|
|
508
|
-
turnId,
|
|
509
|
-
harness: h.name,
|
|
510
|
-
options: detection.block.options.length,
|
|
511
|
-
});
|
|
512
|
-
asked = true;
|
|
513
|
-
await queue.push({
|
|
514
|
-
kind: "question",
|
|
515
|
-
question: detection.block.question,
|
|
516
|
-
options: detection.block.options,
|
|
517
|
-
...(detection.block.recommended !== undefined
|
|
518
|
-
? { recommended: detection.block.recommended }
|
|
519
|
-
: {}),
|
|
520
|
-
});
|
|
521
|
-
};
|
|
522
|
-
|
|
523
437
|
const pumpStderr = async (): Promise<void> => {
|
|
524
438
|
const lines = new LineBuffer();
|
|
525
439
|
for await (const chunk of proc.stderr) {
|
|
526
440
|
if (cancelled) break;
|
|
527
|
-
|
|
528
|
-
for (const line of lines.push(chunk))
|
|
529
|
-
const limit = detectLimitInLine(h, line);
|
|
530
|
-
if (limit !== null) {
|
|
531
|
-
state.limitSeen = true;
|
|
532
|
-
const failure = failureFromLimit(limit);
|
|
533
|
-
// Emit both limit (for 0.1.3 compat) and failure
|
|
534
|
-
await queue.push({
|
|
535
|
-
kind: "limit",
|
|
536
|
-
code: limit,
|
|
537
|
-
message: `limit wall detected (${limit})`,
|
|
538
|
-
});
|
|
539
|
-
await pushFailure(failure);
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
const auth = detectAuthFailureInLine(h, line);
|
|
543
|
-
if (auth !== null) {
|
|
544
|
-
const failure = failureFromAuth(auth);
|
|
545
|
-
await pushFailure(failure);
|
|
546
|
-
// Emit error alongside failure for 0.1.3 compat
|
|
547
|
-
await queue.push({ kind: "error", message: `auth wall: ${auth}` });
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
stderrTail.push(line);
|
|
551
|
-
}
|
|
441
|
+
sup.rearm();
|
|
442
|
+
for (const line of lines.push(chunk)) await sup.stderrLine(line);
|
|
552
443
|
}
|
|
553
444
|
};
|
|
554
445
|
|
|
@@ -568,7 +459,7 @@ export async function* streamTurn(
|
|
|
568
459
|
});
|
|
569
460
|
await queue.push({ kind: "error", message: pumpFailureMessage(stream, cause) });
|
|
570
461
|
await pushFailure(failureFromTransport(pumpFailureMessage(stream, cause)));
|
|
571
|
-
escalate();
|
|
462
|
+
sup.escalate();
|
|
572
463
|
await proc.exited;
|
|
573
464
|
proc.disposeOutput();
|
|
574
465
|
}
|
|
@@ -577,8 +468,14 @@ export async function* streamTurn(
|
|
|
577
468
|
observePump("stdout", pumpStdout()),
|
|
578
469
|
observePump("stderr", pumpStderr()),
|
|
579
470
|
]);
|
|
471
|
+
// issue #41: the turn closes after the pumps settle (the last message is
|
|
472
|
+
// only last then); the supervisor detects the block and emits it.
|
|
580
473
|
void Promise.all([proc.exited, pumpSettlements])
|
|
581
|
-
.then(() =>
|
|
474
|
+
.then(() => {
|
|
475
|
+
const close = sup.close();
|
|
476
|
+
escalationDetection = close.detection;
|
|
477
|
+
asked = close.asked;
|
|
478
|
+
})
|
|
582
479
|
.then(() => queue.close());
|
|
583
480
|
|
|
584
481
|
try {
|
|
@@ -709,18 +606,18 @@ export async function* streamTurn(
|
|
|
709
606
|
const abandoned = !terminalEventReached;
|
|
710
607
|
cancelled = true;
|
|
711
608
|
queue.close();
|
|
712
|
-
disarm();
|
|
609
|
+
sup.disarm();
|
|
610
|
+
disarmDeadline();
|
|
713
611
|
if (abandoned) {
|
|
714
612
|
log({ event: "abandoned", turnId, harness: h.name });
|
|
715
|
-
if (!exited) escalate();
|
|
613
|
+
if (!exited) sup.escalate();
|
|
716
614
|
}
|
|
717
615
|
proc.disposeOutput();
|
|
718
616
|
const [settledExit] = await Promise.all([proc.exited, pumpSettlements]);
|
|
719
617
|
exitCode = settledExit;
|
|
720
618
|
if (pipeGrace !== null) deps.clock.clearTimeout(pipeGrace);
|
|
721
619
|
pipeGrace = null;
|
|
722
|
-
|
|
723
|
-
escalationTimer = null;
|
|
620
|
+
sup.settle();
|
|
724
621
|
if (abandoned) {
|
|
725
622
|
log({
|
|
726
623
|
event: "abandonment_settled",
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn supervisor: the one owner of the supervising policies
|
|
3
|
+
* CONTEXT.md names (RFC-02 change 5), composed by both runners instead of
|
|
4
|
+
* written into each. It owns the stall clock, the SIGTERM-then-SIGKILL
|
|
5
|
+
* escalation, the classification of every stderr line, the tracking of
|
|
6
|
+
* the last assistant message, and the turn close that turns a detected
|
|
7
|
+
* hcn-question block into events. It emits through the callbacks its
|
|
8
|
+
* runner hands it and knows nothing about channels or turns.
|
|
9
|
+
*/
|
|
10
|
+
import { detectAuthFailureInLine, detectLimitInLine } from "../interpretation/limits.js";
|
|
11
|
+
import {
|
|
12
|
+
type QuestionDetectionKind,
|
|
13
|
+
type QuestionMode,
|
|
14
|
+
questionEventOf,
|
|
15
|
+
} from "../interpretation/question.js";
|
|
16
|
+
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
17
|
+
import type { Clock, SignalName, TimerHandle } from "./deps.js";
|
|
18
|
+
import type { HarnessEvent } from "./events.js";
|
|
19
|
+
import {
|
|
20
|
+
type FailureSummary,
|
|
21
|
+
failureFromAuth,
|
|
22
|
+
failureFromLimit,
|
|
23
|
+
failureFromTask,
|
|
24
|
+
} from "./failure.js";
|
|
25
|
+
|
|
26
|
+
/** SIGTERM -> SIGKILL escalation budget for a child that ignores the first
|
|
27
|
+
* signal. */
|
|
28
|
+
export const KILL_GRACE_MS = 5_000;
|
|
29
|
+
|
|
30
|
+
/** Bounded tail of unmatched stderr - the crash context a nonzero exit is
|
|
31
|
+
* explained by (v1 kept the turn's output slice for exactly this). */
|
|
32
|
+
export class StderrTail {
|
|
33
|
+
private readonly lines: string[] = [];
|
|
34
|
+
private bytes = 0;
|
|
35
|
+
push(line: string): void {
|
|
36
|
+
this.lines.push(line);
|
|
37
|
+
this.bytes += line.length;
|
|
38
|
+
while (this.lines.length > 20 || (this.bytes > 4096 && this.lines.length > 1)) {
|
|
39
|
+
const dropped = this.lines.shift();
|
|
40
|
+
this.bytes -= dropped?.length ?? 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
snapshot(): readonly string[] {
|
|
44
|
+
return [...this.lines];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SupervisorIo {
|
|
49
|
+
readonly clock: Clock;
|
|
50
|
+
/** Inactivity budget; undefined means no stall clock at all. */
|
|
51
|
+
readonly stallMs: number | undefined;
|
|
52
|
+
/** Signal the supervised process. The supervisor never signals after
|
|
53
|
+
* `settle()`. */
|
|
54
|
+
readonly signal: (sig: SignalName) => void;
|
|
55
|
+
/** Deliver an event to the turn's consumer. */
|
|
56
|
+
readonly emit: (event: HarnessEvent) => Promise<void>;
|
|
57
|
+
/** Record a failure for the turn's done AND deliver it as an event. */
|
|
58
|
+
readonly fail: (summary: FailureSummary) => Promise<void>;
|
|
59
|
+
/** Where unclassified stderr goes. */
|
|
60
|
+
readonly tail: StderrTail;
|
|
61
|
+
/** The stall clock fired: the runner records its own cause and ends the
|
|
62
|
+
* turn; the supervisor has already begun escalation. */
|
|
63
|
+
readonly onStall: () => void;
|
|
64
|
+
/** A question was detected at close, with its option count (for the
|
|
65
|
+
* runner's boundary log). */
|
|
66
|
+
readonly onQuestion: (optionCount: number) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TurnClose {
|
|
70
|
+
readonly detection: QuestionDetectionKind;
|
|
71
|
+
/** True when the turn ended by asking: a successful turn, cause
|
|
72
|
+
* awaiting-input. */
|
|
73
|
+
readonly asked: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface TurnSupervisor {
|
|
77
|
+
/** Start a turn: reset per-turn state and arm the stall clock. */
|
|
78
|
+
beginTurn(): void;
|
|
79
|
+
/** Any output chunk arrived: restart the inactivity budget, if a turn is
|
|
80
|
+
* open and a budget exists. */
|
|
81
|
+
rearm(): void;
|
|
82
|
+
/** The turn ended, or the process did: stop the stall clock. */
|
|
83
|
+
disarm(): void;
|
|
84
|
+
/** SIGTERM now, SIGKILL after KILL_GRACE_MS unless the process exits.
|
|
85
|
+
* Idempotent while an escalation is pending. */
|
|
86
|
+
escalate(): void;
|
|
87
|
+
/** The process is gone: no further signals, no pending escalation, no
|
|
88
|
+
* stall clock. */
|
|
89
|
+
settle(): void;
|
|
90
|
+
/** Classify one stderr line: a limit wall becomes a limit event plus a
|
|
91
|
+
* failure, an auth wall a failure plus an error event, anything else
|
|
92
|
+
* goes to the tail. Returns the limit code when the line was a limit. */
|
|
93
|
+
stderrLine(line: string): Promise<void>;
|
|
94
|
+
/** A decoded event passed by: the last assistant message is where the
|
|
95
|
+
* protocol says the hcn-question block lives (tracked in ask mode). */
|
|
96
|
+
noteEvent(event: HarnessEvent): void;
|
|
97
|
+
/** At turn end: detect the block in the last assistant message, emit the
|
|
98
|
+
* question (or the malformation as an error plus a task failure), and
|
|
99
|
+
* report what was found. Synchronous: the events are enqueued, not
|
|
100
|
+
* awaited, so a runner can close a turn from a timer callback. */
|
|
101
|
+
close(): TurnClose;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const superviseTurn = (
|
|
105
|
+
h: HarnessDescriptor,
|
|
106
|
+
questionMode: QuestionMode,
|
|
107
|
+
io: SupervisorIo,
|
|
108
|
+
): TurnSupervisor => {
|
|
109
|
+
let settled = false;
|
|
110
|
+
let killTimer: TimerHandle | null = null;
|
|
111
|
+
let stallTimer: TimerHandle | null = null;
|
|
112
|
+
let turnOpen = false;
|
|
113
|
+
let lastAssistantText: string | null = null;
|
|
114
|
+
|
|
115
|
+
const signal = (sig: SignalName): void => {
|
|
116
|
+
if (!settled) io.signal(sig);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const disarm = (): void => {
|
|
120
|
+
if (stallTimer !== null) io.clock.clearTimeout(stallTimer);
|
|
121
|
+
stallTimer = null;
|
|
122
|
+
turnOpen = false;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const escalate = (): void => {
|
|
126
|
+
if (settled || killTimer !== null) return;
|
|
127
|
+
signal("SIGTERM");
|
|
128
|
+
killTimer = io.clock.setTimeout(() => {
|
|
129
|
+
killTimer = null;
|
|
130
|
+
signal("SIGKILL");
|
|
131
|
+
}, KILL_GRACE_MS);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const armStall = (): void => {
|
|
135
|
+
if (io.stallMs === undefined || !turnOpen) return;
|
|
136
|
+
if (stallTimer !== null) io.clock.clearTimeout(stallTimer);
|
|
137
|
+
stallTimer = io.clock.setTimeout(() => {
|
|
138
|
+
stallTimer = null;
|
|
139
|
+
// The turn may have ended between the timer firing and this callback
|
|
140
|
+
// running; a closed turn is never reported as a stall.
|
|
141
|
+
if (!turnOpen) return;
|
|
142
|
+
io.onStall();
|
|
143
|
+
escalate();
|
|
144
|
+
}, io.stallMs);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
beginTurn(): void {
|
|
149
|
+
lastAssistantText = null;
|
|
150
|
+
turnOpen = true;
|
|
151
|
+
armStall();
|
|
152
|
+
},
|
|
153
|
+
rearm(): void {
|
|
154
|
+
armStall();
|
|
155
|
+
},
|
|
156
|
+
disarm,
|
|
157
|
+
escalate,
|
|
158
|
+
settle(): void {
|
|
159
|
+
settled = true;
|
|
160
|
+
if (killTimer !== null) io.clock.clearTimeout(killTimer);
|
|
161
|
+
killTimer = null;
|
|
162
|
+
disarm();
|
|
163
|
+
},
|
|
164
|
+
async stderrLine(line: string): Promise<void> {
|
|
165
|
+
const limit = detectLimitInLine(h, line);
|
|
166
|
+
if (limit !== null) {
|
|
167
|
+
await io.emit({ kind: "limit", code: limit, message: `limit wall detected (${limit})` });
|
|
168
|
+
await io.fail(failureFromLimit(limit));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const auth = detectAuthFailureInLine(h, line);
|
|
172
|
+
if (auth !== null) {
|
|
173
|
+
await io.fail(failureFromAuth(auth));
|
|
174
|
+
await io.emit({ kind: "error", message: `auth wall: ${auth}` });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
io.tail.push(line);
|
|
178
|
+
},
|
|
179
|
+
noteEvent(event: HarnessEvent): void {
|
|
180
|
+
if (questionMode === "ask" && event.kind === "message" && event.role === "assistant") {
|
|
181
|
+
lastAssistantText = event.text;
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
close(): TurnClose {
|
|
185
|
+
const found = questionEventOf(lastAssistantText, questionMode);
|
|
186
|
+
if (found.detection === "malformed") {
|
|
187
|
+
void io.emit({ kind: "error", message: found.malformed });
|
|
188
|
+
void io.fail(failureFromTask(`malformed hcn-question block: ${found.malformed}`));
|
|
189
|
+
return { detection: "malformed", asked: false };
|
|
190
|
+
}
|
|
191
|
+
if (found.detection === "block") {
|
|
192
|
+
io.onQuestion(found.event.options.length);
|
|
193
|
+
void io.emit(found.event);
|
|
194
|
+
return { detection: "block", asked: true };
|
|
195
|
+
}
|
|
196
|
+
return { detection: "none", asked: false };
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
};
|