@akagilnc/pi-workflow-roles 0.1.3891 → 0.1.3900

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.
@@ -40,6 +40,30 @@ async function readInvocationHost(runDirectory) {
40
40
  throw error;
41
41
  }
42
42
  }
43
+ /**
44
+ * Factory-seat resume method-material face (#833): one authority for
45
+ * load → adapters / controlled-failure short-circuit. Seats only declare
46
+ * irreducible differences (loader, adapter factories, optional gate, knownCause).
47
+ */
48
+ export async function resolveResumeMethodMaterialAdapters(input) {
49
+ if (input.shouldLoad === false) {
50
+ return { kind: "continue", adapters: input.emptyAdapters };
51
+ }
52
+ try {
53
+ const material = await input.loadMaterial();
54
+ return { kind: "continue", adapters: input.adaptersWith(material) };
55
+ }
56
+ catch (error) {
57
+ const terminal = await presentControlledFailure(input.admitted, {
58
+ timedOut: false,
59
+ code: null,
60
+ stderr: "",
61
+ thrown: error,
62
+ ...(input.knownCause === undefined ? {} : { knownCause: input.knownCause }),
63
+ }, input.emptyAdapters, input.authority, input.io);
64
+ return { kind: "terminal", ...terminal };
65
+ }
66
+ }
43
67
  export async function presentControlledFailure(admitted, failureInput, adapters, authority, io) {
44
68
  const hasThrown = Object.hasOwn(failureInput, "thrown");
45
69
  const resumeObservation = await resolveControlledFailureResumeObservation({
@@ -219,7 +243,7 @@ export async function dispatchPostAdmissionTurn(input) {
219
243
  }, adapters, env.principalAuthority, io));
220
244
  }
221
245
  if (settled !== undefined && shouldPresent(settled)) {
222
- // This court sealed — drop open-court pointer so bare resume is run-scoped idempotent.
246
+ // This court sealed — drop open-court pointer (bare resume no longer continues it).
223
247
  if (settled.roleOutcome.kind === "accepted" &&
224
248
  request.courtAttemptId !== undefined &&
225
249
  request.courtAttemptId.length > 0) {
@@ -326,6 +350,7 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
326
350
  kind: "resume",
327
351
  prompt,
328
352
  },
353
+ ...(request.message === undefined ? {} : { courtAttemptId: randomUUID() }),
329
354
  };
330
355
  }
