@akagilnc/pi-workflow-roles 0.1.3932 → 0.1.3975

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 (162) hide show
  1. package/CLAUDE.md +1 -1
  2. package/dist/acp-host/production-host.js +2389 -6018
  3. package/dist/analyst-gate-cycles-read.js +15 -19
  4. package/dist/audit-escalation.js +16 -46
  5. package/dist/auditor-dossier-tool.js +7 -62
  6. package/dist/collector-identity.js +0 -157
  7. package/dist/collector-ledger.js +2 -42
  8. package/dist/compliance-transport.js +40 -14
  9. package/dist/diarist-contracts.js +2 -40
  10. package/dist/doctor-contracts.js +39 -108
  11. package/dist/doctor-evidence.js +1 -5
  12. package/dist/engine-detour-tool.js +1 -1
  13. package/dist/gatekeeper-role.js +49 -38
  14. package/dist/headless-host/production-host.js +4814 -8450
  15. package/dist/host-contracts.js +0 -5
  16. package/dist/merger-contracts.js +4 -6
  17. package/dist/navigator-attendance.js +30 -52
  18. package/dist/navigator-invocation-identity.js +0 -1
  19. package/dist/navigator-public-session.js +2 -1
  20. package/dist/notary-contracts.js +4 -10
  21. package/dist/package-contracts/auditor-output.js +5 -23
  22. package/dist/package-contracts/fixer-output.js +25 -49
  23. package/dist/package-contracts/gatekeeper-output.js +3 -13
  24. package/dist/package-contracts/navigator-output.js +4 -7
  25. package/dist/package-contracts/reviewer-output.js +1 -95
  26. package/dist/package-contracts/terminating-tools.js +33 -144
  27. package/dist/package-contracts/worker-output.js +1 -3
  28. package/dist/public-cli/auto-resume.js +30 -68
  29. package/dist/public-cli/case-dossier-delivery.js +5 -17
  30. package/dist/public-cli/diarist-run.js +9 -4
  31. package/dist/public-cli/inspector-run.js +2 -8
  32. package/dist/public-cli/instruction-seat-run.js +3 -7
  33. package/dist/public-cli/invocation.js +5 -2
  34. package/dist/public-cli/main.js +6068 -6964
  35. package/dist/public-cli/notary-run.js +1 -8
  36. package/dist/public-cli/post-admission.js +314 -113
  37. package/dist/public-cli/run-lifecycle.js +14 -38
  38. package/dist/public-cli/settlement.js +209 -390
  39. package/dist/public-cli/terminal.js +51 -45
  40. package/dist/public-role-summons.js +1 -12
  41. package/dist/reviewer-dispatch.js +12 -258
  42. package/dist/submission-correctable-error.js +0 -12
  43. package/dist/submission-errors.js +6 -1
  44. package/dist/submission-ledger.js +125 -168
  45. package/dist/ticket-provenance-contracts.js +2 -49
  46. package/dist/ticket-provenance.js +82 -31
  47. package/extensions/role-runtime.ts +1 -6
  48. package/package.json +1 -1
  49. package/resources/836-deleted-machine-instruction-inventory.md +18 -0
  50. package/src/acp-host/role-turn-host.ts +8 -3
  51. package/src/analyst-gate-cycles-read.ts +17 -29
  52. package/src/audit-escalation.ts +20 -50
  53. package/src/auditor-dossier-tool.ts +5 -103
  54. package/src/canonical-skill-binding.ts +3 -2
  55. package/src/collector-identity.ts +0 -179
  56. package/src/collector-ledger.ts +3 -46
  57. package/src/collector-role.ts +32 -61
  58. package/src/collector-tool-schemas.ts +20 -14
  59. package/src/compliance-transport.ts +55 -17
  60. package/src/countersign-role.ts +17 -8
  61. package/src/diarist-contracts.ts +3 -36
  62. package/src/diarist-role.ts +13 -7
  63. package/src/diarist.ts +4 -16
  64. package/src/doctor-contracts.ts +73 -94
  65. package/src/doctor-evidence.ts +1 -1
  66. package/src/doctor-role.ts +3 -3
  67. package/src/engine-detour-tool.ts +4 -1
  68. package/src/external-host-turn-loop.ts +9 -3
  69. package/src/gatekeeper-pass-envelope.ts +7 -12
  70. package/src/gatekeeper-role.ts +56 -50
  71. package/src/gleaner-left-role.ts +4 -2
  72. package/src/headless-host/role-turn-host.ts +4 -5
  73. package/src/host-contracts.ts +3 -8
  74. package/src/judge-role.ts +19 -13
  75. package/src/merger-contracts.ts +10 -6
  76. package/src/merger-role.ts +1 -5
  77. package/src/navigator-attendance.ts +37 -74
  78. package/src/navigator-invocation-identity.ts +0 -6
  79. package/src/navigator-public-session.ts +2 -1
  80. package/src/notary-contracts.ts +7 -10
  81. package/src/package-contracts/auditor-output.ts +4 -21
  82. package/src/package-contracts/fixer-output.ts +34 -52
  83. package/src/package-contracts/gatekeeper-output.ts +5 -22
  84. package/src/package-contracts/navigator-output.ts +7 -7
  85. package/src/package-contracts/reviewer-output.ts +2 -150
  86. package/src/package-contracts/terminating-tools.ts +32 -147
  87. package/src/package-contracts/worker-output.ts +0 -1
  88. package/src/pi/adapter.ts +3 -8
  89. package/src/public-cli/auto-resume.ts +41 -94
  90. package/src/public-cli/case-dossier-delivery.ts +5 -15
  91. package/src/public-cli/countersign-run.ts +14 -6
  92. package/src/public-cli/diarist-run.ts +11 -9
  93. package/src/public-cli/inspector-run.ts +2 -18
  94. package/src/public-cli/instruction-seat-run.ts +3 -17
  95. package/src/public-cli/invocation.ts +5 -2
  96. package/src/public-cli/merger-run.ts +1 -1
  97. package/src/public-cli/notary-run.ts +1 -18
  98. package/src/public-cli/post-admission.ts +385 -115
  99. package/src/public-cli/run-lifecycle.ts +13 -44
  100. package/src/public-cli/settlement.ts +314 -562
  101. package/src/public-cli/terminal.ts +66 -72
  102. package/src/public-role-summons.ts +5 -30
  103. package/src/reviewer-dispatch.ts +29 -331
  104. package/src/reviewer-role.ts +16 -83
  105. package/src/role-envelope.ts +16 -24
  106. package/src/role-runtime-dependencies.ts +1 -6
  107. package/src/role-runtime.ts +65 -201
  108. package/src/submission-correctable-error.ts +0 -14
  109. package/src/submission-errors.ts +6 -1
  110. package/src/submission-ledger.ts +187 -187
  111. package/src/ticket-provenance-contracts.ts +4 -60
  112. package/src/ticket-provenance.ts +98 -32
  113. package/src/worker-role.ts +35 -36
  114. package/dist/activation-ledger-session.js +0 -105
  115. package/dist/activation-ledger.js +0 -139
  116. package/dist/activation-trace.js +0 -33
  117. package/dist/atomic-write.js +0 -23
  118. package/dist/auditor-role.js +0 -12
  119. package/dist/canonical-skill-binding.js +0 -61
  120. package/dist/collector-handbook.js +0 -142
  121. package/dist/collector-receipt.js +0 -107
  122. package/dist/collector-role.js +0 -566
  123. package/dist/collector-tool-schemas.js +0 -107
  124. package/dist/countersign-role.js +0 -28
  125. package/dist/diarist-role.js +0 -44
  126. package/dist/diarist.js +0 -66
  127. package/dist/doctor-role.js +0 -75
  128. package/dist/fixer-bash-seatbelt.js +0 -13
  129. package/dist/gatekeeper-pass-envelope.js +0 -77
  130. package/dist/gleaner-left-role.js +0 -26
  131. package/dist/inspector-role.js +0 -29
  132. package/dist/judge-role.js +0 -104
  133. package/dist/merger-role.js +0 -55
  134. package/dist/navigator-role.js +0 -12
  135. package/dist/notary-role.js +0 -134
  136. package/dist/oauth-keepalive.js +0 -158
  137. package/dist/pi/adapter.js +0 -223
  138. package/dist/reviewer-agent.js +0 -122
  139. package/dist/reviewer-child-executor.js +0 -194
  140. package/dist/reviewer-construction.js +0 -103
  141. package/dist/reviewer-execution-ledger.js +0 -166
  142. package/dist/reviewer-failure-diagnostic.js +0 -17
  143. package/dist/reviewer-role.js +0 -159
  144. package/dist/reviewer-scope-prompt.js +0 -21
  145. package/dist/reviewer-settlement.js +0 -66
  146. package/dist/reviewer-workspace.js +0 -132
  147. package/dist/role-runtime.js +0 -1604
  148. package/dist/stderr-jsonl.js +0 -26
  149. package/dist/tool-execution-observation.js +0 -141
  150. package/dist/worker-role.js +0 -345
  151. package/dist/worker-submission-gates.js +0 -305
  152. package/src/collector-receipt.ts +0 -140
  153. package/src/diagnostic-tail.ts +0 -18
  154. package/src/reviewer-agent.ts +0 -135
  155. package/src/reviewer-child-executor.ts +0 -252
  156. package/src/reviewer-construction.ts +0 -195
  157. package/src/reviewer-execution-ledger.ts +0 -212
  158. package/src/reviewer-failure-diagnostic.ts +0 -18
  159. package/src/reviewer-scope-prompt.ts +0 -24
  160. package/src/reviewer-settlement.ts +0 -74
  161. package/src/reviewer-workspace.ts +0 -105
  162. package/src/shape-unreadable-failure.ts +0 -49
