@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
@@ -30,6 +30,10 @@ import { readDeclaredTicketNumber } from "../run-ticket-number.ts";
30
30
  import { pathContainedIn } from "../activation-ledger-topology.ts";
31
31
  import { pickEngineAxis } from "../package-resources/engine-material.ts";
32
32
  import { readStoredHostSessionId } from "../session-identity.ts";
33
+ import {
34
+ createReceiptDeliveryPolicy,
35
+ deliveryLimitFromConfig,
36
+ } from "../receipt-delivery-policy.ts";
33
37
  import { rewriteRunDirectoryPathValue } from "../role-run-relocation.ts";
34
38
 
35
39
  import type {
@@ -69,44 +73,37 @@ import {
69
73
  import type { CredentialProviders, SeatModelConfig } from "./config.ts";
70
74
  import {
71
75
  clearCurrentCourt,
72
- clearTypedProviderHttpObservation,
73
76
  describeErrorIdentity,
74
77
  markRunRunning,
75
78
  readCurrentCourt,
76
79
  recordCurrentCourt,
77
- renderResumeCommand,
78
80
  type CurrentCourtState,
79
81
  type RunWriterLease,
80
- type TypedProviderHttpObservation,
81
82
  } from "./run-lifecycle.ts";
82
83
  import {
83
84
  processCancelDiagnostic,
84
85
  processCancelSignalName,
85
86
  type CatchableProcessSignal,
86
87
  } from "./process-cancel.ts";
87
- import { homeFromRunDirectory } from "../activation-ledger-topology.ts";
88
+ import { DEFAULT_ROLE_TURN_HOST } from "../host-descriptions.ts";
88
89
  import { recordRunStart } from "../host-session-record.ts";
89
90
  import {
90
- attemptProducedFreshSubmission,
91
91
  classifyPostAdmissionFailure,
92
- controlledFailureInputFromResolution,
93
92
  exitCodeForTerminalOutcome,
94
93
  explicitInternalKnownFailureClassificationInput,
95
94
  formatCliDiagnostic,
96
95
  formatErrorCauseDetail,
97
- formatTerminalResult,
98
- inspectJudgeSession,
96
+ ledgerReadScope,
99
97
  isLawfulTypedTerminalOutcome,
100
- presentFailureTerminal,
101
- publishedFailureErrorPath,
102
98
  presentStructuralRejection,
103
- resolveAuditedRunnerFailureResolution,
104
- resolveControlledFailureResumeObservation,
105
99
  settleFailureTerminalResult,
106
100
  settleHostEndedNoReceipt,
101
+ noteSettlementFault,
107
102
  attachRecordedSubmissions,
108
- rewritePublishedFailureErrorPath,
103
+ retainPackageFault,
109
104
  type ControlledFailure,
105
+ type PackageSideFact,
106
+ type SettlementCourtScope,
110
107
  } from "./settlement.ts";
111
108
  import type { CliIo } from "./cli-io.ts";
112
109
  import type { AdmittedRoleInvocation, RunDirectoryRelocation } from "./invocation.ts";
@@ -117,9 +114,9 @@ import {
117
114
  import {
118
115
  ensureRealArtifactsDirectory,
119
116
  persistReturnedRunState,
117
+ presentTerminal,
120
118
  runWithAutoResumeLoop,
121
119
  TurnDispatchedFailure,
122
- writeHardenedArtifactFile,
123
120
  } from "./auto-resume.ts";
124
121
 
125
122
  import { isRecord, errorText } from "../unknown-value.ts";
@@ -146,11 +143,6 @@ function projectRelocatedTurnIdentity(
146
143
  }
147
144
  }
148
145
  if (result.terminal !== undefined) {
149
- rewritePublishedFailureErrorPath(
150
- result.terminal,
151
- relocation.oldRunDirectory,
152
- relocation.newRunDirectory,
153
- );
154
146
  for (const artifact of result.terminal.artifacts) {
155
147
  artifact.path = rewrite(artifact.path) as string;
156
148
  }
@@ -182,6 +174,7 @@ async function settleProcessCancelAfterTurn<A extends AdmittedRoleInvocation, T
182
174
  persistRunState: boolean;
183
175
  deferredPersist: { needsPersist?: true } | Record<string, never>;
184
176
  invocationScopeId: string | undefined;
177
+ notePackageFault?: (diagnostic: string) => void | Promise<void>;
185
178
  }): Promise<{
186
179
  exitCode: number;
187
180
  admitted: A;
@@ -197,7 +190,13 @@ async function settleProcessCancelAfterTurn<A extends AdmittedRoleInvocation, T
197
190
  timedOut: input.result.timedOut,
198
191
  code: input.result.code,
199
192
  stderr: input.result.stderr,
200
- knownDiagnostic: processCancelDiagnostic(input.cancelName),
193
+ ...(input.result.signal === undefined ? {} : { signal: input.result.signal }),
194
+ // A host report already on this result stays the report. The cancel
195
+ // name is wording only when the host gave none.
196
+ ...(input.result.knownFailure === undefined
197
+ ? { knownDiagnostic: processCancelDiagnostic(input.cancelName) }
198
+ : { knownFailure: input.result.knownFailure }),
199
+ cancelName: input.cancelName,
201
200
  },
202
201
  input.invocationScopeId,
203
202
  ),
@@ -205,6 +204,7 @@ async function settleProcessCancelAfterTurn<A extends AdmittedRoleInvocation, T
205
204
  input.env.principalAuthority,
206
205
  input.io,
207
206
  input.persistRunState,
207
+ input.notePackageFault,
208
208
  );
