@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.
Files changed (87) hide show
  1. package/README.md +4 -2
  2. package/README.zh-CN.md +4 -2
  3. package/dist/acp-host/production-host.js +8843 -9012
  4. package/dist/archivist-record-pointer.js +38 -0
  5. package/dist/compliance-transport.js +7 -5
  6. package/dist/doctor-auditor.js +1 -0
  7. package/dist/engine-detour-usage.js +9 -36
  8. package/dist/gatekeeper-role.js +4 -18
  9. package/dist/headless-host/description.js +0 -209
  10. package/dist/headless-host/production-host.js +7828 -8247
  11. package/dist/host-contracts.js +8 -0
  12. package/dist/judge-role.js +14 -1
  13. package/dist/migrate-book-topology.js +215 -96
  14. package/dist/navigator-attendance.js +10 -9
  15. package/dist/navigator-invocation-identity.js +18 -0
  16. package/dist/packaged-role-registry.js +1 -9
  17. package/dist/pi/role-turn-host.js +46 -19
  18. package/dist/public-cli/auto-resume.js +29 -66
  19. package/dist/public-cli/cli.js +1 -0
  20. package/dist/public-cli/countersign-run.js +4 -18
  21. package/dist/public-cli/instruction-seat-run.js +258 -69
  22. package/dist/public-cli/main.js +7488 -7758
  23. package/dist/public-cli/post-admission.js +451 -349
  24. package/dist/public-cli/process-cancel.js +8 -0
  25. package/dist/public-cli/run-lifecycle.js +24 -94
  26. package/dist/public-cli/settlement.js +624 -1471
  27. package/dist/public-cli/terminal.js +34 -53
  28. package/dist/public-cli/turn-request.js +3 -0
  29. package/dist/public-role-summons.js +3 -8
  30. package/dist/receipt-delivery-policy.js +162 -28
  31. package/dist/run-terminal-artifacts.js +19 -14
  32. package/dist/serialize-thrown-value.js +7 -1
  33. package/dist/submission-errors.js +11 -1
  34. package/dist/submission-gate.js +27 -4
  35. package/dist/submission-ledger.js +21 -89
  36. package/package.json +1 -1
  37. package/scripts/build-package.mjs +0 -2
  38. package/src/acp-host/role-turn-host.ts +168 -75
  39. package/src/archivist-record-pointer.ts +48 -0
  40. package/src/compliance-transport.ts +13 -8
  41. package/src/doctor-auditor.ts +3 -0
  42. package/src/engine-detour-usage.ts +10 -39
  43. package/src/external-host-turn-loop.ts +100 -43
  44. package/src/gatekeeper-role.ts +7 -22
  45. package/src/headless-host/description.ts +4 -214
  46. package/src/headless-host/production-host.ts +2 -1
  47. package/src/headless-host/role-turn-host.ts +153 -186
  48. package/src/host-contracts.ts +20 -10
  49. package/src/judge-role.ts +24 -5
  50. package/src/navigator-attendance.ts +17 -12
  51. package/src/navigator-invocation-identity.ts +31 -0
  52. package/src/navigator-public-session.ts +1 -1
  53. package/src/navigator-session-contracts.ts +1 -1
  54. package/src/packaged-role-registry.ts +2 -10
  55. package/src/pi/adapter.ts +33 -11
  56. package/src/pi/role-turn-host.ts +49 -28
  57. package/src/prepared-role-turn.ts +2 -0
  58. package/src/public-cli/auto-resume.ts +27 -66
  59. package/src/public-cli/cli.ts +1 -0
  60. package/src/public-cli/countersign-run.ts +4 -22
  61. package/src/public-cli/instruction-seat-run.ts +307 -74
  62. package/src/public-cli/post-admission.ts +615 -459
  63. package/src/public-cli/process-cancel.ts +9 -0
  64. package/src/public-cli/run-lifecycle.ts +25 -126
  65. package/src/public-cli/settlement.ts +801 -1859
  66. package/src/public-cli/terminal.ts +42 -70
  67. package/src/public-cli/turn-request.ts +10 -1
  68. package/src/public-role-summons.ts +3 -8
  69. package/src/receipt-delivery-policy.ts +219 -28
  70. package/src/role-envelope.ts +21 -10
  71. package/src/role-runtime-dependencies.ts +1 -0
  72. package/src/role-runtime.ts +79 -102
  73. package/src/run-terminal-artifacts.ts +21 -17
  74. package/src/serialize-thrown-value.ts +7 -1
  75. package/src/submission-correctable-error.ts +5 -2
  76. package/src/submission-errors.ts +12 -1
  77. package/src/submission-gate.ts +34 -4
  78. package/src/submission-ledger.ts +30 -116
  79. package/src/worker-role.ts +19 -7
  80. package/src/worker-submission-gates.ts +29 -7
  81. package/dist/audit-escalation.js +0 -7
  82. package/dist/pi/known-failure.js +0 -51
  83. package/dist/typed-provider-http.js +0 -57
  84. package/dist/upstream-error-testimony.js +0 -19
  85. package/src/pi/known-failure.ts +0 -76
  86. package/src/typed-provider-http.ts +0 -89
  87. 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
