@akagilnc/pi-workflow-roles 0.1.5375 → 0.1.5406

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 (79) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/dist/acp-host/production-host.js +6381 -5684
  4. package/dist/archivist-record-pointer.js +38 -0
  5. package/dist/collector-tool-schemas.js +18 -0
  6. package/dist/compliance-transport.js +5 -0
  7. package/dist/diarist-contracts.js +26 -0
  8. package/dist/doctor-auditor.js +1 -0
  9. package/dist/engine-detour-usage.js +6 -6
  10. package/dist/filed-submission.js +28 -0
  11. package/dist/gatekeeper-role.js +6 -10
  12. package/dist/gleaner-left-contracts.js +16 -0
  13. package/dist/headless-host/production-host.js +1644 -947
  14. package/dist/judge-role.js +21 -16
  15. package/dist/migrate-book-topology.js +452 -81
  16. package/dist/navigator-attendance.js +10 -9
  17. package/dist/navigator-invocation-identity.js +18 -0
  18. package/dist/package-contracts/reviewer-output.js +16 -0
  19. package/dist/package-contracts/worker-output.js +18 -2
  20. package/dist/pi/role-turn-host.js +9 -0
  21. package/dist/public-cli/cli.js +1 -0
  22. package/dist/public-cli/instruction-seat-run.js +258 -55
  23. package/dist/public-cli/main.js +1209 -346
  24. package/dist/public-cli/post-admission.js +427 -244
  25. package/dist/public-cli/settlement.js +111 -48
  26. package/dist/public-cli/terminal.js +12 -1
  27. package/dist/public-cli/turn-request.js +3 -0
  28. package/dist/receipt-delivery-policy.js +152 -17
  29. package/dist/role-submission-declarations.js +131 -0
  30. package/dist/secretariat-contracts.js +23 -1
  31. package/dist/submission-errors.js +11 -1
  32. package/dist/submission-gate.js +32 -12
  33. package/package.json +1 -1
  34. package/src/archivist-record-pointer.ts +48 -0
  35. package/src/auditor-role.ts +2 -8
  36. package/src/collector-role.ts +5 -28
  37. package/src/compliance-transport.ts +10 -2
  38. package/src/countersign-role.ts +2 -7
  39. package/src/diarist-contracts.ts +37 -0
  40. package/src/diarist-role.ts +4 -44
  41. package/src/doctor-auditor.ts +3 -0
  42. package/src/doctor-role.ts +16 -11
  43. package/src/engine-detour-usage.ts +7 -6
  44. package/src/external-host-turn-loop.ts +24 -12
  45. package/src/filed-submission.ts +44 -0
  46. package/src/gatekeeper-role.ts +8 -16
  47. package/src/gleaner-left-contracts.ts +26 -1
  48. package/src/gleaner-left-role.ts +4 -32
  49. package/src/host-contracts.ts +7 -0
  50. package/src/inspector-role.ts +2 -7
  51. package/src/judge-role.ts +31 -24
  52. package/src/merger-role.ts +5 -8
  53. package/src/navigator-attendance.ts +17 -12
  54. package/src/navigator-invocation-identity.ts +31 -0
  55. package/src/navigator-role.ts +2 -8
  56. package/src/notary-role.ts +5 -20
  57. package/src/package-contracts/reviewer-output.ts +25 -0
  58. package/src/package-contracts/worker-output.ts +43 -5
  59. package/src/pi/adapter.ts +33 -8
  60. package/src/pi/role-turn-host.ts +7 -0
  61. package/src/public-cli/cli.ts +1 -0
  62. package/src/public-cli/instruction-seat-run.ts +292 -61
  63. package/src/public-cli/post-admission.ts +556 -324
  64. package/src/public-cli/settlement.ts +112 -50
  65. package/src/public-cli/terminal.ts +16 -4
  66. package/src/public-cli/turn-request.ts +10 -1
  67. package/src/receipt-delivery-policy.ts +209 -17
  68. package/src/reviewer-role.ts +7 -35
  69. package/src/role-envelope.ts +5 -1
  70. package/src/role-runtime-dependencies.ts +1 -0
  71. package/src/role-runtime.ts +72 -56
  72. package/src/role-submission-declarations.ts +143 -0
  73. package/src/secretariat-contracts.ts +35 -0
  74. package/src/secretariat-role.ts +4 -40
  75. package/src/submission-correctable-error.ts +5 -2
  76. package/src/submission-errors.ts +12 -1
  77. package/src/submission-gate.ts +41 -12
  78. package/src/worker-role.ts +81 -86
  79. package/src/worker-submission-gates.ts +32 -7