209
209
  return {
210
210
  ...failed,
@@ -253,41 +253,32 @@ export const POST_ADMISSION_CLEANUP_DIAGNOSTIC_ENTRY_TYPE =
253
253
  * zero durable trace (#840 bounce class 2); a healthy session never gets a
254
254
  * redundant second copy (#840 bounce class 1: 同一业务规则只保留一个权威实现).
255
255
  */
256
- async function recordBestEffortPostDispatchDiagnostic<A extends AdmittedRoleInvocation>(
257
- admitted: A,
258
- env: PostAdmissionEnv,
256
+ async function recordBestEffortPostDispatchDiagnostic(
257
+ admitted: Pick<AdmittedRoleInvocation, "runDirectory" | "principal">,
258
+ env: Pick<PostAdmissionEnv, "sessionAppender" | "principalAuthority">,
259
259
  diagnostic: string,
260
- io: CliIo,
260
+ io: Pick<CliIo, "stderr">,
261
261
  ): Promise<void> {
262
- const payload = { diagnostic, recordedAt: new Date().toISOString() };
263
- let retentionFailure: string | undefined;
264
- try {
265
- await env.sessionAppender(
262
+ await retainPackageFault({
263
+ runDirectory: admitted.runDirectory,
264
+ diagnostic,
265
+ appendSession: (payload) => env.sessionAppender(
266
266
  env.principalAuthority,
267
267
  admitted.principal,
268
268
  POST_ADMISSION_CLEANUP_DIAGNOSTIC_ENTRY_TYPE,
269
269
  payload,
270
- );
271
- } catch (appendError) {
272
- try {
273
- const artifactsDir = await ensureRealArtifactsDirectory(admitted.runDirectory);
274
- await writeHardenedArtifactFile(artifactsDir, "post-admission-diagnostic", {
275
- version: 1,
276
- ...payload,
277
- });
278
- } catch (artifactError) {
279
- retentionFailure =
280
- `post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; artifact=${describeErrorIdentity(artifactError)}`;
281
- }
282
- }
283
- try {
284
- io.stderr(formatCliDiagnostic(diagnostic));
285
- if (retentionFailure !== undefined) {
286
- io.stderr(formatCliDiagnostic(retentionFailure));
287
- }
288
- } catch {
289
- // Presentation is best-effort beside an already-formed host terminal.
290
- }
270
+ ),
271
+ stderr: (text) => io.stderr(text),
272
+ });
273
+ }
274
+
275
+ /** The one package-fault receiver, bound to this admitted run's session and io. */
276
+ export function packageFaultNoteFor(
277
+ admitted: Pick<AdmittedRoleInvocation, "runDirectory" | "principal">,
278
+ env: Pick<PostAdmissionEnv, "sessionAppender" | "principalAuthority">,
279
+ io: Pick<CliIo, "stderr">,
280
+ ): (diagnostic: string) => Promise<void> {
281
+ return (diagnostic) => recordBestEffortPostDispatchDiagnostic(admitted, env, diagnostic, io);
291
282
  }
292
283
 
293
284
  export type PostAdmissionEnv = {
@@ -313,6 +304,11 @@ export type PostAdmissionEnv = {
313
304
  principalAuthority: DurablePrincipalAuthority;
314
305
  sessionAppender: SessionCustomEntryAppender;
315
306
  autoResumeLimit?: number;
307
+ /**
308
+ * Unreadable-status reasks already issued on this chain (#1132). Each reask
309
+ * keeps this env's autoResumeLimit; the counter is the only thing that moves.
310
+ */
311
+ unreadableReasksSpent?: number;
316
312
  createRunId?: () => string;
317
313
  /**
318
314
  * Parent cancellation for a nested public summon (#675). Every dispatched turn
@@ -342,18 +338,15 @@ export type PostAdmissionAdapters<
342
338
  A extends AdmittedRoleInvocation = AdmittedRoleInvocation,
343
339
  T extends TerminalResult = TerminalResult,
344
340
  > = {
341
+ /** A present Terminal already carries run history from settlement. */
345
342
  trySettle: (
346
343
  admitted: A,
347
344
  authority: DurablePrincipalAuthority,
348
- /** Current court turn scope (#637); omit for run-scoped sealed reads. */
349
- scope?: { readonly courtAttemptId?: string },
345
+ /** Current host attempt; only this invocation records history (#419). */
346
+ scope?: { readonly courtAttemptId?: string; readonly recordAttemptHistory?: true; readonly previewOnly?: true },
350
347
  ) => Promise<T | undefined>;
351
348
  /** Default: isLawfulTypedTerminalOutcome(terminal.roleOutcome). */
352
349
  shouldPresentSettled?: (terminal: T) => boolean;
353
- resolveRunnerKnownFailure?: (input: {
354
- result: RoleTurnResult;
355
- sessionFile: string;
356
- }) => Promise<RoleTurnKnownFailure | undefined>;
357
350
  /** A child office may pause this run with its unchanged terminal before a host turn starts. */
358
351
  beforeDispatch?: (admitted: A, lease?: RunWriterLease) => Promise<T | void> | T | void;
359
352
  /**
@@ -384,13 +377,22 @@ export type ControlledFailureInput = {
384
377
  knownDiagnostic?: string;
385
378
  /** Secondary evidence already owned by the typed production failure channel. */
386
379
  knownDetails?: Readonly<Record<string, unknown>>;
387
- typedHttpObservationSettled?: true;
388
- typedHttpObservation?: TypedProviderHttpObservation;
389
380
  /**
390
- * #836: set when the caller already attempted markRunTerminal/markRunResumable
391
- * and it is what threw `thrown` — presentControlledFailure must not retry
392
- * the same known-failing run-state write a second time; the caught error is
393
- * already the reported cause.
381
+ * A real failure this package hit while handling the call (e.g. the
382
+ * stderr.log durable write). Recorded beside the host's report; never a
383
+ * cause, and never written into the host's open `details`.
384
+ */
385
+ packageFact?: PackageSideFact;
386
+ /** The signal that killed the host child, when one did. */
387
+ signal?: string;
388
+ /**
389
+ * A catchable process-signal cancellation. Supplies fallback wording only —
390
+ * a host that reported a diagnostic of its own keeps it.
391
+ */
392
+ readonly cancelName?: CatchableProcessSignal;
393
+ /**
394
+ * Set when the caller already attempted markRunTerminal and it is what threw
395
+ * `thrown` — presentControlledFailure must not retry that write.
394
396
  */
395
397
  skipRunStateWrite?: boolean;
396
398
  /** Public-invocation scope from the shared Host envelope (#537). */
@@ -449,6 +451,12 @@ export async function resolveResumeMethodMaterialAdapters<
449
451
  input.emptyAdapters,
450
452
  input.authority,
451
453
  { ...input.io, omitFailureStderrDiagnostic: true },
454
+ true,
455
+ (diagnostic) => retainPackageFault({
456
+ runDirectory: input.admitted.runDirectory,
457
+ diagnostic,
458
+ stderr: (text) => input.io.stderr(text),
459
+ }),
452
460
  );
453
461
  return { kind: "terminal", ...terminal };
454
462
  }
@@ -464,45 +472,41 @@ export async function presentControlledFailure<
464
472
  authority: DurablePrincipalAuthority,
465
473
  io: CliIo,
466
474
  persistRunState = true,
475
+ notePackageFault?: (diagnostic: string) => void | Promise<void>,
467
476
  ): Promise<{
468
477
  exitCode: number;
469
478
  admitted: A;
470
479
  terminal: TerminalResult;
471
480
  }> {
472
481
  const hasThrown = Object.hasOwn(failureInput, "thrown");
473
- const resumeObservation = await resolveControlledFailureResumeObservation({
474
- runDirectory: admitted.runDirectory,
475
- ...(failureInput.typedHttpObservationSettled === true
476
- ? {
477
- typedHttpObservationSettled: true as const,
478
- ...(failureInput.typedHttpObservation === undefined
479
- ? {}
480
- : { typedHttpObservation: failureInput.typedHttpObservation }),
481
- }
482
- : {}),
483
- });
484
- const knownFailure =
485
- failureInput.knownFailure ?? resumeObservation.observationReadFailure;
486
- const session =
487
- !hasThrown &&
488
- !failureInput.timedOut &&
489
- knownFailure === undefined &&
490
- failureInput.knownCause === undefined &&
491
- admitted.principal !== undefined
492
- ? await inspectJudgeSession(authority.decode(admitted.principal).sessionFile)
493
- : undefined;
482
+ const knownFailure = failureInput.knownFailure;
494
483
  // knownFailure channel owns details when present; otherwise caller knownDetails.
495
484
  const fromKnownFailure =
496
485
  explicitInternalKnownFailureClassificationInput(knownFailure);
497
- const failure = classifyPostAdmissionFailure({
486
+ // Merge the two detail sources so a key carried by either survives; a later
487
+ // spread would otherwise drop the earlier one's keys entirely.
488
+ const callerDetails =
489
+ failureInput.knownDetails === undefined && fromKnownFailure.knownDetails === undefined
490
+ ? undefined
491
+ : { ...(failureInput.knownDetails ?? {}), ...(fromKnownFailure.knownDetails ?? {}) };
492
+ const classified = classifyPostAdmissionFailure({
498
493
  timedOut: failureInput.timedOut,
499
494
  code: failureInput.code,
500
495
  stderr: failureInput.stderr,
501
496
  ...(hasThrown ? { thrown: failureInput.thrown } : {}),
502
- ...(failureInput.knownDetails === undefined
497
+ // Both detail sources are real; merging keeps each one's keys instead of
498
+ // letting the later spread drop the earlier one.
499
+ ...(callerDetails === undefined ? {} : { knownDetails: callerDetails }),
500
+ // The knownFailure channel's own cause/identity/diagnostic ride with it.
501
+ ...(fromKnownFailure.knownCause === undefined
502
+ ? {}
503
+ : { knownCause: fromKnownFailure.knownCause }),
504
+ ...(fromKnownFailure.knownIdentity === undefined
505
+ ? {}
506
+ : { knownIdentity: fromKnownFailure.knownIdentity }),
507
+ ...(fromKnownFailure.knownDiagnostic === undefined
503
508
  ? {}
504
- : { knownDetails: failureInput.knownDetails }),
505
- ...fromKnownFailure,
509
+ : { knownDiagnostic: fromKnownFailure.knownDiagnostic }),
506
510
  ...(failureInput.knownCause === undefined
507
511
  ? {}
508
512
  : { knownCause: failureInput.knownCause }),
@@ -512,23 +516,35 @@ export async function presentControlledFailure<
512
516
  ...(failureInput.knownDiagnostic === undefined
513
517
  ? {}
514
518
  : { knownDiagnostic: failureInput.knownDiagnostic }),
515
- ...(session === undefined ? {} : { session }),
519
+ ...(failureInput.signal === undefined ? {} : { signal: failureInput.signal }),
520
+ ...(failureInput.cancelName === undefined ? {} : { cancelName: failureInput.cancelName }),
521
+ ...(failureInput.packageFact === undefined
522
+ ? {}
523
+ : { packageFact: failureInput.packageFact }),
516
524
  });
517
-
518
- // #665 / #416: typed 429 with a run principal offers resume; the host decides availability.
519
- const resumable = admitted.principal !== undefined && resumeObservation.typedHttp429 !== undefined;
525
+ const failure = failureInput.stderr.length === 0 || failureInput.stderr === classified.diagnostic
526
+ ? classified
527
+ : { ...classified, stderr: failureInput.stderr };
520
528
 
521
529
  if (persistRunState && !failureInput.skipRunStateWrite) {
522
- await persistReturnedRunState(admitted);
530
+ try {
531
+ await persistReturnedRunState(admitted);
532
+ } catch (error) {
533
+ // The host failure is already classified. A run-state write stays beside it.
534
+ await noteSettlementFault(
535
+ admitted.runDirectory,
536
+ notePackageFault === undefined ? undefined : { notePackageFault },
537
+ `run-state persistence failed beside host terminal: ${describeErrorIdentity(error)}`,
538
+ );
539
+ }
523
540
  }
524
541
 
525
542
  let publishedErrorPath: string | undefined;
526
543
  let terminal: TerminalResult;
527
544
  try {
528
545
  const settled = await settleFailureTerminalResult(admitted, failure, authority, {
529
- ...(resumable
530
- ? { resume: { command: renderResumeCommand(admitted.runId) } }
531
- : {}),
546
+ recordAttemptHistory: true,
547
+ ...(notePackageFault === undefined ? {} : { notePackageFault }),
532
548
  ...(failureInput.invocationScopeId === undefined ||
533
549
  failureInput.invocationScopeId.length === 0
534
550
  ? {}
@@ -542,7 +558,7 @@ export async function presentControlledFailure<
542
558
  }
543
559
  throw error;
544
560
  }
545
- presentFailureTerminal(terminal, io);
561
+ await presentTerminal(terminal, io, admitted.runDirectory);
546
562
  return {
547
563
  exitCode: exitCodeForTerminalOutcome(terminal.roleOutcome),
548
564
  admitted,
@@ -571,6 +587,7 @@ async function settleAfterTurnStarted<
571
587
  authority: DurablePrincipalAuthority,
572
588
  io: CliIo,
573
589
  persistRunState: boolean,
590
+ notePackageFault?: (diagnostic: string) => void | Promise<void>,
574
591
  ): Promise<{ exitCode: number; admitted: A; terminal: T }> {
575
592
  try {
576
593
  return (await presentControlledFailure(
@@ -580,6 +597,7 @@ async function settleAfterTurnStarted<
580
597
  authority,
581
598
  io,
582
599
  persistRunState,
600
+ notePackageFault,
583
601
  )) as { exitCode: number; admitted: A; terminal: T };
584
602
  } catch (error) {
585
603
  throw new TurnDispatchedFailure(error);
@@ -590,51 +608,300 @@ async function settleAfterTurnStarted<
590
608
  * Persist run-state for a result dispatchPostAdmissionTurn deferred
591
609
  * (needsPersist — station-child / resumable auto-resume loop, #416/#840):
592
610
  * that write must land outside the loop's own retried-dispatch try, but its
593
- * failure still settles through the single existing controlled-failure
594
- * authority (settleAfterTurnStarted / presentControlledFailure, ADR 0080:
595
- * one settlement disposition owner) — never a second hand-rolled
596
- * classify/artifact/Terminal (#836 r12 class 3), and never lawful/non-lawful
597
- * settling differently (#836 r13 class 2: a caller's io here may be a no-op,
598
- * so a stderr-only trace is never seen and the failure is otherwise
599
- * swallowed — 失败诚实宪法 真因必须落痕). Both cases settle through
600
- * settleAfterTurnStarted, which attaches the run's already-recorded ledger
601
- * submissions to the new failure terminal and returns skipAutoResume so the
602
- * caller presents it once and stops — never re-entering auto-resume.
611
+ * failure is a package note beside the terminal already settled. It does
612
+ * not replace that terminal or open another attempt. The note is durable
613
+ * because the caller's io may be a no-op.
603
614
  */
604
615
  async function settleDeferredPersist<
605
616
  A extends AdmittedRoleInvocation,
606
617
  T extends TerminalResult,
607
618
  >(
608
619
  admitted: A,
609
- authority: DurablePrincipalAuthority,
610
- adapters: PostAdmissionAdapters<A, T>,
620
+ env: PostAdmissionEnv,
611
621
  io: CliIo,
612
- result: {
613
- exitCode: number;
614
- admitted: A;
615
- terminal?: T;
616
- skipAutoResume?: true;
617
- turnDispatched?: true;
618
- needsPersist?: true;
619
- },
620
- ): Promise<typeof result> {
622
+ result: DispatchOutcomeFor<A, T>,
623
+ ): Promise<DispatchOutcomeFor<A, T>> {
621
624
  if (result.needsPersist !== true || result.terminal === undefined) return result;
622
625
  const { needsPersist: _needsPersist, ...settledResult } = result;
623
- const lawful = isLawfulTypedTerminalOutcome(result.terminal.roleOutcome);
624
626
  try {
625
- await persistReturnedRunState(admitted, lawful ? { lawful: true } : undefined);
627
+ await persistReturnedRunState(admitted);
626
628
  return settledResult;
627
629
  } catch (error) {
628
- const failed = await settleAfterTurnStarted(
630
+ await recordBestEffortPostDispatchDiagnostic(
629
631
  admitted,
630
- { timedOut: false, code: null, stderr: "", thrown: error, skipRunStateWrite: true },
632
+ env,
633
+ `run-state persistence failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`,
634
+ io,
635
+ );
636
+ return settledResult;
637
+ }
638
+ }
639
+
640
+ /**
641
+ * Court/invocation scope for settlement reads. The one construction of this
642
+ * shape for every turn (#1132 — the first turn and each 催交 turn share it).
643
+ */
644
+ function settlementScopeForTurn(request: RoleTurnRequest): SettlementCourtScope | undefined {
645
+ const hasCourt = request.courtAttemptId !== undefined && request.courtAttemptId.length > 0;
646
+ const hasScope = request.invocationScopeId !== undefined && request.invocationScopeId.length > 0;
647
+ if (!hasCourt && !hasScope) return undefined;
648
+ return {
649
+ ...(hasCourt ? { courtAttemptId: request.courtAttemptId as string } : {}),
650
+ ...(hasScope ? { invocationScopeId: request.invocationScopeId as string } : {}),
651
+ };
652
+ }
653
+
654
+ /**
655
+ * #1132: one 没交卷催交 turn, dispatched through the SAME settlement path as the
656
+ * first turn.
657
+ *
658
+ * There is deliberately no second settlement implementation here. A delivery
659
+ * turn is an ordinary host turn on this same run/session: this builds the
660
+ * host-neutral resume request (the adapter's stored native session id, ADR 0082;
661
+ * pi's existing delivery-state content reused verbatim — no new prompt wording,
662
+ * no new host capability). The caller settles it through the same
663
+ * `settleCompletedHostTurn` the first turn uses (ADR 0080): the runner/host
664
+ * failure re-read, the shouldPresent gate, the fresh-seal check, open-court
665
+ * cleanup, cancel re-reads, run-state persist, and the stderr mirror.
666
+ *
667
+ * The caller owns the live delivery policy; the request projects its next send.
668
+ */
669
+ async function buildReceiptDeliveryRequest<A extends AdmittedRoleInvocation>(input: {
670
+ admitted: A;
671
+ env: PostAdmissionEnv;
672
+ request: RoleTurnRequest;
673
+ receiptDelivery: ReturnType<typeof createReceiptDeliveryPolicy>;
674
+ }): Promise<RoleTurnRequest> {
675
+ const { admitted, env, request, receiptDelivery } = input;
676
+ const hostSessionId = await readStoredHostSessionId(
677
+ env.host,
678
+ env.principalAuthority,
679
+ admitted.principal,
680
+ );
681
+ return {
682
+ ...request,
683
+ continuation: {
684
+ kind: "resume",
685
+ prompt: JSON.stringify({
686
+ ...receiptDelivery.deliveryState(),
687
+ deliveryTurns: receiptDelivery.issuedDeliveryRequests() + 1,
688
+ }),
689
+ ...(hostSessionId === undefined ? {} : { hostSessionId }),
690
+ },
691
+ deliveryRequestLimit: receiptDelivery.limit,
692
+ };
693
+ }
694
+
695
+ /** The one dispatch-result shape. */
696
+ type DispatchOutcomeFor<A extends AdmittedRoleInvocation, T extends TerminalResult> = {
697
+ exitCode: number;
698
+ admitted: A;
699
+ terminal?: T;
700
+ skipAutoResume?: true;
701
+ turnDispatched?: true;
702
+ needsPersist?: true;
703
+ };
704
+
705
+ /**
706
+ * #1132: settle ONE completed host turn — the single post-turn settlement
707
+ * authority shared by the first turn and by every 没交卷催交 turn (ADR 0080).
708
+ *
709
+ * Extracted deliberately instead of re-entering `dispatchPostAdmissionTurn`.
710
+ * The turn-BEFORE concerns (beforeDispatch / markRunRunning / relocate /
711
+ * afterDispatch / writer-lease release) belong to the outermost dispatch and
712
+ * run exactly once per run; everything from the stderr mirror onward is
713
+ * per-turn. A 催交 turn is a real host turn on this same run/session, so it
714
+ * settles through THIS function — not a reduced copy — and it re-runs neither
715
+ * the admission hooks nor the run-state terminal write.
716
+ *
717
+ * Returns `still-missing` when the turn simply produced no receipt: the caller
718
+ * (first turn or the 催交 loop) then decides between issuing another delivery
719
+ * request and settling no_receipt.
720
+ */
721
+ async function settleCompletedHostTurn<
722
+ A extends AdmittedRoleInvocation,
723
+ T extends TerminalResult,
724
+ >(input: {
725
+ admitted: A;
726
+ env: PostAdmissionEnv;
727
+ io: CliIo;
728
+ adapters: PostAdmissionAdapters<A, T>;
729
+ shouldPresent: (terminal: T) => boolean;
730
+ request: RoleTurnRequest;
731
+ result: RoleTurnResult;
732
+ persistRunState: boolean;
733
+ deferredPersist: { needsPersist?: true } | Record<string, never>;
734
+ finishAfterTurn: (result: DispatchOutcomeFor<A, T>) => Promise<DispatchOutcomeFor<A, T>>;
735
+ courtScope: SettlementCourtScope & { notePackageFault: (diagnostic: string) => Promise<void> };
736
+ markPending: (terminal: T) => void;
737
+ }): Promise<{ kind: "settled"; outcome: DispatchOutcomeFor<A, T> } | { kind: "still-missing" }> {
738
+ const { admitted, env, io, adapters, shouldPresent, request, result, persistRunState, deferredPersist } = input;
739
+ const finishAfterTurn = input.finishAfterTurn;
740
+
741
+ const courtScope = input.courtScope;
742
+ // `result.stderr` stays live in memory for host-failure settlement
743
+ // whether or not this mirror write succeeds. A write failure is noted.
744
+ // It does not replace the host terminal or an already-sealed acceptance.
745
+ //
746
+ // Everything below runs after the host turn genuinely started (#840 r9
747
+ // 判词 class 1 boundary — 覆盖 executeTurn 已启动后至返回带 turnDispatched
748
+ // 结果前的全部异常). Each fallible step routes any failure through the
749
+ // single existing settlement authority (presentControlledFailure /
750
+ // settleFailureTerminalResult) rather than a second one (ADR 0080:
751
+ // one settlement disposition owner) — never by fabricating a terminal here.
752
+ // A cleanup step that runs only after a real settlement (clearCurrentCourt)
753
+ // is protected by logging and keeping that already-obtained result, never
754
+ // by discarding it (#840 已交劳动只整理终局不重做). A failure inside the
755
+ // settlement authority itself (presentControlledFailure's own reads) goes
756
+ // through settleAfterTurnStarted: still no fabricated terminal, but the
757
+ // re-thrown TurnDispatchedFailure still tells runWithAutoResumeLoop the
758
+ // turn genuinely started, so the next retry sends a resume payload — the
759
+ // true cause settles through the loop's own existing dispatch-exception
760
+ // machinery once the retry budget is exhausted.
761
+ let stderrLogWriteFailure: unknown;
762
+ try {
763
+ await writeFile(
764
+ join(admitted.runDirectory, "stderr.log"),
765
+ result.stderr,
766
+ "utf8",
767
+ );
768
+ } catch (error) {
769
+ stderrLogWriteFailure = error;
770
+ // Best-effort: stderr.log is secondary to the host terminal. A write
771
+ // failure leaves a durable note and, on a host failure, rides in
772
+ // packageFact. It does not become the cause.
773
+ await recordBestEffortPostDispatchDiagnostic(
774
+ admitted,
775
+ env,
776
+ `stderr.log write failed (best-effort continue): ${describeErrorIdentity(error)}`,
777
+ io,
778
+ );
779
+ }
780
+
781
+ // Host facts are resolved before trySettle. A host failure skips trySettle
782
+ // so a later read cannot replace that report. A clean host whose settlement
783
+ // read throws is noted above and is not turned into a failure terminal.
784
+ // stderr.log and run-state writes are notes beside the host terminal.
785
+ let settled: T | undefined;
786
+ let settledOutcome:
787
+ | { exitCode: number; admitted: A; terminal: T; turnDispatched: true }
788
+ | undefined;
789
+ // The host CLI's own report: timeout, declared failure, cancel, or exit.
790
+ // A null code is a signal death (Node close). No session or history read
791
+ // takes part (owner ea321c6d / 4743ade7).
792
+ const processCancelName = processCancelSignalName(env.signal);
793
+ const hostSignalFailed =
794
+ result.timedOut
795
+ || result.knownFailure !== undefined
796
+ || processCancelName !== undefined
797
+ || result.code === null
798
+ || result.code !== 0;
799
+ const hostReportedFailure = result.knownFailure;
800
+ try {
801
+ if (!hostSignalFailed) {
802
+ settled = await adapters.trySettle(admitted, env.principalAuthority, { ...courtScope, previewOnly: true });
803
+ const cancelAfterSettle = processCancelSignalName(env.signal);
804
+ if (
805
+ settled !== undefined
806
+ && shouldPresent(settled)
807
+ && cancelAfterSettle === undefined
808
+ ) {
809
+ settledOutcome = {
810
+ exitCode: exitCodeForTerminalOutcome(settled.roleOutcome),
811
+ admitted,
812
+ terminal: settled,
813
+ turnDispatched: true as const,
814
+ };
815
+ }
816
+ }
817
+ } catch (error) {
818
+ // The host did not fail. A settlement read here is a package fault and
819
+ // must not become the terminal.
820
+ await recordBestEffortPostDispatchDiagnostic(
821
+ admitted,
822
+ env,
823
+ `settlement read failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`,
824
+ io,
825
+ );
826
+ }
827
+ if (settledOutcome !== undefined) {
828
+ // #855: final cancel re-read before lawful seal — settlement window may
829
+ // have received SIGTERM/SIGINT/SIGHUP after the earlier snapshot.
830
+ const lateCancel = processCancelSignalName(env.signal);
831
+ if (lateCancel !== undefined) {
832
+ return { kind: "settled", outcome: await finishAfterTurn(
833
+ await settleProcessCancelAfterTurn({
834
+ admitted,
835
+ cancelName: lateCancel,
836
+ result,
837
+ adapters,
838
+ env,
839
+ io,
840
+ persistRunState,
841
+ deferredPersist,
842
+ invocationScopeId: request.invocationScopeId,
843
+ notePackageFault: courtScope.notePackageFault,
844
+ }),
845
+ ) };
846
+ }
847
+ if (persistRunState) {
848
+ try {
849
+ await persistReturnedRunState(admitted);
850
+ } catch (error) {
851
+ await recordBestEffortPostDispatchDiagnostic(
852
+ admitted,
853
+ env,
854
+ `run-state persistence failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`,
855
+ io,
856
+ );
857
+ }
858
+ }
859
+ // Persist + present is the caller's stop seam (auto-resume loop /
860
+ // manual resume), not this retried host-turn function.
861
+ // #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
862
+ input.markPending(settledOutcome.terminal);
863
+ return { kind: "settled", outcome: await finishAfterTurn({ ...settledOutcome, ...deferredPersist }) };
864
+ }
865
+
866
+ // Host/runner true failure coexists with already-recorded payloads — never wash as accepted.
867
+ if (hostSignalFailed) {
868
+ const stderrLogWriteFact =
869
+ stderrLogWriteFailure === undefined
870
+ ? undefined
871
+ : { stderrLogWriteFailure: describeCaughtError(stderrLogWriteFailure) };
872
+ const failed = await settleAfterTurnStarted(
873
+ admitted,
874
+ withEngineDetourInvocationScope({
875
+ timedOut: result.timedOut,
876
+ code: result.code,
877
+ stderr: result.stderr,
878
+ // A signal-killed child is a fact of its own. The synthetic line is
879
+ // only a fallback for when the host declared no failure of its own —
880
+ // it must never displace words the host actually reported.
881
+ ...(result.signal === undefined ? {} : { signal: result.signal }),
882
+ ...(hostReportedFailure === undefined ? {} : { knownFailure: hostReportedFailure }),
883
+ // The cancellation is the host's own report of why this call ended;
884
+ // it supplies the wording only when the host declared nothing else.
885
+ ...(processCancelName === undefined ? {} : { cancelName: processCancelName }),
886
+ // Secondary fact only — never the cause, and never merged into the
887
+ // host's own details.
888
+ ...(stderrLogWriteFact === undefined ? {} : { packageFact: stderrLogWriteFact }),
889
+ }, request.invocationScopeId),
631
890
  adapters,
632
- authority,
891
+ env.principalAuthority,
633
892
  io,
634
- true,
893
+ persistRunState,
894
+ courtScope.notePackageFault,
635
895
  );
636
- return { ...failed, turnDispatched: true as const, skipAutoResume: true as const };
896
+ return { kind: "settled", outcome: await finishAfterTurn(
897
+ withProcessCancelSkipAutoResume(
898
+ { ...failed, turnDispatched: true as const, ...deferredPersist },
899
+ env.signal,
900
+ ),
901
+ ) };
637
902
  }
903
+
904
+ return { kind: "still-missing" };
638
905
  }
639
906
 
640
907
  export async function dispatchPostAdmissionTurn<
@@ -650,39 +917,54 @@ export async function dispatchPostAdmissionTurn<
650
917
  adapters: PostAdmissionAdapters<A, T>;
651
918
  effectiveEngine?: string;
652
919
  persistRunState?: boolean;
653
- }): Promise<{
654
- exitCode: number;
655
- admitted: A;
656
- terminal?: T;
657
- skipAutoResume?: true;
658
- turnDispatched?: true;
659
- needsPersist?: true;
660
- }> {
920
+ /** Call-owned delivery policy survives every failure-recovery dispatch. */
921
+ receiptDelivery?: ReturnType<typeof createReceiptDeliveryPolicy>;
922
+ }): Promise<DispatchOutcomeFor<A, T>> {
661
923
  const { admitted, env, io, request, lease, adapters, effectiveEngine } = input;
662
924
  const persistRunState = input.persistRunState !== false;
663
925
  const deferredPersist = persistRunState ? {} : { needsPersist: true as const };
664
926
  const shouldPresent =
665
927
  adapters.shouldPresentSettled ?? ((terminal: T) => isLawfulTypedTerminalOutcome(terminal.roleOutcome));
666
- type DispatchOutcome = {
667
- exitCode: number;
668
- admitted: A;
669
- terminal?: T;
670
- skipAutoResume?: true;
671
- turnDispatched?: true;
672
- needsPersist?: true;
673
- };
674
928
  /**
675
929
  * Once-only post-host-turn hook (Diarist bind/relocate, etc.) under the still-held
676
930
  * writer lease. Every exit after the host turn has started must pass here before
677
931
  * lease release — success and failure alike. When a primary failure terminal already
678
932
  * exists, keep that cause and leave relocate failure on the shared diagnostic channel.
679
933
  */
934
+ const courtScope = {
935
+ ...settlementScopeForTurn(request),
936
+ notePackageFault: (diagnostic: string) => recordBestEffortPostDispatchDiagnostic(
937
+ admitted,
938
+ env,
939
+ diagnostic,
940
+ io,
941
+ ),
942
+ };
943
+ let pendingSettlement: "sealed" | "no_receipt" | undefined;
944
+ /**
945
+ * The terminal this pending publication belongs to. A later real failure
946
+ * (e.g. a cancel re-read) replaces `result.terminal` with its own settled
947
+ * failure; that failure is already durable, so the superseded preview must
948
+ * not publish over it (失败诚实宪法:接住可以,洗白不行).
949
+ */
950
+ let pendingTerminal: T | undefined;
951
+
680
952
  let afterDispatchApplied = false;
681
- const finishAfterTurn = async (result: DispatchOutcome): Promise<DispatchOutcome> => {
953
+ const receiptDelivery = input.receiptDelivery ?? createReceiptDeliveryPolicy(env.autoResumeLimit);
954
+ let turnDispatched = false;
955
+ // #1132: the AK-side 催交 covers hosts that do not deliver to themselves.
956
+ // Pi runs the package role runtime in-process, and that runtime already issues
957
+ // its own delivery requests at `agent_end` and records the real count — asking
958
+ // again here would double-deliver on one run. External hosts (ACP / headless
959
+ // CLI) have no such in-child loop, so the AK execution seam owns it for them.
960
+ const akSeamDeliversReceipt =
961
+ env.host !== undefined && env.host !== "" && env.host !== DEFAULT_ROLE_TURN_HOST;
962
+ const finishAfterTurn = async (result: DispatchOutcomeFor<A, T>): Promise<DispatchOutcomeFor<A, T>> => {
682
963
  if (afterDispatchApplied) return result;
683
964
  afterDispatchApplied = true;
684
- // #855: re-read cancel before afterDispatch finalization — never present
685
- // lawful success after a catchable process signal. skipRunStateWrite: run-state
965
+ // #855: re-read cancel before committing the terminal and afterDispatch.
966
+ // A signal after the history-first commit cannot retroactively erase it.
967
+ // skipRunStateWrite: run-state
686
968
  // may already be terminal from a prior lawful persist; do not reopen as resumable.
687
969
  const cancelBeforeFinish = processCancelSignalName(env.signal);
688
970
  if (
@@ -699,6 +981,7 @@ export async function dispatchPostAdmissionTurn<
699
981
  code: null,
700
982
  stderr: "",
701
983
  knownDiagnostic: processCancelDiagnostic(cancelBeforeFinish),
984
+ cancelName: cancelBeforeFinish,
702
985
  skipRunStateWrite: true,
703
986
  },
704
987
  request.invocationScopeId,
@@ -707,12 +990,51 @@ export async function dispatchPostAdmissionTurn<
707
990
  env.principalAuthority,
708
991
  io,
709
992
  persistRunState,
993
+ courtScope.notePackageFault,
710
994
  )),
711
995
  turnDispatched: true as const,
712
996
  skipAutoResume: true as const,
713
997
  ...deferredPersist,
714
998
  };
715
999
  }
1000
+ // The last cancellation check precedes the one durable settlement. A preview
1001
+ // never publishes pointers or history; a superseding failure owns this turn —
1002
+ // `result.terminal === pendingTerminal` is what makes the publication this
1003
+ // turn's own rather than a superseded one.
1004
+ if (
1005
+ pendingSettlement !== undefined &&
1006
+ pendingTerminal !== undefined &&
1007
+ result.terminal === pendingTerminal
1008
+ ) {
1009
+ try {
1010
+ const terminal = pendingSettlement === "sealed"
1011
+ ? await adapters.trySettle(admitted, env.principalAuthority,
1012
+ { ...courtScope, recordAttemptHistory: true })
1013
+ : await attachRecordedSubmissions(admitted,
1014
+ await settleHostEndedNoReceipt(admitted, env.principalAuthority,
1015
+ { ...courtScope, recordAttemptHistory: true }, receiptDelivery.issuedDeliveryRequests()) as T,
1016
+ courtScope);
1017
+ if (terminal === undefined) throw new Error("settled host attempt vanished before publication");
1018
+ result = { ...result, terminal, exitCode: exitCodeForTerminalOutcome(terminal.roleOutcome) };
1019
+ if (pendingSettlement === "sealed" && terminal.roleOutcome.kind === "accepted"
1020
+ && request.courtAttemptId !== undefined && request.courtAttemptId.length > 0) {
1021
+ try {
1022
+ await clearCurrentCourt(admitted.runDirectory, request.courtAttemptId);
1023
+ } catch (error) {
1024
+ // The accepted attempt is already durable; cleanup is diagnostic only.
1025
+ await recordBestEffortPostDispatchDiagnostic(admitted, env,
1026
+ `current-court cleanup failed after accepted settlement (best-effort continue): ${describeErrorIdentity(error)}`, io);
1027
+ }
1028
+ }
1029
+ } catch (error) {
1030
+ await recordBestEffortPostDispatchDiagnostic(
1031
+ admitted,
1032
+ env,
1033
+ `terminal publication failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`,
1034
+ io,
1035
+ );
1036
+ }
1037
+ }
716
1038
  try {
717
1039
  // #858 / #1071: the first identifiable ticketNumber field declaration
718
1040
  // files an unbound run (integer, digit string, or leading #N token).
@@ -724,7 +1046,7 @@ export async function dispatchPostAdmissionTurn<
724
1046
  const rows = await readRecordedSubmissionRows(
725
1047
  admitted.projectRoot,
726
1048
  admitted.runId,
727
- { home: homeFromRunDirectory(admitted.runDirectory), sessionParent: join(admitted.runDirectory, "session", "session.jsonl") },
1049
+ ledgerReadScope(admitted),
728
1050
  );
729
1051
  let ticketNumber: number | undefined;
730
1052
  for (const row of rows) {
@@ -779,6 +1101,7 @@ export async function dispatchPostAdmissionTurn<
779
1101
  env.principalAuthority,
780
1102
  io,
781
1103
  persistRunState,
1104
+ courtScope.notePackageFault,
782
1105
  )) as { exitCode: number; admitted: A; terminal: T },
783
1106
  ...(result.turnDispatched === true ? { turnDispatched: true as const } : {}),
784
1107
  ...(result.skipAutoResume === true ? { skipAutoResume: true as const } : {}),
@@ -789,7 +1112,6 @@ export async function dispatchPostAdmissionTurn<
789
1112
  try {
790
1113
  // #987: do not pre-block or classify host dispatch from AK credential
791
1114
  // catalog facts. Per-invocation runner/host evidence owns provider identity.
792
- await clearTypedProviderHttpObservation(admitted.runDirectory);
793
1115
  // The authoritative host write (markRunRunning) is delayed to just before
794
1116
  // executeTurn — not merely past beforeDispatch (#840 r9 判词 class 2). Any
795
1117
  // pre-turn retry (beforeDispatch, continuation assembly) must not commit a host
@@ -817,6 +1139,7 @@ export async function dispatchPostAdmissionTurn<
817
1139
  env.principalAuthority,
818
1140
  io,
819
1141
  persistRunState,
1142
+ courtScope.notePackageFault,
820
1143
  )) as { exitCode: number; admitted: A; terminal: T };
821
1144
  return { ...settled, ...deferredPersist };
822
1145
  }
@@ -859,7 +1182,12 @@ export async function dispatchPostAdmissionTurn<
859
1182
 
860
1183
  let result: RoleTurnResult;
861
1184
  try {
1185
+ // A bare `ak-role resume` only forwards the caller's instruction to the
1186
+ // host CLI's native resume. Nothing here may gate, redirect or reshape
1187
+ // that dispatch on prior conclusions, row counts or report presence;
1188
+ // the authoritative post-turn settlement reads whatever really happened.
862
1189
  recordRunStart(admitted.runDirectory);
1190
+ turnDispatched = true;
863
1191
  result = await env.roleTurnHost.executeTurn(turnRequest);
864
1192
  } catch (error) {
865
1193
  const processCancelName = processCancelSignalName(env.signal);
@@ -869,15 +1197,16 @@ export async function dispatchPostAdmissionTurn<
869
1197
  timedOut: false,
870
1198
  code: null,
871
1199
  stderr: "",
872
- thrown:
873
- processCancelName === undefined
874
- ? error
875
- : new Error(processCancelDiagnostic(processCancelName), { cause: error }),
1200
+ // The thrown value is the host's report. A cancel received on this
1201
+ // call is a second fact; it must not become a new Error in its place.
1202
+ ...(processCancelName === undefined ? {} : { cancelName: processCancelName }),
1203
+ thrown: error,
876
1204
  }, request.invocationScopeId),
877
1205
  adapters,
878
1206
  env.principalAuthority,
879
1207
  io,
880
1208
  persistRunState,
1209
+ courtScope.notePackageFault,
881
1210
  );
882
1211
  return await finishAfterTurn(
883
1212
  withProcessCancelSkipAutoResume(
@@ -887,320 +1216,135 @@ export async function dispatchPostAdmissionTurn<
887
1216
  );
888
1217
  }
889
1218
 
890
- // `result.stderr` stays live in memory for the real classification below
891
- // regardless of whether this durable mirror write succeeds. A write
892
- // failure here (unwritable run directory, stderr.log occupied as a
893
- // directory, ...) is a real infrastructure problem and must never be
894
- // dropped silently (catch-and-continue with no trace is a defect) — but
895
- // it also must never become THE controlling failure and wash out a
896
- // primary child signal that already arrived on `result`, or an
897
- // already-sealed accepted payload (#836: a secondary failure must not
898
- // overwrite an already-known cause or an already-recorded leg). It rides
899
- // beside whatever the real classification below determines, and only
900
- // becomes the reported failure itself when nothing else is wrong.
901
- //
902
- // Everything below runs after the host turn genuinely started (#840 r9
903
- // 判词 class 1 boundary — 覆盖 executeTurn 已启动后至返回带 turnDispatched
904
- // 结果前的全部异常). Each fallible step routes any failure through the
905
- // single existing settlement authority (presentControlledFailure /
906
- // settleFailureTerminalResult) rather than a second one (ADR 0080:
907
- // one settlement disposition owner) — never by fabricating a terminal here.
908
- // A cleanup step that runs only after a real settlement (clearCurrentCourt)
909
- // is protected by logging and keeping that already-obtained result, never
910
- // by discarding it (#840 已交劳动只整理终局不重做). A failure inside the
911
- // settlement authority itself (presentControlledFailure's own reads) goes
912
- // through settleAfterTurnStarted: still no fabricated terminal, but the
913
- // re-thrown TurnDispatchedFailure still tells runWithAutoResumeLoop the
914
- // turn genuinely started, so the next retry sends a resume payload — the
915
- // true cause settles through the loop's own existing dispatch-exception
916
- // machinery once the retry budget is exhausted.
917
- let stderrLogWriteFailure: unknown;
918
- try {
919
- await writeFile(
920
- join(admitted.runDirectory, "stderr.log"),
921
- result.stderr,
922
- "utf8",
923
- );
924
- } catch (error) {
925
- stderrLogWriteFailure = error;
926
- // Best-effort: the turn's own stderr capture is secondary to lawful /
927
- // controlled-failure settlement below, but the failure itself must
928
- // still leave a real trace (失败诚实宪法 真因必须落痕) — durably, since
929
- // every auto-resume attempt's io is dummyIo (#840 r9 判词 class 1). It
930
- // still rides beside the classification below and, if nothing else is
931
- // wrong, becomes the reported failure itself (#836).
932
- await recordBestEffortPostDispatchDiagnostic(
1219
+ // #1132: settle this turn through the ONE post-turn settlement authority,
1220
+ // then — if it simply produced no receipt — 催交 before settling no_receipt.
1221
+ const settledTurn = await settleCompletedHostTurn({
1222
+ admitted,
1223
+ env,
1224
+ io,
1225
+ adapters,
1226
+ shouldPresent,
1227
+ request,
1228
+ result,
1229
+ persistRunState,
1230
+ deferredPersist,
1231
+ finishAfterTurn,
1232
+ courtScope,
1233
+ markPending: (terminal) => { pendingSettlement = "sealed"; pendingTerminal = terminal; },
1234
+ });
1235
+ if (settledTurn.kind === "settled") return settledTurn.outcome;
1236
+
1237
+ // #1132: 没交卷先回本人催交,不直接结为 no_receipt. The AK execution seam
1238
+ // owns the 催交 and its real count. Each request is a real host turn on this
1239
+ // same run/session, dispatched and settled through the same two seams as the
1240
+ // first turn — never a reduced copy. 催交 never substitutes for host failure
1241
+ // recovery: a delivery turn that fails keeps its true cause below.
1242
+ // The delivery budget is spent HERE, at the outermost level; only when it is
1243
+ // exhausted with still no receipt does the settlement seam record no_receipt.
1244
+ while (akSeamDeliversReceipt && receiptDelivery.nextAction() === "request-delivery") {
1245
+ // Assembly sits in the same post-start catch as the host call. A throw
1246
+ // here is a started turn: resume the session, and do not count a request
1247
+ // that never reached the host.
1248
+ let deliveryResult: RoleTurnResult;
1249
+ let deliveryTurnRequest: RoleTurnRequest;
1250
+ try {
1251
+ const deliveryRequest = await buildReceiptDeliveryRequest({
1252
+ admitted,
1253
+ env,
1254
+ // Host and the other axes already selected on this turn ride the
1255
+ // shared projection. The pre-projection request does not have them.
1256
+ request: turnRequest,
1257
+ receiptDelivery,
1258
+ });
1259
+ // The same native-resume projection the first turn uses (host adapter owns
1260
+ // the resume; AK only hands it the request — ADR 0082).
1261
+ deliveryTurnRequest = {
1262
+ ...deliveryRequest,
1263
+ principal: admitted.principal,
1264
+ runDirectory: admitted.runDirectory,
1265
+ ...(env.signal === undefined ? {} : { signal: env.signal }),
1266
+ };
1267
+ receiptDelivery.recordDeliveryRequest();
1268
+ deliveryResult = await env.roleTurnHost.executeTurn(deliveryTurnRequest);
1269
+ } catch (error) {
1270
+ // 催交 cannot substitute for host failure recovery: a real failure with
1271
+ // its true cause, through the one settlement authority (ADR 0080).
1272
+ // The turn started, so the caller's loop resumes instead of replaying
1273
+ // the initial payload. The issued count survives that re-entry.
1274
+ const settled = await settleAfterTurnStarted(
1275
+ admitted,
1276
+ withEngineDetourInvocationScope(
1277
+ { timedOut: false, code: null, stderr: "", thrown: error },
1278
+ request.invocationScopeId,
1279
+ ),
1280
+ adapters,
1281
+ env.principalAuthority,
1282
+ io,
1283
+ persistRunState,
1284
+ );
1285
+ return await finishAfterTurn(
1286
+ withProcessCancelSkipAutoResume(
1287
+ {
1288
+ ...settled,
1289
+ turnDispatched: true as const,
1290
+ ...deferredPersist,
1291
+ },
1292
+ env.signal,
1293
+ ),
1294
+ );
1295
+ }
1296
+ const deliveryTurn = await settleCompletedHostTurn({
933
1297
  admitted,
934
1298
  env,
935
- `stderr.log write failed (best-effort continue): ${describeErrorIdentity(error)}`,
936
- io,
937
- );
938
- }
939
-
940
- const courtScope =
941
- (request.courtAttemptId === undefined || request.courtAttemptId.length === 0) &&
942
- (request.invocationScopeId === undefined || request.invocationScopeId.length === 0)
943
- ? undefined
944
- : {
945
- ...(request.courtAttemptId === undefined || request.courtAttemptId.length === 0
946
- ? {}
947
- : { courtAttemptId: request.courtAttemptId }),
948
- ...(request.invocationScopeId === undefined ||
949
- request.invocationScopeId.length === 0
950
- ? {}
951
- : { invocationScopeId: request.invocationScopeId }),
952
- };
953
-
954
- // Single complete boundary (#840 r9 判词 class 1): resolving the
955
- // host/runner failure facts, trySettle, its shouldPresent gate, and the
956
- // accepted-settlement cleanup all settle through this one catch — a
957
- // throw from any of them (including the session-file decode itself)
958
- // still routes through settleAfterTurnStarted's TurnDispatchedFailure
959
- // instead of losing turnDispatched to an uncaught throw. The facts are
960
- // resolved before trySettle (#836) so an already-accepted settlement is
961
- // never presented over a real current host/runner failure signal or a
962
- // real stderr.log durable-write failure — both stay a real failure with
963
- // the recorded payload riding beside it, not replacing it.
964
- let settled: T | undefined;
965
- let settledOutcome:
966
- | { exitCode: number; admitted: A; terminal: T; turnDispatched: true }
967
- | undefined;
968
- let hostSignalFailed = false;
969
- let resolution: Awaited<ReturnType<typeof resolveAuditedRunnerFailureResolution>> | undefined;
970
- try {
971
- const sessionFile =
972
- admitted.principal !== undefined
973
- ? env.principalAuthority.decode(admitted.principal).sessionFile
974
- : "";
975
- const runnerKnownFailure =
976
- adapters.resolveRunnerKnownFailure !== undefined && sessionFile !== ""
977
- ? await adapters.resolveRunnerKnownFailure({ result, sessionFile })
978
- : result.knownFailure;
979
- resolution = await resolveAuditedRunnerFailureResolution({
980
- runner: runnerKnownFailure,
981
- sessionFile,
982
- credential: undefined,
983
- runDirectory: admitted.runDirectory,
1299
+ // Only the final Terminal presents; intermediate delivery attempts stay quiet.
1300
+ io: { ...io, stdout: () => {} },
1301
+ adapters,
1302
+ shouldPresent,
1303
+ request: deliveryTurnRequest,
1304
+ result: deliveryResult,
1305
+ // Run-state terminal write belongs to the OUTERMOST turn only, so a
1306
+ // still-silent delivery attempt never makes the run look lawfully terminal.
1307
+ persistRunState: false,
1308
+ deferredPersist,
1309
+ finishAfterTurn,
1310
+ courtScope,
1311
+ markPending: (terminal) => { pendingSettlement = "sealed"; pendingTerminal = terminal; },
984
1312
  });
985
- // A direct, current signal from the host/runner itself (timeout / host
986
- // knownFailure / runner knownFailure / missing credential) is a real
987
- // problem regardless of what else already settled — a settled accepted
988
- // outcome must not paper over it (it rides beside the recorded payload
989
- // via `submissions`, never replacing the payload). The audited
990
- // resolution's own typed session read (malformed JSONL, provider-stop,
991
- // an illegal/unsealed accepted status, a stale/superseded typed-HTTP
992
- // observation, ...) makes a run with no sealed acceptance a true
993
- // failure too — it must not be discarded into a lawful no_receipt just
994
- // because the raw runner signal alone looked clean. But it must not
995
- // retroactively invalidate an acceptance that already sealed this turn
996
- // — a resolved 429 observed earlier in the same session is exactly
997
- // that.
998
- const processCancelName = processCancelSignalName(env.signal);
999
- const directHostFailureSignal =
1000
- result.timedOut
1001
- || result.knownFailure !== undefined
1002
- || runnerKnownFailure !== undefined
1003
- || processCancelName !== undefined;
1004
- hostSignalFailed =
1005
- directHostFailureSignal
1006
- || (result.code !== null && result.code !== 0)
1007
- || resolution.knownFailure !== undefined;
1008
- settled = await adapters.trySettle(admitted, env.principalAuthority, courtScope);
1009
- if (settled !== undefined) {
1010
- settled = await attachRecordedSubmissions(admitted, settled, courtScope) as T;
1011
- }
1012
- // #836 r12 class 2: an accepted/audit_escalation settlement can be
1013
- // entirely a prior attempt's stale payload (courtAttempt is a
1014
- // recording tag, not a visibility gate — attachRecordedSubmissions
1015
- // above still surfaces that historical payload honestly either way).
1016
- // Consume the ledger's own subject.attemptId (submission-ledger.ts)
1017
- // only to learn whether *this* attempt itself produced a fresh seal —
1018
- // never to filter what presents.
1019
- const settledIsFreshThisAttempt =
1020
- settled === undefined
1021
- || (settled.roleOutcome.kind !== "accepted" && settled.roleOutcome.kind !== "audit_escalation")
1022
- ? true
1023
- : await attemptProducedFreshSubmission(admitted, courtScope);
1024
- // A lawful settled outcome already reached this turn takes precedence
1025
- // over a later bare exit-code / session-inspection signal (trailing
1026
- // nonzero exit, late stderr noise, a stale already-superseded
1027
- // typed-HTTP observation, ...) — but never over a direct current
1028
- // host/runner failure signal, nor over a real stderr.log durable-write
1029
- // failure (confirmed infrastructure trouble, not weak/bare evidence),
1030
- // both of which stay a real failure with the recorded payload riding
1031
- // beside it, not replacing it (#836: never kill an already-recorded
1032
- // leg, but never wash a real failure away either). Note: the narrower
1033
- // directHostFailureSignal gates acceptance here; the broader
1034
- // hostSignalFailed (adds bare exit code / resolution.knownFailure)
1035
- // only gates the no-settlement fallback below — except when the
1036
- // settlement itself is entirely stale (no fresh seal this attempt),
1037
- // where a bare nonzero exit / resolution failure must not be outranked
1038
- // by someone else's earlier success (#836 r12 class 2).
1039
- const staleAcceptanceOutranksRealFailure = !settledIsFreshThisAttempt && hostSignalFailed;
1040
- // #855: re-read cancel after trySettle/attach awaits — a signal in this
1041
- // window must not land as lawful accepted.
1042
- const cancelAfterSettle = processCancelSignalName(env.signal);
1043
- if (
1044
- settled !== undefined
1045
- && shouldPresent(settled)
1046
- && !directHostFailureSignal
1047
- && !staleAcceptanceOutranksRealFailure
1048
- && stderrLogWriteFailure === undefined
1049
- && cancelAfterSettle === undefined
1050
- ) {
1051
- // This court sealed — drop open-court pointer (bare resume no longer continues it).
1052
- if (
1053
- settled.roleOutcome.kind === "accepted" &&
1054
- request.courtAttemptId !== undefined &&
1055
- request.courtAttemptId.length > 0
1056
- ) {
1313
+ // The delivery turn's own outcome IS the run's outcome, except when it
1314
+ // simply still has no receipt — that is the loop's own case to keep going.
1315
+ if (deliveryTurn.kind === "settled") {
1316
+ // #1132: the delivery turn settles with persistRunState:false, so a run
1317
+ // that ends HERE (得卷 or a real failure) still needs the outermost
1318
+ // single run-state write. Without it the ledger says accepted while
1319
+ // run-state stays `running`, and every later resume entry (ADR 0080)
1320
+ // reads a finished run as live. The still-silent attempts above must
1321
+ // NOT write; this one, which ends the run, must.
1322
+ if (persistRunState && deliveryTurn.outcome.terminal !== undefined) {
1057
1323
  try {
1058
- await clearCurrentCourt(admitted.runDirectory, request.courtAttemptId);
1324
+ await persistReturnedRunState(admitted);
1059
1325
  } catch (error) {
1060
- // Settlement already sealed accepted — a cleanup failure here must
1061
- // not erase that fact or make the caller replay this court's
1062
- // summons over already-delivered work (#840 已交劳动只整理终局不重做).
1063
- // Preserve the cleanup failure as a post-dispatch diagnostic; the
1064
- // accepted settlement remains authoritative (#840 r9 判词 class 1).
1065
1326
  await recordBestEffortPostDispatchDiagnostic(
1066
1327
  admitted,
1067
1328
  env,
1068
- `current-court cleanup failed after accepted settlement (best-effort continue): ${describeErrorIdentity(error)}`,
1329
+ `run-state persistence failed beside催交 terminal (best-effort continue): ${describeErrorIdentity(error)}`,
1069
1330
  io,
1070
1331
  );
1071
1332
  }
1072
1333
  }
1073
- settledOutcome = {
1074
- exitCode: exitCodeForTerminalOutcome(settled.roleOutcome),
1075
- admitted,
1076
- terminal: settled,
1077
- turnDispatched: true as const,
1078
- };
1079
- }
1080
- } catch (error) {
1081
- // Settle (or its shouldPresent gate, or the failure-fact resolution
1082
- // above) throw is a real failure fact — never swallow into undefined.
1083
- const settledFailure = await settleAfterTurnStarted(
1084
- admitted,
1085
- withEngineDetourInvocationScope({
1086
- timedOut: false,
1087
- code: result.code,
1088
- stderr: result.stderr,
1089
- thrown: error,
1090
- }, request.invocationScopeId),
1091
- adapters,
1092
- env.principalAuthority,
1093
- io,
1094
- persistRunState,
1095
- );
1096
- return await finishAfterTurn({ ...settledFailure, turnDispatched: true as const, ...deferredPersist });
1097
- }
1098
- if (settledOutcome !== undefined) {
1099
- // #855: final cancel re-read before lawful seal — settlement window may
1100
- // have received SIGTERM/SIGINT/SIGHUP after the earlier snapshot.
1101
- const lateCancel = processCancelSignalName(env.signal);
1102
- if (lateCancel !== undefined) {
1103
- return await finishAfterTurn(
1104
- await settleProcessCancelAfterTurn({
1105
- admitted,
1106
- cancelName: lateCancel,
1107
- result,
1108
- adapters,
1109
- env,
1110
- io,
1111
- persistRunState,
1112
- deferredPersist,
1113
- invocationScopeId: request.invocationScopeId,
1114
- }),
1115
- );
1116
- }
1117
- if (persistRunState) {
1118
- try {
1119
- await persistReturnedRunState(admitted, { lawful: true });
1120
- } catch (error) {
1121
- await recordBestEffortPostDispatchDiagnostic(
1122
- admitted,
1123
- env,
1124
- `run-state persistence failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`,
1125
- io,
1126
- );
1127
- }
1334
+ return await finishAfterTurn(deliveryTurn.outcome);
1128
1335
  }
1129
- // Lawful persist + present is the caller's stop seam (auto-resume loop /
1130
- // manual resume), not this retried host-turn function.
1131
- // #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
1132
- return await finishAfterTurn({ ...settledOutcome, ...deferredPersist });
1133
- }
1134
-
1135
- // Host/runner true failure coexists with already-recorded payloads — never wash as accepted.
1136
- if (hostSignalFailed) {
1137
- const resolutionInput = controlledFailureInputFromResolution(resolution!);
1138
- const processCancelName = processCancelSignalName(env.signal);
1139
- const stderrLogWriteDetails =
1140
- stderrLogWriteFailure === undefined
1141
- ? undefined
1142
- : { stderrLogWriteFailure: describeCaughtError(stderrLogWriteFailure) };
1143
- const failed = await settleAfterTurnStarted(
1144
- admitted,
1145
- withEngineDetourInvocationScope({
1146
- timedOut: result.timedOut,
1147
- code: result.code,
1148
- stderr: result.stderr,
1149
- ...resolutionInput,
1150
- ...(processCancelName === undefined
1151
- ? {}
1152
- : { knownDiagnostic: processCancelDiagnostic(processCancelName) }),
1153
- // Secondary fact only — never the cause. Rides on whichever channel
1154
- // classification actually reads (knownFailure.details owns it when
1155
- // a knownFailure exists; the top-level knownDetails otherwise).
1156
- ...(stderrLogWriteDetails === undefined
1157
- ? {}
1158
- : resolutionInput.knownFailure !== undefined
1159
- ? {
1160
- knownFailure: {
1161
- ...resolutionInput.knownFailure,
1162
- details: { ...(resolutionInput.knownFailure.details ?? {}), ...stderrLogWriteDetails },
1163
- },
1164
- }
1165
- : { knownDetails: stderrLogWriteDetails }),
1166
- }, request.invocationScopeId),
1167
- adapters,
1168
- env.principalAuthority,
1169
- io,
1170
- persistRunState,
1171
- );
1172
- return await finishAfterTurn(
1173
- withProcessCancelSkipAutoResume(
1174
- { ...failed, turnDispatched: true as const, ...deferredPersist },
1175
- env.signal,
1176
- ),
1177
- );
1178
- }
1179
-
1180
- // Nothing else was wrong, but the durable stderr mirror itself failed to
1181
- // write — that is the real (infrastructure) problem in this case, not a
1182
- // lawful absence of a receipt. Honest and loud, not silently no_receipt.
1183
- if (stderrLogWriteFailure !== undefined) {
1184
- const failed = await settleAfterTurnStarted(
1185
- admitted,
1186
- withEngineDetourInvocationScope({
1187
- timedOut: false,
1188
- code: result.code,
1189
- stderr: result.stderr,
1190
- thrown: stderrLogWriteFailure,
1191
- }, request.invocationScopeId),
1192
- adapters,
1193
- env.principalAuthority,
1194
- io,
1195
- persistRunState,
1196
- );
1197
- return await finishAfterTurn({ ...failed, turnDispatched: true as const, ...deferredPersist });
1198
1336
  }
1199
1337
 
1338
+ const noReceiptScope = { ...courtScope, previewOnly: true as const };
1200
1339
  const noReceipt = await attachRecordedSubmissions(
1201
1340
  admitted,
1202
- await settleHostEndedNoReceipt(admitted, env.principalAuthority, courtScope) as T,
1203
- courtScope,
1341
+ await settleHostEndedNoReceipt(
1342
+ admitted,
1343
+ env.principalAuthority,
1344
+ noReceiptScope,
1345
+ receiptDelivery.issuedDeliveryRequests(),
1346
+ ) as T,
1347
+ noReceiptScope,
1204
1348
  );
1205
1349
  // #855: cancel during no_receipt settlement window is not lawful success.
1206
1350
  const noReceiptCancel = processCancelSignalName(env.signal);
@@ -1216,12 +1360,13 @@ export async function dispatchPostAdmissionTurn<
1216
1360
  persistRunState,
1217
1361
  deferredPersist,
1218
1362
  invocationScopeId: request.invocationScopeId,
1363
+ notePackageFault: courtScope.notePackageFault,
1219
1364
  }),
1220
1365
  );
1221
1366
  }
1222
1367
  if (persistRunState) {
1223
1368
  try {
1224
- await persistReturnedRunState(admitted, { lawful: true });
1369
+ await persistReturnedRunState(admitted);
1225
1370
  } catch (error) {
1226
1371
  await recordBestEffortPostDispatchDiagnostic(
1227
1372
  admitted,
@@ -1232,6 +1377,8 @@ export async function dispatchPostAdmissionTurn<
1232
1377
  }
1233
1378
  }
1234
1379
  // #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
1380
+ pendingSettlement = "no_receipt";
1381
+ pendingTerminal = noReceipt;
1235
1382
  return await finishAfterTurn({
1236
1383
  exitCode: exitCodeForTerminalOutcome(noReceipt.roleOutcome),
1237
1384
  admitted,
@@ -1239,6 +1386,11 @@ export async function dispatchPostAdmissionTurn<
1239
1386
  turnDispatched: true as const,
1240
1387
  ...deferredPersist,
1241
1388
  });
1389
+ } catch (error) {
1390
+ if (turnDispatched && !(error instanceof TurnDispatchedFailure)) {
1391
+ throw new TurnDispatchedFailure(error);
1392
+ }
1393
+ throw error;
1242
1394
  } finally {
1243
1395
  // Public manual resume (#987) holds no package writer lease.
1244
1396
  if (lease !== undefined) {
@@ -1261,6 +1413,7 @@ export async function dispatchPostAdmissionTurn<
1261
1413
  }
1262
1414
  }
1263
1415
 
1416
+
1264
1417
  /**
1265
1418
  * Shared resume continuation projection (#471 / #600 / #633 / #637 / #755 / #879):
1266
1419
  * seat-table model/engine/timeout axes, restored correlation, and either
@@ -1329,8 +1482,9 @@ export function resumeTurnRequestProjectionOptions(
1329
1482
  kind: "resume",
1330
1483
  prompt,
1331
1484
  },
1332
- ...(request.message === undefined ? {} : { courtAttemptId: randomUUID() }),
1333
1485
  ...(env.stationChild === undefined ? {} : { stationChild: env.stationChild }),
1486
+ // #1132: one configured ceiling, already resolved by the caller (#422).
1487
+ deliveryRequestLimit: deliveryLimitFromConfig(env.autoResumeLimit),
1334
1488
  };
1335
1489
  }
1336
1490
 
@@ -1349,6 +1503,8 @@ export function roleTurnOptions(
1349
1503
  : { correlationId }),
1350
1504
  continuation,
1351
1505
  ...(env.stationChild === undefined ? {} : { stationChild: env.stationChild }),
1506
+ // #1132: one configured ceiling, already resolved by the caller (#422).
1507
+ deliveryRequestLimit: deliveryLimitFromConfig(env.autoResumeLimit),
1352
1508
  };
1353
1509
  }
1354
1510
 
@@ -1397,6 +1553,7 @@ async function runSettledAutoResumeLoop<
1397
1553
  }): Promise<{ exitCode: number; admitted?: A; terminal?: T }> {
1398
1554
  const adapters = withOnceSuccessfulBeforeDispatch(input.adapters);
1399
1555
  const dispatchEnv = input.dispatchEnv ?? input.env;
1556
+ const receiptDelivery = createReceiptDeliveryPolicy(input.env.autoResumeLimit);
1400
1557
  return runWithAutoResumeLoop({
1401
1558
  admitted: input.admitted,
1402
1559
  principalAuthority: input.env.principalAuthority,
@@ -1419,14 +1576,14 @@ async function runSettledAutoResumeLoop<
1419
1576
  ...(lease === undefined ? {} : { lease }),
1420
1577
  adapters,
1421
1578
  persistRunState: false,
1579
+ receiptDelivery,
1422
1580
  ...(input.effectiveEngine === undefined
1423
1581
  ? {}
1424
1582
  : { effectiveEngine: input.effectiveEngine }),
1425
1583
  });
1426
1584
  return settleDeferredPersist(
1427
1585
  input.admitted,
1428
- input.env.principalAuthority,
1429
- adapters,
1586
+ input.env,
1430
1587
  attemptIo,
1431
1588
  result,
1432
1589
  );
@@ -1619,13 +1776,13 @@ export async function runPostAdmissionSeatResume<
1619
1776
  }
1620
1777
  }
1621
1778
 
1622
- // Open court continue, or new court for summons / message re-review
1623
- // (clause 0 新庭可再交卷; #833). Bare resume without open court omits id.
1624
- if (
1625
- openCourtAttemptId !== undefined ||
1626
- request.summons !== undefined ||
1627
- request.message !== undefined
1628
- ) {
1779
+ // Open court continue, or a new court for a real re-summons
1780
+ // (clause 0 新庭可再交卷; #833). Bare resume — with or without caller
1781
+ // words — carries no summons and no open court, so it mints none:
1782
+ // the package must not turn ordinary continuation bytes into an audit
1783
+ // court whose attempt scope would reshape settlement (#1032 修订票:
1784
+ // 此要求不延伸到没有该传召庭次的裸 resume).
1785
+ if (openCourtAttemptId !== undefined || request.summons !== undefined) {
1629
1786
  const courtAttemptId =
1630
1787
  openCourtAttemptId ??
1631
1788
  (turnRequest.courtAttemptId !== undefined &&
@@ -1778,8 +1935,7 @@ export function showResumeErrorPointer(
1778
1935
  terminal: TerminalResult | undefined,
1779
1936
  ): void {
1780
1937
  if (exitCode === 0 || terminal === undefined) return;
1781
- const publishedError = terminal.artifacts.find((artifact) => artifact.kind === "error")?.path
1782
- ?? publishedFailureErrorPath(terminal);
1938
+ const publishedError = terminal.artifacts.find((artifact) => artifact.kind === "error")?.path;
1783
1939
  if (publishedError !== undefined) writeResumeFailurePointer(io, publishedError);
1784
1940
  }
1785
1941
 
@@ -2001,7 +2157,7 @@ export async function runPostAdmissionManualResume<
2001
2157
  // through its own settleAfterTurnStarted-backed branches; a lawful result
2002
2158
  // only ever reaches this point once that persist has already succeeded
2003
2159
  // (#836 r12 class 3 dedup — one persist owner, not a second here).
2004
- io.stdout(formatTerminalResult(result.terminal));
2160
+ await presentTerminal(result.terminal, io, admitted.runDirectory);
2005
2161
  }
2006
2162
  if (result.terminal !== undefined) {
2007
2163
  (result.terminal as { autoResumeCount?: number }).autoResumeCount = 0;