- readonly onClose?: () => void;
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 failLine = (error: unknown): void => {
263
- if (settled) return;
264
- settled = true;
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
- failLine(error);
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
- let aborted = false;
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
- reject(stdinDeliveryError);
312
- return;
313
- }
314
- resolve({ code, stdout: resultStdout, stderr, timedOut });
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
- options.onClose?.();
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: { code: number | null; stderr: string; timedOut: boolean },
348
- knownFailure: NonNullable<RoleTurnResult["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
- knownFailure,
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
- /** Closed schema; omit for #959 navigator prose-exit seats. */
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
- // #959: navigator is a prose-exit seat — no closed output-schema.
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
- outputSchemaPath,
449
- `${JSON.stringify(closeJsonSchemaForCodex(prepared.jsonSchema), null, 2)}\n`,
450
- "utf8",
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: { code: number | null; stdout: string; stderr: string; timedOut: boolean };
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
- recordNativeSessionPointer({ host: config.hostName, sessionId: roundSessionId, cwd: request.cwd, sessionParent, home: request.home });
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
- if (spawned.timedOut) {
585
- return terminalFromSpawned(spawned, {
586
- cause: "timeout",
587
- identity: { name: "HeadlessTimeout", code: "timeout" },
588
- details: { sessionId },
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 or package persistence failure.
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
- // The host failure remains primary; retain persistence failure as
607
- // secondary typed evidence instead of replacing the terminal.
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
- if (observation.threadId === undefined || observation.threadId === "") {
640
- return terminalFromSpawned(spawned, {
641
- cause: "session",
642
- identity: { name: "HeadlessMissingThreadId", code: "missing-thread-id" },
643
- diagnostic: "codex exec emitted no thread.started thread_id",
644
- details: { sessionId, exitCode: spawned.code },
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
- await prepared.ingestStructuredOutput({ prose: observation.finalMessage });
680
- return { status: "delivered", stderr: spawned.stderr };
681
- }
682
- let receipt: unknown;
683
- try {
684
- receipt = JSON.parse(observation.finalMessage);
685
- } catch {
686
- return terminalFromSpawned(spawned, {
687
- cause: "output",
688
- identity: { name: "HeadlessEmptyOutput", code: "unparseable-final-message" },
689
- diagnostic: "codex final agent_message was not JSON",
690
- details: { sessionId, exitCode: spawned.code },
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
- // Bind the host-reported session id (authoritative for --resume).
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
- await config.sessionIdentity.bind(request.principal, sessionId);
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 (envelope.is_error === true || (typeof envelope.subtype === "string" && envelope.subtype.startsWith("error_"))) {
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.map(String).join("\n")
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 { status: "delivered", stderr: spawned.stderr };
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
- if (exitedSessionId !== undefined && exitedSessionId !== "") copyAndRecordHostDossier({
755
- host: config.hostName, sessionId: exitedSessionId, cwd: request.cwd,
756
- sessionDirectory: join(request.runDirectory, "session"), sessionParent,
757
- continuation: request.continuation,
758
- ...(request.model !== undefined ? { model: request.model } : {}),
759
- ...(request.home !== undefined ? { home: request.home } : {}),
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
- try {
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
  });
@@ -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 type ControlledFailureCause =
24
- | "activation"
25
- | "provider"
26
- | "session"
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;