@akagilnc/pi-workflow-roles 0.1.5406 → 0.1.5475

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 (93) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +42 -55
  3. package/README.zh-CN.md +59 -73
  4. package/THIRD_PARTY_NOTICES.md +4 -7
  5. package/dist/acp-host/production-host.js +8261 -9073
  6. package/dist/collector-tool-schemas.js +1 -1
  7. package/dist/compliance-transport.js +2 -5
  8. package/dist/engine-detour-usage.js +3 -30
  9. package/dist/gatekeeper-role.js +4 -18
  10. package/dist/headless-host/description.js +0 -209
  11. package/dist/headless-host/production-host.js +6973 -8035
  12. package/dist/host-contracts.js +8 -0
  13. package/dist/migrate-book-topology.js +30 -27
  14. package/dist/packaged-role-registry.js +1 -9
  15. package/dist/pi/role-turn-host.js +37 -19
  16. package/dist/public-cli/auto-resume.js +29 -66
  17. package/dist/public-cli/countersign-run.js +4 -18
  18. package/dist/public-cli/instruction-seat-run.js +59 -76
  19. package/dist/public-cli/main.js +9844 -10720
  20. package/dist/public-cli/option-definitions.js +1 -1
  21. package/dist/public-cli/post-admission.js +204 -285
  22. package/dist/public-cli/process-cancel.js +8 -0
  23. package/dist/public-cli/run-lifecycle.js +24 -94
  24. package/dist/public-cli/settlement.js +614 -1524
  25. package/dist/public-cli/terminal.js +23 -53
  26. package/dist/public-role-summons.js +3 -8
  27. package/dist/receipt-delivery-policy.js +11 -12
  28. package/dist/run-terminal-artifacts.js +19 -14
  29. package/dist/serialize-thrown-value.js +7 -1
  30. package/dist/submission-ledger.js +21 -89
  31. package/package.json +1 -1
  32. package/packets/fixer-repair.md +3 -1
  33. package/packets/judge-apply.md +7 -16
  34. package/packets/judge-authority.md +7 -17
  35. package/packets/judge-plan.md +7 -18
  36. package/packets/judge-review.md +8 -22
  37. package/packets/judge-submission.md +4 -7
  38. package/resources/collector-bot-handbook.md +2 -2
  39. package/resources/diarist-collect.md +2 -2
  40. package/resources/engine-dispatch.md +2 -2
  41. package/resources/navigator-route-playbook.md +1 -1
  42. package/resources/secretariat-ticket-identity.md +1 -1
  43. package/scripts/build-package.mjs +0 -2
  44. package/souls/coder-output-guide.md +3 -3
  45. package/souls/coder.md +2 -3
  46. package/souls/doctor-auditor.md +0 -3
  47. package/souls/fixer-auditor.md +0 -2
  48. package/souls/fixer-output-guide.md +3 -3
  49. package/souls/fixer.md +2 -2
  50. package/souls/gate-output-guide.md +1 -1
  51. package/souls/quality-law.md +2 -0
  52. package/src/acp-host/role-turn-host.ts +168 -75
  53. package/src/collector-tool-schemas.ts +1 -1
  54. package/src/compliance-transport.ts +3 -6
  55. package/src/engine-detour-usage.ts +3 -33
  56. package/src/external-host-turn-loop.ts +81 -36
  57. package/src/gatekeeper-role.ts +5 -20
  58. package/src/headless-host/description.ts +4 -214
  59. package/src/headless-host/production-host.ts +2 -1
  60. package/src/headless-host/role-turn-host.ts +153 -186
  61. package/src/host-contracts.ts +13 -10
  62. package/src/navigator-public-session.ts +1 -1
  63. package/src/navigator-session-contracts.ts +1 -1
  64. package/src/package-contracts/gatekeeper-output.ts +1 -1
  65. package/src/package-contracts/navigator-output.ts +1 -1
  66. package/src/package-contracts/terminating-infrastructure.ts +1 -1
  67. package/src/packaged-role-registry.ts +2 -10
  68. package/src/pi/adapter.ts +0 -3
  69. package/src/pi/role-turn-host.ts +42 -28
  70. package/src/prepared-role-turn.ts +2 -0
  71. package/src/public-cli/auto-resume.ts +27 -66
  72. package/src/public-cli/countersign-run.ts +4 -22
  73. package/src/public-cli/instruction-seat-run.ts +79 -80
  74. package/src/public-cli/option-definitions.ts +1 -1
  75. package/src/public-cli/post-admission.ts +264 -340
  76. package/src/public-cli/process-cancel.ts +9 -0
  77. package/src/public-cli/run-lifecycle.ts +25 -126
  78. package/src/public-cli/settlement.ts +792 -1912
  79. package/src/public-cli/terminal.ts +29 -69
  80. package/src/public-role-summons.ts +3 -8
  81. package/src/receipt-delivery-policy.ts +11 -12
  82. package/src/role-envelope.ts +16 -9
  83. package/src/role-runtime.ts +14 -81
  84. package/src/run-terminal-artifacts.ts +21 -17
  85. package/src/serialize-thrown-value.ts +7 -1
  86. package/src/submission-ledger.ts +30 -116
  87. package/dist/audit-escalation.js +0 -7
  88. package/dist/pi/known-failure.js +0 -51
  89. package/dist/typed-provider-http.js +0 -57
  90. package/dist/upstream-error-testimony.js +0 -19
  91. package/src/pi/known-failure.ts +0 -76
  92. package/src/typed-provider-http.ts +0 -89
  93. package/src/upstream-error-testimony.ts +0 -45
