@dungle-scrubs/harness-cli-normalizer 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -6
- package/dist/cli/args.d.ts +9 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +36 -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 +8 -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 +2 -2
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +10 -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 +268 -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 +35 -43
- 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 +1 -1
- package/dist/execution/open-session.d.ts.map +1 -1
- package/dist/execution/open-session.js +150 -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 +39 -12
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +28 -20
- 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/isolation.d.ts +10 -0
- package/dist/interpretation/isolation.d.ts.map +1 -0
- package/dist/interpretation/isolation.js +30 -0
- package/dist/interpretation/isolation.js.map +1 -0
- 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 +53 -22
- 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 +25 -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 +107 -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.d.ts +3 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +17 -4
- 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 +50 -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 +13 -6
- 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 +39 -27
- package/src/cli/check.ts +10 -3
- package/src/cli/config.ts +14 -11
- package/src/cli/exit-codes.ts +0 -2
- package/src/cli/help.ts +10 -2
- package/src/cli/index.ts +3 -1
- package/src/cli/inspect.ts +88 -269
- package/src/cli/plan-turn.ts +350 -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 +51 -44
- 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 +154 -228
- package/src/execution/stream-turn.ts +90 -193
- package/src/execution/supervisor.ts +199 -0
- package/src/interpretation/argv.ts +65 -34
- package/src/interpretation/content.ts +30 -13
- package/src/interpretation/hints.ts +2 -12
- package/src/interpretation/index.ts +0 -1
- package/src/interpretation/isolation.ts +39 -0
- 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 +98 -33
- 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 +21 -6
- package/src/interpretation/tool-selection.ts +67 -122
- package/src/interpretation/tool-vocabulary.ts +48 -53
- package/src/interpretation/turn-options.ts +125 -220
- package/src/interpretation/vocabulary.ts +15 -10
- package/src/knowledge/claude-code.ts +17 -4
- package/src/knowledge/codex.ts +31 -7
- package/src/knowledge/descriptor.ts +106 -49
- package/src/knowledge/matchers.ts +35 -0
- package/src/knowledge/muse.ts +13 -6
- 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
|
@@ -10,36 +10,43 @@
|
|
|
10
10
|
* silently dropped. Structured lifecycle events (session open/close, turn
|
|
11
11
|
* start/end, send dispositions, drops) are always-on evidence with
|
|
12
12
|
* sessionId + turnId correlation.
|
|
13
|
+
*
|
|
14
|
+
* The supervising policies - stall clock, signal escalation, stderr
|
|
15
|
+
* classification, question detection at close - are the turn supervisor's
|
|
16
|
+
* (RFC-02 change 5), composed once per session and begun per turn. This
|
|
17
|
+
* runner owns only what is particular to sessions: turn delimiting, the
|
|
18
|
+
* send correlation, and the close-versus-abandon distinction.
|
|
13
19
|
*/
|
|
14
20
|
import { buildSessionArgv } from "../interpretation/argv.js";
|
|
15
21
|
import { capabilitiesOf } from "../interpretation/capabilities.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
composeEscalatedPrompt,
|
|
19
|
-
detectQuestionBlock,
|
|
20
|
-
type QuestionMode,
|
|
21
|
-
} from "../interpretation/question.js";
|
|
22
|
+
import { composeEscalatedPrompt, type QuestionMode } from "../interpretation/question.js";
|
|
22
23
|
import {
|
|
24
|
+
decodeSessionRecord,
|
|
25
|
+
encodeIdentityProbe,
|
|
23
26
|
encodeSessionInput,
|
|
24
27
|
resolveSessionInput,
|
|
25
28
|
SessionInputRefusalError,
|
|
26
29
|
} from "../interpretation/session-input.js";
|
|
27
30
|
import type { HarnessDescriptor, SessionInputContract } from "../knowledge/descriptor.js";
|
|
28
31
|
import { AsyncChannel } from "./channel.js";
|
|
29
|
-
import { decodeParsed, freshDecodeState } from "./decode.js";
|
|
30
|
-
import type { RunnerDeps, SpawnedProcess
|
|
31
|
-
import
|
|
32
|
+
import { decodeLine, decodeParsed, freshDecodeState } from "./decode.js";
|
|
33
|
+
import type { RunnerDeps, SpawnedProcess } from "./deps.js";
|
|
34
|
+
import {
|
|
35
|
+
DROPPABLE_KINDS,
|
|
36
|
+
type EscalationDetection,
|
|
37
|
+
type ExitCause,
|
|
38
|
+
type HarnessEvent,
|
|
39
|
+
} from "./events.js";
|
|
32
40
|
import type { FailureSummary } from "./failure.js";
|
|
33
41
|
import {
|
|
34
|
-
failureFromAuth,
|
|
35
42
|
failureFromLimit,
|
|
36
|
-
failureFromTask,
|
|
37
43
|
failureFromTerminalError,
|
|
38
44
|
failureFromTransport,
|
|
39
45
|
reduceFailures,
|
|
40
46
|
} from "./failure.js";
|
|
41
47
|
import { LineBuffer } from "./lines.js";
|
|
42
|
-
import {
|
|
48
|
+
import { PIPE_GRACE_MS, redactArgv } from "./stream-turn.js";
|
|
49
|
+
import { StderrTail, superviseTurn } from "./supervisor.js";
|
|
43
50
|
|
|
44
51
|
/** Grace after stdin EOF before concluding the child will not exit on its
|
|
45
52
|
* own and escalating signals. */
|
|
@@ -197,40 +204,26 @@ export const openSession = (
|
|
|
197
204
|
let turnLimitSeen = false;
|
|
198
205
|
let turnFailures: FailureSummary[] = [];
|
|
199
206
|
let pumpError: unknown = null;
|
|
200
|
-
// issue #44: the active turn
|
|
201
|
-
//
|
|
202
|
-
let lastAssistantText: string | null = null;
|
|
207
|
+
// issue #44: whether the active turn ended by asking, and what its close
|
|
208
|
+
// detected.
|
|
203
209
|
let turnAsked = false;
|
|
204
210
|
let turnEscalationDetection: EscalationDetection = "none";
|
|
205
211
|
let identityAnnounced = false;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (!dead) deps.signal(proc, sig);
|
|
209
|
-
};
|
|
210
|
-
const escalate = (): void => {
|
|
211
|
-
safeSignal("SIGTERM");
|
|
212
|
-
deps.clock.setTimeout(() => safeSignal("SIGKILL"), KILL_GRACE_MS);
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// Per-turn inactivity budget. A session turn can hang with the process
|
|
216
|
-
// alive and the pipes open, which no exit code reports; without this the
|
|
217
|
-
// consumer waits forever. Armed at turn start, rearmed on any output
|
|
218
|
-
// chunk, disarmed at turn end and at exit - the same discipline
|
|
219
|
-
// streamTurn uses, scoped to the turn rather than the process.
|
|
220
|
-
let stallTimer: TimerHandle | null = null;
|
|
212
|
+
// A stall killed the process on purpose, so the signal death it caused
|
|
213
|
+
// reports as "stall", not "killed".
|
|
221
214
|
let stalled = false;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
215
|
+
|
|
216
|
+
// The supervisor spans the session; each turn begins and ends on it. A
|
|
217
|
+
// session turn can hang with the process alive and the pipes open, which
|
|
218
|
+
// no exit code reports; the stall clock is what bounds that wait.
|
|
219
|
+
const sup = superviseTurn(h, questionMode, {
|
|
220
|
+
clock: deps.clock,
|
|
221
|
+
stallMs: deps.stallMs,
|
|
222
|
+
signal: (sig) => deps.signal(proc, sig),
|
|
223
|
+
emit: (event) => routeEvent(event),
|
|
224
|
+
fail: (summary) => pushFailure(summary),
|
|
225
|
+
tail: stderrTail,
|
|
226
|
+
onStall: () => {
|
|
234
227
|
stalled = true;
|
|
235
228
|
log({
|
|
236
229
|
event: "stall",
|
|
@@ -244,9 +237,17 @@ export const openSession = (
|
|
|
244
237
|
// the exit path then closes the session with the same cause.
|
|
245
238
|
void pushFailure(failureFromTransport("stalled: inactivity"));
|
|
246
239
|
endTurn({ kind: "done", exitCode: null, cause: "stall" });
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
240
|
+
},
|
|
241
|
+
onQuestion: (options) => {
|
|
242
|
+
log({
|
|
243
|
+
event: "question",
|
|
244
|
+
sessionId: opts.sessionId,
|
|
245
|
+
turnId: activeTurnId,
|
|
246
|
+
harness: h.name,
|
|
247
|
+
options,
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
});
|
|
250
251
|
|
|
251
252
|
const writeUser = (text: string): boolean => {
|
|
252
253
|
try {
|
|
@@ -261,7 +262,7 @@ export const openSession = (
|
|
|
261
262
|
// very signal that stops it. The exit path then finalizes as usual.
|
|
262
263
|
void routeEvent({ kind: "error", message: "send failed: session stdin is gone" });
|
|
263
264
|
closing = true;
|
|
264
|
-
escalate();
|
|
265
|
+
sup.escalate();
|
|
265
266
|
return false;
|
|
266
267
|
}
|
|
267
268
|
};
|
|
@@ -277,7 +278,6 @@ export const openSession = (
|
|
|
277
278
|
turnFailures = [];
|
|
278
279
|
turnAsked = false;
|
|
279
280
|
turnEscalationDetection = "none";
|
|
280
|
-
lastAssistantText = null;
|
|
281
281
|
activeTurn = new AsyncChannel<HarnessEvent>();
|
|
282
282
|
activeTurnId = `${opts.sessionId}:turn-${++turnCounter}`;
|
|
283
283
|
// Tag the turn with the id of the send that opened it, so the consumer
|
|
@@ -291,58 +291,21 @@ export const openSession = (
|
|
|
291
291
|
activeTurn.push(held);
|
|
292
292
|
}
|
|
293
293
|
turnsChannel.push(activeTurn as SessionTurn);
|
|
294
|
-
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
/** issue #44: at a turn boundary, scan the last assistant message for
|
|
298
|
-
* the hcn-question block - same structured-first discipline and
|
|
299
|
-
* last-message rule as streamTurn. The question event lands in the
|
|
300
|
-
* turn stream right before its done; a malformed block surfaces as an
|
|
301
|
-
* error event, never a silent no-op. */
|
|
302
|
-
const emitQuestionIfAsked = (): void => {
|
|
303
|
-
if (questionMode !== "ask" || lastAssistantText === null) {
|
|
304
|
-
turnEscalationDetection = "none";
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
const detection = detectQuestionBlock(lastAssistantText);
|
|
308
|
-
if (detection === null) {
|
|
309
|
-
turnEscalationDetection = "none";
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
if ("malformed" in detection) {
|
|
313
|
-
turnEscalationDetection = "malformed";
|
|
314
|
-
activeTurn?.push({ kind: "error", message: detection.malformed });
|
|
315
|
-
const failure = failureFromTask(`malformed hcn-question block: ${detection.malformed}`);
|
|
316
|
-
turnFailures.push(failure);
|
|
317
|
-
void activeTurn?.push({ kind: "failure", ...failure });
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
turnEscalationDetection = "block";
|
|
321
|
-
turnAsked = true;
|
|
322
|
-
log({
|
|
323
|
-
event: "question",
|
|
324
|
-
sessionId: opts.sessionId,
|
|
325
|
-
turnId: activeTurnId,
|
|
326
|
-
harness: h.name,
|
|
327
|
-
options: detection.block.options.length,
|
|
328
|
-
});
|
|
329
|
-
activeTurn?.push({
|
|
330
|
-
kind: "question",
|
|
331
|
-
question: detection.block.question,
|
|
332
|
-
options: detection.block.options,
|
|
333
|
-
...(detection.block.recommended !== undefined
|
|
334
|
-
? { recommended: detection.block.recommended }
|
|
335
|
-
: {}),
|
|
336
|
-
});
|
|
294
|
+
sup.beginTurn();
|
|
337
295
|
};
|
|
338
296
|
|
|
339
297
|
const endTurn = (done: Omit<HarnessEvent & { kind: "done" }, "escalation">): void => {
|
|
340
298
|
if (activeTurn === null) return;
|
|
341
|
-
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
299
|
+
sup.disarm();
|
|
300
|
+
// issue #44: the supervisor scans the last assistant message for the
|
|
301
|
+
// hcn-question block; the question event lands in the turn stream
|
|
302
|
+
// right before its done. Asking is a successful turn: the session
|
|
303
|
+
// semantic is "blocked on answer, session alive" - the done stays
|
|
304
|
+
// TURN-scoped (exitCode null in sessions) and the caller answers with
|
|
305
|
+
// the next send().
|
|
306
|
+
const close = sup.close();
|
|
307
|
+
turnEscalationDetection = close.detection;
|
|
308
|
+
turnAsked = close.asked;
|
|
346
309
|
if (turnAsked && done.cause === "clean") done = { ...done, cause: "awaiting-input" };
|
|
347
310
|
// RFC-01 every turn end carries the escalation record
|
|
348
311
|
let fullDone: HarnessEvent & { kind: "done" } = {
|
|
@@ -385,9 +348,7 @@ export const openSession = (
|
|
|
385
348
|
state.limitSeen = true;
|
|
386
349
|
turnLimitSeen = true;
|
|
387
350
|
}
|
|
388
|
-
|
|
389
|
-
lastAssistantText = event.text;
|
|
390
|
-
}
|
|
351
|
+
sup.noteEvent(event);
|
|
391
352
|
if (activeTurn !== null) {
|
|
392
353
|
// Awaited by the pumps: past the channel's high water mark this
|
|
393
354
|
// blocks the pump, so OS pipe backpressure reaches the child.
|
|
@@ -397,9 +358,7 @@ export const openSession = (
|
|
|
397
358
|
// droppable events go first, then oldest.
|
|
398
359
|
preTurnEvents.push(event);
|
|
399
360
|
if (preTurnEvents.length > PRETURN_MAX) {
|
|
400
|
-
const droppableAt = preTurnEvents.findIndex(
|
|
401
|
-
(e) => e.kind === "token" || e.kind === "progress" || e.kind === "context",
|
|
402
|
-
);
|
|
361
|
+
const droppableAt = preTurnEvents.findIndex((e) => DROPPABLE_KINDS.has(e.kind));
|
|
403
362
|
preTurnEvents.splice(droppableAt === -1 ? 0 : droppableAt, 1);
|
|
404
363
|
}
|
|
405
364
|
return Promise.resolve();
|
|
@@ -410,35 +369,63 @@ export const openSession = (
|
|
|
410
369
|
return routeEvent({ kind: "failure", ...f });
|
|
411
370
|
};
|
|
412
371
|
|
|
413
|
-
/** A decoded event other than a failure: a
|
|
414
|
-
*
|
|
372
|
+
/** A decoded event other than a failure: a limit also records the
|
|
373
|
+
* failure it stands for, and so does a terminal error, the way
|
|
374
|
+
* streamTurn does. */
|
|
415
375
|
const routeDecoded = async (event: HarnessEvent): Promise<void> => {
|
|
376
|
+
if (event.kind === "failure") {
|
|
377
|
+
await pushFailure(summaryOf(event));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
416
380
|
await routeEvent(event);
|
|
381
|
+
if (event.kind === "limit") await pushFailure(failureFromLimit(event.code));
|
|
417
382
|
if (event.kind === "error" && event.terminal === true) {
|
|
418
383
|
await pushFailure(failureFromTerminalError(h, event.message));
|
|
419
384
|
}
|
|
420
385
|
};
|
|
421
386
|
|
|
387
|
+
/** The probe answered: bind the announced id under the descriptor's
|
|
388
|
+
* authority, or surface a rotation. */
|
|
389
|
+
const announceIdentity = async (announced: string): Promise<void> => {
|
|
390
|
+
if (identityAnnounced) return;
|
|
391
|
+
if (sessionInputMode?.idFlag === null) {
|
|
392
|
+
// Harness-MINTED identity (pi rpc: `--session` refuses unknown ids,
|
|
393
|
+
// so fresh sessions omit the flag). The minted id IS the identity;
|
|
394
|
+
// opts.sessionId stays the caller-side handle.
|
|
395
|
+
identityAnnounced = true;
|
|
396
|
+
state.lastSeenId = announced;
|
|
397
|
+
await routeEvent({
|
|
398
|
+
kind: "identity",
|
|
399
|
+
sessionId: announced,
|
|
400
|
+
authority: "harness-minted",
|
|
401
|
+
capabilities: capabilitiesOf(h, opts.model ?? "", "headless-session"),
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (announced === opts.sessionId) {
|
|
406
|
+
identityAnnounced = true;
|
|
407
|
+
await routeEvent({
|
|
408
|
+
kind: "identity",
|
|
409
|
+
sessionId: announced,
|
|
410
|
+
authority: "caller-assigned",
|
|
411
|
+
capabilities: capabilitiesOf(h, opts.model ?? "", "headless-session"),
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
await routeEvent({
|
|
416
|
+
kind: "error",
|
|
417
|
+
message: `identity rotated: session announced ${JSON.stringify(announced)} but ${opts.sessionId} was requested`,
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
|
|
422
421
|
const pumpStdout = async (): Promise<void> => {
|
|
423
422
|
const lines = new LineBuffer();
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
for (const [key, expected] of Object.entries(spec)) {
|
|
429
|
-
if (record[key] !== expected) return false;
|
|
430
|
-
}
|
|
431
|
-
return true;
|
|
432
|
-
};
|
|
433
|
-
// issue #44: pi rpc is identity-silent at startup; the probe round
|
|
434
|
-
// trip is the only way to read the id (spike fixtures). The response
|
|
435
|
-
// echoes our marker id, so it cannot be confused with a user-visible
|
|
436
|
-
// get_state response.
|
|
437
|
-
if (sessionInputMode?.identityProbe !== null && sessionInputMode !== null) {
|
|
423
|
+
// issue #44: a harness that is identity-silent at startup gets the
|
|
424
|
+
// probe its descriptor declares; interpretation encodes it (ADR 0005).
|
|
425
|
+
const probe = encodeIdentityProbe(h);
|
|
426
|
+
if (probe !== null) {
|
|
438
427
|
try {
|
|
439
|
-
stdin.write(
|
|
440
|
-
`${JSON.stringify({ id: "hcn-identity", type: sessionInputMode.identityProbe.command })}\n`,
|
|
441
|
-
);
|
|
428
|
+
stdin.write(probe);
|
|
442
429
|
} catch {
|
|
443
430
|
// stdin already gone; the exited handler will surface the death.
|
|
444
431
|
}
|
|
@@ -448,99 +435,57 @@ export const openSession = (
|
|
|
448
435
|
try {
|
|
449
436
|
parsed = JSON.parse(line) as Record<string, unknown>;
|
|
450
437
|
} catch {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
await
|
|
438
|
+
// Not a record: the shared decoder reads the one signal a plain
|
|
439
|
+
// line can carry (a wall) and nothing else.
|
|
440
|
+
for (const event of decodeLine(h, line, state, opts.model ?? "")) {
|
|
441
|
+
await routeDecoded(event);
|
|
455
442
|
}
|
|
456
443
|
return;
|
|
457
444
|
}
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
// identity; opts.sessionId stays the caller-side handle.
|
|
480
|
-
if (!identityAnnounced) {
|
|
481
|
-
identityAnnounced = true;
|
|
482
|
-
state.lastSeenId = announced;
|
|
483
|
-
await routeEvent({
|
|
484
|
-
kind: "identity",
|
|
485
|
-
sessionId: announced,
|
|
486
|
-
authority: "harness-minted",
|
|
487
|
-
capabilities: capabilitiesOf(h, opts.model ?? "", "headless-session"),
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
} else if (announced === opts.sessionId) {
|
|
491
|
-
if (!identityAnnounced) {
|
|
492
|
-
identityAnnounced = true;
|
|
493
|
-
await routeEvent({
|
|
494
|
-
kind: "identity",
|
|
495
|
-
sessionId: announced,
|
|
496
|
-
authority: "caller-assigned",
|
|
497
|
-
capabilities: capabilitiesOf(h, opts.model ?? "", "headless-session"),
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
} else {
|
|
501
|
-
await routeEvent({
|
|
502
|
-
kind: "error",
|
|
503
|
-
message: `identity rotated: session announced ${JSON.stringify(announced)} but ${opts.sessionId} was requested`,
|
|
504
|
-
});
|
|
445
|
+
// Interpretation says what the record means; this runner only routes
|
|
446
|
+
// on the kind (ADR 0005: no harness field names here).
|
|
447
|
+
const record = decodeSessionRecord(h, parsed);
|
|
448
|
+
switch (record.kind) {
|
|
449
|
+
case "identity":
|
|
450
|
+
await announceIdentity(record.sessionId);
|
|
451
|
+
return;
|
|
452
|
+
case "probe-failed":
|
|
453
|
+
case "command-failed":
|
|
454
|
+
await routeEvent({ kind: "error", message: record.message });
|
|
455
|
+
return;
|
|
456
|
+
case "ignored":
|
|
457
|
+
return;
|
|
458
|
+
case "turn-end": {
|
|
459
|
+
// The turn-end record still feeds identity dedupe (claude includes
|
|
460
|
+
// session_id on result - a rotation announced there must not be
|
|
461
|
+
// missed) and content decoding, which already surfaces a failed
|
|
462
|
+
// result as an error event; the flag here only classifies the
|
|
463
|
+
// done cause.
|
|
464
|
+
for (const event of decodeParsed(h, parsed, state, opts.model ?? "")) {
|
|
465
|
+
await routeDecoded(event);
|
|
505
466
|
}
|
|
467
|
+
if (record.isError) resultError = true;
|
|
468
|
+
endTurn({
|
|
469
|
+
kind: "done",
|
|
470
|
+
exitCode: null,
|
|
471
|
+
cause: turnLimitSeen ? "limit" : resultError ? "crash" : "clean",
|
|
472
|
+
});
|
|
506
473
|
return;
|
|
507
474
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
475
|
+
case "content": {
|
|
476
|
+
for (const event of decodeParsed(h, parsed, state, opts.model ?? "")) {
|
|
477
|
+
await routeDecoded(event);
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
513
480
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
// session_id on result - a rotation announced there must not be
|
|
518
|
-
// missed).
|
|
519
|
-
const events = decodeParsed(h, parsed, state, opts.model ?? "");
|
|
520
|
-
const isTurnEnd = sessionInputMode !== null && matches(parsed, sessionInputMode.turnEnd);
|
|
521
|
-
if (isTurnEnd) {
|
|
522
|
-
// decodeParsed already surfaces the is_error case as an error event
|
|
523
|
-
// (content.ts claude reader); routing the events is enough - we only
|
|
524
|
-
// still track resultError here to classify the done cause.
|
|
525
|
-
for (const event of events) {
|
|
526
|
-
if (event.kind === "failure") await pushFailure(summaryOf(event));
|
|
527
|
-
else await routeDecoded(event);
|
|
481
|
+
default: {
|
|
482
|
+
const exhaustive: never = record;
|
|
483
|
+
return exhaustive;
|
|
528
484
|
}
|
|
529
|
-
if (parsed.is_error === true) resultError = true;
|
|
530
|
-
endTurn({
|
|
531
|
-
kind: "done",
|
|
532
|
-
exitCode: null,
|
|
533
|
-
cause: turnLimitSeen ? "limit" : resultError ? "crash" : "clean",
|
|
534
|
-
});
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
for (const event of events) {
|
|
538
|
-
if (event.kind === "failure") await pushFailure(summaryOf(event));
|
|
539
|
-
else await routeDecoded(event);
|
|
540
485
|
}
|
|
541
486
|
};
|
|
542
487
|
for await (const chunk of proc.stdout) {
|
|
543
|
-
|
|
488
|
+
sup.rearm();
|
|
544
489
|
for (const line of lines.push(chunk)) await handleLine(line);
|
|
545
490
|
}
|
|
546
491
|
const rest = lines.flush();
|
|
@@ -550,26 +495,8 @@ export const openSession = (
|
|
|
550
495
|
const pumpStderr = async (): Promise<void> => {
|
|
551
496
|
const lines = new LineBuffer();
|
|
552
497
|
for await (const chunk of proc.stderr) {
|
|
553
|
-
|
|
554
|
-
for (const line of lines.push(chunk))
|
|
555
|
-
const limit = detectLimitInLine(h, line);
|
|
556
|
-
if (limit !== null) {
|
|
557
|
-
await routeEvent({
|
|
558
|
-
kind: "limit",
|
|
559
|
-
code: limit,
|
|
560
|
-
message: `limit wall detected (${limit})`,
|
|
561
|
-
});
|
|
562
|
-
await pushFailure(failureFromLimit(limit));
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
const auth = detectAuthFailureInLine(h, line);
|
|
566
|
-
if (auth !== null) {
|
|
567
|
-
await pushFailure(failureFromAuth(auth));
|
|
568
|
-
await routeEvent({ kind: "error", message: `auth wall: ${auth}` });
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
stderrTail.push(line);
|
|
572
|
-
}
|
|
498
|
+
sup.rearm();
|
|
499
|
+
for (const line of lines.push(chunk)) await sup.stderrLine(line);
|
|
573
500
|
}
|
|
574
501
|
};
|
|
575
502
|
|
|
@@ -586,8 +513,6 @@ export const openSession = (
|
|
|
586
513
|
const finalize = (): void => {
|
|
587
514
|
if (finalized) return;
|
|
588
515
|
finalized = true;
|
|
589
|
-
// A stall killed the process on purpose, so the signal death it caused
|
|
590
|
-
// reports as "stall", not "killed".
|
|
591
516
|
const cause: ExitCause = stalled
|
|
592
517
|
? "stall"
|
|
593
518
|
: state.limitSeen
|
|
@@ -618,7 +543,7 @@ export const openSession = (
|
|
|
618
543
|
pendingLengths.length = 0;
|
|
619
544
|
}
|
|
620
545
|
endTurn({ kind: "done", exitCode, cause });
|
|
621
|
-
if (preTurnEvents.some((e) =>
|
|
546
|
+
if (preTurnEvents.some((e) => !DROPPABLE_KINDS.has(e.kind))) {
|
|
622
547
|
log({
|
|
623
548
|
event: "preturn_events_dropped",
|
|
624
549
|
sessionId: opts.sessionId,
|
|
@@ -646,8 +571,9 @@ export const openSession = (
|
|
|
646
571
|
turnSettled = null;
|
|
647
572
|
release();
|
|
648
573
|
}
|
|
649
|
-
// The process is gone:
|
|
650
|
-
|
|
574
|
+
// The process is gone: no signal, no pending escalation, no stall
|
|
575
|
+
// clock - a later fire would flip a finished turn to stall.
|
|
576
|
+
sup.settle();
|
|
651
577
|
// Pipes held open past exit (a grandchild) must not hang the session.
|
|
652
578
|
const pipeGrace = deps.clock.setTimeout(() => {
|
|
653
579
|
pipesOpenAtExit = true;
|
|
@@ -683,7 +609,7 @@ export const openSession = (
|
|
|
683
609
|
// stdin may already be gone - escalation below still bounds close.
|
|
684
610
|
}
|
|
685
611
|
// A child that does not exit on stdin EOF gets signalled after grace.
|
|
686
|
-
const closeGrace = deps.clock.setTimeout(() => escalate(), CLOSE_GRACE_MS);
|
|
612
|
+
const closeGrace = deps.clock.setTimeout(() => sup.escalate(), CLOSE_GRACE_MS);
|
|
687
613
|
await shutdown;
|
|
688
614
|
deps.clock.clearTimeout(closeGrace);
|
|
689
615
|
};
|