@@ -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. */
@@ -39,6 +40,7 @@ import { mintEngineDetourInvocationScope, withEngineDetourInvocationScope, } fro
39
40
  import { clearCurrentCourt, clearTypedProviderHttpObservation, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, renderResumeCommand, } from "./run-lifecycle.js";
40
41
  import { processCancelDiagnostic, processCancelSignalName, } from "./process-cancel.js";
41
42
  import { homeFromRunDirectory } from "../activation-ledger-topology.js";
43
+ import { DEFAULT_ROLE_TURN_HOST } from "../host-descriptions.js";
42
44
  import { recordRunStart } from "../host-session-record.js";
43
45
  import { attemptProducedFreshSubmission, classifyPostAdmissionFailure, controlledFailureInputFromResolution, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatErrorCauseDetail, formatTerminalResult, inspectJudgeSession, isLawfulTypedTerminalOutcome, presentFailureTerminal, publishedFailureErrorPath, presentStructuralRejection, resolveAuditedRunnerFailureResolution, resolveControlledFailureResumeObservation, settleFailureTerminalResult, settleHostEndedNoReceipt, attachRecordedSubmissions, rewritePublishedFailureErrorPath, } from "./settlement.js";
44
46
  import {} from "./terminal.js";
@@ -298,9 +300,320 @@ async function settleDeferredPersist(admitted, authority, adapters, io, result)
298
300
  }
299
301
  catch (error) {
300
302
  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 };
303
+ return {
304
+ ...failed,
305
+ turnDispatched: true,
306
+ skipAutoResume: true,
307
+ };
302
308
  }
303
309
  }