@@ -73,45 +73,36 @@ import {
73
73
  import type { CredentialProviders, SeatModelConfig } from "./config.ts";
74
74
  import {
75
75
  clearCurrentCourt,
76
- clearTypedProviderHttpObservation,
77
76
  describeErrorIdentity,
78
77
  markRunRunning,
79
78
  readCurrentCourt,
80
79
  recordCurrentCourt,
81
- renderResumeCommand,
82
80
  type CurrentCourtState,
83
81
  type RunWriterLease,
84
- type TypedProviderHttpObservation,
85
82
  } from "./run-lifecycle.ts";
86
83
  import {
87
84
  processCancelDiagnostic,
88
85
  processCancelSignalName,
89
86
  type CatchableProcessSignal,
90
87
  } from "./process-cancel.ts";
91
- import { homeFromRunDirectory } from "../activation-ledger-topology.ts";
92
88
  import { DEFAULT_ROLE_TURN_HOST } from "../host-descriptions.ts";
93
89
  import { recordRunStart } from "../host-session-record.ts";
94
90
  import {
95
- attemptProducedFreshSubmission,
96
91
  classifyPostAdmissionFailure,
97
- controlledFailureInputFromResolution,
98
92
  exitCodeForTerminalOutcome,
99
93
  explicitInternalKnownFailureClassificationInput,
100
94
  formatCliDiagnostic,
101
95
  formatErrorCauseDetail,
102
- formatTerminalResult,
103
- inspectJudgeSession,
96
+ ledgerReadScope,
104
97
  isLawfulTypedTerminalOutcome,
105
- presentFailureTerminal,
106
- publishedFailureErrorPath,
107
98
  presentStructuralRejection,
108
- resolveAuditedRunnerFailureResolution,
109
- resolveControlledFailureResumeObservation,
110
99
  settleFailureTerminalResult,
111
100
  settleHostEndedNoReceipt,
101
+ noteSettlementFault,
112
102
  attachRecordedSubmissions,
113
- rewritePublishedFailureErrorPath,
103
+ retainPackageFault,
114
104
  type ControlledFailure,
105
+ type PackageSideFact,
115
106
  type SettlementCourtScope,
116
107
  } from "./settlement.ts";
117
108
  import type { CliIo } from "./cli-io.ts";
@@ -123,9 +114,9 @@ import {
123
114
  import {
124
115
  ensureRealArtifactsDirectory,
125
116
  persistReturnedRunState,
117
+ presentTerminal,
126
118
  runWithAutoResumeLoop,
127
119
  TurnDispatchedFailure,
128
- writeHardenedArtifactFile,
129
120
  } from "./auto-resume.ts";
130
121
 
131
122
  import { isRecord, errorText } from "../unknown-value.ts";
@@ -152,11 +143,6 @@ function projectRelocatedTurnIdentity(
152
143
  }
153
144
  }
154
145
  if (result.terminal !== undefined) {
155
- rewritePublishedFailureErrorPath(
156
- result.terminal,
157
- relocation.oldRunDirectory,
158
- relocation.newRunDirectory,
159
- );
160
146
  for (const artifact of result.terminal.artifacts) {
161
147
  artifact.path = rewrite(artifact.path) as string;
162
148
  }
@@ -188,6 +174,7 @@ async function settleProcessCancelAfterTurn<A extends AdmittedRoleInvocation, T
188
174
  persistRunState: boolean;
189
175
  deferredPersist: { needsPersist?: true } | Record<string, never>;
190
176
  invocationScopeId: string | undefined;
177
+ notePackageFault?: (diagnostic: string) => void | Promise<void>;
191
178
  }): Promise<{
192
179
  exitCode: number;
193
180
  admitted: A;
@@ -203,7 +190,13 @@ async function settleProcessCancelAfterTurn<A extends AdmittedRoleInvocation, T
203
190
  timedOut: input.result.timedOut,
204
191
  code: input.result.code,
205
192
  stderr: input.result.stderr,
206
- 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,
207
200
  },
208
201
  input.invocationScopeId,
209
202
  ),
@@ -211,6 +204,7 @@ async function settleProcessCancelAfterTurn<A extends AdmittedRoleInvocation, T
211
204
  input.env.principalAuthority,
212
205
  input.io,
213
206
  input.persistRunState,
207
+ input.notePackageFault,
214
208
  );