331
356
  function isAlreadyFrozenSummonsAttachment(runDirectory, attachmentPath) {
@@ -360,9 +385,10 @@ export async function prepareSummonsResumeMaterials(runDirectory, summons) {
360
385
  }
361
386
  /**
362
387
  * Shared manual-resume orchestration for seats whose continuation is the
363
- * package resume envelope (#599 / #633): load → structural rejection → seat
364
- * turn projection → runPostAdmissionManualResume. Seat-owned loader validation,
365
- * turn builder, and adapters stay on the seat.
388
+ * package resume envelope (#599 / #633): load once → structural rejection →
389
+ * optional seat afterAdmittedLoad (method material / controlled failure) →
390
+ * seat turn projection → runPostAdmissionManualResume. Seat-owned loader
391
+ * validation, turn builder, and adapters stay on the seat.
366
392
  *
367
393
  * Court open/recovery transaction (#637): under the existing writer lease,
368
394
  * read currentCourt, judge seal, clear (bound to the judged court id), freeze,
@@ -370,8 +396,8 @@ export async function prepareSummonsResumeMaterials(runDirectory, summons) {
370
396
  */
371
397
  export async function runPostAdmissionSeatResume(input) {
372
398
  let request = input.request;
373
- // Load once for runDirectory / structural rejection; court identity is judged
374
- // only after the writer lease is held (below).
399
+ // Load once for runDirectory / structural rejection / afterAdmittedLoad;
400
+ // court identity is judged only after the writer lease is held (below).
375
401
  let loaded;
376
402
  try {
377
403
  loaded = await input.load(request);
@@ -383,20 +409,28 @@ export async function runPostAdmissionSeatResume(input) {
383
409
  }
384
410
  throw error;
385
411
  }
386
- // Entire court recovery / open path runs after lease. forceContinuation skips
387
- // the pre-lease sealed short-circuit; when the after-lease builder leaves no
388
- // courtAttemptId, manual resume still presents run-scoped sealed idempotence
389
- // under the same held lease.
412
+ let adapters = input.adapters;
413
+ if (input.afterAdmittedLoad !== undefined) {
414
+ const prepared = await input.afterAdmittedLoad(loaded.admitted);
415
+ if (prepared.kind === "terminal") {
416
+ return {
417
+ exitCode: prepared.exitCode,
418
+ admitted: prepared.admitted,
419
+ terminal: prepared.terminal,
420
+ };
421
+ }
422
+ adapters = prepared.adapters;
423
+ }
424
+ // Court recovery / open under lease, then always dispatch (resume is pass-through).
390
425
  try {
391
426
  return await runPostAdmissionManualResume({
392
427
  admitted: loaded.admitted,
393
428
  env: input.env,
394
429
  io: input.io,
395
- adapters: input.adapters,
430
+ adapters,
396
431
  ...(input.effectiveEngine === undefined
397
432
  ? {}
398
433
  : { effectiveEngine: input.effectiveEngine }),
399
- forceContinuation: true,
400
434
  buildRequestAfterLease: async () => {
401
435
  let openCourtAttemptId;
402
436
  // Build uses the admitted judged under this lease (rehydrated when open
@@ -450,10 +484,11 @@ export async function runPostAdmissionSeatResume(input) {
450
484
  }
451
485
  }
452
486
  let turnRequest = await input.buildTurnRequest(admittedForBuild, request);
453
- // Court path only when continuing an open court or opening a new summons court.
454
- // Bare resume with no open court (or cleared sealed pointer) keeps no
455
- // courtAttemptId so run-scoped sealed idempotence can present under lease.
456
- if (openCourtAttemptId !== undefined || request.summons !== undefined) {
487
+ // Open court continue, or new court for summons / message re-review
488
+ // (clause 0 新庭可再交卷; #833). Bare resume without open court omits id.
489
+ if (openCourtAttemptId !== undefined ||
490
+ request.summons !== undefined ||
491
+ request.message !== undefined) {
457
492
  const courtAttemptId = openCourtAttemptId ??
458
493
  (turnRequest.courtAttemptId !== undefined &&
459
494
  turnRequest.courtAttemptId.length > 0
@@ -543,79 +578,15 @@ export async function runPostAdmissionResumable(input) {
543
578
  });
544
579
  }
545
580
  /**
546
- * Single authority for manual-resume run-scoped sealed-accepted presentation
547
- * (#599 / #648 / #672 / #637). Used both before lease (eager request path) and
548
- * after court-recovery builder under lease when no courtAttemptId remains.
549
- * Returns undefined to fall through to dispatch; never rebuilds the gate.
550
- */
551
- async function presentSealedAcceptedManualResumeIfAny(input) {
552
- const { admitted, env, io, adapters, shouldPresent } = input;
553
- try {
554
- const existing = await adapters.trySettle(admitted, env.principalAuthority);
555
- if (existing !== undefined &&
556
- existing.roleOutcome.kind === "accepted" &&
557
- shouldPresent(existing)) {
558
- existing.autoResumeCount = 0;
559
- io.stdout(formatTerminalResult(existing));
560
- return {
561
- exitCode: exitCodeForTerminalOutcome(existing.roleOutcome),
562
- admitted,
563
- terminal: existing,
564
- };
565
- }
566
- }
567
- catch (error) {
568
- // Settlement-owned sealed disposition: sealed accepted + publication/settle
569
- // throw fail closed without redispatch; authority failure preserves cause.
570
- const disposition = await sealedAcceptanceRedispatchDisposition(admitted);
571
- if (disposition.kind === "block") {
572
- return (await presentControlledFailure(admitted, {
573
- timedOut: false,
574
- code: null,
575
- stderr: "",
576
- thrown: disposition.reason === "authority-failed"
577
- ? disposition.cause
578
- : error,
579
- }, adapters, env.principalAuthority, io));
580
- }
581
- // Pre-dispatch settle failure without a sealed accepted projection is not
582
- // proof of seal; fall through to dispatch so the attempt path can settle
583
- // or fail honestly.
584
- }
585
- return undefined;
586
- }
587
- /**
588
- * Shared post-admission manual resume path: acquire writer lease and dispatch turn.
589
- * When the submission ledger is already sealed, project that accepted terminal
590
- * idempotently — do not dispatch a doomed turn that would append
591
- * post-seal-anomaly and erase the sealed read (#599; keep #416 open load).
592
- * Same-ticket re-summons (#637) pass forceContinuation + courtAttemptId so a new
593
- * court turn still runs with this summons' materials despite a prior sealed
594
- * acceptance, while submission-ledger sole-final stays per-attempt.
581
+ * Manual resume: lease + dispatch. Pass-through to the host — no sealed-accepted
582
+ * short-circuit (#833 / #416). Court open (summons / message / open court) is
583
+ * built under lease when using buildRequestAfterLease; sole-final stays per-attempt.
595
584
  */
596
585
  export async function runPostAdmissionManualResume(input) {
597
- const { admitted, env, io, adapters, effectiveEngine, forceContinuation, buildRequestAfterLease, } = input;
586
+ const { admitted, env, io, adapters, effectiveEngine, buildRequestAfterLease, } = input;
598
587
  let request = input.request;
599
588
  // #617 DK-3: manual resume writes the live seat/env model (same as new legs).
600
589
  const effectiveModel = env.model;
601
- const shouldPresent = adapters.shouldPresentSettled ??
602
- ((terminal) => isLawfulTypedTerminalOutcome(terminal.roleOutcome));
603
- const sealedIdempotenceInput = {
604
- admitted,
605
- env,
606
- io,
607
- adapters,
608
- shouldPresent,
609
- };
610
- // Sealed accepted receipt only — audit_escalation / residual failure must not
611
- // short-circuit; those still need a real continuation turn.
612
- // Same-ticket re-summons / court recovery forceContinuation skips the pre-lease
613
- // face; post-builder reuses the same presenter when no courtAttemptId remains.
614
- if (forceContinuation !== true && request !== undefined) {
615
- const presented = await presentSealedAcceptedManualResumeIfAny(sealedIdempotenceInput);
616
- if (presented !== undefined)
617
- return presented;
618
- }
619
590
  let lease;
620
591
  let staleWriterLeaseReclaimed;
621
592
  try {
@@ -644,8 +615,8 @@ export async function runPostAdmissionManualResume(input) {
644
615
  throw error;
645
616
  }
646
617
  // Court open/recovery under held lease until dispatch owns release (finally
647
- // below). Builder, sealed presenter, and any throw on this seam must release
648
- // here — dispatch's finally only runs after handoff.
618
+ // below). Builder and any throw on this seam must release here — dispatch's
619
+ // finally only runs after handoff.
649
620
  let handedOffToDispatch = false;
650
621
  try {
651
622
  if (request === undefined) {
@@ -653,18 +624,6 @@ export async function runPostAdmissionManualResume(input) {
653
624
  throw new Error("runPostAdmissionManualResume requires request or buildRequestAfterLease");
654
625
  }
655
626
  request = await buildRequestAfterLease();
656
- if (request.courtAttemptId === undefined ||
657
- request.courtAttemptId.length === 0) {
658
- const presented = await presentSealedAcceptedManualResumeIfAny(sealedIdempotenceInput);
659
- if (presented !== undefined) {
660
- return {
661
- ...presented,
662
- ...(staleWriterLeaseReclaimed === true
663
- ? { staleWriterLeaseReclaimed: true }
664
- : {}),
665
- };
666
- }
667
- }
668
627
  }
669
628
  handedOffToDispatch = true;
670
629
  const result = await dispatchPostAdmissionTurn({
@@ -417,9 +417,9 @@ export async function recordCurrentCourt(runDirectory, court) {
417
417
  }
418
418
  /**
419
419
  * Clear open court after this courtAttemptId seals, or when the open court is
420
- * already sealed and bare resume returns to run-scoped idempotence (#637).
421
- * When expectedCourtAttemptId is set, clear only if it still matches — never
422
- * drop a different court recorded under the writer lease after our judgment.
420
+ * already sealed (#637). When expectedCourtAttemptId is set, clear only if it
421
+ * still matches — never drop a different court recorded under the writer lease
422
+ * after our judgment.
423
423
  */
424
424
  export async function clearCurrentCourt(runDirectory, expectedCourtAttemptId) {
425
425
  const current = await readRoleRunStateDisk(runDirectory);
@@ -1810,8 +1810,8 @@ async function readLawfulSettlementEntries(sessionFile) {
1810
1810
  * Returns undefined for genuine absence (missing path / no lawful verdict).
1811
1811
  * Session-read failures propagate with typed identity.
1812
1812
  */
1813
- export async function readLawfulJudgeRoleOutcome(admitted, authority) {
1814
- const sealed = await sealedLedgerOutcome(admitted);
1813
+ export async function readLawfulJudgeRoleOutcome(admitted, authority, scope) {
1814
+ const sealed = await sealedLedgerOutcome(admitted, scope);
1815
1815
  if (sealed?.role === "judge") {
1816
1816
  const details = sealed.decisiveFacts;
1817
1817
  // sealed.status is the sole authority (written by acceptedFacts at seal).
@@ -1824,7 +1824,7 @@ export async function readLawfulJudgeRoleOutcome(admitted, authority) {
1824
1824
  };
1825
1825
  }
1826
1826
  // Non-final: consume ledger audit-escalation projection (no JSONL accepted rebuild).
1827
- return auditEscalationLedgerOutcome(admitted, "judge");
1827
+ return auditEscalationLedgerOutcome(admitted, "judge", scope);
1828
1828
  }
1829
1829
  /**
1830
1830
  * Single lawful-settlement implementation (session → outcome/Navigator/artifacts).
@@ -1837,8 +1837,8 @@ export async function readLawfulJudgeRoleOutcome(admitted, authority) {
1837
1837
  * - Lawful outcome presence is decided before publication so a later write error
1838
1838
  * cannot erase the fact that a lawful result already exists.
1839
1839
  */
1840
- async function settleLawfulJudgeTerminalResult(admitted, authority) {
1841
- const roleOutcome = await readLawfulJudgeRoleOutcome(admitted, authority);
1840
+ async function settleLawfulJudgeTerminalResult(admitted, authority, scope) {
1841
+ const roleOutcome = await readLawfulJudgeRoleOutcome(admitted, authority, scope);
1842
1842
  if (roleOutcome === undefined)
1843
1843
  return undefined;
1844
1844
  const coordinates = coordinatesFromAdmitted(authority, admitted);
@@ -1858,8 +1858,8 @@ async function settleLawfulJudgeTerminalResult(admitted, authority) {
1858
1858
  * Throws when no lawful outcome is present (tests/callers that require success).
1859
1859
  * Session-read and publication failures retain their typed identity.
1860
1860
  */
1861
- export async function settleJudgeTerminalResult(admitted, authority) {
1862
- const settled = await settleLawfulJudgeTerminalResult(admitted, authority);
1861
+ export async function settleJudgeTerminalResult(admitted, authority, scope) {
1862
+ const settled = await settleLawfulJudgeTerminalResult(admitted, authority, scope);
1863
1863
  if (settled === undefined) {
1864
1864
  throw new Error("Judge Role run completed without a lawful typed terminal result");
1865
1865
  }
@@ -1870,11 +1870,11 @@ export async function settleJudgeTerminalResult(admitted, authority) {
1870
1870
  * Returns undefined only for genuine absence (no lawful verdict / missing path).
1871
1871
  * Session malformation and publication exceptions propagate with typed identity.
1872
1872
  */
1873
- export async function trySettleJudgeTerminalResult(admitted, authority) {
1874
- return settleLawfulJudgeTerminalResult(admitted, authority);
1873
+ export async function trySettleJudgeTerminalResult(admitted, authority, scope) {
1874
+ return settleLawfulJudgeTerminalResult(admitted, authority, scope);
1875
1875
  }
1876
- async function settleLawfulCoderTerminalResult(admitted, authority, options = {}) {
1877
- const ledgerOutcome = await closedLedgerOutcome(admitted, "coder");
1876
+ async function settleLawfulCoderTerminalResult(admitted, authority, options = {}, scope) {
1877
+ const ledgerOutcome = await closedLedgerOutcome(admitted, "coder", scope);
1878
1878
  if (ledgerOutcome === undefined)
1879
1879
  return undefined;
1880
1880
  // #757: sealed facts pass through — no shape re-project / field drop.
@@ -1899,8 +1899,8 @@ async function settleLawfulCoderTerminalResult(admitted, authority, options = {}
1899
1899
  }, coordinates.sessionDirectory);
1900
1900
  }
1901
1901
  /** Settle a lawful Coder Terminal from the admitted session (shared #106 success interface). */
1902
- export async function settleCoderTerminalResult(admitted, authority, options = {}) {
1903
- const settled = await settleLawfulCoderTerminalResult(admitted, authority, options);
1902
+ export async function settleCoderTerminalResult(admitted, authority, options = {}, scope) {
1903
+ const settled = await settleLawfulCoderTerminalResult(admitted, authority, options, scope);
1904
1904
  if (settled === undefined) {
1905
1905
  throw new Error("Coder Role run completed without a lawful typed terminal result");
1906
1906
  }
@@ -1995,8 +1995,8 @@ export async function publishFixerArtifacts(admitted, roleOutcome, coordinates,
1995
1995
  { kind: "evidence", path: evidencePath },
1996
1996
  ];
1997
1997
  }
1998
- async function settleLawfulFixerTerminalResult(admitted, authority, options) {
1999
- const ledgerOutcome = await closedLedgerOutcome(admitted, "fixer");
1998
+ async function settleLawfulFixerTerminalResult(admitted, authority, options, scope) {
1999
+ const ledgerOutcome = await closedLedgerOutcome(admitted, "fixer", scope);
2000
2000
  if (ledgerOutcome === undefined)
2001
2001
  return undefined;
2002
2002
  // #757: sealed facts pass through — no shape re-project / field drop.
@@ -2027,8 +2027,8 @@ async function settleLawfulFixerTerminalResult(admitted, authority, options) {
2027
2027
  }, sessionDirectory);
2028
2028
  }
2029
2029
  /** Settle a lawful Fixer Terminal from the admitted session (shared #106 success interface). */
2030
- export async function settleFixerTerminalResult(admitted, authority, options) {
2031
- const settled = await settleLawfulFixerTerminalResult(admitted, authority, options);
2030
+ export async function settleFixerTerminalResult(admitted, authority, options, scope) {
2031
+ const settled = await settleLawfulFixerTerminalResult(admitted, authority, options, scope);
2032
2032
  if (settled === undefined) {
2033
2033
  throw new Error("Fixer Role run completed without a lawful typed terminal result");
2034
2034
  }
@@ -2068,11 +2068,11 @@ export async function publishCollectorArtifacts(admitted, roleOutcome, coordinat
2068
2068
  { kind: "evidence", path: evidencePath },
2069
2069
  ];
2070
2070
  }
2071
- async function settleLawfulCollectorTerminalResult(admitted, authority) {
2071
+ async function settleLawfulCollectorTerminalResult(admitted, authority, scope) {
2072
2072
  const coordinates = coordinatesFromAdmitted(authority, admitted);
2073
2073
  const { sessionDirectory, sessionFile } = coordinates;
2074
2074
  const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
2075
- const roleOutcome = await sealedLedgerOutcome(admitted);
2075
+ const roleOutcome = await sealedLedgerOutcome(admitted, scope);
2076
2076
  if (roleOutcome?.role !== "collector") {
2077
2077
  // Bounded to the current attempt so multi-attempt resume timeout/no-output
2078
2078
  // is not masked by a prior wait-tool residual (#633).
@@ -2122,16 +2122,16 @@ async function settleLawfulCollectorTerminalResult(admitted, authority) {
2122
2122
  }, sessionDirectory);
2123
2123
  }
2124
2124
  /** Settle a lawful Collector Terminal from the admitted session. */
2125
- export async function settleCollectorTerminalResult(admitted, authority) {
2126
- const settled = await settleLawfulCollectorTerminalResult(admitted, authority);
2125
+ export async function settleCollectorTerminalResult(admitted, authority, scope) {
2126
+ const settled = await settleLawfulCollectorTerminalResult(admitted, authority, scope);
2127
2127
  if (settled === undefined) {
2128
2128
  throw new Error("Collector Role run completed without a lawful typed terminal result");
2129
2129
  }
2130
2130
  return settled;
2131
2131
  }
2132
2132
  /** Try to settle a lawful Collector Terminal; undefined only for genuine absence. */
2133
- export async function trySettleCollectorTerminalResult(admitted, authority) {
2134
- return settleLawfulCollectorTerminalResult(admitted, authority);
2133
+ export async function trySettleCollectorTerminalResult(admitted, authority, scope) {
2134
+ return settleLawfulCollectorTerminalResult(admitted, authority, scope);
2135
2135
  }
2136
2136
  export async function publishDoctorArtifacts(admitted, roleOutcome, coordinates, options = {}) {
2137
2137
  await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
@@ -2167,13 +2167,13 @@ export async function publishDoctorArtifacts(admitted, roleOutcome, coordinates,
2167
2167
  { kind: "evidence", path: evidencePath },
2168
2168
  ];
2169
2169
  }
2170
- async function settleLawfulDoctorTerminalResult(admitted, authority) {
2171
- const sealed = await sealedLedgerOutcome(admitted);
2170
+ async function settleLawfulDoctorTerminalResult(admitted, authority, scope) {
2171
+ const sealed = await sealedLedgerOutcome(admitted, scope);
2172
2172
  const coordinates = coordinatesFromAdmitted(authority, admitted);
2173
2173
  const { sessionDirectory, sessionFile } = coordinates;
2174
2174
  const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
2175
2175
  if (sealed?.role !== "doctor") {
2176
- const escalation = await auditEscalationLedgerOutcome(admitted, "doctor");
2176
+ const escalation = await auditEscalationLedgerOutcome(admitted, "doctor", scope);
2177
2177
  if (escalation === undefined)
2178
2178
  return undefined;
2179
2179
  const artifacts = await publishDoctorArtifacts(admitted, escalation, coordinates);
@@ -2213,16 +2213,16 @@ async function settleLawfulDoctorTerminalResult(admitted, authority) {
2213
2213
  }, sessionDirectory);
2214
2214
  }
2215
2215
  /** Settle a lawful Doctor Terminal from the admitted session. */
2216
- export async function settleDoctorTerminalResult(admitted, authority) {
2217
- const settled = await settleLawfulDoctorTerminalResult(admitted, authority);
2216
+ export async function settleDoctorTerminalResult(admitted, authority, scope) {
2217
+ const settled = await settleLawfulDoctorTerminalResult(admitted, authority, scope);
2218
2218
  if (settled === undefined) {
2219
2219
  throw new Error("Doctor Role run completed without a lawful typed terminal result");
2220
2220
  }
2221
2221
  return settled;
2222
2222
  }
2223
2223
  /** Try to settle a lawful Doctor Terminal; undefined only for genuine absence. */
2224
- export async function trySettleDoctorTerminalResult(admitted, authority) {
2225
- return settleLawfulDoctorTerminalResult(admitted, authority);
2224
+ export async function trySettleDoctorTerminalResult(admitted, authority, scope) {
2225
+ return settleLawfulDoctorTerminalResult(admitted, authority, scope);
2226
2226
  }
2227
2227
  /** Latest top-level user message index; 0 when the session has none (initial attempt). */
2228
2228
  function currentAttemptStartIndex(entries) {
@@ -2388,12 +2388,12 @@ export async function trySettleAuditorTerminalResult(admitted, authority, scope)
2388
2388
  return settleLawfulAuditorTerminalResult(admitted, authority, scope);
2389
2389
  }
2390
2390
  /** Try to settle a lawful Coder Terminal; undefined only for genuine absence. */
2391
- export async function trySettleCoderTerminalResult(admitted, authority, options = {}) {
2392
- return settleLawfulCoderTerminalResult(admitted, authority, options);
2391
+ export async function trySettleCoderTerminalResult(admitted, authority, options = {}, scope) {
2392
+ return settleLawfulCoderTerminalResult(admitted, authority, options, scope);
2393
2393
  }
2394
2394
  /** Try to settle a lawful Fixer Terminal; undefined only for genuine absence. */
2395
- export async function trySettleFixerTerminalResult(admitted, authority, options) {
2396
- return settleLawfulFixerTerminalResult(admitted, authority, options);
2395
+ export async function trySettleFixerTerminalResult(admitted, authority, options, scope) {
2396
+ return settleLawfulFixerTerminalResult(admitted, authority, options, scope);
2397
2397
  }
2398
2398
  /**
2399
2399
  * Observe forced Reviewer code-review Skill expansions from the session.
@@ -2468,8 +2468,8 @@ export async function publishReviewerArtifacts(admitted, roleOutcome, coordinate
2468
2468
  { kind: "evidence", path: evidencePath },
2469
2469
  ];
2470
2470
  }
2471
- async function settleLawfulReviewerTerminalResult(admitted, authority, options) {
2472
- const sealed = await sealedLedgerOutcome(admitted);
2471
+ async function settleLawfulReviewerTerminalResult(admitted, authority, options, scope) {
2472
+ const sealed = await sealedLedgerOutcome(admitted, scope);
2473
2473
  if (sealed?.role !== "reviewer")
2474
2474
  return undefined;
2475
2475
  const coordinates = coordinatesFromAdmitted(authority, admitted);
@@ -2503,16 +2503,16 @@ async function settleLawfulReviewerTerminalResult(admitted, authority, options)
2503
2503
  }, sessionDirectory);
2504
2504
  }
2505
2505
  /** Settle a lawful Reviewer Terminal from the admitted session (shared #106 success interface). */
2506
- export async function settleReviewerTerminalResult(admitted, authority, options) {
2507
- const settled = await settleLawfulReviewerTerminalResult(admitted, authority, options);
2506
+ export async function settleReviewerTerminalResult(admitted, authority, options, scope) {
2507
+ const settled = await settleLawfulReviewerTerminalResult(admitted, authority, options, scope);
2508
2508
  if (settled === undefined) {
2509
2509
  throw new Error("Reviewer Role run completed without a lawful typed terminal result");
2510
2510
  }
2511
2511
  return settled;
2512
2512
  }
2513
2513
  /** Try to settle a lawful Reviewer Terminal; undefined only for genuine absence. */
2514
- export async function trySettleReviewerTerminalResult(admitted, authority, options) {
2515
- return settleLawfulReviewerTerminalResult(admitted, authority, options);
2514
+ export async function trySettleReviewerTerminalResult(admitted, authority, options, scope) {
2515
+ return settleLawfulReviewerTerminalResult(admitted, authority, options, scope);
2516
2516
  }
2517
2517
  /**
2518
2518
  * Observe forced Merger resolving-merge-conflicts Skill expansions from the session.
@@ -2579,14 +2579,14 @@ export async function publishMergerArtifacts(admitted, roleOutcome, coordinates,
2579
2579
  { kind: "evidence", path: evidencePath },
2580
2580
  ];
2581
2581
  }
2582
- async function settleLawfulMergerTerminalResult(admitted, authority, options) {
2582
+ async function settleLawfulMergerTerminalResult(admitted, authority, options, scope) {
2583
2583
  const coordinates = coordinatesFromAdmitted(authority, admitted);
2584
2584
  const { sessionDirectory, sessionFile } = coordinates;
2585
2585
  const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
2586
- const roleOutcome = await sealedLedgerOutcome(admitted);
2586
+ const roleOutcome = await sealedLedgerOutcome(admitted, scope);
2587
2587
  if (roleOutcome?.role !== "merger") {
2588
2588
  // Ledger owns 0041: a non-sole closed round is not residual-incomplete material.
2589
- const latestOutcome = await readLatestSubmissionOutcome(admitted.projectRoot, admitted.runId, sealedLedgerHome(admitted));
2589
+ const latestOutcome = await readLatestSubmissionOutcome(admitted.projectRoot, admitted.runId, ledgerReadScope(admitted, scope));
2590
2590
  if (latestOutcome?.outcome === "correctable-rejection" && latestOutcome.code === "non-sole-round") {
2591
2591
  return undefined;
2592
2592
  }
@@ -2650,16 +2650,16 @@ async function settleLawfulMergerTerminalResult(admitted, authority, options) {
2650
2650
  }, sessionDirectory);
2651
2651
  }
2652
2652
  /** Settle a lawful Merger Terminal from the admitted session (shared #106 success interface). */
2653
- export async function settleMergerTerminalResult(admitted, authority, options) {
2654
- const settled = await settleLawfulMergerTerminalResult(admitted, authority, options);
2653
+ export async function settleMergerTerminalResult(admitted, authority, options, scope) {
2654
+ const settled = await settleLawfulMergerTerminalResult(admitted, authority, options, scope);
2655
2655
  if (settled === undefined) {
2656
2656
  throw new Error("Merger Role run completed without a lawful typed terminal result");
2657
2657
  }
2658
2658
  return settled;
2659
2659
  }
2660
2660
  /** Try to settle a lawful Merger Terminal; undefined only for genuine absence. */
2661
- export async function trySettleMergerTerminalResult(admitted, authority, options) {
2662
- return settleLawfulMergerTerminalResult(admitted, authority, options);
2661
+ export async function trySettleMergerTerminalResult(admitted, authority, options, scope) {
2662
+ return settleLawfulMergerTerminalResult(admitted, authority, options, scope);
2663
2663
  }
2664
2664
  function publicationAttemptFromError(path, error) {
2665
2665
  if (error instanceof Error) {
@@ -21,5 +21,8 @@ export function projectRoleTurnRequest(admitted, roleDetails, options) {
21
21
  ? {}
22
22
  : { correlationId: options.correlationId }),
23
23
  ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
24
+ ...(options.courtAttemptId === undefined || options.courtAttemptId.length === 0
25
+ ? {}
26
+ : { courtAttemptId: options.courtAttemptId }),
24
27
  };
25
28
  }
@@ -25,10 +25,10 @@ function runIdentity(context) {
25
25
  throw new Error("提交账需要已受理的 run 身份");
26
26
  }
27
27
  /**
28
- * Court-turn attempt identity (#637 same-ticket re-summons).
29
- * AK_ROLE_COURT_ATTEMPT is injected only for a new court turn on an already-retained
30
- * run (forceContinuation / summons). Manual resume and first mint omit it so the
31
- * session-stable id keeps sole-final + sealed-idempotent semantics.
28
+ * Court-turn attempt identity (#637 same-ticket re-summons / #833 message re-review).
29
+ * AK_ROLE_COURT_ATTEMPT is injected for a new court turn on an already-retained run
30
+ * (summons / message / open-court continue). Bare resume and first mint omit it so
31
+ * the session-stable id keeps sole-final per attempt.
32
32
  */
33
33
  export const COURT_ATTEMPT_ENV = "AK_ROLE_COURT_ATTEMPT";
34
34
  function attemptIdentity(context, runId) {
@@ -96,16 +96,15 @@ function recordsForAttempt(owned, attemptId) {
96
96
  return owned.filter((record) => recordAttemptId(record) === attemptId);
97
97
  }
98
98
  /** Settlement read seam: typed sealed projection only, never host session JSONL.
99
- * Omit attemptId for run-scoped latest sealed (manual resume idempotent).
100
- * Pass attemptId for the current court turn so a prior seal cannot wash this turn. */
99
+ * Omit attemptId for run-scoped latest sealed.
100
+ * Pass attemptId for the current court turn so a prior seal cannot wash this turn.
101
+ * post-seal-anomaly is forensic only — later turns must not erase an earlier seal (#833). */
101
102
  export async function readSealedSubmission(cwd, runId, homeOrScope) {
102
103
  const scope = resolveReadScope(homeOrScope);
103
104
  const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
104
105
  const scoped = recordsForAttempt(owned, scope.attemptId);
105
106
  for (let index = scoped.length - 1; index >= 0; index -= 1) {
106
107
  const record = scoped[index];
107
- if (record?.kind === "post-seal-anomaly")
108
- return undefined;
109
108
  if (record?.kind !== "sealed")
110
109
  continue;
111
110
  const payload = record.payload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3891",
3
+ "version": "0.1.3900",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -179,9 +179,8 @@ export type RoleTurnRequest = {
179
179
  /** Set by post-admission only on a real host switch; never on same-host resume. */
180
180
  readonly hostTransition?: RoleTurnHostTransition;
181
181
  /**
182
- * Same-ticket re-summons only (#637): opens a new court-turn attempt on the
183
- * retained run so submission-ledger sole-final is per attempt, not forever.
184
- * Manual resume never sets this — sealed idempotent short-circuit stays intact.
182
+ * Court-turn attempt (#637 / #833): sole-final per attempt. Open court, summons,
183
+ * and resume-with-message set this; bare resume without an open court omits it.
185
184
  */
186
185
  readonly courtAttemptId?: string;
187
186
  };