310
+ /**
311
+ * Court/invocation scope for settlement reads. The one construction of this
312
+ * shape for every turn (#1132 — the first turn and each 催交 turn share it).
313
+ */
314
+ function settlementScopeForTurn(request) {
315
+ const hasCourt = request.courtAttemptId !== undefined && request.courtAttemptId.length > 0;
316
+ const hasScope = request.invocationScopeId !== undefined && request.invocationScopeId.length > 0;
317
+ if (!hasCourt && !hasScope)
318
+ return undefined;
319
+ return {
320
+ ...(hasCourt ? { courtAttemptId: request.courtAttemptId } : {}),
321
+ ...(hasScope ? { invocationScopeId: request.invocationScopeId } : {}),
322
+ };
323
+ }
324
+ /**
325
+ * #1132: one 没交卷催交 turn, dispatched through the SAME settlement path as the
326
+ * first turn.
327
+ *
328
+ * There is deliberately no second settlement implementation here. A delivery
329
+ * turn is an ordinary host turn on this same run/session: this builds the
330
+ * host-neutral resume request (the adapter's stored native session id, ADR 0082;
331
+ * pi's existing delivery-state content reused verbatim — no new prompt wording,
332
+ * no new host capability). The caller settles it through the same
333
+ * `settleCompletedHostTurn` the first turn uses (ADR 0080): the runner/host
334
+ * failure re-read, the shouldPresent gate, the fresh-seal check, open-court
335
+ * cleanup, cancel re-reads, run-state persist, and the stderr mirror.
336
+ *
337
+ * The caller owns the live delivery policy; the request projects its next send.
338
+ */
339
+ async function buildReceiptDeliveryRequest(input) {
340
+ const { admitted, env, request, receiptDelivery } = input;
341
+ const hostSessionId = await readStoredHostSessionId(env.host, env.principalAuthority, admitted.principal);
342
+ return {
343
+ ...request,
344
+ continuation: {
345
+ kind: "resume",
346
+ prompt: JSON.stringify({
347
+ ...receiptDelivery.deliveryState(),
348
+ deliveryTurns: receiptDelivery.issuedDeliveryRequests() + 1,
349
+ }),
350
+ ...(hostSessionId === undefined ? {} : { hostSessionId }),
351
+ },
352
+ deliveryRequestLimit: receiptDelivery.limit,
353
+ };
354
+ }
355
+ /**
356
+ * #1132: settle ONE completed host turn — the single post-turn settlement
357
+ * authority shared by the first turn and by every 没交卷催交 turn (ADR 0080).
358
+ *
359
+ * Extracted deliberately instead of re-entering `dispatchPostAdmissionTurn`.
360
+ * The turn-BEFORE concerns (beforeDispatch / markRunRunning / relocate /
361
+ * afterDispatch / writer-lease release) belong to the outermost dispatch and
362
+ * run exactly once per run; everything from the stderr mirror onward is
363
+ * per-turn. A 催交 turn is a real host turn on this same run/session, so it
364
+ * settles through THIS function — not a reduced copy — and it re-runs neither
365
+ * the admission hooks nor the run-state terminal write.
366
+ *
367
+ * Returns `still-missing` when the turn simply produced no receipt: the caller
368
+ * (first turn or the 催交 loop) then decides between issuing another delivery
369
+ * request and settling no_receipt.
370
+ */
371
+ async function settleCompletedHostTurn(input) {
372
+ const { admitted, env, io, adapters, shouldPresent, request, result, persistRunState, deferredPersist } = input;
373
+ const finishAfterTurn = input.finishAfterTurn;
374
+ // `result.stderr` stays live in memory for the real classification below
375
+ // regardless of whether this durable mirror write succeeds. A write
376
+ // failure here (unwritable run directory, stderr.log occupied as a
377
+ // directory, ...) is a real infrastructure problem and must never be
378
+ // dropped silently (catch-and-continue with no trace is a defect) — but
379
+ // it also must never become THE controlling failure and wash out a
380
+ // primary child signal that already arrived on `result`, or an
381
+ // already-sealed accepted payload (#836: a secondary failure must not
382
+ // overwrite an already-known cause or an already-recorded leg). It rides
383
+ // beside whatever the real classification below determines, and only
384
+ // becomes the reported failure itself when nothing else is wrong.
385
+ //
386
+ // Everything below runs after the host turn genuinely started (#840 r9
387
+ // 判词 class 1 boundary — 覆盖 executeTurn 已启动后至返回带 turnDispatched
388
+ // 结果前的全部异常). Each fallible step routes any failure through the
389
+ // single existing settlement authority (presentControlledFailure /
390
+ // settleFailureTerminalResult) rather than a second one (ADR 0080:
391
+ // one settlement disposition owner) — never by fabricating a terminal here.
392
+ // A cleanup step that runs only after a real settlement (clearCurrentCourt)
393
+ // is protected by logging and keeping that already-obtained result, never
394
+ // by discarding it (#840 已交劳动只整理终局不重做). A failure inside the
395
+ // settlement authority itself (presentControlledFailure's own reads) goes
396
+ // through settleAfterTurnStarted: still no fabricated terminal, but the
397
+ // re-thrown TurnDispatchedFailure still tells runWithAutoResumeLoop the
398
+ // turn genuinely started, so the next retry sends a resume payload — the
399
+ // true cause settles through the loop's own existing dispatch-exception
400
+ // machinery once the retry budget is exhausted.
401
+ let stderrLogWriteFailure;
402
+ try {
403
+ await writeFile(join(admitted.runDirectory, "stderr.log"), result.stderr, "utf8");
404
+ }
405
+ catch (error) {
406
+ stderrLogWriteFailure = error;
407
+ // Best-effort: the turn's own stderr capture is secondary to lawful /
408
+ // controlled-failure settlement below, but the failure itself must
409
+ // still leave a real trace (失败诚实宪法 真因必须落痕) — durably, since
410
+ // every auto-resume attempt's io is dummyIo (#840 r9 判词 class 1). It
411
+ // still rides beside the classification below and, if nothing else is
412
+ // wrong, becomes the reported failure itself (#836).
413
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `stderr.log write failed (best-effort continue): ${describeErrorIdentity(error)}`, io);
414
+ }
415
+ const courtScope = settlementScopeForTurn(request);
416
+ // Single complete boundary (#840 r9 判词 class 1): resolving the
417
+ // host/runner failure facts, trySettle, its shouldPresent gate, and the
418
+ // accepted-settlement cleanup all settle through this one catch — a
419
+ // throw from any of them (including the session-file decode itself)
420
+ // still routes through settleAfterTurnStarted's TurnDispatchedFailure
421
+ // instead of losing turnDispatched to an uncaught throw. The facts are
422
+ // resolved before trySettle (#836) so an already-accepted settlement is
423
+ // never presented over a real current host/runner failure signal or a
424
+ // real stderr.log durable-write failure — both stay a real failure with
425
+ // the recorded payload riding beside it, not replacing it.
426
+ let settled;
427
+ let settledOutcome;
428
+ let hostSignalFailed = false;
429
+ let resolution;
430
+ try {
431
+ const sessionFile = admitted.principal !== undefined
432
+ ? env.principalAuthority.decode(admitted.principal).sessionFile
433
+ : "";
434
+ const runnerKnownFailure = adapters.resolveRunnerKnownFailure !== undefined && sessionFile !== ""
435
+ ? await adapters.resolveRunnerKnownFailure({ result, sessionFile })
436
+ : result.knownFailure;
437
+ resolution = await resolveAuditedRunnerFailureResolution({
438
+ runner: runnerKnownFailure,
439
+ sessionFile,
440
+ credential: undefined,
441
+ runDirectory: admitted.runDirectory,
442
+ });
443
+ // A direct, current signal from the host/runner itself (timeout / host
444
+ // knownFailure / runner knownFailure / missing credential) is a real
445
+ // problem regardless of what else already settled — a settled accepted
446
+ // outcome must not paper over it (it rides beside the recorded payload
447
+ // via `submissions`, never replacing the payload). The audited
448
+ // resolution's own typed session read (malformed JSONL, provider-stop,
449
+ // an illegal/unsealed accepted status, a stale/superseded typed-HTTP
450
+ // observation, ...) makes a run with no sealed acceptance a true
451
+ // failure too — it must not be discarded into a lawful no_receipt just
452
+ // because the raw runner signal alone looked clean. But it must not
453
+ // retroactively invalidate an acceptance that already sealed this turn
454
+ // — a resolved 429 observed earlier in the same session is exactly
455
+ // that.
456
+ const processCancelName = processCancelSignalName(env.signal);
457
+ const directHostFailureSignal = result.timedOut
458
+ || result.knownFailure !== undefined
459
+ || runnerKnownFailure !== undefined
460
+ || processCancelName !== undefined;
461
+ hostSignalFailed =
462
+ directHostFailureSignal
463
+ || (result.code !== null && result.code !== 0)
464
+ || resolution.knownFailure !== undefined;
465
+ settled = await adapters.trySettle(admitted, env.principalAuthority, courtScope);
466
+ if (settled !== undefined) {
467
+ settled = await attachRecordedSubmissions(admitted, settled, courtScope);
468
+ }
469
+ // #836 r12 class 2: an accepted/audit_escalation settlement can be
470
+ // entirely a prior attempt's stale payload (courtAttempt is a
471
+ // recording tag, not a visibility gate — attachRecordedSubmissions
472
+ // above still surfaces that historical payload honestly either way).
473
+ // Consume the ledger's own subject.attemptId (submission-ledger.ts)
474
+ // only to learn whether *this* attempt itself produced a fresh seal —
475
+ // never to filter what presents.
476
+ const settledIsFreshThisAttempt = settled === undefined
477
+ || (settled.roleOutcome.kind !== "accepted" && settled.roleOutcome.kind !== "audit_escalation")
478
+ ? true
479
+ : await attemptProducedFreshSubmission(admitted, courtScope);
480
+ // A lawful settled outcome already reached this turn takes precedence
481
+ // over a later bare exit-code / session-inspection signal (trailing
482
+ // nonzero exit, late stderr noise, a stale already-superseded
483
+ // typed-HTTP observation, ...) — but never over a direct current
484
+ // host/runner failure signal, nor over a real stderr.log durable-write
485
+ // failure (confirmed infrastructure trouble, not weak/bare evidence),
486
+ // both of which stay a real failure with the recorded payload riding
487
+ // beside it, not replacing it (#836: never kill an already-recorded
488
+ // leg, but never wash a real failure away either). Note: the narrower
489
+ // directHostFailureSignal gates acceptance here; the broader
490
+ // hostSignalFailed (adds bare exit code / resolution.knownFailure)
491
+ // only gates the no-settlement fallback below — except when the
492
+ // settlement itself is entirely stale (no fresh seal this attempt),
493
+ // where a bare nonzero exit / resolution failure must not be outranked
494
+ // by someone else's earlier success (#836 r12 class 2).
495
+ const staleAcceptanceOutranksRealFailure = !settledIsFreshThisAttempt && hostSignalFailed;
496
+ // #855: re-read cancel after trySettle/attach awaits — a signal in this
497
+ // window must not land as lawful accepted.
498
+ const cancelAfterSettle = processCancelSignalName(env.signal);
499
+ if (settled !== undefined
500
+ && shouldPresent(settled)
501
+ && !directHostFailureSignal
502
+ && !staleAcceptanceOutranksRealFailure
503
+ && stderrLogWriteFailure === undefined
504
+ && cancelAfterSettle === undefined) {
505
+ // This court sealed — drop open-court pointer (bare resume no longer continues it).
506
+ if (settled.roleOutcome.kind === "accepted" &&
507
+ request.courtAttemptId !== undefined &&
508
+ request.courtAttemptId.length > 0) {
509
+ try {
510
+ await clearCurrentCourt(admitted.runDirectory, request.courtAttemptId);
511
+ }
512
+ catch (error) {
513
+ // Settlement already sealed accepted — a cleanup failure here must
514
+ // not erase that fact or make the caller replay this court's
515
+ // summons over already-delivered work (#840 已交劳动只整理终局不重做).
516
+ // Preserve the cleanup failure as a post-dispatch diagnostic; the
517
+ // accepted settlement remains authoritative (#840 r9 判词 class 1).
518
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `current-court cleanup failed after accepted settlement (best-effort continue): ${describeErrorIdentity(error)}`, io);
519
+ }
520
+ }
521
+ settledOutcome = {
522
+ exitCode: exitCodeForTerminalOutcome(settled.roleOutcome),
523
+ admitted,
524
+ terminal: settled,
525
+ turnDispatched: true,
526
+ };
527
+ }
528
+ }
529
+ catch (error) {
530
+ // Settle (or its shouldPresent gate, or the failure-fact resolution
531
+ // above) throw is a real failure fact — never swallow into undefined.
532
+ const settledFailure = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
533
+ timedOut: false,
534
+ code: result.code,
535
+ stderr: result.stderr,
536
+ thrown: error,
537
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
538
+ return { kind: "settled", outcome: await finishAfterTurn({ ...settledFailure, turnDispatched: true, ...deferredPersist }) };
539
+ }
540
+ if (settledOutcome !== undefined) {
541
+ // #855: final cancel re-read before lawful seal — settlement window may
542
+ // have received SIGTERM/SIGINT/SIGHUP after the earlier snapshot.
543
+ const lateCancel = processCancelSignalName(env.signal);
544
+ if (lateCancel !== undefined) {
545
+ return { kind: "settled", outcome: await finishAfterTurn(await settleProcessCancelAfterTurn({
546
+ admitted,
547
+ cancelName: lateCancel,
548
+ result,
549
+ adapters,
550
+ env,
551
+ io,
552
+ persistRunState,
553
+ deferredPersist,
554
+ invocationScopeId: request.invocationScopeId,
555
+ })) };
556
+ }
557
+ if (persistRunState) {
558
+ try {
559
+ await persistReturnedRunState(admitted, { lawful: true });
560
+ }
561
+ catch (error) {
562
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `run-state persistence failed beside host terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
563
+ }
564
+ }
565
+ // Lawful persist + present is the caller's stop seam (auto-resume loop /
566
+ // manual resume), not this retried host-turn function.
567
+ // #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
568
+ return { kind: "settled", outcome: await finishAfterTurn({ ...settledOutcome, ...deferredPersist }) };
569
+ }
570
+ // Host/runner true failure coexists with already-recorded payloads — never wash as accepted.
571
+ if (hostSignalFailed) {
572
+ const resolutionInput = controlledFailureInputFromResolution(resolution);
573
+ const processCancelName = processCancelSignalName(env.signal);
574
+ const stderrLogWriteDetails = stderrLogWriteFailure === undefined
575
+ ? undefined
576
+ : { stderrLogWriteFailure: describeCaughtError(stderrLogWriteFailure) };
577
+ const failed = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
578
+ timedOut: result.timedOut,
579
+ code: result.code,
580
+ stderr: result.stderr,
581
+ ...resolutionInput,
582
+ ...(processCancelName === undefined
583
+ ? {}
584
+ : { knownDiagnostic: processCancelDiagnostic(processCancelName) }),
585
+ // Secondary fact only — never the cause. Rides on whichever channel
586
+ // classification actually reads (knownFailure.details owns it when
587
+ // a knownFailure exists; the top-level knownDetails otherwise).
588
+ ...(stderrLogWriteDetails === undefined
589
+ ? {}
590
+ : resolutionInput.knownFailure !== undefined
591
+ ? {
592
+ knownFailure: {
593
+ ...resolutionInput.knownFailure,
594
+ details: { ...(resolutionInput.knownFailure.details ?? {}), ...stderrLogWriteDetails },
595
+ },
596
+ }
597
+ : { knownDetails: stderrLogWriteDetails }),
598
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
599
+ return { kind: "settled", outcome: await finishAfterTurn(withProcessCancelSkipAutoResume({ ...failed, turnDispatched: true, ...deferredPersist }, env.signal)) };
600
+ }
601
+ // Nothing else was wrong, but the durable stderr mirror itself failed to
602
+ // write — that is the real (infrastructure) problem in this case, not a
603
+ // lawful absence of a receipt. Honest and loud, not silently no_receipt.
604
+ if (stderrLogWriteFailure !== undefined) {
605
+ const failed = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
606
+ timedOut: false,
607
+ code: result.code,
608
+ stderr: result.stderr,
609
+ thrown: stderrLogWriteFailure,
610
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
611
+ return { kind: "settled", outcome: await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist }) };
612
+ }
613
+ // No receipt at all after a clean host turn: lawful no_receipt, but the
614
+ // caller owns whether to催交 first (票面 3).
615
+ return { kind: "still-missing" };
616
+ }
304
617
  export async function dispatchPostAdmissionTurn(input) {
305
618
  const { admitted, env, io, request, lease, adapters, effectiveEngine } = input;
306
619
  const persistRunState = input.persistRunState !== false;
@@ -313,6 +626,14 @@ export async function dispatchPostAdmissionTurn(input) {
313
626
  * exists, keep that cause and leave relocate failure on the shared diagnostic channel.
314
627
  */
315
628
  let afterDispatchApplied = false;
629
+ const receiptDelivery = input.receiptDelivery ?? createReceiptDeliveryPolicy(env.autoResumeLimit);
630
+ let turnDispatched = false;
631
+ // #1132: the AK-side 催交 covers hosts that do not deliver to themselves.
632
+ // Pi runs the package role runtime in-process, and that runtime already issues
633
+ // its own delivery requests at `agent_end` and records the real count — asking
634
+ // again here would double-deliver on one run. External hosts (ACP / headless
635
+ // CLI) have no such in-child loop, so the AK execution seam owns it for them.
636
+ const akSeamDeliversReceipt = env.host !== undefined && env.host !== "" && env.host !== DEFAULT_ROLE_TURN_HOST;
316
637
  const finishAfterTurn = async (result) => {
317
638
  if (afterDispatchApplied)
318
639
  return result;
@@ -452,6 +773,7 @@ export async function dispatchPostAdmissionTurn(input) {
452
773
  let result;
453
774
  try {
454
775
  recordRunStart(admitted.runDirectory);
776
+ turnDispatched = true;
455
777
  result = await env.roleTurnHost.executeTurn(turnRequest);
456
778
  }
457
779
  catch (error) {
@@ -466,257 +788,106 @@ export async function dispatchPostAdmissionTurn(input) {
466
788
  }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
467
789
  return await finishAfterTurn(withProcessCancelSkipAutoResume({ ...settled, turnDispatched: true, ...deferredPersist }, env.signal));
468
790
  }
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);
791
+ // #1132: settle this turn through the ONE post-turn settlement authority,
792
+ // then — if it simply produced no receipt — 催交 before settling no_receipt.
793
+ const settledTurn = await settleCompletedHostTurn({
794
+ admitted,
795
+ env,
796
+ io,
797
+ adapters,
798
+ shouldPresent,
799
+ request,
800
+ result,
801
+ persistRunState,
802
+ deferredPersist,
803
+ finishAfterTurn,
804
+ });
805
+ if (settledTurn.kind === "settled")
806
+ return settledTurn.outcome;
807
+ // #1132: 没交卷先回本人催交,不直接结为 no_receipt. The AK execution seam
808
+ // owns the 催交 and its real count. Each request is a real host turn on this
809
+ // same run/session, dispatched and settled through the same two seams as the
810
+ // first turn — never a reduced copy. 催交 never substitutes for host failure
811
+ // recovery: a delivery turn that fails keeps its true cause below.
812
+ // The delivery budget is spent HERE, at the outermost level; only when it is
813
+ // exhausted with still no receipt does the settlement seam record no_receipt.
814
+ while (akSeamDeliversReceipt && receiptDelivery.nextAction() === "request-delivery") {
815
+ // Assembly sits in the same post-start catch as the host call. A throw
816
+ // here is a started turn: resume the session, and do not count a request
817
+ // that never reached the host.
818
+ let deliveryResult;
819
+ let deliveryTurnRequest;
820
+ try {
821
+ const deliveryRequest = await buildReceiptDeliveryRequest({
822
+ admitted,
823
+ env,
824
+ // Host and the other axes already selected on this turn ride the
825
+ // shared projection. The pre-projection request does not have them.
826
+ request: turnRequest,
827
+ receiptDelivery,
828
+ });
829
+ // The same native-resume projection the first turn uses (host adapter owns
830
+ // the resume; AK only hands it the request — ADR 0082).
831
+ deliveryTurnRequest = {
832
+ ...deliveryRequest,
833
+ principal: admitted.principal,
834
+ runDirectory: admitted.runDirectory,
835
+ ...(env.signal === undefined ? {} : { signal: env.signal }),
836
+ };
837
+ receiptDelivery.recordDeliveryRequest();
838
+ deliveryResult = await env.roleTurnHost.executeTurn(deliveryTurnRequest);
839
+ }
840
+ catch (error) {
841
+ // 催交 cannot substitute for host failure recovery: a real failure with
842
+ // its true cause, through the one settlement authority (ADR 0080).
843
+ // The turn started, so the caller's loop resumes instead of replaying
844
+ // the initial payload. The issued count survives that re-entry.
845
+ const settled = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({ timedOut: false, code: null, stderr: "", thrown: error }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
846
+ return await finishAfterTurn(withProcessCancelSkipAutoResume({
847
+ ...settled,
848
+ turnDispatched: true,
849
+ ...deferredPersist,
850
+ }, env.signal));
574
851
  }
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) {
852
+ const deliveryTurn = await settleCompletedHostTurn({
853
+ admitted,
854
+ env,
855
+ // Only the final Terminal presents; intermediate delivery attempts stay quiet.
856
+ io: { ...io, stdout: () => { } },
857
+ adapters,
858
+ shouldPresent,
859
+ request: deliveryTurnRequest,
860
+ result: deliveryResult,
861
+ // Run-state terminal write belongs to the OUTERMOST turn only, so a
862
+ // still-silent delivery attempt never makes the run look lawfully terminal.
863
+ persistRunState: false,
864
+ deferredPersist,
865
+ finishAfterTurn,
866
+ });
867
+ // The delivery turn's own outcome IS the run's outcome, except when it
868
+ // simply still has no receipt — that is the loop's own case to keep going.
869
+ if (deliveryTurn.kind === "settled") {
870
+ // #1132: the delivery turn settles with persistRunState:false, so a run
871
+ // that ends HERE (得卷 or a real failure) still needs the outermost
872
+ // single run-state write. Without it the ledger says accepted while
873
+ // run-state stays `running`, and every later resume entry (ADR 0080)
874
+ // reads a finished run as live. The still-silent attempts above must
875
+ // NOT write; this one, which ends the run, must.
876
+ if (persistRunState && deliveryTurn.outcome.terminal !== undefined) {
615
877
  try {
616
- await clearCurrentCourt(admitted.runDirectory, request.courtAttemptId);
878
+ await persistReturnedRunState(admitted, {
879
+ lawful: isLawfulTypedTerminalOutcome(deliveryTurn.outcome.terminal.roleOutcome),
880
+ });
617
881
  }
618
882
  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);
883
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `run-state persistence failed beside催交 terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
625
884
  }
626
885
  }
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
- }
886
+ return await finishAfterTurn(deliveryTurn.outcome);
670
887
  }
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
888
  }
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
- }
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);
889
+ const noReceiptScope = settlementScopeForTurn(request);
890
+ const noReceipt = await attachRecordedSubmissions(admitted, await settleHostEndedNoReceipt(admitted, env.principalAuthority, noReceiptScope, receiptDelivery.issuedDeliveryRequests()), noReceiptScope);
720
891
  // #855: cancel during no_receipt settlement window is not lawful success.