215
209
  return {
216
210
  ...failed,
@@ -259,41 +253,32 @@ export const POST_ADMISSION_CLEANUP_DIAGNOSTIC_ENTRY_TYPE =
259
253
  * zero durable trace (#840 bounce class 2); a healthy session never gets a
260
254
  * redundant second copy (#840 bounce class 1: 同一业务规则只保留一个权威实现).
261
255
  */
262
- async function recordBestEffortPostDispatchDiagnostic<A extends AdmittedRoleInvocation>(
263
- admitted: A,
264
- env: PostAdmissionEnv,
256
+ async function recordBestEffortPostDispatchDiagnostic(
257
+ admitted: Pick<AdmittedRoleInvocation, "runDirectory" | "principal">,
258
+ env: Pick<PostAdmissionEnv, "sessionAppender" | "principalAuthority">,
265
259
  diagnostic: string,
266
- io: CliIo,
260
+ io: Pick<CliIo, "stderr">,
267
261
  ): Promise<void> {
268
- const payload = { diagnostic, recordedAt: new Date().toISOString() };
269
- let retentionFailure: string | undefined;
270
- try {
271
- await env.sessionAppender(
262
+ await retainPackageFault({
263
+ runDirectory: admitted.runDirectory,
264
+ diagnostic,
265
+ appendSession: (payload) => env.sessionAppender(
272
266
  env.principalAuthority,
273
267
  admitted.principal,
274
268
  POST_ADMISSION_CLEANUP_DIAGNOSTIC_ENTRY_TYPE,
275
269
  payload,
276
- );
277
- } catch (appendError) {
278
- try {
279
- const artifactsDir = await ensureRealArtifactsDirectory(admitted.runDirectory);
280
- await writeHardenedArtifactFile(artifactsDir, "post-admission-diagnostic", {
281
- version: 1,
282
- ...payload,
283
- });
284
- } catch (artifactError) {
285
- retentionFailure =
286
- `post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; artifact=${describeErrorIdentity(artifactError)}`;
287
- }
288
- }
289
- try {
290
- io.stderr(formatCliDiagnostic(diagnostic));
291
- if (retentionFailure !== undefined) {
292
- io.stderr(formatCliDiagnostic(retentionFailure));
293
- }
294
- } catch {
295
- // Presentation is best-effort beside an already-formed host terminal.
296
- }
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);
297
282
  }
298
283
 
299
284
  export type PostAdmissionEnv = {
@@ -353,18 +338,15 @@ export type PostAdmissionAdapters<
353
338
  A extends AdmittedRoleInvocation = AdmittedRoleInvocation,
354
339
  T extends TerminalResult = TerminalResult,
355
340
  > = {
341
+ /** A present Terminal already carries run history from settlement. */
356
342
  trySettle: (
357
343
  admitted: A,
358
344
  authority: DurablePrincipalAuthority,
359
- /** Current court turn scope (#637); omit for run-scoped sealed reads. */
360
- 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 },
361
347
  ) => Promise<T | undefined>;
362
348
  /** Default: isLawfulTypedTerminalOutcome(terminal.roleOutcome). */
363
349
  shouldPresentSettled?: (terminal: T) => boolean;
364
- resolveRunnerKnownFailure?: (input: {
365
- result: RoleTurnResult;
366
- sessionFile: string;
367
- }) => Promise<RoleTurnKnownFailure | undefined>;
368
350
  /** A child office may pause this run with its unchanged terminal before a host turn starts. */
369
351
  beforeDispatch?: (admitted: A, lease?: RunWriterLease) => Promise<T | void> | T | void;
370
352
  /**
@@ -395,13 +377,22 @@ export type ControlledFailureInput = {
395
377
  knownDiagnostic?: string;
396
378
  /** Secondary evidence already owned by the typed production failure channel. */
397
379
  knownDetails?: Readonly<Record<string, unknown>>;
398
- typedHttpObservationSettled?: true;
399
- typedHttpObservation?: TypedProviderHttpObservation;
400
380
  /**
401
- * #836: set when the caller already attempted markRunTerminal/markRunResumable
402
- * and it is what threw `thrown` — presentControlledFailure must not retry
403
- * the same known-failing run-state write a second time; the caught error is
404
- * 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.
405
396
  */
406
397
  skipRunStateWrite?: boolean;
407
398
  /** Public-invocation scope from the shared Host envelope (#537). */
@@ -460,6 +451,12 @@ export async function resolveResumeMethodMaterialAdapters<
460
451
  input.emptyAdapters,
461
452
  input.authority,
462
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
+ }),
463
460
  );
464
461
  return { kind: "terminal", ...terminal };
465
462
  }
@@ -475,45 +472,41 @@ export async function presentControlledFailure<
475
472
  authority: DurablePrincipalAuthority,
476
473
  io: CliIo,
477
474
  persistRunState = true,
475
+ notePackageFault?: (diagnostic: string) => void | Promise<void>,
478
476
  ): Promise<{
479
477
  exitCode: number;
480
478
  admitted: A;
481
479
  terminal: TerminalResult;
482
480
  }> {
483
481
  const hasThrown = Object.hasOwn(failureInput, "thrown");
484
- const resumeObservation = await resolveControlledFailureResumeObservation({
485
- runDirectory: admitted.runDirectory,
486
- ...(failureInput.typedHttpObservationSettled === true
487
- ? {
488
- typedHttpObservationSettled: true as const,
489
- ...(failureInput.typedHttpObservation === undefined
490
- ? {}
491
- : { typedHttpObservation: failureInput.typedHttpObservation }),
492
- }
493
- : {}),
494
- });
495
- const knownFailure =
496
- failureInput.knownFailure ?? resumeObservation.observationReadFailure;
497
- const session =
498
- !hasThrown &&
499
- !failureInput.timedOut &&
500
- knownFailure === undefined &&
501
- failureInput.knownCause === undefined &&
502
- admitted.principal !== undefined
503
- ? await inspectJudgeSession(authority.decode(admitted.principal).sessionFile)
504
- : undefined;
482
+ const knownFailure = failureInput.knownFailure;
505
483
  // knownFailure channel owns details when present; otherwise caller knownDetails.
506
484
  const fromKnownFailure =
507
485
  explicitInternalKnownFailureClassificationInput(knownFailure);
508
- 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({
509
493
  timedOut: failureInput.timedOut,
510
494
  code: failureInput.code,
511
495
  stderr: failureInput.stderr,
512
496
  ...(hasThrown ? { thrown: failureInput.thrown } : {}),
513
- ...(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
514
502
  ? {}
515
- : { knownDetails: failureInput.knownDetails }),
516
- ...fromKnownFailure,
503
+ : { knownCause: fromKnownFailure.knownCause }),
504
+ ...(fromKnownFailure.knownIdentity === undefined
505
+ ? {}
506
+ : { knownIdentity: fromKnownFailure.knownIdentity }),
507
+ ...(fromKnownFailure.knownDiagnostic === undefined
508
+ ? {}
509
+ : { knownDiagnostic: fromKnownFailure.knownDiagnostic }),
517
510
  ...(failureInput.knownCause === undefined
518
511
  ? {}
519
512
  : { knownCause: failureInput.knownCause }),
@@ -523,23 +516,35 @@ export async function presentControlledFailure<
523
516
  ...(failureInput.knownDiagnostic === undefined
524
517
  ? {}
525
518
  : { knownDiagnostic: failureInput.knownDiagnostic }),
526
- ...(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 }),
527
524
  });
528
-
529
- // #665 / #416: typed 429 with a run principal offers resume; the host decides availability.
530
- 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 };
531
528
 
532
529
  if (persistRunState && !failureInput.skipRunStateWrite) {
533
- 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
+ }
534
540
  }
535
541
 
536
542
  let publishedErrorPath: string | undefined;
537
543
  let terminal: TerminalResult;
