@akagilnc/pi-workflow-roles 0.1.5382 → 0.1.5462
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 +4 -2
- package/README.zh-CN.md +4 -2
- package/dist/acp-host/production-host.js +8843 -9012
- package/dist/archivist-record-pointer.js +38 -0
- package/dist/compliance-transport.js +7 -5
- package/dist/doctor-auditor.js +1 -0
- package/dist/engine-detour-usage.js +9 -36
- package/dist/gatekeeper-role.js +4 -18
- package/dist/headless-host/description.js +0 -209
- package/dist/headless-host/production-host.js +7828 -8247
- package/dist/host-contracts.js +8 -0
- package/dist/judge-role.js +14 -1
- package/dist/migrate-book-topology.js +215 -96
- package/dist/navigator-attendance.js +10 -9
- package/dist/navigator-invocation-identity.js +18 -0
- package/dist/packaged-role-registry.js +1 -9
- package/dist/pi/role-turn-host.js +46 -19
- package/dist/public-cli/auto-resume.js +29 -66
- package/dist/public-cli/cli.js +1 -0
- package/dist/public-cli/countersign-run.js +4 -18
- package/dist/public-cli/instruction-seat-run.js +258 -69
- package/dist/public-cli/main.js +7488 -7758
- package/dist/public-cli/post-admission.js +451 -349
- package/dist/public-cli/process-cancel.js +8 -0
- package/dist/public-cli/run-lifecycle.js +24 -94
- package/dist/public-cli/settlement.js +624 -1471
- package/dist/public-cli/terminal.js +34 -53
- package/dist/public-cli/turn-request.js +3 -0
- package/dist/public-role-summons.js +3 -8
- package/dist/receipt-delivery-policy.js +162 -28
- package/dist/run-terminal-artifacts.js +19 -14
- package/dist/serialize-thrown-value.js +7 -1
- package/dist/submission-errors.js +11 -1
- package/dist/submission-gate.js +27 -4
- package/dist/submission-ledger.js +21 -89
- package/package.json +1 -1
- package/scripts/build-package.mjs +0 -2
- package/src/acp-host/role-turn-host.ts +168 -75
- package/src/archivist-record-pointer.ts +48 -0
- package/src/compliance-transport.ts +13 -8
- package/src/doctor-auditor.ts +3 -0
- package/src/engine-detour-usage.ts +10 -39
- package/src/external-host-turn-loop.ts +100 -43
- package/src/gatekeeper-role.ts +7 -22
- package/src/headless-host/description.ts +4 -214
- package/src/headless-host/production-host.ts +2 -1
- package/src/headless-host/role-turn-host.ts +153 -186
- package/src/host-contracts.ts +20 -10
- package/src/judge-role.ts +24 -5
- package/src/navigator-attendance.ts +17 -12
- package/src/navigator-invocation-identity.ts +31 -0
- package/src/navigator-public-session.ts +1 -1
- package/src/navigator-session-contracts.ts +1 -1
- package/src/packaged-role-registry.ts +2 -10
- package/src/pi/adapter.ts +33 -11
- package/src/pi/role-turn-host.ts +49 -28
- package/src/prepared-role-turn.ts +2 -0
- package/src/public-cli/auto-resume.ts +27 -66
- package/src/public-cli/cli.ts +1 -0
- package/src/public-cli/countersign-run.ts +4 -22
- package/src/public-cli/instruction-seat-run.ts +307 -74
- package/src/public-cli/post-admission.ts +615 -459
- package/src/public-cli/process-cancel.ts +9 -0
- package/src/public-cli/run-lifecycle.ts +25 -126
- package/src/public-cli/settlement.ts +801 -1859
- package/src/public-cli/terminal.ts +42 -70
- package/src/public-cli/turn-request.ts +10 -1
- package/src/public-role-summons.ts +3 -8
- package/src/receipt-delivery-policy.ts +219 -28
- package/src/role-envelope.ts +21 -10
- package/src/role-runtime-dependencies.ts +1 -0
- package/src/role-runtime.ts +79 -102
- package/src/run-terminal-artifacts.ts +21 -17
- package/src/serialize-thrown-value.ts +7 -1
- package/src/submission-correctable-error.ts +5 -2
- package/src/submission-errors.ts +12 -1
- package/src/submission-gate.ts +34 -4
- package/src/submission-ledger.ts +30 -116
- package/src/worker-role.ts +19 -7
- package/src/worker-submission-gates.ts +29 -7
- package/dist/audit-escalation.js +0 -7
- package/dist/pi/known-failure.js +0 -51
- package/dist/typed-provider-http.js +0 -57
- package/dist/upstream-error-testimony.js +0 -19
- package/src/pi/known-failure.ts +0 -76
- package/src/typed-provider-http.ts +0 -89
- package/src/upstream-error-testimony.ts +0 -45
|
@@ -13,11 +13,13 @@ import type { RoleTurnHost, RoleTurnRequest, RoleTurnResult } from "../host-cont
|
|
|
13
13
|
import {
|
|
14
14
|
createSerializedRoleTurnHost,
|
|
15
15
|
driveExternalRoleTurnRounds,
|
|
16
|
+
disposeExternalRoleTurn,
|
|
16
17
|
externalHostFailure as failure,
|
|
17
18
|
hostAbortedError,
|
|
18
19
|
isHostAbortedError,
|
|
19
|
-
withExternalHostCleanupFailure,
|
|
20
20
|
} from "../external-host-turn-loop.ts";
|
|
21
|
+
import { describeErrorIdentity } from "../public-cli/run-lifecycle.ts";
|
|
22
|
+
import { projectThrownFailureLeaf, retainPackageFault } from "../public-cli/settlement.ts";
|
|
21
23
|
import {
|
|
22
24
|
renderSystemPromptOverride,
|
|
23
25
|
resolveBoundHostSessionId,
|
|
@@ -30,7 +32,6 @@ import {
|
|
|
30
32
|
recordNativeSessionPointer,
|
|
31
33
|
} from "../host-session-record.ts";
|
|
32
34
|
import {
|
|
33
|
-
closeJsonSchemaForCodex,
|
|
34
35
|
codexTurnArgs,
|
|
35
36
|
headlessMcpConfigDocument,
|
|
36
37
|
headlessTurnArgs,
|
|
@@ -118,6 +119,7 @@ export type CodexExecTurnObservation = Readonly<{
|
|
|
118
119
|
finalMessage?: string;
|
|
119
120
|
/** Present only for terminal `turn.failed` (not recoverable `error` events). */
|
|
120
121
|
failureDiagnostic?: string;
|
|
122
|
+
failureEvent?: Readonly<Record<string, unknown>>;
|
|
121
123
|
turnCompleted: boolean;
|
|
122
124
|
}>;
|
|
123
125
|
|
|
@@ -128,6 +130,7 @@ function createCodexExecTurnObserver(): {
|
|
|
128
130
|
let threadId: string | undefined;
|
|
129
131
|
let finalMessage: string | undefined;
|
|
130
132
|
let failureDiagnostic: string | undefined;
|
|
133
|
+
let failureEvent: Readonly<Record<string, unknown>> | undefined;
|
|
131
134
|
let turnCompleted = false;
|
|
132
135
|
|
|
133
136
|
return {
|
|
@@ -143,8 +146,10 @@ function createCodexExecTurnObserver(): {
|
|
|
143
146
|
} else if (type === "turn.completed") {
|
|
144
147
|
turnCompleted = true;
|
|
145
148
|
failureDiagnostic = undefined;
|
|
149
|
+
failureEvent = undefined;
|
|
146
150
|
} else if (type === "turn.failed") {
|
|
147
151
|
turnCompleted = false;
|
|
152
|
+
failureEvent = value;
|
|
148
153
|
failureDiagnostic = formatCodexFailurePayload(value.error ?? value);
|
|
149
154
|
}
|
|
150
155
|
// Top-level `error` is non-terminal; exit/receipt handling remains downstream.
|
|
@@ -154,6 +159,7 @@ function createCodexExecTurnObserver(): {
|
|
|
154
159
|
...(threadId === undefined ? {} : { threadId }),
|
|
155
160
|
...(finalMessage === undefined ? {} : { finalMessage }),
|
|
156
161
|
...(failureDiagnostic === undefined ? {} : { failureDiagnostic }),
|
|
162
|
+
...(failureEvent === undefined ? {} : { failureEvent }),
|
|
157
163
|
turnCompleted,
|
|
158
164
|
};
|
|
159
165
|
},
|
|
@@ -227,8 +233,7 @@ function spawnHeadlessTurn(options: {
|
|
|
227
233
|
readonly stdin?: string;
|
|
228
234
|
/** Called for each complete stdout line as it arrives (live stream-json). */
|
|
229
235
|
readonly onStdoutLine?: (line: string) => void;
|
|
230
|
-
|
|
231
|
-
}): Promise<{ code: number | null; stdout: string; stderr: string; timedOut: boolean }> {
|
|
236
|
+
}): Promise<HeadlessSpawnResult> {
|
|
232
237
|
return new Promise((resolve, reject) => {
|
|
233
238
|
if (options.signal?.aborted) {
|
|
234
239
|
reject(hostAbortedError("headless host aborted"));
|
|
@@ -259,14 +264,9 @@ function spawnHeadlessTurn(options: {
|
|
|
259
264
|
let settled = false;
|
|
260
265
|
let timedOut = false;
|
|
261
266
|
let timer: NodeJS.Timeout | undefined;
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
266
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
267
|
-
try { child.kill("SIGTERM"); } catch { /* already exiting */ }
|
|
268
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
269
|
-
};
|
|
267
|
+
const packageErrors: unknown[] = [];
|
|
268
|
+
let hasSpawned = false;
|
|
269
|
+
child.once("spawn", () => { hasSpawned = true; });
|
|
270
270
|
const emitStdoutLine = (line: string): void => {
|
|
271
271
|
const candidate = resultCandidateText(line);
|
|
272
272
|
if (candidate !== undefined) resultStdout = candidate;
|
|
@@ -274,7 +274,7 @@ function spawnHeadlessTurn(options: {
|
|
|
274
274
|
try {
|
|
275
275
|
options.onStdoutLine(line);
|
|
276
276
|
} catch (error) {
|
|
277
|
-
|
|
277
|
+
packageErrors.push(error);
|
|
278
278
|
}
|
|
279
279
|
};
|
|
280
280
|
const flushStdoutLines = (chunk: string, final: boolean): void => {
|
|
@@ -292,30 +292,25 @@ function spawnHeadlessTurn(options: {
|
|
|
292
292
|
lineBuffer = "";
|
|
293
293
|
}
|
|
294
294
|
};
|
|
295
|
-
|
|
296
|
-
const settle = (code: number | null): void => {
|
|
295
|
+
const settle = (code: number | null, closeSignal: NodeJS.Signals | null): void => {
|
|
297
296
|
if (settled) return;
|
|
298
|
-
if (aborted) {
|
|
299
|
-
settled = true;
|
|
300
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
301
|
-
reject(hostAbortedError("headless host aborted"));
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
// Final flush first: onStdoutLine may failLine (reject + settled=true).
|
|
305
297
|
flushStdoutLines("", true);
|
|
306
298
|
if (settled) return;
|
|
307
299
|
settled = true;
|
|
308
300
|
if (timer !== undefined) clearTimeout(timer);
|
|
309
301
|
options.signal?.removeEventListener("abort", onAbort);
|
|
310
|
-
if (stdinDeliveryError !== undefined)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
302
|
+
if (stdinDeliveryError !== undefined) packageErrors.push(stdinDeliveryError);
|
|
303
|
+
resolve({
|
|
304
|
+
packageErrors,
|
|
305
|
+
code,
|
|
306
|
+
stdout: resultStdout,
|
|
307
|
+
stderr,
|
|
308
|
+
timedOut,
|
|
309
|
+
...(closeSignal === null ? {} : { signal: closeSignal }),
|
|
310
|
+
});
|
|
315
311
|
};
|
|
316
312
|
const onAbort = (): void => {
|
|
317
313
|
if (settled) return;
|
|
318
|
-
aborted = true;
|
|
319
314
|
child.kill("SIGTERM");
|
|
320
315
|
};
|
|
321
316
|
child.stdout.setEncoding("utf8").on("data", (chunk: string) => { flushStdoutLines(chunk, false); });
|
|
@@ -324,14 +319,17 @@ function spawnHeadlessTurn(options: {
|
|
|
324
319
|
});
|
|
325
320
|
child.on("error", (error) => {
|
|
326
321
|
if (settled) return;
|
|
322
|
+
if (hasSpawned) {
|
|
323
|
+
packageErrors.push(error);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
327
326
|
settled = true;
|
|
328
327
|
if (timer !== undefined) clearTimeout(timer);
|
|
329
328
|
options.signal?.removeEventListener("abort", onAbort);
|
|
330
329
|
reject(error);
|
|
331
330
|
});
|
|
332
|
-
child.on("close", (code) => {
|
|
333
|
-
|
|
334
|
-
settle(code);
|
|
331
|
+
child.on("close", (code, closeSignal) => {
|
|
332
|
+
settle(code, closeSignal);
|
|
335
333
|
});
|
|
336
334
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
337
335
|
if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
|
|
@@ -343,9 +341,34 @@ function spawnHeadlessTurn(options: {
|
|
|
343
341
|
});
|
|
344
342
|
}
|
|
345
343
|
|
|
344
|
+
type HeadlessSpawnResult = {
|
|
345
|
+
code: number | null;
|
|
346
|
+
signal?: string;
|
|
347
|
+
stdout: string;
|
|
348
|
+
stderr: string;
|
|
349
|
+
timedOut: boolean;
|
|
350
|
+
packageErrors: readonly unknown[];
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
function signalField(spawned: { signal?: string }): { signal?: string } {
|
|
354
|
+
return spawned.signal === undefined || spawned.signal.length === 0 ? {} : { signal: spawned.signal };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function deliveredFromSpawned(
|
|
358
|
+
spawned: HeadlessSpawnResult,
|
|
359
|
+
): { readonly status: "delivered"; readonly stderr: string; readonly code: number | null; readonly timedOut: boolean; readonly signal?: string } {
|
|
360
|
+
return {
|
|
361
|
+
status: "delivered",
|
|
362
|
+
stderr: spawned.stderr,
|
|
363
|
+
code: spawned.code,
|
|
364
|
+
timedOut: spawned.timedOut,
|
|
365
|
+
...signalField(spawned),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
346
369
|
function terminalFromSpawned(
|
|
347
|
-
spawned:
|
|
348
|
-
knownFailure
|
|
370
|
+
spawned: HeadlessSpawnResult,
|
|
371
|
+
knownFailure?: RoleTurnResult["knownFailure"],
|
|
349
372
|
): { readonly status: "terminal"; readonly result: RoleTurnResult } {
|
|
350
373
|
return {
|
|
351
374
|
status: "terminal",
|
|
@@ -353,7 +376,8 @@ function terminalFromSpawned(
|
|
|
353
376
|
code: spawned.code,
|
|
354
377
|
stderr: spawned.stderr,
|
|
355
378
|
timedOut: spawned.timedOut,
|
|
356
|
-
|
|
379
|
+
...signalField(spawned),
|
|
380
|
+
...(knownFailure === undefined ? {} : { knownFailure }),
|
|
357
381
|
},
|
|
358
382
|
};
|
|
359
383
|
}
|
|
@@ -361,7 +385,7 @@ function terminalFromSpawned(
|
|
|
361
385
|
function buildTurnArgs(options: {
|
|
362
386
|
readonly description: HeadlessHostDescription;
|
|
363
387
|
readonly systemPromptPath: string;
|
|
364
|
-
/**
|
|
388
|
+
/** Open schema; omit for #959 navigator prose-exit seats. Codex does not receive a narrowed copy. */
|
|
365
389
|
readonly jsonSchema?: Readonly<Record<string, unknown>>;
|
|
366
390
|
readonly mcpServers: readonly Readonly<Record<string, unknown>>[];
|
|
367
391
|
readonly mcpConfigPath?: string;
|
|
@@ -441,14 +465,16 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
441
465
|
let mcpConfigPath: string | undefined;
|
|
442
466
|
let outputSchemaPath: string | undefined;
|
|
443
467
|
if (codex) {
|
|
444
|
-
//
|
|
468
|
+
// Send the declaration unchanged. Any native rejection belongs to Codex;
|
|
469
|
+
// do not close the schema or substitute a different receipt outlet.
|
|
445
470
|
if (prepared.terminatingToolName !== NAVIGATOR_OUTPUT_TOOL_NAME) {
|
|
446
471
|
outputSchemaPath = join(request.runDirectory, "headless-output-schema.json");
|
|
447
|
-
await writeFile(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
472
|
+
await writeFile(outputSchemaPath, `${JSON.stringify(prepared.jsonSchema, null, 2)}\n`, "utf8");
|
|
473
|
+
await retainPackageFault({
|
|
474
|
+
runDirectory: request.runDirectory,
|
|
475
|
+
diagnostic:
|
|
476
|
+
"codex --output-schema cannot promise this open contract: native strict output requires all properties and additionalProperties:false; the declaration is sent unchanged",
|
|
477
|
+
});
|
|
452
478
|
}
|
|
453
479
|
} else {
|
|
454
480
|
mcpConfigPath = join(request.runDirectory, "headless-mcp-config.json");
|
|
@@ -508,11 +534,9 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
508
534
|
}) !== undefined;
|
|
509
535
|
}
|
|
510
536
|
|
|
511
|
-
let spawned:
|
|
512
|
-
let childExited = false;
|
|
537
|
+
let spawned: HeadlessSpawnResult;
|
|
513
538
|
try {
|
|
514
539
|
spawned = await spawnHeadlessTurn({
|
|
515
|
-
onClose() { childExited = true; },
|
|
516
540
|
binary: config.binary,
|
|
517
541
|
args,
|
|
518
542
|
cwd: request.cwd,
|
|
@@ -555,23 +579,15 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
555
579
|
const roundSessionId = (codex ? codexObserver?.result().threadId : undefined) ?? sessionId;
|
|
556
580
|
exitedSessionId = roundSessionId;
|
|
557
581
|
if (codex && roundSessionId !== undefined && roundSessionId !== "" && !pointerRecorded) {
|
|
558
|
-
|
|
582
|
+
try {
|
|
583
|
+
recordNativeSessionPointer({ host: config.hostName, sessionId: roundSessionId, cwd: request.cwd, sessionParent, home: request.home });
|
|
584
|
+
} catch (error) {
|
|
585
|
+
spawned.packageErrors = [...spawned.packageErrors, error];
|
|
586
|
+
}
|
|
559
587
|
}
|
|
560
588
|
} catch (error) {
|
|
561
|
-
if (childExited) exitedSessionId = (codex ? codexObserver?.result().threadId : undefined) ?? sessionId;
|
|
562
589
|
if (isHostAbortedError(error)) throw error;
|
|
563
590
|
const message = errorText(error);
|
|
564
|
-
const observedHostFailure = codexObserver?.result().failureDiagnostic;
|
|
565
|
-
if (observedHostFailure !== undefined) {
|
|
566
|
-
return {
|
|
567
|
-
status: "terminal",
|
|
568
|
-
result: failure("output", "HeadlessCliError", "codex-turn-failed", {
|
|
569
|
-
diagnostic: observedHostFailure,
|
|
570
|
-
sessionRecordDiagnostic: message,
|
|
571
|
-
sessionId,
|
|
572
|
-
}, observedHostFailure),
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
591
|
return {
|
|
576
592
|
status: "terminal",
|
|
577
593
|
result: failure("activation", "HeadlessSpawnFailure", "spawn-failed", {
|
|
@@ -581,158 +597,94 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
581
597
|
};
|
|
582
598
|
}
|
|
583
599
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
600
|
+
for (const error of spawned.packageErrors) {
|
|
601
|
+
await retainPackageFault({
|
|
602
|
+
runDirectory: request.runDirectory,
|
|
603
|
+
diagnostic: `headless transport handling failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
604
|
+
error,
|
|
589
605
|
});
|
|
590
606
|
}
|
|
591
607
|
|
|
608
|
+
try {
|
|
609
|
+
const noteBeside = async (error: unknown, what: string): Promise<void> => {
|
|
610
|
+
await retainPackageFault({
|
|
611
|
+
runDirectory: request.runDirectory,
|
|
612
|
+
diagnostic: `${what}: ${describeErrorIdentity(error)}`,
|
|
613
|
+
error,
|
|
614
|
+
});
|
|
615
|
+
};
|
|
616
|
+
|
|
592
617
|
if (codex) {
|
|
593
618
|
const observation = codexObserver!.result();
|
|
594
619
|
// #987 result 6 / 失败诚实: host-reported failure wins over a package
|
|
595
|
-
// missing-thread-id label
|
|
620
|
+
// missing-thread-id label. A bind error is a separate package fault.
|
|
596
621
|
if (observation.threadId !== undefined && observation.threadId !== "") {
|
|
597
622
|
sessionId = observation.threadId;
|
|
598
|
-
}
|
|
599
|
-
let sessionBindingDiagnostic: string | undefined;
|
|
600
|
-
const hostFailed = observation.failureDiagnostic !== undefined
|
|
601
|
-
|| (spawned.code !== 0 && spawned.code !== null);
|
|
602
|
-
if (hostFailed && observation.threadId !== undefined && observation.threadId !== "") {
|
|
603
623
|
try {
|
|
604
624
|
await config.sessionIdentity.bind(request.principal, observation.threadId);
|
|
605
625
|
} catch (error) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
sessionBindingDiagnostic = errorText(error);
|
|
626
|
+
if (observation.failureDiagnostic === undefined) throw error;
|
|
627
|
+
await noteBeside(error, "session bind failed beside host terminal");
|
|
609
628
|
}
|
|
610
629
|
}
|
|
611
630
|
|
|
612
631
|
if (observation.failureDiagnostic !== undefined) {
|
|
613
632
|
return terminalFromSpawned(spawned, {
|
|
614
|
-
cause: "output",
|
|
615
|
-
identity: { name: "HeadlessCliError", code: "codex-turn-failed" },
|
|
616
633
|
diagnostic: observation.failureDiagnostic,
|
|
617
|
-
details:
|
|
618
|
-
sessionId,
|
|
619
|
-
exitCode: spawned.code,
|
|
620
|
-
...(sessionBindingDiagnostic === undefined ? {} : { sessionBindingDiagnostic }),
|
|
621
|
-
},
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Non-zero exit without a parseable failure event still fails loud.
|
|
626
|
-
if (spawned.code !== 0 && spawned.code !== null) {
|
|
627
|
-
return terminalFromSpawned(spawned, {
|
|
628
|
-
cause: "output",
|
|
629
|
-
identity: { name: "HeadlessCliError", code: "codex-nonzero-exit" },
|
|
630
|
-
diagnostic: spawned.stderr.trim() || `codex exec exited ${String(spawned.code)}`,
|
|
631
|
-
details: {
|
|
632
|
-
sessionId,
|
|
633
|
-
exitCode: spawned.code,
|
|
634
|
-
...(sessionBindingDiagnostic === undefined ? {} : { sessionBindingDiagnostic }),
|
|
635
|
-
},
|
|
634
|
+
...(observation.failureEvent === undefined ? {} : { details: observation.failureEvent }),
|
|
636
635
|
});
|
|
637
636
|
}
|
|
638
637
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
sessionId = observation.threadId;
|
|
648
|
-
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
649
|
-
|
|
650
|
-
if (!observation.turnCompleted) {
|
|
651
|
-
return terminalFromSpawned(spawned, {
|
|
652
|
-
cause: "output",
|
|
653
|
-
identity: { name: "HeadlessCliError", code: "codex-missing-terminal-event" },
|
|
654
|
-
diagnostic: "codex exec exited without turn.completed",
|
|
655
|
-
details: { sessionId, exitCode: spawned.code },
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
if (observation.finalMessage === undefined) {
|
|
660
|
-
return terminalFromSpawned(spawned, {
|
|
661
|
-
cause: "output",
|
|
662
|
-
identity: { name: "HeadlessEmptyOutput", code: "empty-stdout" },
|
|
663
|
-
diagnostic: spawned.stderr.trim() || "codex exec produced no agent_message",
|
|
664
|
-
details: { sessionId, exitCode: spawned.code },
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// #959: navigator prose exit — final agent_message is presented as-is.
|
|
669
|
-
// Empty text is not a lawful accepted receipt (align pi no_receipt / loud empty).
|
|
670
|
-
if (prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME) {
|
|
671
|
-
if (observation.finalMessage.trim() === "") {
|
|
672
|
-
return terminalFromSpawned(spawned, {
|
|
673
|
-
cause: "output",
|
|
674
|
-
identity: { name: "HeadlessEmptyOutput", code: "empty-stdout" },
|
|
675
|
-
diagnostic: spawned.stderr.trim() || "codex exec produced empty agent_message",
|
|
676
|
-
details: { sessionId, exitCode: spawned.code },
|
|
677
|
-
});
|
|
638
|
+
const navigator = prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME;
|
|
639
|
+
const deliverReply = async (): Promise<ReturnType<typeof deliveredFromSpawned> | undefined> => {
|
|
640
|
+
if (!observation.turnCompleted || observation.finalMessage === undefined) return undefined;
|
|
641
|
+
if (navigator) {
|
|
642
|
+
if (observation.finalMessage.trim() === "") return undefined;
|
|
643
|
+
await prepared.ingestStructuredOutput({ prose: observation.finalMessage });
|
|
644
|
+
return deliveredFromSpawned(spawned);
|
|
678
645
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
await prepared.ingestStructuredOutput(receipt);
|
|
694
|
-
return { status: "delivered", stderr: spawned.stderr };
|
|
646
|
+
let receipt: unknown;
|
|
647
|
+
try {
|
|
648
|
+
receipt = JSON.parse(observation.finalMessage);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
await noteBeside(error, "native receipt JSON could not be read");
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
await prepared.ingestStructuredOutput(receipt);
|
|
654
|
+
return deliveredFromSpawned(spawned);
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Absence of a reply is not a host-stated reason for failure.
|
|
658
|
+
// Delivery and exit facts are independent on every exit path.
|
|
659
|
+
return await deliverReply() ?? terminalFromSpawned(spawned);
|
|
695
660
|
}
|
|
696
661
|
|
|
697
662
|
// Claude print-mode path.
|
|
698
663
|
const envelope = parseHeadlessCliStdout(spawned.stdout);
|
|
699
|
-
if (envelope === undefined)
|
|
700
|
-
return terminalFromSpawned(spawned, {
|
|
701
|
-
cause: "output",
|
|
702
|
-
identity: { name: "HeadlessEmptyOutput", code: "empty-stdout" },
|
|
703
|
-
diagnostic: spawned.stderr.length > 0 ? spawned.stderr : "headless CLI produced no parseable result",
|
|
704
|
-
details: { sessionId, exitCode: spawned.code },
|
|
705
|
-
});
|
|
706
|
-
}
|
|
664
|
+
if (envelope === undefined) return terminalFromSpawned(spawned);
|
|
707
665
|
|
|
708
|
-
|
|
666
|
+
const hostReportedFailure = envelope.is_error === true
|
|
667
|
+
|| (typeof envelope.subtype === "string" && envelope.subtype.startsWith("error_"));
|
|
668
|
+
// Required binding fails a clean result; an actual host error stays primary.
|
|
709
669
|
if (typeof envelope.session_id === "string" && envelope.session_id !== "") {
|
|
710
670
|
sessionId = envelope.session_id;
|
|
711
|
-
|
|
671
|
+
try {
|
|
672
|
+
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
if (!hostReportedFailure) throw error;
|
|
675
|
+
await noteBeside(error, "session bind failed beside host terminal");
|
|
676
|
+
}
|
|
712
677
|
}
|
|
713
678
|
|
|
714
|
-
if (
|
|
715
|
-
const errorCode =
|
|
716
|
-
typeof envelope.subtype === "string" && envelope.subtype.startsWith("error_")
|
|
717
|
-
? envelope.subtype
|
|
718
|
-
: envelope.is_error === true
|
|
719
|
-
? "is_error"
|
|
720
|
-
: "cli-error";
|
|
679
|
+
if (hostReportedFailure) {
|
|
721
680
|
const diagnostic = typeof envelope.result === "string"
|
|
722
681
|
? envelope.result
|
|
723
682
|
: Array.isArray(envelope.errors)
|
|
724
|
-
? envelope.errors
|
|
683
|
+
? JSON.stringify(envelope.errors)
|
|
725
684
|
: spawned.stderr.length > 0 ? spawned.stderr : "headless CLI reported is_error";
|
|
726
685
|
return terminalFromSpawned(spawned, {
|
|
727
|
-
cause: "output",
|
|
728
|
-
identity: { name: "HeadlessCliError", code: errorCode },
|
|
729
686
|
diagnostic,
|
|
730
|
-
details:
|
|
731
|
-
sessionId,
|
|
732
|
-
subtype: envelope.subtype,
|
|
733
|
-
errors: envelope.errors,
|
|
734
|
-
exitCode: spawned.code,
|
|
735
|
-
},
|
|
687
|
+
details: envelope,
|
|
736
688
|
});
|
|
737
689
|
}
|
|
738
690
|
|
|
@@ -747,25 +699,40 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
747
699
|
// when structured_output is absent (json-schema omitted for navigator).
|
|
748
700
|
await prepared.ingestStructuredOutput({ prose: envelope.result });
|
|
749
701
|
}
|
|
750
|
-
return
|
|
702
|
+
return deliveredFromSpawned(spawned);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
await retainPackageFault({
|
|
705
|
+
runDirectory: request.runDirectory,
|
|
706
|
+
diagnostic: `required turn handling failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
707
|
+
error,
|
|
708
|
+
});
|
|
709
|
+
return terminalFromSpawned(spawned,
|
|
710
|
+
spawned.code === 0 && !spawned.timedOut && spawned.signal === undefined
|
|
711
|
+
? projectThrownFailureLeaf(error)
|
|
712
|
+
: undefined);
|
|
713
|
+
}
|
|
751
714
|
},
|
|
752
715
|
});
|
|
753
716
|
} finally {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
717
|
+
try {
|
|
718
|
+
if (exitedSessionId !== undefined && exitedSessionId !== "") copyAndRecordHostDossier({
|
|
719
|
+
host: config.hostName, sessionId: exitedSessionId, cwd: request.cwd,
|
|
720
|
+
sessionDirectory: join(request.runDirectory, "session"), sessionParent,
|
|
721
|
+
continuation: request.continuation,
|
|
722
|
+
...(request.model !== undefined ? { model: request.model } : {}),
|
|
723
|
+
...(request.home !== undefined ? { home: request.home } : {}),
|
|
724
|
+
});
|
|
725
|
+
} catch (error) {
|
|
726
|
+
await retainPackageFault({
|
|
727
|
+
runDirectory: request.runDirectory,
|
|
728
|
+
diagnostic: `host dossier copy failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
729
|
+
error,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
761
732
|
}
|
|
762
733
|
}
|
|
763
734
|
} finally {
|
|
764
|
-
|
|
765
|
-
await prepared.dispose?.();
|
|
766
|
-
} catch (cleanupError) {
|
|
767
|
-
outcome = withExternalHostCleanupFailure(outcome, cleanupError, "HeadlessDisposeFailure");
|
|
768
|
-
}
|
|
735
|
+
outcome = await disposeExternalRoleTurn(prepared, request, outcome);
|
|
769
736
|
}
|
|
770
737
|
return outcome;
|
|
771
738
|
});
|
package/src/host-contracts.ts
CHANGED
|
@@ -20,12 +20,10 @@ export type DurablePrincipal = object & { readonly __durablePrincipal?: never };
|
|
|
20
20
|
* Closed set of typed facts only — never a fabricated "could not classify" label (#881).
|
|
21
21
|
* When no typed confirmation exists, omit cause and keep the original diagnostic / error pointer.
|
|
22
22
|
*/
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
| "output"
|
|
28
|
-
| "timeout";
|
|
23
|
+
export const CONTROLLED_FAILURE_CAUSES = [
|
|
24
|
+
"activation", "provider", "session", "output", "timeout",
|
|
25
|
+
] as const;
|
|
26
|
+
export type ControlledFailureCause = (typeof CONTROLLED_FAILURE_CAUSES)[number];
|
|
29
27
|
|
|
30
28
|
/** Production-owned typed failure carried on a resolved turn result. */
|
|
31
29
|
export type RoleTurnKnownFailure = {
|
|
@@ -198,13 +196,29 @@ export type RoleTurnRequest = {
|
|
|
198
196
|
readonly host?: string;
|
|
199
197
|
/** Station child role run (#840): omit automatic navigator attendance. */
|
|
200
198
|
readonly stationChild?: boolean;
|
|
199
|
+
/**
|
|
200
|
+
* #1132: the effective delivery-request ceiling resolved once by the caller
|
|
201
|
+
* from the single configured `autoResumeLimit` value and projected here, so
|
|
202
|
+
* the AK execution seam and the host adapter's own re-ask loop share one
|
|
203
|
+
* number. Absent = package default. Never re-read from disk downstream.
|
|
204
|
+
*/
|
|
205
|
+
readonly deliveryRequestLimit?: number;
|
|
201
206
|
};
|
|
202
207
|
|
|
203
208
|
/** Turn result — only fields upper layers currently consume. */
|
|
204
209
|
export type RoleTurnResult = {
|
|
210
|
+
/**
|
|
211
|
+
* The child's exit code, or null when it was killed by a signal — Node's
|
|
212
|
+
* native close contract. A null code is a non-normal exit, never a success.
|
|
213
|
+
*/
|
|
205
214
|
readonly code: number | null;
|
|
206
215
|
readonly stderr: string;
|
|
207
216
|
readonly timedOut: boolean;
|
|
217
|
+
/**
|
|
218
|
+
* The signal that killed the child, when one did. Carried so a signal death
|
|
219
|
+
* reports the real cause instead of settling as a successful no_receipt.
|
|
220
|
+
*/
|
|
221
|
+
readonly signal?: string;
|
|
208
222
|
readonly knownFailure?: RoleTurnKnownFailure;
|
|
209
223
|
};
|
|
210
224
|
|
|
@@ -275,7 +289,6 @@ type InputEvent = { text: string; images?: Array<{ type: "image"; data: string;
|
|
|
275
289
|
type ToolCallEvent = { toolName: string; toolCallId: string; input: Record<string, unknown> };
|
|
276
290
|
type ToolResultEvent = { toolName: string; toolCallId: string; isError: boolean; content: HostToolResult["content"]; details: unknown };
|
|
277
291
|
type SessionStartEvent = { reason: string };
|
|
278
|
-
type ProviderResponseEvent = { status?: number };
|
|
279
292
|
type AgentEndEvent = { messages: readonly HostEventMessage[] };
|
|
280
293
|
/** Typed closure of exactly one assistant turn; calls come from the host event, not transcript inspection. */
|
|
281
294
|
type TurnEndEvent = { readonly turnIndex: number; readonly calls: readonly ToolExecutionEvent[] };
|
|
@@ -290,7 +303,6 @@ type HostEventMap = {
|
|
|
290
303
|
tool_result: ToolResultEvent;
|
|
291
304
|
session_start: SessionStartEvent;
|
|
292
305
|
session_shutdown: Record<never, never>;
|
|
293
|
-
after_provider_response: ProviderResponseEvent;
|
|
294
306
|
agent_end: AgentEndEvent;
|
|
295
307
|
turn_end: TurnEndEvent;
|
|
296
308
|
agent_settled: Record<never, never>;
|
|
@@ -313,7 +325,6 @@ type HostEventResultMap = {
|
|
|
313
325
|
tool_result: { content?: HostToolResult["content"]; details?: unknown; isError?: boolean };
|
|
314
326
|
session_start: void;
|
|
315
327
|
session_shutdown: void;
|
|
316
|
-
after_provider_response: void;
|
|
317
328
|
agent_end: void;
|
|
318
329
|
turn_end: void;
|
|
319
330
|
agent_settled: void;
|
|
@@ -360,7 +371,6 @@ export interface RoleHost {
|
|
|
360
371
|
on(event: "tool_result", handler: HostEventHandler<"tool_result">): void;
|
|
361
372
|
on(event: "session_start", handler: HostEventHandler<"session_start">): void;
|
|
362
373
|
on(event: "session_shutdown", handler: HostEventHandler<"session_shutdown">): void;
|
|
363
|
-
on(event: "after_provider_response", handler: HostEventHandler<"after_provider_response">): void;
|
|
364
374
|
on(event: "agent_end", handler: HostEventHandler<"agent_end">): void;
|
|
365
375
|
on(event: "turn_end", handler: HostEventHandler<"turn_end">): void;
|
|
366
376
|
on(event: "agent_settled", handler: HostEventHandler<"agent_settled">): void;
|