721
892
  const noReceiptCancel = processCancelSignalName(env.signal);
722
893
  if (noReceiptCancel !== undefined) {
@@ -749,6 +920,12 @@ export async function dispatchPostAdmissionTurn(input) {
749
920
  ...deferredPersist,
750
921
  });
751
922
  }
923
+ catch (error) {
924
+ if (turnDispatched && !(error instanceof TurnDispatchedFailure)) {
925
+ throw new TurnDispatchedFailure(error);
926
+ }
927
+ throw error;
928
+ }
752
929
  finally {
753
930
  // Public manual resume (#987) holds no package writer lease.
754
931
  if (lease !== undefined) {
@@ -833,6 +1010,8 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
833
1010
  },
834
1011
  ...(request.message === undefined ? {} : { courtAttemptId: randomUUID() }),
835
1012
  ...(env.stationChild === undefined ? {} : { stationChild: env.stationChild }),
1013
+ // #1132: one configured ceiling, already resolved by the caller (#422).
1014
+ deliveryRequestLimit: deliveryLimitFromConfig(env.autoResumeLimit),
836
1015
  };
837
1016
  }
838
1017
  /** Shared new-turn and in-call auto-resume projection. Seat code supplies prompt and activation. */
@@ -846,6 +1025,8 @@ export function roleTurnOptions(env, admitted, continuation) {
846
1025
  : { correlationId }),