538
544
  try {
539
545
  const settled = await settleFailureTerminalResult(admitted, failure, authority, {
540
- ...(resumable
541
- ? { resume: { command: renderResumeCommand(admitted.runId) } }
542
- : {}),
546
+ recordAttemptHistory: true,
547
+ ...(notePackageFault === undefined ? {} : { notePackageFault }),
543
548
  ...(failureInput.invocationScopeId === undefined ||
544
549
  failureInput.invocationScopeId.length === 0
545
550
  ? {}
@@ -553,7 +558,7 @@ export async function presentControlledFailure<
553
558
  }
554
559
  throw error;
555
560
  }
556
- presentFailureTerminal(terminal, io);
561
+ await presentTerminal(terminal, io, admitted.runDirectory);
557
562
  return {
558
563
  exitCode: exitCodeForTerminalOutcome(terminal.roleOutcome),
559
564
  admitted,
@@ -582,6 +587,7 @@ async function settleAfterTurnStarted<
582
587
  authority: DurablePrincipalAuthority,
583
588
  io: CliIo,
584
589
  persistRunState: boolean,
590
+ notePackageFault?: (diagnostic: string) => void | Promise<void>,
585
591
  ): Promise<{ exitCode: number; admitted: A; terminal: T }> {
586
592
  try {
587
593
  return (await presentControlledFailure(
@@ -591,6 +597,7 @@ async function settleAfterTurnStarted<
591
597
  authority,
592
598
  io,
593
599
  persistRunState,
600
+ notePackageFault,
594
601
  )) as { exitCode: number; admitted: A; terminal: T };
595
602
  } catch (error) {
596
603
  throw new TurnDispatchedFailure(error);
@@ -601,47 +608,32 @@ async function settleAfterTurnStarted<
601
608
  * Persist run-state for a result dispatchPostAdmissionTurn deferred
602
609
  * (needsPersist — station-child / resumable auto-resume loop, #416/#840):
603
610
  * that write must land outside the loop's own retried-dispatch try, but its
604
- * failure still settles through the single existing controlled-failure
605
- * authority (settleAfterTurnStarted / presentControlledFailure, ADR 0080:
606
- * one settlement disposition owner) — never a second hand-rolled
607
- * classify/artifact/Terminal (#836 r12 class 3), and never lawful/non-lawful
608
- * settling differently (#836 r13 class 2: a caller's io here may be a no-op,
609
- * so a stderr-only trace is never seen and the failure is otherwise
610
- * swallowed — 失败诚实宪法 真因必须落痕). Both cases settle through
611
- * settleAfterTurnStarted, which attaches the run's already-recorded ledger
612
- * submissions to the new failure terminal and returns skipAutoResume so the
613
- * 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.
614
614
  */
615
615
  async function settleDeferredPersist<
616
616
  A extends AdmittedRoleInvocation,
617
617
  T extends TerminalResult,
618
618
  >(
619
619
  admitted: A,
620
- authority: DurablePrincipalAuthority,
621
- adapters: PostAdmissionAdapters<A, T>,
620
+ env: PostAdmissionEnv,
622
621
  io: CliIo,
623
622
  result: DispatchOutcomeFor<A, T>,
624
623
  ): Promise<DispatchOutcomeFor<A, T>> {
625
624
  if (result.needsPersist !== true || result.terminal === undefined) return result;
626
625
  const { needsPersist: _needsPersist, ...settledResult } = result;
627
- const lawful = isLawfulTypedTerminalOutcome(result.terminal.roleOutcome);
628
626
  try {
629
- await persistReturnedRunState(admitted, lawful ? { lawful: true } : undefined);
627
+ await persistReturnedRunState(admitted);
630
628
  return settledResult;
631
629
  } catch (error) {
632
- const failed = await settleAfterTurnStarted(
630
+ await recordBestEffortPostDispatchDiagnostic(
633
631
  admitted,
634
- { timedOut: false, code: null, stderr: "", thrown: error, skipRunStateWrite: true },
635
- adapters,
636
- authority,
632
+ env,
633
+ `run-state persistence failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`,
637
634
  io,
638
- true,
639
635
  );
640
- return {
641
- ...failed,
642
- turnDispatched: true as const,
643
- skipAutoResume: true as const,
644
- };
636
+ return settledResult;
645
637
  }
646
638
  }
647
639
 
@@ -740,21 +732,16 @@ async function settleCompletedHostTurn<
740
732
  persistRunState: boolean;
741
733
  deferredPersist: { needsPersist?: true } | Record<string, never>;
742
734
  finishAfterTurn: (result: DispatchOutcomeFor<A, T>) => Promise<DispatchOutcomeFor<A, T>>;
735
+ courtScope: SettlementCourtScope & { notePackageFault: (diagnostic: string) => Promise<void> };
736
+ markPending: (terminal: T) => void;
743
737
  }): Promise<{ kind: "settled"; outcome: DispatchOutcomeFor<A, T> } | { kind: "still-missing" }> {
744
738
  const { admitted, env, io, adapters, shouldPresent, request, result, persistRunState, deferredPersist } = input;
745
739
  const finishAfterTurn = input.finishAfterTurn;
746
740
 
747
- // `result.stderr` stays live in memory for the real classification below
748
- // regardless of whether this durable mirror write succeeds. A write
749
- // failure here (unwritable run directory, stderr.log occupied as a
750
- // directory, ...) is a real infrastructure problem and must never be
751
- // dropped silently (catch-and-continue with no trace is a defect) — but
752
- // it also must never become THE controlling failure and wash out a
753
- // primary child signal that already arrived on `result`, or an
754
- // already-sealed accepted payload (#836: a secondary failure must not
755
- // overwrite an already-known cause or an already-recorded leg). It rides
756
- // beside whatever the real classification below determines, and only
757
- // becomes the reported failure itself when nothing else is wrong.
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.
758
745
  //
759
746
  // Everything below runs after the host turn genuinely started (#840 r9
760
747
  // 判词 class 1 boundary — 覆盖 executeTurn 已启动后至返回带 turnDispatched
@@ -780,12 +767,9 @@ async function settleCompletedHostTurn<
780
767
  );
781
768
  } catch (error) {
782
769
  stderrLogWriteFailure = error;
783
- // Best-effort: the turn's own stderr capture is secondary to lawful /
784
- // controlled-failure settlement below, but the failure itself must
785
- // still leave a real trace (失败诚实宪法 真因必须落痕) — durably, since
786
- // every auto-resume attempt's io is dummyIo (#840 r9 判词 class 1). It
787
- // still rides beside the classification below and, if nothing else is
788
- // wrong, becomes the reported failure itself (#836).
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.
789
773
  await recordBestEffortPostDispatchDiagnostic(
790
774
  admitted,
791
775
  env,
@@ -794,151 +778,51 @@ async function settleCompletedHostTurn<
794
778
  );
795
779
  }
796
780
 
797
- const courtScope = settlementScopeForTurn(request);
798
-
799
- // Single complete boundary (#840 r9 判词 class 1): resolving the
800
- // host/runner failure facts, trySettle, its shouldPresent gate, and the
801
- // accepted-settlement cleanup all settle through this one catch — a
802
- // throw from any of them (including the session-file decode itself)
803
- // still routes through settleAfterTurnStarted's TurnDispatchedFailure
804
- // instead of losing turnDispatched to an uncaught throw. The facts are
805
- // resolved before trySettle (#836) so an already-accepted settlement is
806
- // never presented over a real current host/runner failure signal or a
807
- // real stderr.log durable-write failure — both stay a real failure with
808
- // the recorded payload riding beside it, not replacing it.
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.
809
785
  let settled: T | undefined;
810
786
  let settledOutcome:
811
787
  | { exitCode: number; admitted: A; terminal: T; turnDispatched: true }
812
788
  | undefined;
813
- let hostSignalFailed = false;
814
- let resolution: Awaited<ReturnType<typeof resolveAuditedRunnerFailureResolution>> | 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;
815
800
  try {
816
- const sessionFile =
817
- admitted.principal !== undefined
818
- ? env.principalAuthority.decode(admitted.principal).sessionFile
819
- : "";
820
- const runnerKnownFailure =
821
- adapters.resolveRunnerKnownFailure !== undefined && sessionFile !== ""
822
- ? await adapters.resolveRunnerKnownFailure({ result, sessionFile })
823
- : result.knownFailure;
824
- resolution = await resolveAuditedRunnerFailureResolution({
825
- runner: runnerKnownFailure,
826
- sessionFile,
827
- credential: undefined,
828
- runDirectory: admitted.runDirectory,
829
- });
830
- // A direct, current signal from the host/runner itself (timeout / host
831
- // knownFailure / runner knownFailure / missing credential) is a real
832
- // problem regardless of what else already settled — a settled accepted
833
- // outcome must not paper over it (it rides beside the recorded payload
834
- // via `submissions`, never replacing the payload). The audited
835
- // resolution's own typed session read (malformed JSONL, provider-stop,
836
- // an illegal/unsealed accepted status, a stale/superseded typed-HTTP
837
- // observation, ...) makes a run with no sealed acceptance a true
838
- // failure too — it must not be discarded into a lawful no_receipt just
839
- // because the raw runner signal alone looked clean. But it must not
840
- // retroactively invalidate an acceptance that already sealed this turn
841
- // — a resolved 429 observed earlier in the same session is exactly
842
- // that.
843
- const processCancelName = processCancelSignalName(env.signal);
844
- const directHostFailureSignal =
845
- result.timedOut
846
- || result.knownFailure !== undefined
847
- || runnerKnownFailure !== undefined
848
- || processCancelName !== undefined;
849
- hostSignalFailed =
850
- directHostFailureSignal
851
- || (result.code !== null && result.code !== 0)
852
- || resolution.knownFailure !== undefined;
853
- settled = await adapters.trySettle(admitted, env.principalAuthority, courtScope);
854
- if (settled !== undefined) {
855
- settled = await attachRecordedSubmissions(admitted, settled, courtScope) as T;
856
- }
857
- // #836 r12 class 2: an accepted/audit_escalation settlement can be
858
- // entirely a prior attempt's stale payload (courtAttempt is a
859
- // recording tag, not a visibility gate — attachRecordedSubmissions
860
- // above still surfaces that historical payload honestly either way).
861
- // Consume the ledger's own subject.attemptId (submission-ledger.ts)
862
- // only to learn whether *this* attempt itself produced a fresh seal —
863
- // never to filter what presents.
864
- const settledIsFreshThisAttempt =
865
- settled === undefined
866
- || (settled.roleOutcome.kind !== "accepted" && settled.roleOutcome.kind !== "audit_escalation")
867
- ? true
868
- : await attemptProducedFreshSubmission(admitted, courtScope);
869
- // A lawful settled outcome already reached this turn takes precedence
870
- // over a later bare exit-code / session-inspection signal (trailing
871
- // nonzero exit, late stderr noise, a stale already-superseded
872
- // typed-HTTP observation, ...) — but never over a direct current
873
- // host/runner failure signal, nor over a real stderr.log durable-write
874
- // failure (confirmed infrastructure trouble, not weak/bare evidence),
875
- // both of which stay a real failure with the recorded payload riding
876
- // beside it, not replacing it (#836: never kill an already-recorded
877
- // leg, but never wash a real failure away either). Note: the narrower
878
- // directHostFailureSignal gates acceptance here; the broader
879
- // hostSignalFailed (adds bare exit code / resolution.knownFailure)
880
- // only gates the no-settlement fallback below — except when the
881
- // settlement itself is entirely stale (no fresh seal this attempt),
882
- // where a bare nonzero exit / resolution failure must not be outranked
883
- // by someone else's earlier success (#836 r12 class 2).
884
- const staleAcceptanceOutranksRealFailure = !settledIsFreshThisAttempt && hostSignalFailed;
885
- // #855: re-read cancel after trySettle/attach awaits — a signal in this
886
- // window must not land as lawful accepted.
887
- const cancelAfterSettle = processCancelSignalName(env.signal);
888
- if (
889
- settled !== undefined
890
- && shouldPresent(settled)
891
- && !directHostFailureSignal
892
- && !staleAcceptanceOutranksRealFailure
893
- && stderrLogWriteFailure === undefined
894
- && cancelAfterSettle === undefined
895
- ) {
896
- // This court sealed — drop open-court pointer (bare resume no longer continues it).
801
+ if (!hostSignalFailed) {
802
+ settled = await adapters.trySettle(admitted, env.principalAuthority, { ...courtScope, previewOnly: true });
803
+ const cancelAfterSettle = processCancelSignalName(env.signal);
897
804
  if (
898
- settled.roleOutcome.kind === "accepted" &&
899
- request.courtAttemptId !== undefined &&
900
- request.courtAttemptId.length > 0
805
+ settled !== undefined
806
+ && shouldPresent(settled)
807
+ && cancelAfterSettle === undefined
901
808
  ) {
902
- try {
903
- await clearCurrentCourt(admitted.runDirectory, request.courtAttemptId);
904
- } catch (error) {
905
- // Settlement already sealed accepted — a cleanup failure here must
906
- // not erase that fact or make the caller replay this court's
907
- // summons over already-delivered work (#840 已交劳动只整理终局不重做).
908
- // Preserve the cleanup failure as a post-dispatch diagnostic; the
909
- // accepted settlement remains authoritative (#840 r9 判词 class 1).
910
- await recordBestEffortPostDispatchDiagnostic(
911
- admitted,
912
- env,
913
- `current-court cleanup failed after accepted settlement (best-effort continue): ${describeErrorIdentity(error)}`,
914
- io,
915
- );
916
- }
809
+ settledOutcome = {
810
+ exitCode: exitCodeForTerminalOutcome(settled.roleOutcome),
811
+ admitted,
812
+ terminal: settled,
813
+ turnDispatched: true as const,
814
+ };
917
815
  }
918
- settledOutcome = {
919
- exitCode: exitCodeForTerminalOutcome(settled.roleOutcome),
920
- admitted,
921
- terminal: settled,
922
- turnDispatched: true as const,
923
- };
924
816
  }
925
817
  } catch (error) {
926
- // Settle (or its shouldPresent gate, or the failure-fact resolution
927
- // above) throw is a real failure fact — never swallow into undefined.
928
- const settledFailure = await settleAfterTurnStarted(
929
- admitted,
930
- withEngineDetourInvocationScope({
931
- timedOut: false,
932
- code: result.code,
933
- stderr: result.stderr,
934
- thrown: error,
935
- }, request.invocationScopeId),
936
- adapters,
937
- env.principalAuthority,
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)}`,
938
824
  io,
939
- persistRunState,
940
825
  );
941
- return { kind: "settled", outcome: await finishAfterTurn({ ...settledFailure, turnDispatched: true as const, ...deferredPersist }) };
942
826
  }
943
827
  if (settledOutcome !== undefined) {
944
828
  // #855: final cancel re-read before lawful seal — settlement window may
@@ -956,12 +840,13 @@ async function settleCompletedHostTurn<
956
840
  persistRunState,
957
841
  deferredPersist,
958
842
  invocationScopeId: request.invocationScopeId,
843
+ notePackageFault: courtScope.notePackageFault,
959
844
  }),
960
845
  ) };
961
846
  }
962
847
  if (persistRunState) {
963
848
  try {
964
- await persistReturnedRunState(admitted, { lawful: true });
849
+ await persistReturnedRunState(admitted);
965
850
  } catch (error) {
966
851
  await recordBestEffortPostDispatchDiagnostic(
967
852
  admitted,
@@ -971,17 +856,16 @@ async function settleCompletedHostTurn<
971
856
  );
972
857
  }
973
858
  }
974
- // Lawful persist + present is the caller's stop seam (auto-resume loop /
859
+ // Persist + present is the caller's stop seam (auto-resume loop /
975
860
  // manual resume), not this retried host-turn function.
976
861
  // #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
862
+ input.markPending(settledOutcome.terminal);
977
863
  return { kind: "settled", outcome: await finishAfterTurn({ ...settledOutcome, ...deferredPersist }) };
978
864
  }
979
865
 
980
866
  // Host/runner true failure coexists with already-recorded payloads — never wash as accepted.
981
867
  if (hostSignalFailed) {
982
- const resolutionInput = controlledFailureInputFromResolution(resolution!);
983
- const processCancelName = processCancelSignalName(env.signal);
984
- const stderrLogWriteDetails =
868
+ const stderrLogWriteFact =
985
869
  stderrLogWriteFailure === undefined
986
870
  ? undefined
987
871
  : { stderrLogWriteFailure: describeCaughtError(stderrLogWriteFailure) };
@@ -991,28 +875,23 @@ async function settleCompletedHostTurn<
991
875
  timedOut: result.timedOut,
992
876
  code: result.code,
993
877
  stderr: result.stderr,
994
- ...resolutionInput,
995
- ...(processCancelName === undefined
996
- ? {}
997
- : { knownDiagnostic: processCancelDiagnostic(processCancelName) }),
998
- // Secondary fact only — never the cause. Rides on whichever channel
999
- // classification actually reads (knownFailure.details owns it when
1000
- // a knownFailure exists; the top-level knownDetails otherwise).
1001
- ...(stderrLogWriteDetails === undefined
1002
- ? {}
1003
- : resolutionInput.knownFailure !== undefined
1004
- ? {
1005
- knownFailure: {
1006
- ...resolutionInput.knownFailure,
1007
- details: { ...(resolutionInput.knownFailure.details ?? {}), ...stderrLogWriteDetails },
1008
- },
1009
- }
1010
- : { knownDetails: stderrLogWriteDetails }),
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 }),
1011
889
  }, request.invocationScopeId),
1012
890
  adapters,
1013
891
  env.principalAuthority,
1014
892
  io,
1015
893
  persistRunState,
894
+ courtScope.notePackageFault,
1016
895
  );
1017
896
  return { kind: "settled", outcome: await finishAfterTurn(
1018
897
  withProcessCancelSkipAutoResume(
@@ -1022,28 +901,6 @@ async function settleCompletedHostTurn<
1022
901
  ) };
1023
902
  }
1024
903
 
1025
- // Nothing else was wrong, but the durable stderr mirror itself failed to
1026
- // write — that is the real (infrastructure) problem in this case, not a
1027
- // lawful absence of a receipt. Honest and loud, not silently no_receipt.
1028
- if (stderrLogWriteFailure !== undefined) {
1029
- const failed = await settleAfterTurnStarted(
1030
- admitted,
1031
- withEngineDetourInvocationScope({
1032
- timedOut: false,
1033
- code: result.code,
1034
- stderr: result.stderr,
1035
- thrown: stderrLogWriteFailure,
1036
- }, request.invocationScopeId),
1037
- adapters,
1038
- env.principalAuthority,
1039
- io,
1040
- persistRunState,
1041
- );
1042
- return { kind: "settled", outcome: await finishAfterTurn({ ...failed, turnDispatched: true as const, ...deferredPersist }) };
1043
- }
1044
-
1045
- // No receipt at all after a clean host turn: lawful no_receipt, but the
1046
- // caller owns whether to催交 first (票面 3).
1047
904
  return { kind: "still-missing" };
1048
905
  }
1049
906
 
@@ -1074,6 +931,24 @@ export async function dispatchPostAdmissionTurn<
1074
931
  * lease release — success and failure alike. When a primary failure terminal already
1075
932
  * exists, keep that cause and leave relocate failure on the shared diagnostic channel.
1076
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
+
1077
952
  let afterDispatchApplied = false;
1078
953
  const receiptDelivery = input.receiptDelivery ?? createReceiptDeliveryPolicy(env.autoResumeLimit);
1079
954
  let turnDispatched = false;
@@ -1083,12 +958,13 @@ export async function dispatchPostAdmissionTurn<
1083
958
  // again here would double-deliver on one run. External hosts (ACP / headless
1084
959
  // CLI) have no such in-child loop, so the AK execution seam owns it for them.
1085
960
  const akSeamDeliversReceipt =
1086
- env.host !== undefined && env.host !== "" && env.host !== DEFAULT_ROLE_TURN_HOST;
961
+ env.host !== undefined && env.host !== "" && env.host !== DEFAULT_ROLE_TURN_HOST;
1087
962
  const finishAfterTurn = async (result: DispatchOutcomeFor<A, T>): Promise<DispatchOutcomeFor<A, T>> => {
1088
963
  if (afterDispatchApplied) return result;
1089
964
  afterDispatchApplied = true;
1090
- // #855: re-read cancel before afterDispatch finalization — never present
1091
- // 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
1092
968
  // may already be terminal from a prior lawful persist; do not reopen as resumable.
1093
969
  const cancelBeforeFinish = processCancelSignalName(env.signal);
1094
970
  if (
@@ -1105,6 +981,7 @@ export async function dispatchPostAdmissionTurn<
1105
981
  code: null,
1106
982
  stderr: "",
1107
983
  knownDiagnostic: processCancelDiagnostic(cancelBeforeFinish),
984
+ cancelName: cancelBeforeFinish,
1108
985
  skipRunStateWrite: true,
1109
986
  },
1110
987
  request.invocationScopeId,
@@ -1113,12 +990,51 @@ export async function dispatchPostAdmissionTurn<
1113
990
  env.principalAuthority,
1114
991
  io,
1115
992
  persistRunState,
993
+ courtScope.notePackageFault,
1116
994
  )),
1117
995
  turnDispatched: true as const,
1118
996
  skipAutoResume: true as const,
1119
997
  ...deferredPersist,
1120
998
  };
1121
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
+ }
1122
1038
  try {
1123
1039
  // #858 / #1071: the first identifiable ticketNumber field declaration
1124
1040
  // files an unbound run (integer, digit string, or leading #N token).
@@ -1130,7 +1046,7 @@ export async function dispatchPostAdmissionTurn<
1130
1046
  const rows = await readRecordedSubmissionRows(
1131
1047
  admitted.projectRoot,
1132
1048
  admitted.runId,
1133
- { home: homeFromRunDirectory(admitted.runDirectory), sessionParent: join(admitted.runDirectory, "session", "session.jsonl") },
1049
+ ledgerReadScope(admitted),
1134
1050
  );
1135
1051
  let ticketNumber: number | undefined;
1136
1052
  for (const row of rows) {
@@ -1185,6 +1101,7 @@ export async function dispatchPostAdmissionTurn<
1185
1101
  env.principalAuthority,
1186
1102
  io,
1187
1103
  persistRunState,
1104
+ courtScope.notePackageFault,
1188
1105
  )) as { exitCode: number; admitted: A; terminal: T },
1189
1106
  ...(result.turnDispatched === true ? { turnDispatched: true as const } : {}),
1190
1107
  ...(result.skipAutoResume === true ? { skipAutoResume: true as const } : {}),
@@ -1195,7 +1112,6 @@ export async function dispatchPostAdmissionTurn<
1195
1112
  try {
1196
1113
  // #987: do not pre-block or classify host dispatch from AK credential
1197
1114
  // catalog facts. Per-invocation runner/host evidence owns provider identity.
1198
- await clearTypedProviderHttpObservation(admitted.runDirectory);
1199
1115
  // The authoritative host write (markRunRunning) is delayed to just before
1200
1116
  // executeTurn — not merely past beforeDispatch (#840 r9 判词 class 2). Any
1201
1117
  // pre-turn retry (beforeDispatch, continuation assembly) must not commit a host
@@ -1223,6 +1139,7 @@ export async function dispatchPostAdmissionTurn<
1223
1139
  env.principalAuthority,
1224
1140
  io,
1225
1141
  persistRunState,
1142
+ courtScope.notePackageFault,
1226
1143
  )) as { exitCode: number; admitted: A; terminal: T };
1227
1144
  return { ...settled, ...deferredPersist };
1228
1145
  }
@@ -1265,6 +1182,10 @@ export async function dispatchPostAdmissionTurn<
1265
1182
 
1266
1183
  let result: RoleTurnResult;
1267
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.
1268
1189
  recordRunStart(admitted.runDirectory);
1269
1190
  turnDispatched = true;
1270
1191
  result = await env.roleTurnHost.executeTurn(turnRequest);
@@ -1276,15 +1197,16 @@ export async function dispatchPostAdmissionTurn<
1276
1197
  timedOut: false,
1277
1198
  code: null,
1278
1199
  stderr: "",
1279
- thrown:
1280
- processCancelName === undefined
1281
- ? error
1282
- : 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,
1283
1204
  }, request.invocationScopeId),
1284
1205
  adapters,
1285
1206
  env.principalAuthority,
1286
1207
  io,
1287
1208
  persistRunState,
1209
+ courtScope.notePackageFault,
1288
1210
  );
1289
1211
  return await finishAfterTurn(
1290
1212
  withProcessCancelSkipAutoResume(
@@ -1307,6 +1229,8 @@ export async function dispatchPostAdmissionTurn<
1307
1229
  persistRunState,
1308
1230
  deferredPersist,
1309
1231
  finishAfterTurn,
1232
+ courtScope,
1233
+ markPending: (terminal) => { pendingSettlement = "sealed"; pendingTerminal = terminal; },
1310
1234
  });
1311
1235
  if (settledTurn.kind === "settled") return settledTurn.outcome;
1312
1236
 
@@ -1383,6 +1307,8 @@ export async function dispatchPostAdmissionTurn<
1383
1307
  persistRunState: false,
1384
1308
  deferredPersist,
1385
1309
  finishAfterTurn,
1310
+ courtScope,
1311
+ markPending: (terminal) => { pendingSettlement = "sealed"; pendingTerminal = terminal; },
1386
1312
  });
1387
1313
  // The delivery turn's own outcome IS the run's outcome, except when it
1388
1314
  // simply still has no receipt — that is the loop's own case to keep going.
@@ -1395,9 +1321,7 @@ export async function dispatchPostAdmissionTurn<
1395
1321
  // NOT write; this one, which ends the run, must.
1396
1322
  if (persistRunState && deliveryTurn.outcome.terminal !== undefined) {
1397
1323
  try {
1398
- await persistReturnedRunState(admitted, {
1399
- lawful: isLawfulTypedTerminalOutcome(deliveryTurn.outcome.terminal.roleOutcome),
1400
- });
1324
+ await persistReturnedRunState(admitted);
1401
1325
  } catch (error) {
1402
1326
  await recordBestEffortPostDispatchDiagnostic(
1403
1327
  admitted,
@@ -1411,7 +1335,7 @@ export async function dispatchPostAdmissionTurn<
1411
1335
  }
1412
1336
  }
1413
1337
 
1414
- const noReceiptScope = settlementScopeForTurn(request);
1338
+ const noReceiptScope = { ...courtScope, previewOnly: true as const };
1415
1339
  const noReceipt = await attachRecordedSubmissions(
1416
1340
  admitted,
1417
1341
  await settleHostEndedNoReceipt(
@@ -1436,12 +1360,13 @@ export async function dispatchPostAdmissionTurn<
1436
1360
  persistRunState,
1437
1361
  deferredPersist,
1438
1362
  invocationScopeId: request.invocationScopeId,
1363
+ notePackageFault: courtScope.notePackageFault,
1439
1364
  }),
1440
1365
  );
1441
1366
  }
1442
1367
  if (persistRunState) {
1443
1368
  try {
1444
- await persistReturnedRunState(admitted, { lawful: true });
1369
+ await persistReturnedRunState(admitted);
1445
1370
  } catch (error) {
1446
1371
  await recordBestEffortPostDispatchDiagnostic(
1447
1372
  admitted,
@@ -1452,6 +1377,8 @@ export async function dispatchPostAdmissionTurn<
1452
1377
  }
1453
1378
  }
1454
1379
  // #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
1380
+ pendingSettlement = "no_receipt";
1381
+ pendingTerminal = noReceipt;
1455
1382
  return await finishAfterTurn({
1456
1383
  exitCode: exitCodeForTerminalOutcome(noReceipt.roleOutcome),
1457
1384
  admitted,
@@ -1555,7 +1482,6 @@ export function resumeTurnRequestProjectionOptions(
1555
1482
  kind: "resume",
1556
1483
  prompt,
1557
1484
  },
1558
- ...(request.message === undefined ? {} : { courtAttemptId: randomUUID() }),
1559
1485
  ...(env.stationChild === undefined ? {} : { stationChild: env.stationChild }),
1560
1486
  // #1132: one configured ceiling, already resolved by the caller (#422).
1561
1487
  deliveryRequestLimit: deliveryLimitFromConfig(env.autoResumeLimit),
@@ -1657,8 +1583,7 @@ async function runSettledAutoResumeLoop<
1657
1583
  });
1658
1584
  return settleDeferredPersist(
1659
1585
  input.admitted,
1660
- input.env.principalAuthority,
1661
- adapters,
1586
+ input.env,
1662
1587
  attemptIo,
1663
1588
  result,
1664
1589
  );
@@ -1851,13 +1776,13 @@ export async function runPostAdmissionSeatResume<
1851
1776
  }
1852
1777
  }
1853
1778
 
1854
- // Open court continue, or new court for summons / message re-review
1855
- // (clause 0 新庭可再交卷; #833). Bare resume without open court omits id.
1856
- if (
1857
- openCourtAttemptId !== undefined ||
1858
- request.summons !== undefined ||
1859
- request.message !== undefined
1860
- ) {
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) {
1861
1786
  const courtAttemptId =
1862
1787
  openCourtAttemptId ??
1863
1788
  (turnRequest.courtAttemptId !== undefined &&
@@ -2010,8 +1935,7 @@ export function showResumeErrorPointer(
2010
1935
  terminal: TerminalResult | undefined,
2011
1936
  ): void {
2012
1937
  if (exitCode === 0 || terminal === undefined) return;
2013
- const publishedError = terminal.artifacts.find((artifact) => artifact.kind === "error")?.path
2014
- ?? publishedFailureErrorPath(terminal);
1938
+ const publishedError = terminal.artifacts.find((artifact) => artifact.kind === "error")?.path;
2015
1939
  if (publishedError !== undefined) writeResumeFailurePointer(io, publishedError);
2016
1940
  }
2017
1941
 
@@ -2233,7 +2157,7 @@ export async function runPostAdmissionManualResume<
2233
2157
  // through its own settleAfterTurnStarted-backed branches; a lawful result
2234
2158
  // only ever reaches this point once that persist has already succeeded
2235
2159
  // (#836 r12 class 3 dedup — one persist owner, not a second here).
2236
- io.stdout(formatTerminalResult(result.terminal));
2160
+ await presentTerminal(result.terminal, io, admitted.runDirectory);
2237
2161
  }
2238
2162
  if (result.terminal !== undefined) {
2239
2163
  (result.terminal as { autoResumeCount?: number }).autoResumeCount = 0;