@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
@@ -17,6 +17,7 @@ import { readDeclaredTicketNumber } from "../run-ticket-number.js";
17
17
  import { pathContainedIn } from "../activation-ledger-topology.js";
18
18
  import { pickEngineAxis } from "../package-resources/engine-material.js";
19
19
  import { readStoredHostSessionId } from "../session-identity.js";
20
+ import { createReceiptDeliveryPolicy, deliveryLimitFromConfig, } from "../receipt-delivery-policy.js";
20
21
  import { rewriteRunDirectoryPathValue } from "../role-run-relocation.js";
21
22
  import { isOfficerReviewSeat } from "../packaged-role-registry.js";
22
23
  /** Original error bytes, never relabeled — a secondary fact riding beside a classified cause. */
@@ -36,13 +37,13 @@ function isStationChildOfficerDialogue(role, env) {
36
37
  return env.stationChild === true && isOfficerReviewSeat(role);
37
38
  }
38
39
  import { mintEngineDetourInvocationScope, withEngineDetourInvocationScope, } from "../engine-detour-usage.js";
39
- import { clearCurrentCourt, clearTypedProviderHttpObservation, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, renderResumeCommand, } from "./run-lifecycle.js";
40
+ import { clearCurrentCourt, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, } from "./run-lifecycle.js";
40
41
  import { processCancelDiagnostic, processCancelSignalName, } from "./process-cancel.js";
41
- import { homeFromRunDirectory } from "../activation-ledger-topology.js";
42
+ import { DEFAULT_ROLE_TURN_HOST } from "../host-descriptions.js";
42
43
  import { recordRunStart } from "../host-session-record.js";
43
- import { attemptProducedFreshSubmission, classifyPostAdmissionFailure, controlledFailureInputFromResolution, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatErrorCauseDetail, formatTerminalResult, inspectJudgeSession, isLawfulTypedTerminalOutcome, presentFailureTerminal, publishedFailureErrorPath, presentStructuralRejection, resolveAuditedRunnerFailureResolution, resolveControlledFailureResumeObservation, settleFailureTerminalResult, settleHostEndedNoReceipt, attachRecordedSubmissions, rewritePublishedFailureErrorPath, } from "./settlement.js";
44
+ import { classifyPostAdmissionFailure, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatErrorCauseDetail, ledgerReadScope, isLawfulTypedTerminalOutcome, presentStructuralRejection, settleFailureTerminalResult, settleHostEndedNoReceipt, noteSettlementFault, attachRecordedSubmissions, retainPackageFault, } from "./settlement.js";
44
45
  import {} from "./terminal.js";
45
- import { ensureRealArtifactsDirectory, persistReturnedRunState, runWithAutoResumeLoop, TurnDispatchedFailure, writeHardenedArtifactFile, } from "./auto-resume.js";
46
+ import { ensureRealArtifactsDirectory, persistReturnedRunState, presentTerminal, runWithAutoResumeLoop, TurnDispatchedFailure, } from "./auto-resume.js";
46
47
  import { isRecord, errorText } from "../unknown-value.js";
47
48
  function projectRelocatedTurnIdentity(request, result, admitted, relocation) {
48
49
  const rewrite = (value) => rewriteRunDirectoryPathValue(value, relocation.oldRunDirectory, relocation.newRunDirectory);
@@ -58,7 +59,6 @@ function projectRelocatedTurnIdentity(request, result, admitted, relocation) {
58
59
  }
59
60
  }