847
1026
  continuation,
848
1027
  ...(env.stationChild === undefined ? {} : { stationChild: env.stationChild }),
1028
+ // #1132: one configured ceiling, already resolved by the caller (#422).
1029
+ deliveryRequestLimit: deliveryLimitFromConfig(env.autoResumeLimit),
849
1030
  };
850
1031
  }
851
1032
  /**
@@ -875,6 +1056,7 @@ async function dispatchAfterWriterLease(input) {
875
1056
  async function runSettledAutoResumeLoop(input) {
876
1057
  const adapters = withOnceSuccessfulBeforeDispatch(input.adapters);
877
1058
  const dispatchEnv = input.dispatchEnv ?? input.env;
1059
+ const receiptDelivery = createReceiptDeliveryPolicy(input.env.autoResumeLimit);
878
1060
  return runWithAutoResumeLoop({
879
1061
  admitted: input.admitted,
880
1062
  principalAuthority: input.env.principalAuthority,
@@ -896,6 +1078,7 @@ async function runSettledAutoResumeLoop(input) {
896
1078
  ...(lease === undefined ? {} : { lease }),
897
1079
  adapters,
898
1080
  persistRunState: false,
1081
+ receiptDelivery,
899
1082
  ...(input.effectiveEngine === undefined
900
1083
  ? {}
901
1084
  : { effectiveEngine: input.effectiveEngine }),