@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
|
@@ -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. */
|
|
@@ -90,6 +97,11 @@ export interface OpenSessionOptions {
|
|
|
90
97
|
readonly questions?: QuestionMode;
|
|
91
98
|
/** Provider selector (pi); refused on a harness without one. */
|
|
92
99
|
readonly provider?: string;
|
|
100
|
+
/** Effort level for the session spawn, validated per harness/model the
|
|
101
|
+
* way a one-shot launch validates it. Undefined means the caller made
|
|
102
|
+
* no effort decision: the session runs at the harness's own default
|
|
103
|
+
* effort (sessions carry no profile entry). */
|
|
104
|
+
readonly effort?: string;
|
|
93
105
|
/** True when resuming an existing conversation; controls which flag
|
|
94
106
|
* (resumeFlag vs idFlag) buildSessionArgv renders. */
|
|
95
107
|
readonly isResume?: boolean;
|
|
@@ -120,6 +132,7 @@ export const openSession = (
|
|
|
120
132
|
const argv = buildSessionArgv(h, {
|
|
121
133
|
sessionId: opts.sessionId,
|
|
122
134
|
...(opts.model !== undefined ? { model: opts.model } : {}),
|
|
135
|
+
...(opts.effort !== undefined ? { effort: opts.effort } : {}),
|
|
123
136
|
...(opts.provider !== undefined ? { provider: opts.provider } : {}),
|
|
124
137
|
...(opts.isResume !== undefined ? { isResume: opts.isResume } : {}),
|
|
125
138
|
});
|
|
@@ -191,40 +204,26 @@ export const openSession = (
|
|
|
191
204
|
let turnLimitSeen = false;
|
|
192
205
|
let turnFailures: FailureSummary[] = [];
|
|
193
206
|
let pumpError: unknown = null;
|
|
194
|
-
// issue #44: the active turn
|
|
195
|
-
//
|
|
196
|
-
let lastAssistantText: string | null = null;
|
|
207
|
+
// issue #44: whether the active turn ended by asking, and what its close
|
|
208
|
+
// detected.
|
|
197
209
|
let turnAsked = false;
|
|
198
210
|
let turnEscalationDetection: EscalationDetection = "none";
|
|
199
211
|
let identityAnnounced = false;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (!dead) deps.signal(proc, sig);
|
|
203
|
-
};
|
|
204
|
-
const escalate = (): void => {
|
|
205
|
-
safeSignal("SIGTERM");
|
|
206
|
-
deps.clock.setTimeout(() => safeSignal("SIGKILL"), KILL_GRACE_MS);
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
// Per-turn inactivity budget. A session turn can hang with the process
|
|
210
|
-
// alive and the pipes open, which no exit code reports; without this the
|
|
211
|
-
// consumer waits forever. Armed at turn start, rearmed on any output
|
|
212
|
-
// chunk, disarmed at turn end and at exit - the same discipline
|
|
213
|
-
// streamTurn uses, scoped to the turn rather than the process.
|
|
214
|
-
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".
|
|
215
214
|
let stalled = false;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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: () => {
|
|
228
227
|
stalled = true;
|
|
229
228
|
log({
|
|
230
229
|
event: "stall",
|
|
@@ -238,9 +237,17 @@ export const openSession = (
|
|
|
238
237
|
// the exit path then closes the session with the same cause.
|
|
239
238
|
void pushFailure(failureFromTransport("stalled: inactivity"));
|
|
240
239
|
endTurn({ kind: "done", exitCode: null, cause: "stall" });
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
});
|
|
244
251
|
|
|
245
252
|
const writeUser = (text: string): boolean => {
|
|
246
253
|
try {
|
|
@@ -255,7 +262,7 @@ export const openSession = (
|
|
|
255
262
|
// very signal that stops it. The exit path then finalizes as usual.
|
|
256
263
|
void routeEvent({ kind: "error", message: "send failed: session stdin is gone" });
|
|
257
264
|
closing = true;
|
|
258
|
-
escalate();
|
|
265
|
+
sup.escalate();
|
|
259
266
|
return false;
|
|
260
267
|
}
|
|
261
268
|
};
|
|
@@ -271,7 +278,6 @@ export const openSession = (
|
|
|
271
278
|
turnFailures = [];
|
|
272
279
|
turnAsked = false;
|
|
273
280
|
turnEscalationDetection = "none";
|
|
274
|
-
lastAssistantText = null;
|
|
275
281
|
activeTurn = new AsyncChannel<HarnessEvent>();
|
|
276
282
|
activeTurnId = `${opts.sessionId}:turn-${++turnCounter}`;
|
|
277
283
|
// Tag the turn with the id of the send that opened it, so the consumer
|
|
@@ -285,58 +291,21 @@ export const openSession = (
|
|
|
285
291
|
activeTurn.push(held);
|
|
286
292
|
}
|
|
287
293
|
turnsChannel.push(activeTurn as SessionTurn);
|
|
288
|
-
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
/** issue #44: at a turn boundary, scan the last assistant message for
|
|
292
|
-
* the hcn-question block - same structured-first discipline and
|
|
293
|
-
* last-message rule as streamTurn. The question event lands in the
|
|
294
|
-
* turn stream right before its done; a malformed block surfaces as an
|
|
295
|
-
* error event, never a silent no-op. */
|
|
296
|
-
const emitQuestionIfAsked = (): void => {
|
|
297
|
-
if (questionMode !== "ask" || lastAssistantText === null) {
|
|
298
|
-
turnEscalationDetection = "none";
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
const detection = detectQuestionBlock(lastAssistantText);
|
|
302
|
-
if (detection === null) {
|
|
303
|
-
turnEscalationDetection = "none";
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if ("malformed" in detection) {
|
|
307
|
-
turnEscalationDetection = "malformed";
|
|
308
|
-
activeTurn?.push({ kind: "error", message: detection.malformed });
|
|
309
|
-
const failure = failureFromTask(`malformed hcn-question block: ${detection.malformed}`);
|
|
310
|
-
turnFailures.push(failure);
|
|
311
|
-
void activeTurn?.push({ kind: "failure", ...failure });
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
turnEscalationDetection = "block";
|
|
315
|
-
turnAsked = true;
|
|
316
|
-
log({
|
|
317
|
-
event: "question",
|
|
318
|
-
sessionId: opts.sessionId,
|
|
319
|
-
turnId: activeTurnId,
|
|
320
|
-
harness: h.name,
|
|
321
|
-
options: detection.block.options.length,
|
|
322
|
-
});
|
|
323
|
-
activeTurn?.push({
|
|
324
|
-
kind: "question",
|
|
325
|
-
question: detection.block.question,
|
|
326
|
-
options: detection.block.options,
|
|
327
|
-
...(detection.block.recommended !== undefined
|
|
328
|
-
? { recommended: detection.block.recommended }
|
|
329
|
-
: {}),
|
|
330
|
-
});
|
|
294
|
+
sup.beginTurn();
|
|
331
295
|
};
|
|
332
296
|
|
|
333
297
|
const endTurn = (done: Omit<HarnessEvent & { kind: "done" }, "escalation">): void => {
|
|
334
298
|
if (activeTurn === null) return;
|
|
335
|
-
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
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;
|
|
340
309
|
if (turnAsked && done.cause === "clean") done = { ...done, cause: "awaiting-input" };
|
|
341
310
|
// RFC-01 every turn end carries the escalation record
|
|
342
311
|
let fullDone: HarnessEvent & { kind: "done" } = {
|
|
@@ -379,9 +348,7 @@ export const openSession = (
|
|
|
379
348
|
state.limitSeen = true;
|
|
380
349
|
turnLimitSeen = true;
|
|
381
350
|
}
|
|
382
|
-
|
|
383
|
-
lastAssistantText = event.text;
|
|
384
|
-
}
|
|
351
|
+
sup.noteEvent(event);
|
|
385
352
|
if (activeTurn !== null) {
|
|
386
353
|
// Awaited by the pumps: past the channel's high water mark this
|
|
387
354
|
// blocks the pump, so OS pipe backpressure reaches the child.
|
|
@@ -391,9 +358,7 @@ export const openSession = (
|
|
|
391
358
|
// droppable events go first, then oldest.
|
|
392
359
|
preTurnEvents.push(event);
|
|
393
360
|
if (preTurnEvents.length > PRETURN_MAX) {
|
|
394
|
-
const droppableAt = preTurnEvents.findIndex(
|
|
395
|
-
(e) => e.kind === "token" || e.kind === "progress" || e.kind === "context",
|
|
396
|
-
);
|
|
361
|
+
const droppableAt = preTurnEvents.findIndex((e) => DROPPABLE_KINDS.has(e.kind));
|
|
397
362
|
preTurnEvents.splice(droppableAt === -1 ? 0 : droppableAt, 1);
|
|
398
363
|
}
|
|
399
364
|
return Promise.resolve();
|
|
@@ -404,35 +369,63 @@ export const openSession = (
|
|
|
404
369
|
return routeEvent({ kind: "failure", ...f });
|
|
405
370
|
};
|
|
406
371
|
|
|
407
|
-
/** A decoded event other than a failure: a
|
|
408
|
-
*
|
|
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. */
|
|
409
375
|
const routeDecoded = async (event: HarnessEvent): Promise<void> => {
|
|
376
|
+
if (event.kind === "failure") {
|
|
377
|
+
await pushFailure(summaryOf(event));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
410
380
|
await routeEvent(event);
|
|
381
|
+
if (event.kind === "limit") await pushFailure(failureFromLimit(event.code));
|
|
411
382
|
if (event.kind === "error" && event.terminal === true) {
|
|
412
383
|
await pushFailure(failureFromTerminalError(h, event.message));
|
|
413
384
|
}
|
|
414
385
|
};
|
|
415
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
|
+
|
|
416
421
|
const pumpStdout = async (): Promise<void> => {
|
|
417
422
|
const lines = new LineBuffer();
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
for (const [key, expected] of Object.entries(spec)) {
|
|
423
|
-
if (record[key] !== expected) return false;
|
|
424
|
-
}
|
|
425
|
-
return true;
|
|
426
|
-
};
|
|
427
|
-
// issue #44: pi rpc is identity-silent at startup; the probe round
|
|
428
|
-
// trip is the only way to read the id (spike fixtures). The response
|
|
429
|
-
// echoes our marker id, so it cannot be confused with a user-visible
|
|
430
|
-
// get_state response.
|
|
431
|
-
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) {
|
|
432
427
|
try {
|
|
433
|
-
stdin.write(
|
|
434
|
-
`${JSON.stringify({ id: "hcn-identity", type: sessionInputMode.identityProbe.command })}\n`,
|
|
435
|
-
);
|
|
428
|
+
stdin.write(probe);
|
|
436
429
|
} catch {
|
|
437
430
|
// stdin already gone; the exited handler will surface the death.
|
|
438
431
|
}
|
|
@@ -442,99 +435,57 @@ export const openSession = (
|
|
|
442
435
|
try {
|
|
443
436
|
parsed = JSON.parse(line) as Record<string, unknown>;
|
|
444
437
|
} catch {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
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);
|
|
449
442
|
}
|
|
450
443
|
return;
|
|
451
444
|
}
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// identity; opts.sessionId stays the caller-side handle.
|
|
474
|
-
if (!identityAnnounced) {
|
|
475
|
-
identityAnnounced = true;
|
|
476
|
-
state.lastSeenId = announced;
|
|
477
|
-
await routeEvent({
|
|
478
|
-
kind: "identity",
|
|
479
|
-
sessionId: announced,
|
|
480
|
-
authority: "harness-minted",
|
|
481
|
-
capabilities: capabilitiesOf(h, opts.model ?? "", "headless-session"),
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
} else if (announced === opts.sessionId) {
|
|
485
|
-
if (!identityAnnounced) {
|
|
486
|
-
identityAnnounced = true;
|
|
487
|
-
await routeEvent({
|
|
488
|
-
kind: "identity",
|
|
489
|
-
sessionId: announced,
|
|
490
|
-
authority: "caller-assigned",
|
|
491
|
-
capabilities: capabilitiesOf(h, opts.model ?? "", "headless-session"),
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
} else {
|
|
495
|
-
await routeEvent({
|
|
496
|
-
kind: "error",
|
|
497
|
-
message: `identity rotated: session announced ${JSON.stringify(announced)} but ${opts.sessionId} was requested`,
|
|
498
|
-
});
|
|
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);
|
|
499
466
|
}
|
|
467
|
+
if (record.isError) resultError = true;
|
|
468
|
+
endTurn({
|
|
469
|
+
kind: "done",
|
|
470
|
+
exitCode: null,
|
|
471
|
+
cause: turnLimitSeen ? "limit" : resultError ? "crash" : "clean",
|
|
472
|
+
});
|
|
500
473
|
return;
|
|
501
474
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
475
|
+
case "content": {
|
|
476
|
+
for (const event of decodeParsed(h, parsed, state, opts.model ?? "")) {
|
|
477
|
+
await routeDecoded(event);
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
507
480
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// session_id on result - a rotation announced there must not be
|
|
512
|
-
// missed).
|
|
513
|
-
const events = decodeParsed(h, parsed, state, opts.model ?? "");
|
|
514
|
-
const isTurnEnd = sessionInputMode !== null && matches(parsed, sessionInputMode.turnEnd);
|
|
515
|
-
if (isTurnEnd) {
|
|
516
|
-
// decodeParsed already surfaces the is_error case as an error event
|
|
517
|
-
// (content.ts claude reader); routing the events is enough - we only
|
|
518
|
-
// still track resultError here to classify the done cause.
|
|
519
|
-
for (const event of events) {
|
|
520
|
-
if (event.kind === "failure") await pushFailure(summaryOf(event));
|
|
521
|
-
else await routeDecoded(event);
|
|
481
|
+
default: {
|
|
482
|
+
const exhaustive: never = record;
|
|
483
|
+
return exhaustive;
|
|
522
484
|
}
|
|
523
|
-
if (parsed.is_error === true) resultError = true;
|
|
524
|
-
endTurn({
|
|
525
|
-
kind: "done",
|
|
526
|
-
exitCode: null,
|
|
527
|
-
cause: turnLimitSeen ? "limit" : resultError ? "crash" : "clean",
|
|
528
|
-
});
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
for (const event of events) {
|
|
532
|
-
if (event.kind === "failure") await pushFailure(summaryOf(event));
|
|
533
|
-
else await routeDecoded(event);
|
|
534
485
|
}
|
|
535
486
|
};
|
|
536
487
|
for await (const chunk of proc.stdout) {
|
|
537
|
-
|
|
488
|
+
sup.rearm();
|
|
538
489
|
for (const line of lines.push(chunk)) await handleLine(line);
|
|
539
490
|
}
|
|
540
491
|
const rest = lines.flush();
|
|
@@ -544,26 +495,8 @@ export const openSession = (
|
|
|
544
495
|
const pumpStderr = async (): Promise<void> => {
|
|
545
496
|
const lines = new LineBuffer();
|
|
546
497
|
for await (const chunk of proc.stderr) {
|
|
547
|
-
|
|
548
|
-
for (const line of lines.push(chunk))
|
|
549
|
-
const limit = detectLimitInLine(h, line);
|
|
550
|
-
if (limit !== null) {
|
|
551
|
-
await routeEvent({
|
|
552
|
-
kind: "limit",
|
|
553
|
-
code: limit,
|
|
554
|
-
message: `limit wall detected (${limit})`,
|
|
555
|
-
});
|
|
556
|
-
await pushFailure(failureFromLimit(limit));
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
const auth = detectAuthFailureInLine(h, line);
|
|
560
|
-
if (auth !== null) {
|
|
561
|
-
await pushFailure(failureFromAuth(auth));
|
|
562
|
-
await routeEvent({ kind: "error", message: `auth wall: ${auth}` });
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
stderrTail.push(line);
|
|
566
|
-
}
|
|
498
|
+
sup.rearm();
|
|
499
|
+
for (const line of lines.push(chunk)) await sup.stderrLine(line);
|
|
567
500
|
}
|
|
568
501
|
};
|
|
569
502
|
|
|
@@ -580,8 +513,6 @@ export const openSession = (
|
|
|
580
513
|
const finalize = (): void => {
|
|
581
514
|
if (finalized) return;
|
|
582
515
|
finalized = true;
|
|
583
|
-
// A stall killed the process on purpose, so the signal death it caused
|
|
584
|
-
// reports as "stall", not "killed".
|
|
585
516
|
const cause: ExitCause = stalled
|
|
586
517
|
? "stall"
|
|
587
518
|
: state.limitSeen
|
|
@@ -612,7 +543,7 @@ export const openSession = (
|
|
|
612
543
|
pendingLengths.length = 0;
|
|
613
544
|
}
|
|
614
545
|
endTurn({ kind: "done", exitCode, cause });
|
|
615
|
-
if (preTurnEvents.some((e) =>
|
|
546
|
+
if (preTurnEvents.some((e) => !DROPPABLE_KINDS.has(e.kind))) {
|
|
616
547
|
log({
|
|
617
548
|
event: "preturn_events_dropped",
|
|
618
549
|
sessionId: opts.sessionId,
|
|
@@ -640,8 +571,9 @@ export const openSession = (
|
|
|
640
571
|
turnSettled = null;
|
|
641
572
|
release();
|
|
642
573
|
}
|
|
643
|
-
// The process is gone:
|
|
644
|
-
|
|
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();
|
|
645
577
|
// Pipes held open past exit (a grandchild) must not hang the session.
|
|
646
578
|
const pipeGrace = deps.clock.setTimeout(() => {
|
|
647
579
|
pipesOpenAtExit = true;
|
|
@@ -677,7 +609,7 @@ export const openSession = (
|
|
|
677
609
|
// stdin may already be gone - escalation below still bounds close.
|
|
678
610
|
}
|
|
679
611
|
// A child that does not exit on stdin EOF gets signalled after grace.
|
|
680
|
-
const closeGrace = deps.clock.setTimeout(() => escalate(), CLOSE_GRACE_MS);
|
|
612
|
+
const closeGrace = deps.clock.setTimeout(() => sup.escalate(), CLOSE_GRACE_MS);
|
|
681
613
|
await shutdown;
|
|
682
614
|
deps.clock.clearTimeout(closeGrace);
|
|
683
615
|
};
|