60
61
  if (result.terminal !== undefined) {
61
- rewritePublishedFailureErrorPath(result.terminal, relocation.oldRunDirectory, relocation.newRunDirectory);
62
62
  for (const artifact of result.terminal.artifacts) {
63
63
  artifact.path = rewrite(artifact.path);
64
64
  }
@@ -80,8 +80,14 @@ async function settleProcessCancelAfterTurn(input) {
80
80
  timedOut: input.result.timedOut,
81
81
  code: input.result.code,
82
82
  stderr: input.result.stderr,
83
- knownDiagnostic: processCancelDiagnostic(input.cancelName),
84
- }, input.invocationScopeId), input.adapters, input.env.principalAuthority, input.io, input.persistRunState);
83
+ ...(input.result.signal === undefined ? {} : { signal: input.result.signal }),
84
+ // A host report already on this result stays the report. The cancel
85
+ // name is wording only when the host gave none.
86
+ ...(input.result.knownFailure === undefined
87
+ ? { knownDiagnostic: processCancelDiagnostic(input.cancelName) }
88
+ : { knownFailure: input.result.knownFailure }),
89
+ cancelName: input.cancelName,
90
+ }, input.invocationScopeId), input.adapters, input.env.principalAuthority, input.io, input.persistRunState, input.notePackageFault);
85
91
  return {
86
92
  ...failed,
87
93
  turnDispatched: true,
@@ -125,33 +131,16 @@ export const POST_ADMISSION_CLEANUP_DIAGNOSTIC_ENTRY_TYPE = "ak_post_admission_c
125
131
  * redundant second copy (#840 bounce class 1: 同一业务规则只保留一个权威实现).
126
132
  */
127
133
  async function recordBestEffortPostDispatchDiagnostic(admitted, env, diagnostic, io) {
128
- const payload = { diagnostic, recordedAt: new Date().toISOString() };
129
- let retentionFailure;
130
- try {
131
- await env.sessionAppender(env.principalAuthority, admitted.principal, POST_ADMISSION_CLEANUP_DIAGNOSTIC_ENTRY_TYPE, payload);
132
- }
133
- catch (appendError) {
134
- try {
135
- const artifactsDir = await ensureRealArtifactsDirectory(admitted.runDirectory);
136
- await writeHardenedArtifactFile(artifactsDir, "post-admission-diagnostic", {
137
- version: 1,
138
- ...payload,
139
- });
140
- }
141
- catch (artifactError) {
142
- retentionFailure =
143
- `post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; artifact=${describeErrorIdentity(artifactError)}`;
144
- }
145
- }
146
- try {
147
- io.stderr(formatCliDiagnostic(diagnostic));
148
- if (retentionFailure !== undefined) {
149
- io.stderr(formatCliDiagnostic(retentionFailure));
150
- }
151
- }
152
- catch {
153
- // Presentation is best-effort beside an already-formed host terminal.
154
- }
134
+ await retainPackageFault({
135
+ runDirectory: admitted.runDirectory,
136
+ diagnostic,
137
+ appendSession: (payload) => env.sessionAppender(env.principalAuthority, admitted.principal, POST_ADMISSION_CLEANUP_DIAGNOSTIC_ENTRY_TYPE, payload),
138
+ stderr: (text) => io.stderr(text),
139
+ });
140
+ }
141
+ /** The one package-fault receiver, bound to this admitted run's session and io. */
142
+ export function packageFaultNoteFor(admitted, env, io) {
143
+ return (diagnostic) => recordBestEffortPostDispatchDiagnostic(admitted, env, diagnostic, io);
155
144
  }
156
145
  /**
157
146
  * Factory-seat resume method-material face (#833): one authority for
@@ -173,42 +162,42 @@ export async function resolveResumeMethodMaterialAdapters(input) {
173
162
  stderr: "",
174
163
  thrown: error,
175
164
  ...(input.knownCause === undefined ? {} : { knownCause: input.knownCause }),
176
- }, input.emptyAdapters, input.authority, { ...input.io, omitFailureStderrDiagnostic: true });
165
+ }, input.emptyAdapters, input.authority, { ...input.io, omitFailureStderrDiagnostic: true }, true, (diagnostic) => retainPackageFault({
166
+ runDirectory: input.admitted.runDirectory,
167
+ diagnostic,
168
+ stderr: (text) => input.io.stderr(text),
169
+ }));
177
170
  return { kind: "terminal", ...terminal };
178
171
  }
179
172
  }
180
- export async function presentControlledFailure(admitted, failureInput, adapters, authority, io, persistRunState = true) {
173
+ export async function presentControlledFailure(admitted, failureInput, adapters, authority, io, persistRunState = true, notePackageFault) {
181
174
  const hasThrown = Object.hasOwn(failureInput, "thrown");
182
- const resumeObservation = await resolveControlledFailureResumeObservation({
183
- runDirectory: admitted.runDirectory,
184
- ...(failureInput.typedHttpObservationSettled === true
185
- ? {
186
- typedHttpObservationSettled: true,
187
- ...(failureInput.typedHttpObservation === undefined
188
- ? {}
189
- : { typedHttpObservation: failureInput.typedHttpObservation }),
190
- }
191
- : {}),
192
- });
193
- const knownFailure = failureInput.knownFailure ?? resumeObservation.observationReadFailure;
194
- const session = !hasThrown &&
195
- !failureInput.timedOut &&
196
- knownFailure === undefined &&
197
- failureInput.knownCause === undefined &&
198
- admitted.principal !== undefined
199
- ? await inspectJudgeSession(authority.decode(admitted.principal).sessionFile)
200
- : undefined;
175
+ const knownFailure = failureInput.knownFailure;
201
176
  // knownFailure channel owns details when present; otherwise caller knownDetails.
202
177
  const fromKnownFailure = explicitInternalKnownFailureClassificationInput(knownFailure);
203
- const failure = classifyPostAdmissionFailure({
178
+ // Merge the two detail sources so a key carried by either survives; a later
179
+ // spread would otherwise drop the earlier one's keys entirely.
180
+ const callerDetails = failureInput.knownDetails === undefined && fromKnownFailure.knownDetails === undefined
181
+ ? undefined
182
+ : { ...(failureInput.knownDetails ?? {}), ...(fromKnownFailure.knownDetails ?? {}) };
183
+ const classified = classifyPostAdmissionFailure({
204
184
  timedOut: failureInput.timedOut,
205
185
  code: failureInput.code,
206
186
  stderr: failureInput.stderr,
207
187
  ...(hasThrown ? { thrown: failureInput.thrown } : {}),
208
- ...(failureInput.knownDetails === undefined
188
+ // Both detail sources are real; merging keeps each one's keys instead of
189
+ // letting the later spread drop the earlier one.
190
+ ...(callerDetails === undefined ? {} : { knownDetails: callerDetails }),
191
+ // The knownFailure channel's own cause/identity/diagnostic ride with it.
192
+ ...(fromKnownFailure.knownCause === undefined
193
+ ? {}
194
+ : { knownCause: fromKnownFailure.knownCause }),
195
+ ...(fromKnownFailure.knownIdentity === undefined
209
196
  ? {}
210
- : { knownDetails: failureInput.knownDetails }),
211
- ...fromKnownFailure,
197
+ : { knownIdentity: fromKnownFailure.knownIdentity }),
198
+ ...(fromKnownFailure.knownDiagnostic === undefined
199
+ ? {}
200
+ : { knownDiagnostic: fromKnownFailure.knownDiagnostic }),
212
201
  ...(failureInput.knownCause === undefined
213
202
  ? {}
214
203
  : { knownCause: failureInput.knownCause }),
@@ -218,20 +207,30 @@ export async function presentControlledFailure(admitted, failureInput, adapters,
218
207
  ...(failureInput.knownDiagnostic === undefined
219
208
  ? {}
220
209
  : { knownDiagnostic: failureInput.knownDiagnostic }),
221
- ...(session === undefined ? {} : { session }),
210
+ ...(failureInput.signal === undefined ? {} : { signal: failureInput.signal }),
211
+ ...(failureInput.cancelName === undefined ? {} : { cancelName: failureInput.cancelName }),
212
+ ...(failureInput.packageFact === undefined
213
+ ? {}
214
+ : { packageFact: failureInput.packageFact }),
222
215
  });
223
- // #665 / #416: typed 429 with a run principal offers resume; the host decides availability.
224
- const resumable = admitted.principal !== undefined && resumeObservation.typedHttp429 !== undefined;
216
+ const failure = failureInput.stderr.length === 0 || failureInput.stderr === classified.diagnostic
217
+ ? classified
218
+ : { ...classified, stderr: failureInput.stderr };
225
219
  if (persistRunState && !failureInput.skipRunStateWrite) {
226
- await persistReturnedRunState(admitted);
220
+ try {
221
+ await persistReturnedRunState(admitted);
222
+ }
223
+ catch (error) {
224
+ // The host failure is already classified. A run-state write stays beside it.
225
+ await noteSettlementFault(admitted.runDirectory, notePackageFault === undefined ? undefined : { notePackageFault }, `run-state persistence failed beside host terminal: ${describeErrorIdentity(error)}`);
226
+ }
227
227
  }
228
228
  let publishedErrorPath;
229
229
  let terminal;
230
230
  try {
231
231
  const settled = await settleFailureTerminalResult(admitted, failure, authority, {
232
- ...(resumable
233
- ? { resume: { command: renderResumeCommand(admitted.runId) } }
234
- : {}),
232
+ recordAttemptHistory: true,
233
+ ...(notePackageFault === undefined ? {} : { notePackageFault }),
235
234
  ...(failureInput.invocationScopeId === undefined ||
236
235
  failureInput.invocationScopeId.length === 0
237
236
  ? {}
@@ -246,7 +245,7 @@ export async function presentControlledFailure(admitted, failureInput, adapters,
246
245
  }
247
246
  throw error;
248
247
  }
249
- presentFailureTerminal(terminal, io);
248
+ await presentTerminal(terminal, io, admitted.runDirectory);
250
249
  return {
251
250
  exitCode: exitCodeForTerminalOutcome(terminal.roleOutcome),
252
251
  admitted,
@@ -264,9 +263,9 @@ export async function presentControlledFailure(admitted, failureInput, adapters,
264
263
  * the true cause through its own existing dispatch-exception machinery once
265
264
  * the retry budget is exhausted.
266
265
  */
267
- async function settleAfterTurnStarted(admitted, failureInput, adapters, authority, io, persistRunState) {
266
+ async function settleAfterTurnStarted(admitted, failureInput, adapters, authority, io, persistRunState, notePackageFault) {
268
267
  try {
269
- return (await presentControlledFailure(admitted, failureInput, adapters, authority, io, persistRunState));
268
+ return (await presentControlledFailure(admitted, failureInput, adapters, authority, io, persistRunState, notePackageFault));
270
269
  }
271
270
  catch (error) {
272
271
  throw new TurnDispatchedFailure(error);
@@ -276,30 +275,211 @@ async function settleAfterTurnStarted(admitted, failureInput, adapters, authorit
276
275
  * Persist run-state for a result dispatchPostAdmissionTurn deferred
277
276
  * (needsPersist — station-child / resumable auto-resume loop, #416/#840):
278
277
  * that write must land outside the loop's own retried-dispatch try, but its
279
- * failure still settles through the single existing controlled-failure
280
- * authority (settleAfterTurnStarted / presentControlledFailure, ADR 0080:
281
- * one settlement disposition owner) — never a second hand-rolled
282
- * classify/artifact/Terminal (#836 r12 class 3), and never lawful/non-lawful
283
- * settling differently (#836 r13 class 2: a caller's io here may be a no-op,
284
- * so a stderr-only trace is never seen and the failure is otherwise
285
- * swallowed — 失败诚实宪法 真因必须落痕). Both cases settle through
286
- * settleAfterTurnStarted, which attaches the run's already-recorded ledger
287
- * submissions to the new failure terminal and returns skipAutoResume so the
288
- * caller presents it once and stops — never re-entering auto-resume.
278
+ * failure is a package note beside the terminal already settled. It does
279
+ * not replace that terminal or open another attempt. The note is durable
280
+ * because the caller's io may be a no-op.
289
281
  */
290
- async function settleDeferredPersist(admitted, authority, adapters, io, result) {
282
+ async function settleDeferredPersist(admitted, env, io, result) {
291
283
  if (result.needsPersist !== true || result.terminal === undefined)
292
284
  return result;
293
285
  const { needsPersist: _needsPersist, ...settledResult } = result;
294
- const lawful = isLawfulTypedTerminalOutcome(result.terminal.roleOutcome);
295
286
  try {
296
- await persistReturnedRunState(admitted, lawful ? { lawful: true } : undefined);
287
+ await persistReturnedRunState(admitted);
297
288
  return settledResult;
298
289
  }
299
290
  catch (error) {
300
- const failed = await settleAfterTurnStarted(admitted, { timedOut: false, code: null, stderr: "", thrown: error, skipRunStateWrite: true }, adapters, authority, io, true);
301
- return { ...failed, turnDispatched: true, skipAutoResume: true };
291
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `run-state persistence failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
292
+ return settledResult;
293
+ }
294
+ }
295
+ /**
296
+ * Court/invocation scope for settlement reads. The one construction of this
297
+ * shape for every turn (#1132 — the first turn and each 催交 turn share it).
298
+ */
299
+ function settlementScopeForTurn(request) {
300
+ const hasCourt = request.courtAttemptId !== undefined && request.courtAttemptId.length > 0;
301
+ const hasScope = request.invocationScopeId !== undefined && request.invocationScopeId.length > 0;
302
+ if (!hasCourt && !hasScope)
303
+ return undefined;
304
+ return {
305
+ ...(hasCourt ? { courtAttemptId: request.courtAttemptId } : {}),
306
+ ...(hasScope ? { invocationScopeId: request.invocationScopeId } : {}),
307
+ };
308
+ }
309
+ /**
310
+ * #1132: one 没交卷催交 turn, dispatched through the SAME settlement path as the
311
+ * first turn.
312
+ *
313
+ * There is deliberately no second settlement implementation here. A delivery
314
+ * turn is an ordinary host turn on this same run/session: this builds the
315
+ * host-neutral resume request (the adapter's stored native session id, ADR 0082;
316
+ * pi's existing delivery-state content reused verbatim — no new prompt wording,
317
+ * no new host capability). The caller settles it through the same
318
+ * `settleCompletedHostTurn` the first turn uses (ADR 0080): the runner/host
319
+ * failure re-read, the shouldPresent gate, the fresh-seal check, open-court
320
+ * cleanup, cancel re-reads, run-state persist, and the stderr mirror.
321
+ *
322
+ * The caller owns the live delivery policy; the request projects its next send.
323
+ */
324
+ async function buildReceiptDeliveryRequest(input) {
325
+ const { admitted, env, request, receiptDelivery } = input;
326
+ const hostSessionId = await readStoredHostSessionId(env.host, env.principalAuthority, admitted.principal);
327
+ return {
328
+ ...request,
329
+ continuation: {
330
+ kind: "resume",
331
+ prompt: JSON.stringify({
332
+ ...receiptDelivery.deliveryState(),
333
+ deliveryTurns: receiptDelivery.issuedDeliveryRequests() + 1,
334
+ }),
335
+ ...(hostSessionId === undefined ? {} : { hostSessionId }),
336
+ },
337
+ deliveryRequestLimit: receiptDelivery.limit,
338
+ };
339
+ }
340
+ /**
341
+ * #1132: settle ONE completed host turn — the single post-turn settlement
342
+ * authority shared by the first turn and by every 没交卷催交 turn (ADR 0080).
343
+ *
344
+ * Extracted deliberately instead of re-entering `dispatchPostAdmissionTurn`.
345
+ * The turn-BEFORE concerns (beforeDispatch / markRunRunning / relocate /
346
+ * afterDispatch / writer-lease release) belong to the outermost dispatch and
347
+ * run exactly once per run; everything from the stderr mirror onward is
348
+ * per-turn. A 催交 turn is a real host turn on this same run/session, so it
349
+ * settles through THIS function — not a reduced copy — and it re-runs neither
350
+ * the admission hooks nor the run-state terminal write.
351
+ *
352
+ * Returns `still-missing` when the turn simply produced no receipt: the caller
353
+ * (first turn or the 催交 loop) then decides between issuing another delivery
354
+ * request and settling no_receipt.
355
+ */
356
+ async function settleCompletedHostTurn(input) {
357
+ const { admitted, env, io, adapters, shouldPresent, request, result, persistRunState, deferredPersist } = input;
358
+ const finishAfterTurn = input.finishAfterTurn;
359
+ const courtScope = input.courtScope;
360
+ // `result.stderr` stays live in memory for host-failure settlement
361
+ // whether or not this mirror write succeeds. A write failure is noted.
362
+ // It does not replace the host terminal or an already-sealed acceptance.
363
+ //
364
+ // Everything below runs after the host turn genuinely started (#840 r9
365
+ // 判词 class 1 boundary — 覆盖 executeTurn 已启动后至返回带 turnDispatched
366
+ // 结果前的全部异常). Each fallible step routes any failure through the
367
+ // single existing settlement authority (presentControlledFailure /
368
+ // settleFailureTerminalResult) rather than a second one (ADR 0080:
369
+ // one settlement disposition owner) — never by fabricating a terminal here.
370
+ // A cleanup step that runs only after a real settlement (clearCurrentCourt)
371
+ // is protected by logging and keeping that already-obtained result, never
372
+ // by discarding it (#840 已交劳动只整理终局不重做). A failure inside the
373
+ // settlement authority itself (presentControlledFailure's own reads) goes
374
+ // through settleAfterTurnStarted: still no fabricated terminal, but the
375
+ // re-thrown TurnDispatchedFailure still tells runWithAutoResumeLoop the
376
+ // turn genuinely started, so the next retry sends a resume payload — the
377
+ // true cause settles through the loop's own existing dispatch-exception
378
+ // machinery once the retry budget is exhausted.
379
+ let stderrLogWriteFailure;
380
+ try {
381
+ await writeFile(join(admitted.runDirectory, "stderr.log"), result.stderr, "utf8");
382
+ }
383
+ catch (error) {
384
+ stderrLogWriteFailure = error;
385
+ // Best-effort: stderr.log is secondary to the host terminal. A write
386
+ // failure leaves a durable note and, on a host failure, rides in
387
+ // packageFact. It does not become the cause.
388
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `stderr.log write failed (best-effort continue): ${describeErrorIdentity(error)}`, io);
389
+ }
390
+ // Host facts are resolved before trySettle. A host failure skips trySettle
391
+ // so a later read cannot replace that report. A clean host whose settlement
392
+ // read throws is noted above and is not turned into a failure terminal.
393
+ // stderr.log and run-state writes are notes beside the host terminal.
394
+ let settled;
395
+ let settledOutcome;
396
+ // The host CLI's own report: timeout, declared failure, cancel, or exit.
397
+ // A null code is a signal death (Node close). No session or history read
398
+ // takes part (owner ea321c6d / 4743ade7).
399
+ const processCancelName = processCancelSignalName(env.signal);
400
+ const hostSignalFailed = result.timedOut
401
+ || result.knownFailure !== undefined
402
+ || processCancelName !== undefined
403
+ || result.code === null
404
+ || result.code !== 0;
405
+ const hostReportedFailure = result.knownFailure;
406
+ try {
407
+ if (!hostSignalFailed) {
408
+ settled = await adapters.trySettle(admitted, env.principalAuthority, { ...courtScope, previewOnly: true });
409
+ const cancelAfterSettle = processCancelSignalName(env.signal);
410
+ if (settled !== undefined
411
+ && shouldPresent(settled)
412
+ && cancelAfterSettle === undefined) {
413
+ settledOutcome = {
414
+ exitCode: exitCodeForTerminalOutcome(settled.roleOutcome),
415
+ admitted,
416
+ terminal: settled,
417
+ turnDispatched: true,
418
+ };
419
+ }
420
+ }
421
+ }
422
+ catch (error) {
423
+ // The host did not fail. A settlement read here is a package fault and
424
+ // must not become the terminal.
425
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `settlement read failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
426
+ }
427
+ if (settledOutcome !== undefined) {
428
+ // #855: final cancel re-read before lawful seal — settlement window may
429
+ // have received SIGTERM/SIGINT/SIGHUP after the earlier snapshot.
430
+ const lateCancel = processCancelSignalName(env.signal);
431
+ if (lateCancel !== undefined) {
432
+ return { kind: "settled", outcome: await finishAfterTurn(await settleProcessCancelAfterTurn({
433
+ admitted,
434
+ cancelName: lateCancel,
435
+ result,
436
+ adapters,
437
+ env,
438
+ io,
439
+ persistRunState,
440
+ deferredPersist,
441
+ invocationScopeId: request.invocationScopeId,
442
+ notePackageFault: courtScope.notePackageFault,
443
+ })) };
444
+ }
445
+ if (persistRunState) {
446
+ try {
447
+ await persistReturnedRunState(admitted);
448
+ }
449
+ catch (error) {
450
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `run-state persistence failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
451
+ }
452
+ }
453
+ // Persist + present is the caller's stop seam (auto-resume loop /
454
+ // manual resume), not this retried host-turn function.
455
+ // #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
456
+ input.markPending(settledOutcome.terminal);
457
+ return { kind: "settled", outcome: await finishAfterTurn({ ...settledOutcome, ...deferredPersist }) };
458
+ }
459
+ // Host/runner true failure coexists with already-recorded payloads — never wash as accepted.
460
+ if (hostSignalFailed) {
461
+ const stderrLogWriteFact = stderrLogWriteFailure === undefined
462
+ ? undefined
463
+ : { stderrLogWriteFailure: describeCaughtError(stderrLogWriteFailure) };
464
+ const failed = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
465
+ timedOut: result.timedOut,
466
+ code: result.code,
467
+ stderr: result.stderr,
468
+ // A signal-killed child is a fact of its own. The synthetic line is
469
+ // only a fallback for when the host declared no failure of its own —
470
+ // it must never displace words the host actually reported.
471
+ ...(result.signal === undefined ? {} : { signal: result.signal }),
472
+ ...(hostReportedFailure === undefined ? {} : { knownFailure: hostReportedFailure }),
473
+ // The cancellation is the host's own report of why this call ended;
474
+ // it supplies the wording only when the host declared nothing else.
475
+ ...(processCancelName === undefined ? {} : { cancelName: processCancelName }),
476
+ // Secondary fact only — never the cause, and never merged into the
477
+ // host's own details.
478
+ ...(stderrLogWriteFact === undefined ? {} : { packageFact: stderrLogWriteFact }),
479
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState, courtScope.notePackageFault);
480
+ return { kind: "settled", outcome: await finishAfterTurn(withProcessCancelSkipAutoResume({ ...failed, turnDispatched: true, ...deferredPersist }, env.signal)) };
302
481
  }
482
+ return { kind: "still-missing" };
303
483
  }
304
484
  export async function dispatchPostAdmissionTurn(input) {
305
485
  const { admitted, env, io, request, lease, adapters, effectiveEngine } = input;
@@ -312,13 +492,34 @@ export async function dispatchPostAdmissionTurn(input) {
312
492
  * lease release — success and failure alike. When a primary failure terminal already
313
493
  * exists, keep that cause and leave relocate failure on the shared diagnostic channel.
314
494
  */
495
+ const courtScope = {
496
+ ...settlementScopeForTurn(request),
497
+ notePackageFault: (diagnostic) => recordBestEffortPostDispatchDiagnostic(admitted, env, diagnostic, io),
498
+ };
499
+ let pendingSettlement;
500
+ /**
501
+ * The terminal this pending publication belongs to. A later real failure
502
+ * (e.g. a cancel re-read) replaces `result.terminal` with its own settled
503
+ * failure; that failure is already durable, so the superseded preview must
504
+ * not publish over it (失败诚实宪法:接住可以,洗白不行).
505
+ */
506
+ let pendingTerminal;
315
507
  let afterDispatchApplied = false;
508
+ const receiptDelivery = input.receiptDelivery ?? createReceiptDeliveryPolicy(env.autoResumeLimit);
509
+ let turnDispatched = false;
510
+ // #1132: the AK-side 催交 covers hosts that do not deliver to themselves.
511
+ // Pi runs the package role runtime in-process, and that runtime already issues
512
+ // its own delivery requests at `agent_end` and records the real count — asking
513
+ // again here would double-deliver on one run. External hosts (ACP / headless
514
+ // CLI) have no such in-child loop, so the AK execution seam owns it for them.
515
+ const akSeamDeliversReceipt = env.host !== undefined && env.host !== "" && env.host !== DEFAULT_ROLE_TURN_HOST;
316
516
  const finishAfterTurn = async (result) => {
317
517
  if (afterDispatchApplied)
318
518
  return result;
319
519
  afterDispatchApplied = true;
320
- // #855: re-read cancel before afterDispatch finalization — never present
321
- // lawful success after a catchable process signal. skipRunStateWrite: run-state
520
+ // #855: re-read cancel before committing the terminal and afterDispatch.
521
+ // A signal after the history-first commit cannot retroactively erase it.
522
+ // skipRunStateWrite: run-state
322
523
  // may already be terminal from a prior lawful persist; do not reopen as resumable.
323
524
  const cancelBeforeFinish = processCancelSignalName(env.signal);
324
525
  if (cancelBeforeFinish !== undefined &&
@@ -330,13 +531,43 @@ export async function dispatchPostAdmissionTurn(input) {
330
531
  code: null,
331
532
  stderr: "",
332
533
  knownDiagnostic: processCancelDiagnostic(cancelBeforeFinish),
534
+ cancelName: cancelBeforeFinish,
333
535
  skipRunStateWrite: true,
334
- }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState)),
536
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState, courtScope.notePackageFault)),
335
537
  turnDispatched: true,
336
538
  skipAutoResume: true,
337
539
  ...deferredPersist,
338
540
  };
339
541
  }
542
+ // The last cancellation check precedes the one durable settlement. A preview
543
+ // never publishes pointers or history; a superseding failure owns this turn —
544
+ // `result.terminal === pendingTerminal` is what makes the publication this
545
+ // turn's own rather than a superseded one.
546
+ if (pendingSettlement !== undefined &&
547
+ pendingTerminal !== undefined &&
548
+ result.terminal === pendingTerminal) {
549
+ try {
550
+ const terminal = pendingSettlement === "sealed"
551
+ ? await adapters.trySettle(admitted, env.principalAuthority, { ...courtScope, recordAttemptHistory: true })
552
+ : await attachRecordedSubmissions(admitted, await settleHostEndedNoReceipt(admitted, env.principalAuthority, { ...courtScope, recordAttemptHistory: true }, receiptDelivery.issuedDeliveryRequests()), courtScope);
553
+ if (terminal === undefined)
554
+ throw new Error("settled host attempt vanished before publication");
555
+ result = { ...result, terminal, exitCode: exitCodeForTerminalOutcome(terminal.roleOutcome) };
556
+ if (pendingSettlement === "sealed" && terminal.roleOutcome.kind === "accepted"
557
+ && request.courtAttemptId !== undefined && request.courtAttemptId.length > 0) {
558
+ try {
559
+ await clearCurrentCourt(admitted.runDirectory, request.courtAttemptId);
560
+ }
561
+ catch (error) {
562
+ // The accepted attempt is already durable; cleanup is diagnostic only.
563
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `current-court cleanup failed after accepted settlement (best-effort continue): ${describeErrorIdentity(error)}`, io);
564
+ }
565
+ }
566
+ }
567
+ catch (error) {
568
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `terminal publication failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
569
+ }
570
+ }
340
571
  try {
341
572
  // #858 / #1071: the first identifiable ticketNumber field declaration
342
573
  // files an unbound run (integer, digit string, or leading #N token).
@@ -345,7 +576,7 @@ export async function dispatchPostAdmissionTurn(input) {
345
576
  // Later receipts remain untouched; prose note/report is never consulted;
346
577
  // this seam does not adjudicate a ticket change.
347
578
  if (admitted.ticketNumber === undefined) {
348
- const rows = await readRecordedSubmissionRows(admitted.projectRoot, admitted.runId, { home: homeFromRunDirectory(admitted.runDirectory), sessionParent: join(admitted.runDirectory, "session", "session.jsonl") });
579
+ const rows = await readRecordedSubmissionRows(admitted.projectRoot, admitted.runId, ledgerReadScope(admitted));
349
580
  let ticketNumber;
350
581
  for (const row of rows) {
351
582
  if (row.role !== admitted.role
@@ -387,7 +618,7 @@ export async function dispatchPostAdmissionTurn(input) {
387
618
  code: null,
388
619
  stderr: "",
389
620
  thrown: error,
390
- }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState)),
621
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState, courtScope.notePackageFault)),
391
622
  ...(result.turnDispatched === true ? { turnDispatched: true } : {}),
392
623
  ...(result.skipAutoResume === true ? { skipAutoResume: true } : {}),
393
624
  ...deferredPersist,
@@ -397,7 +628,6 @@ export async function dispatchPostAdmissionTurn(input) {
397
628
  try {
398
629
  // #987: do not pre-block or classify host dispatch from AK credential
399
630
  // catalog facts. Per-invocation runner/host evidence owns provider identity.
400
- await clearTypedProviderHttpObservation(admitted.runDirectory);
401
631
  // The authoritative host write (markRunRunning) is delayed to just before
402
632
  // executeTurn — not merely past beforeDispatch (#840 r9 判词 class 2). Any
403
633
  // pre-turn retry (beforeDispatch, continuation assembly) must not commit a host
@@ -419,7 +649,7 @@ export async function dispatchPostAdmissionTurn(input) {
419
649
  code: null,
420
650
  stderr: "",
421
651
  thrown: error,
422
- }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState));
652
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState, courtScope.notePackageFault));
423
653
  return { ...settled, ...deferredPersist };
424
654
  }
425
655
  }
@@ -451,7 +681,12 @@ export async function dispatchPostAdmissionTurn(input) {
451
681
  // auto-resume re-enters this dispatch and must reuse the same scope.
452
682
  let result;
453
683
  try {
684
+ // A bare `ak-role resume` only forwards the caller's instruction to the
685
+ // host CLI's native resume. Nothing here may gate, redirect or reshape
686
+ // that dispatch on prior conclusions, row counts or report presence;
687
+ // the authoritative post-turn settlement reads whatever really happened.
454
688
  recordRunStart(admitted.runDirectory);
689
+ turnDispatched = true;
455
690
  result = await env.roleTurnHost.executeTurn(turnRequest);
456
691
  }
457
692
  catch (error) {
@@ -460,263 +695,115 @@ export async function dispatchPostAdmissionTurn(input) {
460
695
  timedOut: false,
461
696
  code: null,
462
697
  stderr: "",
463
- thrown: processCancelName === undefined
464
- ? error
465
- : new Error(processCancelDiagnostic(processCancelName), { cause: error }),
466
- }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
698
+ // The thrown value is the host's report. A cancel received on this
699
+ // call is a second fact; it must not become a new Error in its place.
700
+ ...(processCancelName === undefined ? {} : { cancelName: processCancelName }),
701
+ thrown: error,
702
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState, courtScope.notePackageFault);
467
703
  return await finishAfterTurn(withProcessCancelSkipAutoResume({ ...settled, turnDispatched: true, ...deferredPersist }, env.signal));
468
704
  }
469
- // `result.stderr` stays live in memory for the real classification below
470
- // regardless of whether this durable mirror write succeeds. A write
471
- // failure here (unwritable run directory, stderr.log occupied as a
472
- // directory, ...) is a real infrastructure problem and must never be
473
- // dropped silently (catch-and-continue with no trace is a defect) — but
474
- // it also must never become THE controlling failure and wash out a
475
- // primary child signal that already arrived on `result`, or an
476
- // already-sealed accepted payload (#836: a secondary failure must not
477
- // overwrite an already-known cause or an already-recorded leg). It rides
478
- // beside whatever the real classification below determines, and only
479
- // becomes the reported failure itself when nothing else is wrong.
480
- //
481
- // Everything below runs after the host turn genuinely started (#840 r9
482
- // 判词 class 1 boundary — 覆盖 executeTurn 已启动后至返回带 turnDispatched
483
- // 结果前的全部异常). Each fallible step routes any failure through the
484
- // single existing settlement authority (presentControlledFailure /
485
- // settleFailureTerminalResult) rather than a second one (ADR 0080:
486
- // one settlement disposition owner) — never by fabricating a terminal here.
487
- // A cleanup step that runs only after a real settlement (clearCurrentCourt)
488
- // is protected by logging and keeping that already-obtained result, never
489
- // by discarding it (#840 已交劳动只整理终局不重做). A failure inside the
490
- // settlement authority itself (presentControlledFailure's own reads) goes
491
- // through settleAfterTurnStarted: still no fabricated terminal, but the
492
- // re-thrown TurnDispatchedFailure still tells runWithAutoResumeLoop the
493
- // turn genuinely started, so the next retry sends a resume payload — the
494
- // true cause settles through the loop's own existing dispatch-exception
495
- // machinery once the retry budget is exhausted.
496
- let stderrLogWriteFailure;
497
- try {
498
- await writeFile(join(admitted.runDirectory, "stderr.log"), result.stderr, "utf8");
499
- }
500
- catch (error) {
501
- stderrLogWriteFailure = error;
502
- // Best-effort: the turn's own stderr capture is secondary to lawful /
503
- // controlled-failure settlement below, but the failure itself must
504
- // still leave a real trace (失败诚实宪法 真因必须落痕) — durably, since
505
- // every auto-resume attempt's io is dummyIo (#840 r9 判词 class 1). It
506
- // still rides beside the classification below and, if nothing else is
507
- // wrong, becomes the reported failure itself (#836).
508
- await recordBestEffortPostDispatchDiagnostic(admitted, env, `stderr.log write failed (best-effort continue): ${describeErrorIdentity(error)}`, io);
509
- }
510
- const courtScope = (request.courtAttemptId === undefined || request.courtAttemptId.length === 0) &&
511
- (request.invocationScopeId === undefined || request.invocationScopeId.length === 0)
512
- ? undefined
513
- : {
514
- ...(request.courtAttemptId === undefined || request.courtAttemptId.length === 0
515
- ? {}
516
- : { courtAttemptId: request.courtAttemptId }),
517
- ...(request.invocationScopeId === undefined ||
518
- request.invocationScopeId.length === 0
519
- ? {}
520
- : { invocationScopeId: request.invocationScopeId }),
521
- };
522
- // Single complete boundary (#840 r9 判词 class 1): resolving the
523
- // host/runner failure facts, trySettle, its shouldPresent gate, and the
524
- // accepted-settlement cleanup all settle through this one catch — a
525
- // throw from any of them (including the session-file decode itself)
526
- // still routes through settleAfterTurnStarted's TurnDispatchedFailure
527
- // instead of losing turnDispatched to an uncaught throw. The facts are
528
- // resolved before trySettle (#836) so an already-accepted settlement is
529
- // never presented over a real current host/runner failure signal or a
530
- // real stderr.log durable-write failure — both stay a real failure with
531
- // the recorded payload riding beside it, not replacing it.
532
- let settled;
533
- let settledOutcome;
534
- let hostSignalFailed = false;
535
- let resolution;
536
- try {
537
- const sessionFile = admitted.principal !== undefined
538
- ? env.principalAuthority.decode(admitted.principal).sessionFile
539
- : "";
540
- const runnerKnownFailure = adapters.resolveRunnerKnownFailure !== undefined && sessionFile !== ""
541
- ? await adapters.resolveRunnerKnownFailure({ result, sessionFile })
542
- : result.knownFailure;
543
- resolution = await resolveAuditedRunnerFailureResolution({
544
- runner: runnerKnownFailure,
545
- sessionFile,
546
- credential: undefined,
547
- runDirectory: admitted.runDirectory,
548
- });
549
- // A direct, current signal from the host/runner itself (timeout / host
550
- // knownFailure / runner knownFailure / missing credential) is a real
551
- // problem regardless of what else already settled — a settled accepted
552
- // outcome must not paper over it (it rides beside the recorded payload
553
- // via `submissions`, never replacing the payload). The audited
554
- // resolution's own typed session read (malformed JSONL, provider-stop,
555
- // an illegal/unsealed accepted status, a stale/superseded typed-HTTP
556
- // observation, ...) makes a run with no sealed acceptance a true
557
- // failure too — it must not be discarded into a lawful no_receipt just
558
- // because the raw runner signal alone looked clean. But it must not
559
- // retroactively invalidate an acceptance that already sealed this turn
560
- // — a resolved 429 observed earlier in the same session is exactly
561
- // that.
562
- const processCancelName = processCancelSignalName(env.signal);
563
- const directHostFailureSignal = result.timedOut
564
- || result.knownFailure !== undefined
565
- || runnerKnownFailure !== undefined
566
- || processCancelName !== undefined;
567
- hostSignalFailed =
568
- directHostFailureSignal
569
- || (result.code !== null && result.code !== 0)
570
- || resolution.knownFailure !== undefined;
571
- settled = await adapters.trySettle(admitted, env.principalAuthority, courtScope);
572
- if (settled !== undefined) {
573
- settled = await attachRecordedSubmissions(admitted, settled, courtScope);
705
+ // #1132: settle this turn through the ONE post-turn settlement authority,
706
+ // then — if it simply produced no receipt — 催交 before settling no_receipt.
707
+ const settledTurn = await settleCompletedHostTurn({
708
+ admitted,
709
+ env,
710
+ io,
711
+ adapters,
712
+ shouldPresent,
713
+ request,
714
+ result,
715
+ persistRunState,
716
+ deferredPersist,
717
+ finishAfterTurn,
718
+ courtScope,
719
+ markPending: (terminal) => { pendingSettlement = "sealed"; pendingTerminal = terminal; },
720
+ });
721
+ if (settledTurn.kind === "settled")
722
+ return settledTurn.outcome;
723
+ // #1132: 没交卷先回本人催交,不直接结为 no_receipt. The AK execution seam
724
+ // owns the 催交 and its real count. Each request is a real host turn on this
725
+ // same run/session, dispatched and settled through the same two seams as the
726
+ // first turn — never a reduced copy. 催交 never substitutes for host failure
727
+ // recovery: a delivery turn that fails keeps its true cause below.
728
+ // The delivery budget is spent HERE, at the outermost level; only when it is
729
+ // exhausted with still no receipt does the settlement seam record no_receipt.
730
+ while (akSeamDeliversReceipt && receiptDelivery.nextAction() === "request-delivery") {
731
+ // Assembly sits in the same post-start catch as the host call. A throw
732
+ // here is a started turn: resume the session, and do not count a request
733
+ // that never reached the host.
734
+ let deliveryResult;
735
+ let deliveryTurnRequest;
736
+ try {
737
+ const deliveryRequest = await buildReceiptDeliveryRequest({
738
+ admitted,
739
+ env,
740
+ // Host and the other axes already selected on this turn ride the
741
+ // shared projection. The pre-projection request does not have them.
742
+ request: turnRequest,
743
+ receiptDelivery,
744
+ });
745
+ // The same native-resume projection the first turn uses (host adapter owns
746
+ // the resume; AK only hands it the request — ADR 0082).
747
+ deliveryTurnRequest = {
748
+ ...deliveryRequest,
749
+ principal: admitted.principal,
750
+ runDirectory: admitted.runDirectory,
751
+ ...(env.signal === undefined ? {} : { signal: env.signal }),
752
+ };
753
+ receiptDelivery.recordDeliveryRequest();
754
+ deliveryResult = await env.roleTurnHost.executeTurn(deliveryTurnRequest);
574
755
  }
575
- // #836 r12 class 2: an accepted/audit_escalation settlement can be
576
- // entirely a prior attempt's stale payload (courtAttempt is a
577
- // recording tag, not a visibility gate — attachRecordedSubmissions
578
- // above still surfaces that historical payload honestly either way).
579
- // Consume the ledger's own subject.attemptId (submission-ledger.ts)
580
- // only to learn whether *this* attempt itself produced a fresh seal —
581
- // never to filter what presents.
582
- const settledIsFreshThisAttempt = settled === undefined
583
- || (settled.roleOutcome.kind !== "accepted" && settled.roleOutcome.kind !== "audit_escalation")
584
- ? true
585
- : await attemptProducedFreshSubmission(admitted, courtScope);
586
- // A lawful settled outcome already reached this turn takes precedence
587
- // over a later bare exit-code / session-inspection signal (trailing
588
- // nonzero exit, late stderr noise, a stale already-superseded
589
- // typed-HTTP observation, ...) — but never over a direct current
590
- // host/runner failure signal, nor over a real stderr.log durable-write
591
- // failure (confirmed infrastructure trouble, not weak/bare evidence),
592
- // both of which stay a real failure with the recorded payload riding
593
- // beside it, not replacing it (#836: never kill an already-recorded
594
- // leg, but never wash a real failure away either). Note: the narrower
595
- // directHostFailureSignal gates acceptance here; the broader
596
- // hostSignalFailed (adds bare exit code / resolution.knownFailure)
597
- // only gates the no-settlement fallback below — except when the
598
- // settlement itself is entirely stale (no fresh seal this attempt),
599
- // where a bare nonzero exit / resolution failure must not be outranked
600
- // by someone else's earlier success (#836 r12 class 2).
601
- const staleAcceptanceOutranksRealFailure = !settledIsFreshThisAttempt && hostSignalFailed;
602
- // #855: re-read cancel after trySettle/attach awaits — a signal in this
603
- // window must not land as lawful accepted.
604
- const cancelAfterSettle = processCancelSignalName(env.signal);
605
- if (settled !== undefined
606
- && shouldPresent(settled)
607
- && !directHostFailureSignal
608
- && !staleAcceptanceOutranksRealFailure
609
- && stderrLogWriteFailure === undefined
610
- && cancelAfterSettle === undefined) {
611
- // This court sealed — drop open-court pointer (bare resume no longer continues it).
612
- if (settled.roleOutcome.kind === "accepted" &&
613
- request.courtAttemptId !== undefined &&
614
- request.courtAttemptId.length > 0) {
756
+ catch (error) {
757
+ // 催交 cannot substitute for host failure recovery: a real failure with
758
+ // its true cause, through the one settlement authority (ADR 0080).
759
+ // The turn started, so the caller's loop resumes instead of replaying
760
+ // the initial payload. The issued count survives that re-entry.
761
+ const settled = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({ timedOut: false, code: null, stderr: "", thrown: error }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
762
+ return await finishAfterTurn(withProcessCancelSkipAutoResume({
763
+ ...settled,
764
+ turnDispatched: true,
765
+ ...deferredPersist,
766
+ }, env.signal));
767
+ }
768
+ const deliveryTurn = await settleCompletedHostTurn({
769
+ admitted,
770
+ env,
771
+ // Only the final Terminal presents; intermediate delivery attempts stay quiet.
772
+ io: { ...io, stdout: () => { } },
773
+ adapters,
774
+ shouldPresent,
775
+ request: deliveryTurnRequest,
776
+ result: deliveryResult,
777
+ // Run-state terminal write belongs to the OUTERMOST turn only, so a
778
+ // still-silent delivery attempt never makes the run look lawfully terminal.
779
+ persistRunState: false,
780
+ deferredPersist,
781
+ finishAfterTurn,
782
+ courtScope,
783
+ markPending: (terminal) => { pendingSettlement = "sealed"; pendingTerminal = terminal; },
784
+ });
785
+ // The delivery turn's own outcome IS the run's outcome, except when it
786
+ // simply still has no receipt — that is the loop's own case to keep going.
787
+ if (deliveryTurn.kind === "settled") {
788
+ // #1132: the delivery turn settles with persistRunState:false, so a run
789
+ // that ends HERE (得卷 or a real failure) still needs the outermost
790
+ // single run-state write. Without it the ledger says accepted while
791
+ // run-state stays `running`, and every later resume entry (ADR 0080)
792
+ // reads a finished run as live. The still-silent attempts above must
793
+ // NOT write; this one, which ends the run, must.
794
+ if (persistRunState && deliveryTurn.outcome.terminal !== undefined) {
615
795
  try {
616
- await clearCurrentCourt(admitted.runDirectory, request.courtAttemptId);
796
+ await persistReturnedRunState(admitted);
617
797
  }
618
798
  catch (error) {
619
- // Settlement already sealed accepted — a cleanup failure here must
620
- // not erase that fact or make the caller replay this court's
621
- // summons over already-delivered work (#840 已交劳动只整理终局不重做).
622
- // Preserve the cleanup failure as a post-dispatch diagnostic; the
623
- // accepted settlement remains authoritative (#840 r9 判词 class 1).
624
- await recordBestEffortPostDispatchDiagnostic(admitted, env, `current-court cleanup failed after accepted settlement (best-effort continue): ${describeErrorIdentity(error)}`, io);
799
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `run-state persistence failed beside催交 terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
625
800
  }
626
801
  }
627
- settledOutcome = {
628
- exitCode: exitCodeForTerminalOutcome(settled.roleOutcome),
629
- admitted,
630
- terminal: settled,
631
- turnDispatched: true,
632
- };
633
- }
634
- }
635
- catch (error) {
636
- // Settle (or its shouldPresent gate, or the failure-fact resolution
637
- // above) throw is a real failure fact — never swallow into undefined.
638
- const settledFailure = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
639
- timedOut: false,
640
- code: result.code,
641
- stderr: result.stderr,
642
- thrown: error,
643
- }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
644
- return await finishAfterTurn({ ...settledFailure, turnDispatched: true, ...deferredPersist });
645
- }
646
- if (settledOutcome !== undefined) {
647
- // #855: final cancel re-read before lawful seal — settlement window may
648
- // have received SIGTERM/SIGINT/SIGHUP after the earlier snapshot.
649
- const lateCancel = processCancelSignalName(env.signal);
650
- if (lateCancel !== undefined) {
651
- return await finishAfterTurn(await settleProcessCancelAfterTurn({
652
- admitted,
653
- cancelName: lateCancel,
654
- result,
655
- adapters,
656
- env,
657
- io,
658
- persistRunState,
659
- deferredPersist,
660
- invocationScopeId: request.invocationScopeId,
661
- }));
662
- }
663
- if (persistRunState) {
664
- try {
665
- await persistReturnedRunState(admitted, { lawful: true });
666
- }
667
- catch (error) {
668
- await recordBestEffortPostDispatchDiagnostic(admitted, env, `run-state persistence failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
669
- }
802
+ return await finishAfterTurn(deliveryTurn.outcome);
670
803
  }
671
- // Lawful persist + present is the caller's stop seam (auto-resume loop /
672
- // manual resume), not this retried host-turn function.
673
- // #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
674
- return await finishAfterTurn({ ...settledOutcome, ...deferredPersist });
675
- }
676
- // Host/runner true failure coexists with already-recorded payloads — never wash as accepted.
677
- if (hostSignalFailed) {
678
- const resolutionInput = controlledFailureInputFromResolution(resolution);
679
- const processCancelName = processCancelSignalName(env.signal);
680
- const stderrLogWriteDetails = stderrLogWriteFailure === undefined
681
- ? undefined
682
- : { stderrLogWriteFailure: describeCaughtError(stderrLogWriteFailure) };
683
- const failed = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
684
- timedOut: result.timedOut,
685
- code: result.code,
686
- stderr: result.stderr,
687
- ...resolutionInput,
688
- ...(processCancelName === undefined
689
- ? {}
690
- : { knownDiagnostic: processCancelDiagnostic(processCancelName) }),
691
- // Secondary fact only — never the cause. Rides on whichever channel
692
- // classification actually reads (knownFailure.details owns it when
693
- // a knownFailure exists; the top-level knownDetails otherwise).
694
- ...(stderrLogWriteDetails === undefined
695
- ? {}
696
- : resolutionInput.knownFailure !== undefined
697
- ? {
698
- knownFailure: {
699
- ...resolutionInput.knownFailure,
700
- details: { ...(resolutionInput.knownFailure.details ?? {}), ...stderrLogWriteDetails },
701
- },
702
- }
703
- : { knownDetails: stderrLogWriteDetails }),
704
- }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
705
- return await finishAfterTurn(withProcessCancelSkipAutoResume({ ...failed, turnDispatched: true, ...deferredPersist }, env.signal));
706
804
  }
707
- // Nothing else was wrong, but the durable stderr mirror itself failed to
708
- // write — that is the real (infrastructure) problem in this case, not a
709
- // lawful absence of a receipt. Honest and loud, not silently no_receipt.
710
- if (stderrLogWriteFailure !== undefined) {
711
- const failed = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
712
- timedOut: false,
713
- code: result.code,
714
- stderr: result.stderr,
715
- thrown: stderrLogWriteFailure,
716
- }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
717
- return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
718
- }
719
- const noReceipt = await attachRecordedSubmissions(admitted, await settleHostEndedNoReceipt(admitted, env.principalAuthority, courtScope), courtScope);
805
+ const noReceiptScope = { ...courtScope, previewOnly: true };
806
+ const noReceipt = await attachRecordedSubmissions(admitted, await settleHostEndedNoReceipt(admitted, env.principalAuthority, noReceiptScope, receiptDelivery.issuedDeliveryRequests()), noReceiptScope);
720
807
  // #855: cancel during no_receipt settlement window is not lawful success.
721
808
  const noReceiptCancel = processCancelSignalName(env.signal);
722
809
  if (noReceiptCancel !== undefined) {
@@ -730,17 +817,20 @@ export async function dispatchPostAdmissionTurn(input) {
730
817
  persistRunState,
731
818
  deferredPersist,
732
819
  invocationScopeId: request.invocationScopeId,
820
+ notePackageFault: courtScope.notePackageFault,
733
821
  }));
734
822
  }
735
823
  if (persistRunState) {
736
824
  try {
737
- await persistReturnedRunState(admitted, { lawful: true });
825
+ await persistReturnedRunState(admitted);
738
826
  }
739
827
  catch (error) {
740
828
  await recordBestEffortPostDispatchDiagnostic(admitted, env, `run-state persistence failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
741
829
  }
742
830
  }
743
831
  // #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
832
+ pendingSettlement = "no_receipt";
833
+ pendingTerminal = noReceipt;
744
834
  return await finishAfterTurn({
745
835
  exitCode: exitCodeForTerminalOutcome(noReceipt.roleOutcome),
746
836
  admitted,
@@ -749,6 +839,12 @@ export async function dispatchPostAdmissionTurn(input) {
749
839
  ...deferredPersist,
750
840
  });
751
841
  }
842
+ catch (error) {
843
+ if (turnDispatched && !(error instanceof TurnDispatchedFailure)) {
844
+ throw new TurnDispatchedFailure(error);
845
+ }
846
+ throw error;
847
+ }
752
848
  finally {
753
849
  // Public manual resume (#987) holds no package writer lease.
754
850
  if (lease !== undefined) {
@@ -831,8 +927,9 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
831
927
  kind: "resume",
832
928
  prompt,
833
929
  },
834
- ...(request.message === undefined ? {} : { courtAttemptId: randomUUID() }),
835
930
  ...(env.stationChild === undefined ? {} : { stationChild: env.stationChild }),
931
+ // #1132: one configured ceiling, already resolved by the caller (#422).
932
+ deliveryRequestLimit: deliveryLimitFromConfig(env.autoResumeLimit),
836
933
  };
837
934
  }
838
935
  /** Shared new-turn and in-call auto-resume projection. Seat code supplies prompt and activation. */
@@ -846,6 +943,8 @@ export function roleTurnOptions(env, admitted, continuation) {
846
943
  : { correlationId }),
847
944
  continuation,
848
945
  ...(env.stationChild === undefined ? {} : { stationChild: env.stationChild }),
946
+ // #1132: one configured ceiling, already resolved by the caller (#422).
947
+ deliveryRequestLimit: deliveryLimitFromConfig(env.autoResumeLimit),
849
948
  };
850
949
  }
851
950
  /**
@@ -875,6 +974,7 @@ async function dispatchAfterWriterLease(input) {
875
974
  async function runSettledAutoResumeLoop(input) {
876
975
  const adapters = withOnceSuccessfulBeforeDispatch(input.adapters);
877
976
  const dispatchEnv = input.dispatchEnv ?? input.env;
977
+ const receiptDelivery = createReceiptDeliveryPolicy(input.env.autoResumeLimit);
878
978
  return runWithAutoResumeLoop({
879
979
  admitted: input.admitted,
880
980
  principalAuthority: input.env.principalAuthority,
@@ -896,11 +996,12 @@ async function runSettledAutoResumeLoop(input) {
896
996
  ...(lease === undefined ? {} : { lease }),
897
997
  adapters,
898
998
  persistRunState: false,
999
+ receiptDelivery,
899
1000
  ...(input.effectiveEngine === undefined
900
1001
  ? {}
901
1002
  : { effectiveEngine: input.effectiveEngine }),
902
1003
  });
903
- return settleDeferredPersist(input.admitted, input.env.principalAuthority, adapters, attemptIo, result);
1004
+ return settleDeferredPersist(input.admitted, input.env, attemptIo, result);
904
1005
  },
905
1006
  }),
906
1007
  });
@@ -1021,11 +1122,13 @@ export async function runPostAdmissionSeatResume(input) {
1021
1122
  };
1022
1123
  }
1023
1124
  }
1024
- // Open court continue, or new court for summons / message re-review
1025
- // (clause 0 新庭可再交卷; #833). Bare resume without open court omits id.
1026
- if (openCourtAttemptId !== undefined ||
1027
- request.summons !== undefined ||
1028
- request.message !== undefined) {
1125
+ // Open court continue, or a new court for a real re-summons
1126
+ // (clause 0 新庭可再交卷; #833). Bare resume — with or without caller
1127
+ // words — carries no summons and no open court, so it mints none:
1128
+ // the package must not turn ordinary continuation bytes into an audit
1129
+ // court whose attempt scope would reshape settlement (#1032 修订票:
1130
+ // 此要求不延伸到没有该传召庭次的裸 resume).
1131
+ if (openCourtAttemptId !== undefined || request.summons !== undefined) {
1029
1132
  const courtAttemptId = openCourtAttemptId ??
1030
1133
  (turnRequest.courtAttemptId !== undefined &&
1031
1134
  turnRequest.courtAttemptId.length > 0
@@ -1145,8 +1248,7 @@ function writeResumeFailurePointer(io, errorPath) {
1145
1248
  export function showResumeErrorPointer(io, exitCode, terminal) {
1146
1249
  if (exitCode === 0 || terminal === undefined)
1147
1250
  return;
1148
- const publishedError = terminal.artifacts.find((artifact) => artifact.kind === "error")?.path
1149
- ?? publishedFailureErrorPath(terminal);
1251
+ const publishedError = terminal.artifacts.find((artifact) => artifact.kind === "error")?.path;
1150
1252
  if (publishedError !== undefined)
1151
1253
  writeResumeFailurePointer(io, publishedError);
1152
1254
  }
@@ -1287,7 +1389,7 @@ export async function runPostAdmissionManualResume(input) {
1287
1389
  // through its own settleAfterTurnStarted-backed branches; a lawful result
1288
1390
  // only ever reaches this point once that persist has already succeeded
1289
1391
  // (#836 r12 class 3 dedup — one persist owner, not a second here).
1290
- io.stdout(formatTerminalResult(result.terminal));
1392
+ await presentTerminal(result.terminal, io, admitted.runDirectory);
1291
1393
  }
1292
1394
  if (result.terminal !== undefined) {
1293
1395
  result.terminal.autoResumeCount = 0;