@@ -55,7 +55,7 @@ export async function runPublicNotary(argv, env, io, parseNotaryArgv) {
55
55
  // #753/#786: gate re-ask and verbatim submission body share summons.instruction
56
56
  // (reask wins when both present; no parallel stack).
57
57
  {
58
- const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction;
58
+ const resumeInstruction = env.reviewReask;
59
59
  const summons = {
60
60
  sourceRunPath: source.runDirectory,
61
61
  sourceRun: source,
@@ -74,13 +74,6 @@ export async function runPublicNotary(argv, env, io, parseNotaryArgv) {
74
74
  });
75
75
  if (resumed !== undefined)
76
76
  return resumed;
77
- // Reask without a prior same-parent run cannot deliver the plain-language ask
78
- // on a fresh mint without inventing a second prompt path — fail loud (#753).
79
- // gateReviewInstruction (verbatim body) alone is resume-only; fresh mint ignores it.
80
- if (env.reviewReask !== undefined) {
81
- presentStructuralRejection(new CliUsageError("notary review reask requires a prior same-parent run to resume"), io);
82
- return { exitCode: 2 };
83
- }
84
77
  }
85
78
  let admitted;
86
79
  try {
@@ -8,12 +8,20 @@
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { readFile, writeFile } from "node:fs/promises";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
- import { buildResumeContinuationPrompt, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
11
+ import { buildResumeContinuationPrompt, GATE_DOSSIER_POINTER_PREFIX, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
12
12
  import { CliUsageError } from "./cli-errors.js";
13
13
  import { buildInstructionTransportPrompt, freezeAttachmentsIntoRun, } from "./invocation.js";
14
14
  import { pathContainedIn } from "../activation-ledger-topology.js";
15
15
  import { resolveHostAwareSessionAvailability } from "../session-identity.js";
16
16
  import { projectCaseDossierPointerSection } from "./case-dossier-delivery.js";
17
+ /** Original error bytes, never relabeled — a secondary fact riding beside a classified cause. */
18
+ function describeCaughtError(error) {
19
+ if (error instanceof Error) {
20
+ const code = error.code;
21
+ return { name: error.name, message: error.message, ...(code === undefined ? {} : { code }) };
22
+ }
23
+ return { message: String(error) };
24
+ }
17
25
  /** Append one system section to a continuation prompt, keeping its kind. */
18
26
  function appendContinuationSection(continuation, section) {
19
27
  const prompt = `${continuation.prompt}\n\n${section}`;
@@ -25,9 +33,8 @@ import { projectHostTransitionPriorNative } from "../host-transition-prior-nativ
25
33
  import { missingCredentialPreDispatchFailure, postRunMissingCredentialFailure, } from "./public-run-credentials.js";
26
34
  import { acquireRunWriterLease, clearCurrentCourt, clearTypedProviderHttpObservation, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, renderResumeCommand, RunWriterLeaseHeldError, } from "./run-lifecycle.js";
27
35
  import { homeFromRunDirectory } from "../activation-ledger-topology.js";
28
- import { readSealedSubmission } from "../submission-ledger.js";
29
36
  import { clearReviewerDispatchRejection } from "./reviewer-dispatch-rejection.js";
30
- import { classifyPostAdmissionFailure, controlledFailureInputFromResolution, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatTerminalResult, inspectJudgeSession, isLawfulTypedTerminalOutcome, presentFailureTerminal, presentStructuralRejection, resolveAuditedRunnerFailureResolution, resolveControlledFailureResumeObservation, settleFailureTerminalResult, sealedAcceptanceRedispatchDisposition, } from "./settlement.js";
37
+ import { attemptProducedFreshSubmission, classifyPostAdmissionFailure, controlledFailureInputFromResolution, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatTerminalResult, inspectJudgeSession, isLawfulTypedTerminalOutcome, presentFailureTerminal, presentStructuralRejection, resolveAuditedRunnerFailureResolution, resolveControlledFailureResumeObservation, settleFailureTerminalResult, settleHostEndedNoReceipt, attachRecordedSubmissions, } from "./settlement.js";
31
38
  import {} from "./terminal.js";
32
39
  import { ensureRealArtifactsDirectory, persistReturnedRunState, runWithAutoResumeLoop, TurnDispatchedFailure, } from "./auto-resume.js";
33
40
  /**
@@ -176,12 +183,12 @@ export async function presentControlledFailure(admitted, failureInput, adapters,
176
183
  const sessionPrincipalAvailable = await authority.isAvailable(admitted.principal);
177
184
  resumable = sessionPrincipalAvailable && typedHttp429 !== undefined;
178
185
  }
179
- if (persistRunState) {
186
+ if (persistRunState && !failureInput.skipRunStateWrite) {
180
187
  await persistReturnedRunState(admitted, authority);
181
188
  }
182
- const terminal = await settleFailureTerminalResult(admitted, failure, authority, resumable
189
+ const terminal = await attachRecordedSubmissions(admitted, await settleFailureTerminalResult(admitted, failure, authority, resumable
183
190
  ? { resume: { command: renderResumeCommand(admitted.runId) } }
184
- : {});
191
+ : {}));
185
192
  presentFailureTerminal(terminal, io);
186
193
  return {
187
194
  exitCode: exitCodeForTerminalOutcome(terminal.roleOutcome),
@@ -208,6 +215,35 @@ async function settleAfterTurnStarted(admitted, failureInput, adapters, authorit
208
215
  throw new TurnDispatchedFailure(error);
209
216
  }
210
217
  }
218
+ /**
219
+ * Persist run-state for a result dispatchPostAdmissionTurn deferred
220
+ * (needsPersist — station-child / resumable auto-resume loop, #416/#840):
221
+ * that write must land outside the loop's own retried-dispatch try, but its
222
+ * failure still settles through the single existing controlled-failure
223
+ * authority (settleAfterTurnStarted / presentControlledFailure, ADR 0080
224
+ * single-settlement-disposition) — never a second hand-rolled
225
+ * classify/artifact/Terminal (#836 r12 class 3), and never lawful/non-lawful
226
+ * settling differently (#836 r13 class 2: a caller's io here may be a no-op,
227
+ * so a stderr-only trace is never seen and the failure is otherwise
228
+ * swallowed — 失败诚实宪法 真因必须落痕). Both cases settle through
229
+ * settleAfterTurnStarted, which attaches the run's already-recorded ledger
230
+ * submissions to the new failure terminal and returns skipAutoResume so the
231
+ * caller presents it once and stops — never re-entering auto-resume.
232
+ */
233
+ async function settleDeferredPersist(admitted, authority, adapters, io, result) {
234
+ if (result.needsPersist !== true || result.terminal === undefined)
235
+ return result;
236
+ const { needsPersist: _needsPersist, ...settledResult } = result;
237
+ const lawful = isLawfulTypedTerminalOutcome(result.terminal.roleOutcome);
238
+ try {
239
+ await persistReturnedRunState(admitted, authority, lawful ? { lawful: true } : undefined);
240
+ return settledResult;
241
+ }
242
+ catch (error) {
243
+ const failed = await settleAfterTurnStarted(admitted, { timedOut: false, code: null, stderr: "", thrown: error, skipRunStateWrite: true }, adapters, authority, io, true);
244
+ return { ...failed, turnDispatched: true, skipAutoResume: true };
245
+ }
246
+ }
211
247
  export async function dispatchPostAdmissionTurn(input) {
212
248
  const { admitted, env, io, request, lease, adapters, effectiveEngine } = input;
213
249
  const persistRunState = input.persistRunState !== false;
@@ -325,6 +361,18 @@ export async function dispatchPostAdmissionTurn(input) {
325
361
  }, adapters, env.principalAuthority, io, persistRunState);
326
362
  return { ...settled, turnDispatched: true, ...deferredPersist };
327
363
  }
364
+ // `result.stderr` stays live in memory for the real classification below
365
+ // regardless of whether this durable mirror write succeeds. A write
366
+ // failure here (unwritable run directory, stderr.log occupied as a
367
+ // directory, ...) is a real infrastructure problem and must never be
368
+ // dropped silently (catch-and-continue with no trace is a defect) — but
369
+ // it also must never become THE controlling failure and wash out a
370
+ // primary child signal that already arrived on `result`, or an
371
+ // already-sealed accepted payload (#836: a secondary failure must not
372
+ // overwrite an already-known cause or an already-recorded leg). It rides
373
+ // beside whatever the real classification below determines, and only
374
+ // becomes the reported failure itself when nothing else is wrong.
375
+ //
328
376
  // Everything below runs after the host turn genuinely started (#840 r9
329
377
  // 判词 class 1 boundary — 覆盖 executeTurn 已启动后至返回带 turnDispatched
330
378
  // 结果前的全部异常). Each fallible step routes any failure through the
@@ -340,33 +388,108 @@ export async function dispatchPostAdmissionTurn(input) {
340
388
  // turn genuinely started, so the next retry sends a resume payload — the
341
389
  // true cause settles through the loop's own existing dispatch-exception
342
390
  // machinery once the retry budget is exhausted.
391
+ let stderrLogWriteFailure;
343
392
  try {
344
393
  await writeFile(join(admitted.runDirectory, "stderr.log"), result.stderr, "utf8");
345
394
  }
346
395
  catch (error) {
396
+ stderrLogWriteFailure = error;
347
397
  // Best-effort: the turn's own stderr capture is secondary to lawful /
348
398
  // controlled-failure settlement below, but the failure itself must
349
399
  // still leave a real trace (失败诚实宪法 真因必须落痕) — durably, since
350
- // every auto-resume attempt's io is dummyIo (#840 r9 判词 class 1).
400
+ // every auto-resume attempt's io is dummyIo (#840 r9 判词 class 1). It
401
+ // still rides beside the classification below and, if nothing else is
402
+ // wrong, becomes the reported failure itself (#836).
351
403
  await recordBestEffortPostDispatchDiagnostic(admitted, env, `stderr.log write failed (best-effort continue): ${describeErrorIdentity(error)}`, io);
352
404
  }
353
- let settled;
354
- // Same-ticket re-summons carry courtAttemptId — settle only that attempt so a
355
- // prior sealed pass cannot wash this turn's missing/escalated/failed result.
356
405
  const courtScope = request.courtAttemptId === undefined || request.courtAttemptId.length === 0
357
406
  ? undefined
358
407
  : { courtAttemptId: request.courtAttemptId };
359
- // Single complete boundary (#840 r9 判词 class 1): trySettle, its
360
- // shouldPresent gate, and the accepted-settlement cleanup all settle
361
- // through this one catch. shouldPresent used to sit outside every
362
- // TurnDispatchedFailure wrap, so a throw from it looked like an ordinary
363
- // pre-turn throw to the caller (turnStartedBeforeThrow stayed false) and
364
- // replayed the initial payload even though the host turn had already
365
- // genuinely run.
408
+ // Single complete boundary (#840 r9 判词 class 1): resolving the
409
+ // host/runner failure facts, trySettle, its shouldPresent gate, and the
410
+ // accepted-settlement cleanup all settle through this one catch — a
411
+ // throw from any of them (including the session-file decode itself)
412
+ // still routes through settleAfterTurnStarted's TurnDispatchedFailure
413
+ // instead of losing turnDispatched to an uncaught throw. The facts are
414
+ // resolved before trySettle (#836) so an already-accepted settlement is
415
+ // never presented over a real current host/runner failure signal or a
416
+ // real stderr.log durable-write failure — both stay a real failure with
417
+ // the recorded payload riding beside it, not replacing it.
418
+ let settled;
366
419
  let settledOutcome;
420
+ let hostSignalFailed = false;
421
+ let resolution;
367
422
  try {
423
+ const sessionFile = admitted.principal !== undefined
424
+ ? env.principalAuthority.decode(admitted.principal).sessionFile
425
+ : "";
426
+ const runnerKnownFailure = adapters.resolveRunnerKnownFailure !== undefined && sessionFile !== ""
427
+ ? await adapters.resolveRunnerKnownFailure({ result, sessionFile })
428
+ : result.knownFailure;
429
+ const credentialFailure = postRunMissingCredentialFailure(result, env.model, env.credentials);
430
+ resolution = await resolveAuditedRunnerFailureResolution({
431
+ runner: runnerKnownFailure,
432
+ sessionFile,
433
+ credential: credentialFailure,
434
+ runDirectory: admitted.runDirectory,
435
+ });
436
+ // A direct, current signal from the host/runner itself (timeout / host
437
+ // knownFailure / runner knownFailure / missing credential) is a real
438
+ // problem regardless of what else already settled — a settled accepted
439
+ // outcome must not paper over it (it rides beside the recorded payload
440
+ // via `submissions`, never replacing the payload). The audited
441
+ // resolution's own typed session read (malformed JSONL, provider-stop,
442
+ // an illegal/unsealed accepted status, a stale/superseded typed-HTTP
443
+ // observation, ...) makes a run with no sealed acceptance a true
444
+ // failure too — it must not be discarded into a lawful no_receipt just
445
+ // because the raw runner signal alone looked clean. But it must not
446
+ // retroactively invalidate an acceptance that already sealed this turn
447
+ // — a resolved 429 observed earlier in the same session is exactly
448
+ // that.
449
+ const directHostFailureSignal = result.timedOut
450
+ || result.knownFailure !== undefined
451
+ || runnerKnownFailure !== undefined
452
+ || credentialFailure !== undefined;
453
+ hostSignalFailed =
454
+ directHostFailureSignal
455
+ || (result.code !== null && result.code !== 0)
456
+ || resolution.knownFailure !== undefined;
368
457
  settled = await adapters.trySettle(admitted, env.principalAuthority, courtScope);
369
- if (settled !== undefined && shouldPresent(settled)) {
458
+ if (settled !== undefined) {
459
+ settled = await attachRecordedSubmissions(admitted, settled, courtScope);
460
+ }
461
+ // #836 r12 class 2: an accepted/audit_escalation settlement can be
462
+ // entirely a prior attempt's stale payload (courtAttempt is a
463
+ // recording tag, not a visibility gate — attachRecordedSubmissions
464
+ // above still surfaces that historical payload honestly either way).
465
+ // Consume the ledger's own subject.attemptId (submission-ledger.ts)
466
+ // only to learn whether *this* attempt itself produced a fresh seal —
467
+ // never to filter what presents.
468
+ const settledIsFreshThisAttempt = settled === undefined
469
+ || (settled.roleOutcome.kind !== "accepted" && settled.roleOutcome.kind !== "audit_escalation")
470
+ ? true
471
+ : await attemptProducedFreshSubmission(admitted, courtScope);
472
+ // A lawful settled outcome already reached this turn takes precedence
473
+ // over a later bare exit-code / session-inspection signal (trailing
474
+ // nonzero exit, late stderr noise, a stale already-superseded
475
+ // typed-HTTP observation, ...) — but never over a direct current
476
+ // host/runner failure signal, nor over a real stderr.log durable-write
477
+ // failure (confirmed infrastructure trouble, not weak/bare evidence),
478
+ // both of which stay a real failure with the recorded payload riding
479
+ // beside it, not replacing it (#836: never kill an already-recorded
480
+ // leg, but never wash a real failure away either). Note: the narrower
481
+ // directHostFailureSignal gates acceptance here; the broader
482
+ // hostSignalFailed (adds bare exit code / resolution.knownFailure)
483
+ // only gates the no-settlement fallback below — except when the
484
+ // settlement itself is entirely stale (no fresh seal this attempt),
485
+ // where a bare nonzero exit / resolution failure must not be outranked
486
+ // by someone else's earlier success (#836 r12 class 2).
487
+ const staleAcceptanceOutranksRealFailure = !settledIsFreshThisAttempt && hostSignalFailed;
488
+ if (settled !== undefined
489
+ && shouldPresent(settled)
490
+ && !directHostFailureSignal
491
+ && !staleAcceptanceOutranksRealFailure
492
+ && stderrLogWriteFailure === undefined) {
370
493
  // This court sealed — drop open-court pointer (bare resume no longer continues it).
371
494
  if (settled.roleOutcome.kind === "accepted" &&
372
495
  request.courtAttemptId !== undefined &&
@@ -395,8 +518,8 @@ export async function dispatchPostAdmissionTurn(input) {
395
518
  }
396
519
  }
397
520
  catch (error) {
398
- // Settle (or its shouldPresent gate) throw is a real failure fact —
399
- // never swallow into undefined.
521
+ // Settle (or its shouldPresent gate, or the failure-fact resolution
522
+ // above) throw is a real failure fact — never swallow into undefined.
400
523
  const settledFailure = await settleAfterTurnStarted(admitted, {
401
524
  timedOut: false,
402
525
  code: result.code,
@@ -406,47 +529,102 @@ export async function dispatchPostAdmissionTurn(input) {
406
529
  return { ...settledFailure, turnDispatched: true, ...deferredPersist };
407
530
  }
408
531
  if (settledOutcome !== undefined) {
532
+ if (persistRunState) {
533
+ try {
534
+ await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
535
+ }
536
+ catch (error) {
537
+ // #836: `settledOutcome.terminal` already carries recorded
538
+ // submissions (attachRecordedSubmissions above). A real run-state
539
+ // persistence failure here must surface loudly through the same
540
+ // controlled-failure seam every other dispatch-time failure in
541
+ // this function uses (ADR 0080: one settlement disposition owner)
542
+ // — not escape uncaught to auto-resume's dispatch-retry path,
543
+ // whose exhausted-budget terminal carries no recorded submissions
544
+ // at all. skipRunStateWrite: the write that just threw is the same
545
+ // write presentControlledFailure would otherwise retry — don't
546
+ // call a known-failing operation twice.
547
+ const failed = await settleAfterTurnStarted(admitted, {
548
+ timedOut: false,
549
+ code: null,
550
+ stderr: "",
551
+ thrown: error,
552
+ skipRunStateWrite: true,
553
+ }, adapters, env.principalAuthority, io, persistRunState);
554
+ return { ...failed, turnDispatched: true, ...deferredPersist };
555
+ }
556
+ }
409
557
  // Lawful persist + present is the caller's stop seam (auto-resume loop /
410
558
  // manual resume), not this retried host-turn function.
411
559
  return { ...settledOutcome, ...deferredPersist };
412
560
  }
413
- // Any exception while resolving the failure facts below — including the
414
- // session-file decode itself — still happened after the host turn
415
- // genuinely started (#840 r9 判词 class 1). Fold it into the same single
416
- // controlled-failure settlement instead of losing turnDispatched to an
417
- // uncaught throw.
418
- let failureInput;
419
- try {
420
- const sessionFile = admitted.principal !== undefined
421
- ? env.principalAuthority.decode(admitted.principal).sessionFile
422
- : "";
423
- const runnerKnownFailure = adapters.resolveRunnerKnownFailure !== undefined && sessionFile !== ""
424
- ? await adapters.resolveRunnerKnownFailure({ result, sessionFile })
425
- : result.knownFailure;
426
- const credentialFailure = postRunMissingCredentialFailure(result, env.model, env.credentials);
427
- const resolution = await resolveAuditedRunnerFailureResolution({
428
- runner: runnerKnownFailure,
429
- sessionFile,
430
- credential: credentialFailure,
431
- runDirectory: admitted.runDirectory,
432
- });
433
- failureInput = {
561
+ // Host/runner true failure coexists with already-recorded payloads — never wash as accepted.
562
+ if (hostSignalFailed) {
563
+ const resolutionInput = controlledFailureInputFromResolution(resolution);
564
+ const stderrLogWriteDetails = stderrLogWriteFailure === undefined
565
+ ? undefined
566
+ : { stderrLogWriteFailure: describeCaughtError(stderrLogWriteFailure) };
567
+ const failed = await settleAfterTurnStarted(admitted, {
434
568
  timedOut: result.timedOut,
435
569
  code: result.code,
436
570
  stderr: result.stderr,
437
- ...controlledFailureInputFromResolution(resolution),
438
- };
571
+ ...resolutionInput,
572
+ // Secondary fact only — never the cause. Rides on whichever channel
573
+ // classification actually reads (knownFailure.details owns it when
574
+ // a knownFailure exists; the top-level knownDetails otherwise).
575
+ ...(stderrLogWriteDetails === undefined
576
+ ? {}
577
+ : resolutionInput.knownFailure !== undefined
578
+ ? {
579
+ knownFailure: {
580
+ ...resolutionInput.knownFailure,
581
+ details: { ...(resolutionInput.knownFailure.details ?? {}), ...stderrLogWriteDetails },
582
+ },
583
+ }
584
+ : { knownDetails: stderrLogWriteDetails }),
585
+ }, adapters, env.principalAuthority, io, persistRunState);
586
+ return { ...failed, turnDispatched: true, ...deferredPersist };
439
587
  }
440
- catch (error) {
441
- failureInput = {
588
+ // Nothing else was wrong, but the durable stderr mirror itself failed to
589
+ // write — that is the real (infrastructure) problem in this case, not a
590
+ // lawful absence of a receipt. Honest and loud, not silently no_receipt.
591
+ if (stderrLogWriteFailure !== undefined) {
592
+ const failed = await settleAfterTurnStarted(admitted, {
442
593
  timedOut: false,
443
594
  code: result.code,
444
595
  stderr: result.stderr,
445
- thrown: error,
446
- };
596
+ thrown: stderrLogWriteFailure,
597
+ }, adapters, env.principalAuthority, io, persistRunState);
598
+ return { ...failed, turnDispatched: true, ...deferredPersist };
447
599
  }
448
- const failed = await settleAfterTurnStarted(admitted, failureInput, adapters, env.principalAuthority, io, persistRunState);
449
- return { ...failed, turnDispatched: true, ...deferredPersist };
600
+ const noReceipt = await attachRecordedSubmissions(admitted, await settleHostEndedNoReceipt(admitted, env.principalAuthority), courtScope);
601
+ if (persistRunState) {
602
+ try {
603
+ await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
604
+ }
605
+ catch (error) {
606
+ // #836: same run-state persistence hazard as the settled/accepted
607
+ // branch above — route through the shared controlled-failure seam so
608
+ // the real failure surfaces loudly instead of escaping uncaught to
609
+ // auto-resume's dispatch-retry path with no recorded submissions.
610
+ // skipRunStateWrite: don't retry the write that just threw.
611
+ const failed = await settleAfterTurnStarted(admitted, {
612
+ timedOut: false,
613
+ code: null,
614
+ stderr: "",
615
+ thrown: error,
616
+ skipRunStateWrite: true,
617
+ }, adapters, env.principalAuthority, io, persistRunState);
618
+ return { ...failed, turnDispatched: true, ...deferredPersist };
619
+ }
620
+ }
621
+ return {
622
+ exitCode: exitCodeForTerminalOutcome(noReceipt.roleOutcome),
623
+ admitted,
624
+ terminal: noReceipt,
625
+ turnDispatched: true,
626
+ ...deferredPersist,
627
+ };
450
628
  }
451
629
  finally {
452
630
  try {
@@ -478,20 +656,32 @@ export async function dispatchPostAdmissionTurn(input) {
478
656
  * first when request.summons carries instruction or attachment paths.
479
657
  */
480
658
  export function resumeTurnRequestProjectionOptions(admitted, request, env, summonsPrepared) {
659
+ const officerSourcePath = request.summons?.sourceRunPath;
660
+ const withReread = (body) => {
661
+ if (officerSourcePath === undefined
662
+ || (admitted.role !== "notary"
663
+ && admitted.role !== "inspector"
664
+ && admitted.role !== "auditor")) {
665
+ return body;
666
+ }
667
+ if (body.startsWith("请重读"))
668
+ return body;
669
+ return `请重读\n${body}`;
670
+ };
481
671
  let prompt;
482
672
  if (request.message !== undefined) {
483
673
  if (summonsPrepared !== undefined) {
484
674
  // #755: same-ticket review / open-court — caller words + optional paths.
485
675
  // Attachments are not a gate: message-only summons must stay plain too.
486
- prompt = buildInstructionTransportPrompt({
676
+ prompt = withReread(buildInstructionTransportPrompt({
487
677
  instruction: request.message,
488
678
  instructionEmpty: false,
489
679
  attachments: summonsPrepared.attachments,
490
- });
680
+ }));
491
681
  }
492
682
  else if (request.summons !== undefined) {
493
683
  // #755: same-ticket summons without prepared materials — caller words only.
494
- prompt = request.message;
684
+ prompt = withReread(request.message);
495
685
  }
496
686
  else {
497
687
  // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
@@ -504,12 +694,26 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
504
694
  }
505
695
  else if (summonsPrepared !== undefined) {
506
696
  // #755: same-ticket review summons — instruction/attachments only.
507
- prompt = buildInstructionTransportPrompt(summonsPrepared);
697
+ prompt = withReread(buildInstructionTransportPrompt(summonsPrepared));
508
698
  }
509
699
  else if (request.summons !== undefined) {
510
700
  // #755: same-ticket summons with no instruction/attachments (e.g. notary
511
- // source-run pointer) — plain resume envelope, no handbook / 重新读.
512
- prompt = RESUME_TRANSPORT_ENVELOPE;
701
+ // source-run pointer). #836: officer resume opening adds「请重读」+ path pointer.
702
+ const path = request.summons.sourceRunPath;
703
+ if (path !== undefined &&
704
+ (admitted.role === "notary" ||
705
+ admitted.role === "inspector" ||
706
+ admitted.role === "auditor")) {
707
+ prompt = `请重读\n${GATE_DOSSIER_POINTER_PREFIX}${path}`;
708
+ }
709
+ else if (admitted.role === "notary" ||
710
+ admitted.role === "inspector" ||
711
+ admitted.role === "auditor") {
712
+ prompt = "请重读";
713
+ }
714
+ else {
715
+ prompt = "";
716
+ }
513
717
  }
514
718
  else {
515
719
  // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
@@ -628,35 +832,23 @@ export async function runPostAdmissionSeatResume(input) {
628
832
  // Build uses the admitted judged under this lease (rehydrated when open
629
833
  // court materials ride). Settlement identity stays on the outer admitted.
630
834
  let admittedForBuild = loaded.admitted;
631
- // Bare resume: read + seal-judge + bound clear only under the held lease.
835
+ // Bare resume: open-court pointer is the continue signal (not ledger seal).
632
836
  if (request.summons === undefined) {
633
837
  const openCourt = await readCurrentCourt(admittedForBuild.runDirectory);
634
838
  if (openCourt !== undefined) {
635
- const sealedForOpen = await readSealedSubmission(admittedForBuild.projectRoot, admittedForBuild.runId, {
636
- home: homeFromRunDirectory(admittedForBuild.runDirectory),
637
- attemptId: openCourt.courtAttemptId,
638
- });
639
- if (sealedForOpen === undefined) {
640
- // Continue open court: same summons materials + existing courtAttemptId.
641
- // Caller message (if any) stays on the request — projection keeps it.
642
- openCourtAttemptId = openCourt.courtAttemptId;
643
- request = {
644
- runId: request.runId,
645
- ...(request.message === undefined
646
- ? {}
647
- : { message: request.message }),
648
- ...(openCourt.summons === undefined
649
- ? {}
650
- : { summons: openCourt.summons }),
651
- };
652
- if (openCourt.summons !== undefined) {
653
- const reloaded = await input.load(request);
654
- admittedForBuild = reloaded.admitted;
655
- }
656
- }
657
- else {
658
- // Open court already sealed — clear only the court id just judged.
659
- await clearCurrentCourt(admittedForBuild.runDirectory, openCourt.courtAttemptId);
839
+ openCourtAttemptId = openCourt.courtAttemptId;
840
+ request = {
841
+ runId: request.runId,
842
+ ...(request.message === undefined
843
+ ? {}
844
+ : { message: request.message }),
845
+ ...(openCourt.summons === undefined
846
+ ? {}
847
+ : { summons: openCourt.summons }),
848
+ };
849
+ if (openCourt.summons !== undefined) {
850
+ const reloaded = await input.load(request);
851
+ admittedForBuild = reloaded.admitted;
660
852
  }
661
853
  }
662
854
  }
@@ -743,23 +935,26 @@ export async function runPostAdmissionSeatResume(input) {
743
935
  firstTurn = turnRequest;
744
936
  return turnRequest;
745
937
  },
746
- dispatch: (turnRequest) => dispatchPostAdmissionTurn({
747
- admitted: loaded.admitted,
748
- env: {
749
- ...input.env,
750
- ...(loaded.admitted.correlationId === undefined
938
+ dispatch: async (turnRequest) => {
939
+ const result = await dispatchPostAdmissionTurn({
940
+ admitted: loaded.admitted,
941
+ env: {
942
+ ...input.env,
943
+ ...(loaded.admitted.correlationId === undefined
944
+ ? {}
945
+ : { correlationId: loaded.admitted.correlationId }),
946
+ },
947
+ io: attemptIo,
948
+ request: turnRequest,
949
+ lease,
950
+ adapters: stationAdapters,
951
+ persistRunState: false,
952
+ ...(input.effectiveEngine === undefined
751
953
  ? {}
752
- : { correlationId: loaded.admitted.correlationId }),
753
- },
754
- io: attemptIo,
755
- request: turnRequest,
756
- lease,
757
- adapters: stationAdapters,
758
- persistRunState: false,
759
- ...(input.effectiveEngine === undefined
760
- ? {}
761
- : { effectiveEngine: input.effectiveEngine }),
762
- }),
954
+ : { effectiveEngine: input.effectiveEngine }),
955
+ });
956
+ return settleDeferredPersist(loaded.admitted, input.env.principalAuthority, stationAdapters, attemptIo, result);
957
+ },
763
958
  }),
764
959
  });
765
960
  }
@@ -827,21 +1022,23 @@ export async function runPostAdmissionResumable(input) {
827
1022
  autoResumeLimit: env.autoResumeLimit,
828
1023
  buildInitialPayload: buildInitialRequest,
829
1024
  buildResumePayload: buildResumeRequest,
830
- sealedAcceptanceDisposition: () => sealedAcceptanceRedispatchDisposition(admitted),
831
- dispatch: (request, lease, _isFirst, attemptIo) => dispatchPostAdmissionTurn({
832
- admitted,
833
- env: {
834
- ...env,
835
- ...(admitted.correlationId === undefined ? {} : { correlationId: admitted.correlationId }),
836
- },
837
- io: attemptIo,
838
- request,
839
- lease,
840
- adapters,
841
- persistRunState: false,
842
- // #600: every attempt (initial + auto-resume) writes seat engine when present.
843
- ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
844
- }),
1025
+ dispatch: async (request, lease, _isFirst, attemptIo) => {
1026
+ const result = await dispatchPostAdmissionTurn({
1027
+ admitted,
1028
+ env: {
1029
+ ...env,
1030
+ ...(admitted.correlationId === undefined ? {} : { correlationId: admitted.correlationId }),
1031
+ },
1032
+ io: attemptIo,
1033
+ request,
1034
+ lease,
1035
+ adapters,
1036
+ persistRunState: false,
1037
+ // #600: every attempt (initial + auto-resume) writes seat engine when present.
1038
+ ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
1039
+ });
1040
+ return settleDeferredPersist(admitted, env.principalAuthority, adapters, attemptIo, result);
1041
+ },
845
1042
  });
846
1043
  }
847
1044
  /**
@@ -911,7 +1108,11 @@ export async function runPostAdmissionManualResume(input) {
911
1108
  });
912
1109
  if (result.terminal !== undefined &&
913
1110
  isLawfulTypedTerminalOutcome(result.terminal.roleOutcome)) {
914
- await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
1111
+ // dispatchPostAdmissionTurn already persisted this lawful outcome inline
1112
+ // (persistRunState defaults true here — no station-child/loop deferral)
1113
+ // through its own settleAfterTurnStarted-backed branches; a lawful result
1114
+ // only ever reaches this point once that persist has already succeeded
1115
+ // (#836 r12 class 3 dedup — one persist owner, not a second here).
915
1116
  io.stdout(formatTerminalResult(result.terminal));
916
1117
  }
917
1118
  if (result.terminal !== undefined) {