@akagilnc/pi-workflow-roles 0.1.3452 → 0.1.3471

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 (36) hide show
  1. package/CLAUDE.md +2 -0
  2. package/dist/evidence-child-executor.js +2 -11
  3. package/dist/grok/production-host.js +196 -205
  4. package/dist/pi/in-process-session.js +4 -0
  5. package/dist/public-cli/main.js +998 -920
  6. package/package.json +1 -1
  7. package/scripts/build-package.mjs +1 -3
  8. package/src/analyst-gate-cycles-read.ts +3 -21
  9. package/src/analyst-ledger.ts +11 -42
  10. package/src/archivist-record-entry.ts +5 -6
  11. package/src/evidence-child-executor.ts +5 -15
  12. package/src/grok/production-host.ts +3 -32
  13. package/src/grok/role-envelope.ts +10 -4
  14. package/src/host-contracts.ts +4 -11
  15. package/src/host-transition-prior-native.ts +3 -7
  16. package/src/ledger-session-read.ts +1 -17
  17. package/src/pi/in-process-session.ts +6 -2
  18. package/src/pi/role-turn-host.ts +9 -0
  19. package/src/public-cli/collector-run.ts +4 -4
  20. package/src/public-cli/countersign-run.ts +90 -20
  21. package/src/public-cli/doctor-run.ts +4 -4
  22. package/src/public-cli/gleaner-left-run.ts +9 -6
  23. package/src/public-cli/inspector-run.ts +95 -32
  24. package/src/public-cli/instruction-seat-run.ts +7 -6
  25. package/src/public-cli/invocation.ts +14 -8
  26. package/src/public-cli/judge-run.ts +4 -4
  27. package/src/public-cli/notary-run.ts +91 -30
  28. package/src/public-cli/post-admission.ts +424 -360
  29. package/src/public-cli/run-lifecycle.ts +205 -0
  30. package/src/public-cli/seat-ticket-binding.ts +136 -16
  31. package/src/public-cli/settlement.ts +74 -52
  32. package/src/run-ticket-number.ts +53 -0
  33. package/src/submission-ledger.ts +86 -18
  34. package/dist/ledger-session-read.js +0 -241
  35. package/dist/ticket-seat-memory.js +0 -244
  36. package/src/ticket-seat-memory.ts +0 -454
@@ -12,9 +12,7 @@ import {
12
12
  type AnalystGateCycleRound,
13
13
  } from "../analyst-gate-cycles-read.ts";
14
14
  import { readSitianRecords, resolveSitianRecordPath, sitianReport } from "../sitian-facade.ts";
15
- import {
16
- resolveTicketSeatMemoryNestDirectories,
17
- } from "../ticket-seat-memory.ts";
15
+
18
16
  import {
19
17
  readAuditEscalationSubmission,
20
18
  readLatestSubmissionOutcome,
@@ -161,8 +159,33 @@ function sealedLedgerHome(admitted: AdmittedRoleInvocation): string {
161
159
  return homeFromRunDirectory(admitted.runDirectory);
162
160
  }
163
161
 
164
- async function sealedLedgerOutcome(admitted: AdmittedRoleInvocation): Promise<Extract<TerminalRoleOutcome, { kind: "accepted" }> | undefined> {
165
- return readSealedSubmission(admitted.projectRoot, admitted.runId, sealedLedgerHome(admitted));
162
+ /**
163
+ * Court-turn settlement scope (#637 same-ticket re-summons).
164
+ * When courtAttemptId is set, ledger reads only that attempt so a prior seal
165
+ * cannot present as this turn's result. Omit for manual resume idempotent
166
+ * run-scoped sealed reads (no new court).
167
+ */
168
+ export type SettlementCourtScope = {
169
+ readonly courtAttemptId?: string;
170
+ };
171
+
172
+ function ledgerReadScope(
173
+ admitted: AdmittedRoleInvocation,
174
+ scope?: SettlementCourtScope,
175
+ ): { home: string; attemptId?: string } {
176
+ return {
177
+ home: sealedLedgerHome(admitted),
178
+ ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
179
+ ? {}
180
+ : { attemptId: scope.courtAttemptId }),
181
+ };
182
+ }
183
+
184
+ async function sealedLedgerOutcome(
185
+ admitted: AdmittedRoleInvocation,
186
+ scope?: SettlementCourtScope,
187
+ ): Promise<Extract<TerminalRoleOutcome, { kind: "accepted" }> | undefined> {
188
+ return readSealedSubmission(admitted.projectRoot, admitted.runId, ledgerReadScope(admitted, scope));
166
189
  }
167
190
 
168
191
  /**
@@ -173,6 +196,7 @@ async function sealedLedgerOutcome(admitted: AdmittedRoleInvocation): Promise<Ex
173
196
  * preserved cause; otherwise allow. Callers present entry-specific terminals;
174
197
  * they must not re-derive this judgment. Ledger projection remains the sole
175
198
  * seal truth — this is disposition over that read, not a second state.
199
+ * Always run-scoped (no court attempt filter): any retained seal blocks bare resume.
176
200
  */
177
201
  export type SealedAcceptanceRedispatchDisposition =
178
202
  | { readonly kind: "allow" }
@@ -196,11 +220,12 @@ export async function sealedAcceptanceRedispatchDisposition(
196
220
  async function auditEscalationLedgerOutcome(
197
221
  admitted: AdmittedRoleInvocation,
198
222
  role: TerminalRoleName,
223
+ scope?: SettlementCourtScope,
199
224
  ): Promise<Extract<TerminalRoleOutcome, { kind: "audit_escalation" }> | undefined> {
200
225
  const projection = await readAuditEscalationSubmission(
201
226
  admitted.projectRoot,
202
227
  admitted.runId,
203
- sealedLedgerHome(admitted),
228
+ ledgerReadScope(admitted, scope),
204
229
  );
205
230
  if (projection?.role !== role) return undefined;
206
231
  return projection;
@@ -209,11 +234,12 @@ async function auditEscalationLedgerOutcome(
209
234
  async function closedLedgerOutcome(
210
235
  admitted: AdmittedRoleInvocation,
211
236
  role: TerminalRoleName,
237
+ scope?: SettlementCourtScope,
212
238
  ): Promise<Extract<TerminalRoleOutcome, { kind: "accepted" | "audit_escalation" }> | undefined> {
213
- const sealed = await sealedLedgerOutcome(admitted);
239
+ const sealed = await sealedLedgerOutcome(admitted, scope);
214
240
  return sealed?.role === role
215
241
  ? sealed
216
- : auditEscalationLedgerOutcome(admitted, role);
242
+ : auditEscalationLedgerOutcome(admitted, role, scope);
217
243
  }
218
244
 
219
245
  /** Transitional host-session reads remain only for non-sealed failure and audit evidence. */
@@ -449,8 +475,7 @@ function flattenThrownFailureLeaves(error: unknown): unknown[] {
449
475
 
450
476
  /**
451
477
  * Project one thrown value into a ControlledFailure leaf.
452
- * Sole owner for thrown-leaf identity/diagnostic mapping (last-host write concurrent
453
- * facts reuse this — do not fork a second leaf mapper).
478
+ * Sole owner for thrown-leaf identity/diagnostic mapping.
454
479
  * AggregateError nesting is handled by classifyThrownFailure.
455
480
  */
456
481
  export function projectThrownFailureLeaf(error: unknown): ControlledFailure {
@@ -481,7 +506,7 @@ export function projectThrownFailureLeaf(error: unknown): ControlledFailure {
481
506
  }
482
507
 
483
508
  /**
484
- * Concurrent thrown failures (host + cleanup / last-host write, etc.):
509
+ * Concurrent thrown failures (host + cleanup, etc.):
485
510
  * primary leaf owns cause/diagnostic/identity; remaining leaves stay as
486
511
  * details.concurrentFailures so neither fact covers the other.
487
512
  */
@@ -1008,16 +1033,7 @@ async function loadBoundAuditorVolumes(
1008
1033
  latestParentUserIndex = i;
1009
1034
  break;
1010
1035
  }
1011
- const runDirectory = join(dirname(sessionFile), "..");
1012
- // #636: ticket-seat auditor memory nest via sole discovery helper.
1013
- const memoryNests = await resolveTicketSeatMemoryNestDirectories({
1014
- runDirectory,
1015
- seats: ["auditor"],
1016
- });
1017
- const childDirectories = [
1018
- join(dirname(sessionFile), "auditor-roles"),
1019
- ...memoryNests,
1020
- ];
1036
+ const childDirectories = [join(dirname(sessionFile), "auditor-roles")];
1021
1037
  // Auto-resume seam (owner A): stale check must ignore resume envelope and
1022
1038
  // prioritize retention. Previous `attemptEntryIndex < latest` discarded the
1023
1039
  // first attempt's child after resume advanced latest, losing retentionFailure
@@ -1043,9 +1059,8 @@ async function loadBoundAuditorVolumes(
1043
1059
  }
1044
1060
  const header = entries.find((entry) => entry.type === "session");
1045
1061
  if (!isRecord(header)) continue;
1046
- // Ticket-seat continuous memory keeps the first parent's header.parentSession;
1047
- // binding.parent.sessionFile is the per-summons authority (#636).
1048
- // Each binding owns only its interval — never whole-volume provider/compliance.
1062
+ // Parent-attempt binding owns its interval on multi-attempt volumes
1063
+ // (never whole-volume provider/compliance).
1049
1064
  const bindingIndexes: number[] = [];
1050
1065
  for (let i = 0; i < entries.length; i += 1) {
1051
1066
  const entry = entries[i];
@@ -2366,9 +2381,8 @@ export function projectTerminalGateFact(
2366
2381
  }
2367
2382
 
2368
2383
  /**
2369
- * Read gate facts from the run's session/auditor-roles nest and, when the run
2370
- * carries a ticket, from #636 ticket-seat memory nests via the sole nested-volume
2371
- * reader (#446/#478). Missing directories → undefined (no-gate zero change).
2384
+ * Read gate facts from the run's session/auditor-roles nest (#446/#478).
2385
+ * Missing directories → undefined (no-gate zero change).
2372
2386
  * Damaged discovered volumes propagate — never wash to "no gate".
2373
2387
  */
2374
2388
  export async function extractGateFactFromSessionDirectory(
@@ -2378,15 +2392,7 @@ export async function extractGateFactFromSessionDirectory(
2378
2392
  readonly parentSessionFile?: string;
2379
2393
  } = {},
2380
2394
  ): Promise<TerminalGateFact | undefined> {
2381
- const runDirectory = options.runDirectory ?? join(sessionDirectory, "..");
2382
- const memoryNests = await resolveTicketSeatMemoryNestDirectories({
2383
- runDirectory,
2384
- seats: ["inspector", "notary"],
2385
- });
2386
- const directories = [
2387
- join(sessionDirectory, "auditor-roles"),
2388
- ...memoryNests,
2389
- ];
2395
+ const directories = [join(sessionDirectory, "auditor-roles")];
2390
2396
  const parentSessionFile =
2391
2397
  options.parentSessionFile ?? join(sessionDirectory, "session.jsonl");
2392
2398
  const rounds = await readAnalystGateCyclesFromAuditorRoles(directories, {
@@ -3384,11 +3390,12 @@ async function settleLawfulSeatAcceptedTerminalResult(
3384
3390
  | AdmittedNavigatorInvocation,
3385
3391
  authority: DurablePrincipalAuthority,
3386
3392
  spec: SeatAcceptedSettlementSpec,
3393
+ scope?: SettlementCourtScope,
3387
3394
  ): Promise<TerminalResult | undefined> {
3388
3395
  const coordinates = coordinatesFromAdmitted(authority, admitted);
3389
3396
  const { sessionDirectory, sessionFile } = coordinates;
3390
3397
  const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
3391
- const roleOutcome = await closedLedgerOutcome(admitted, spec.role as TerminalRoleName);
3398
+ const roleOutcome = await closedLedgerOutcome(admitted, spec.role as TerminalRoleName, scope);
3392
3399
  if (roleOutcome?.kind === "audit_escalation") {
3393
3400
  const navigator = extractNavigatorFact(entries);
3394
3401
  return withOptionalGateProjection(
@@ -3490,6 +3497,7 @@ export type LawfulNotaryRoleOutcome = {
3490
3497
  async function settleLawfulNotaryTerminalResult(
3491
3498
  admitted: AdmittedNotaryInvocation,
3492
3499
  authority: DurablePrincipalAuthority,
3500
+ scope?: SettlementCourtScope,
3493
3501
  ): Promise<TerminalResult | undefined> {
3494
3502
  return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
3495
3503
  role: "notary",
@@ -3506,15 +3514,16 @@ async function settleLawfulNotaryTerminalResult(
3506
3514
  };
3507
3515
  return accepted;
3508
3516
  },
3509
- });
3517
+ }, scope);
3510
3518
  }
3511
3519
 
3512
3520
  /** Settle a lawful Notary Terminal from the admitted session. */
3513
3521
  export async function settleNotaryTerminalResult(
3514
3522
  admitted: AdmittedNotaryInvocation,
3515
3523
  authority: DurablePrincipalAuthority,
3524
+ scope?: SettlementCourtScope,
3516
3525
  ): Promise<TerminalResult> {
3517
- const settled = await settleLawfulNotaryTerminalResult(admitted, authority);
3526
+ const settled = await settleLawfulNotaryTerminalResult(admitted, authority, scope);
3518
3527
  if (settled === undefined) {
3519
3528
  throw new Error(
3520
3529
  "Notary Role run completed without a lawful typed terminal result",
@@ -3527,8 +3536,9 @@ export async function settleNotaryTerminalResult(
3527
3536
  export async function trySettleNotaryTerminalResult(
3528
3537
  admitted: AdmittedNotaryInvocation,
3529
3538
  authority: DurablePrincipalAuthority,
3539
+ scope?: SettlementCourtScope,
3530
3540
  ): Promise<TerminalResult | undefined> {
3531
- return settleLawfulNotaryTerminalResult(admitted, authority);
3541
+ return settleLawfulNotaryTerminalResult(admitted, authority, scope);
3532
3542
  }
3533
3543
 
3534
3544
  /** Lawful Countersign accepted outcome (署/封驳/上呈, #572 / ADR 0074). */
@@ -3542,6 +3552,7 @@ export type LawfulCountersignRoleOutcome = {
3542
3552
  async function settleLawfulCountersignTerminalResult(
3543
3553
  admitted: AdmittedCountersignInvocation,
3544
3554
  authority: DurablePrincipalAuthority,
3555
+ scope?: SettlementCourtScope,
3545
3556
  ): Promise<TerminalResult | undefined> {
3546
3557
  return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
3547
3558
  role: "countersign",
@@ -3558,15 +3569,16 @@ async function settleLawfulCountersignTerminalResult(
3558
3569
  };
3559
3570
  return accepted;
3560
3571
  },
3561
- });
3572
+ }, scope);
3562
3573
  }
3563
3574
 
3564
3575
  /** Settle a lawful Countersign Terminal from the admitted session. */
3565
3576
  export async function settleCountersignTerminalResult(
3566
3577
  admitted: AdmittedCountersignInvocation,
3567
3578
  authority: DurablePrincipalAuthority,
3579
+ scope?: SettlementCourtScope,
3568
3580
  ): Promise<TerminalResult> {
3569
- const settled = await settleLawfulCountersignTerminalResult(admitted, authority);
3581
+ const settled = await settleLawfulCountersignTerminalResult(admitted, authority, scope);
3570
3582
  if (settled === undefined) {
3571
3583
  throw new Error(
3572
3584
  "Countersign Role run completed without a lawful typed terminal result",
@@ -3579,8 +3591,9 @@ export async function settleCountersignTerminalResult(
3579
3591
  export async function trySettleCountersignTerminalResult(
3580
3592
  admitted: AdmittedCountersignInvocation,
3581
3593
  authority: DurablePrincipalAuthority,
3594
+ scope?: SettlementCourtScope,
3582
3595
  ): Promise<TerminalResult | undefined> {
3583
- return settleLawfulCountersignTerminalResult(admitted, authority);
3596
+ return settleLawfulCountersignTerminalResult(admitted, authority, scope);
3584
3597
  }
3585
3598
 
3586
3599
  /** Lawful Gleaner-Left accepted outcome (completed 弹章, #502). */
@@ -3594,6 +3607,7 @@ export type LawfulGleanerLeftRoleOutcome = {
3594
3607
  async function settleLawfulGleanerLeftTerminalResult(
3595
3608
  admitted: AdmittedGleanerLeftInvocation,
3596
3609
  authority: DurablePrincipalAuthority,
3610
+ scope?: SettlementCourtScope,
3597
3611
  ): Promise<TerminalResult | undefined> {
3598
3612
  return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
3599
3613
  role: "gleaner-left",
@@ -3610,15 +3624,16 @@ async function settleLawfulGleanerLeftTerminalResult(
3610
3624
  };
3611
3625
  return accepted;
3612
3626
  },
3613
- });
3627
+ }, scope);
3614
3628
  }
3615
3629
 
3616
3630
  /** Settle a lawful Gleaner-Left Terminal from the admitted session. */
3617
3631
  export async function settleGleanerLeftTerminalResult(
3618
3632
  admitted: AdmittedGleanerLeftInvocation,
3619
3633
  authority: DurablePrincipalAuthority,
3634
+ scope?: SettlementCourtScope,
3620
3635
  ): Promise<TerminalResult> {
3621
- const settled = await settleLawfulGleanerLeftTerminalResult(admitted, authority);
3636
+ const settled = await settleLawfulGleanerLeftTerminalResult(admitted, authority, scope);
3622
3637
  if (settled === undefined) {
3623
3638
  throw new Error(
3624
3639
  "Gleaner-Left Role run completed without a lawful typed terminal result",
@@ -3631,8 +3646,9 @@ export async function settleGleanerLeftTerminalResult(
3631
3646
  export async function trySettleGleanerLeftTerminalResult(
3632
3647
  admitted: AdmittedGleanerLeftInvocation,
3633
3648
  authority: DurablePrincipalAuthority,
3649
+ scope?: SettlementCourtScope,
3634
3650
  ): Promise<TerminalResult | undefined> {
3635
- return settleLawfulGleanerLeftTerminalResult(admitted, authority);
3651
+ return settleLawfulGleanerLeftTerminalResult(admitted, authority, scope);
3636
3652
  }
3637
3653
 
3638
3654
  /** Lawful Inspector accepted outcome (pass/bounce/escalate). */
@@ -3646,6 +3662,7 @@ export type LawfulInspectorRoleOutcome = {
3646
3662
  async function settleLawfulInspectorTerminalResult(
3647
3663
  admitted: AdmittedInspectorInvocation,
3648
3664
  authority: DurablePrincipalAuthority,
3665
+ scope?: SettlementCourtScope,
3649
3666
  ): Promise<TerminalResult | undefined> {
3650
3667
  return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
3651
3668
  role: "inspector",
@@ -3662,14 +3679,15 @@ async function settleLawfulInspectorTerminalResult(
3662
3679
  };
3663
3680
  return accepted;
3664
3681
  },
3665
- });
3682
+ }, scope);
3666
3683
  }
3667
3684
 
3668
3685
  export async function trySettleInspectorTerminalResult(
3669
3686
  admitted: AdmittedInspectorInvocation,
3670
3687
  authority: DurablePrincipalAuthority,
3688
+ scope?: SettlementCourtScope,
3671
3689
  ): Promise<TerminalResult | undefined> {
3672
- return settleLawfulInspectorTerminalResult(admitted, authority);
3690
+ return settleLawfulInspectorTerminalResult(admitted, authority, scope);
3673
3691
  }
3674
3692
 
3675
3693
  /** Lawful Gatekeeper accepted outcome (dispatch | pass, #639). */
@@ -3683,6 +3701,7 @@ export type LawfulGatekeeperRoleOutcome = {
3683
3701
  async function settleLawfulGatekeeperTerminalResult(
3684
3702
  admitted: AdmittedGatekeeperInvocation,
3685
3703
  authority: DurablePrincipalAuthority,
3704
+ scope?: SettlementCourtScope,
3686
3705
  ): Promise<TerminalResult | undefined> {
3687
3706
  return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
3688
3707
  role: "gatekeeper",
@@ -3699,15 +3718,16 @@ async function settleLawfulGatekeeperTerminalResult(
3699
3718
  };
3700
3719
  return accepted;
3701
3720
  },
3702
- });
3721
+ }, scope);
3703
3722
  }
3704
3723
 
3705
3724
  /** Try to settle a lawful Gatekeeper Terminal; undefined only for genuine absence. */
3706
3725
  export async function trySettleGatekeeperTerminalResult(
3707
3726
  admitted: AdmittedGatekeeperInvocation,
3708
3727
  authority: DurablePrincipalAuthority,
3728
+ scope?: SettlementCourtScope,
3709
3729
  ): Promise<TerminalResult | undefined> {
3710
- return settleLawfulGatekeeperTerminalResult(admitted, authority);
3730
+ return settleLawfulGatekeeperTerminalResult(admitted, authority, scope);
3711
3731
  }
3712
3732
 
3713
3733
  /** Lawful Navigator accepted outcome (route advice, #639). */
@@ -3721,6 +3741,7 @@ export type LawfulNavigatorRoleOutcome = {
3721
3741
  async function settleLawfulNavigatorTerminalResult(
3722
3742
  admitted: AdmittedNavigatorInvocation,
3723
3743
  authority: DurablePrincipalAuthority,
3744
+ scope?: SettlementCourtScope,
3724
3745
  ): Promise<TerminalResult | undefined> {
3725
3746
  return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
3726
3747
  role: "navigator",
@@ -3737,15 +3758,16 @@ async function settleLawfulNavigatorTerminalResult(
3737
3758
  };
3738
3759
  return accepted;
3739
3760
  },
3740
- });
3761
+ }, scope);
3741
3762
  }
3742
3763
 
3743
3764
  /** Try to settle a lawful Navigator Terminal; undefined only for genuine absence. */
3744
3765
  export async function trySettleNavigatorTerminalResult(
3745
3766
  admitted: AdmittedNavigatorInvocation,
3746
3767
  authority: DurablePrincipalAuthority,
3768
+ scope?: SettlementCourtScope,
3747
3769
  ): Promise<TerminalResult | undefined> {
3748
- return settleLawfulNavigatorTerminalResult(admitted, authority);
3770
+ return settleLawfulNavigatorTerminalResult(admitted, authority, scope);
3749
3771
  }
3750
3772
 
3751
3773
  /** Try to settle a lawful Coder Terminal; undefined only for genuine absence. */
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Typed ticketNumber reader for a retained run's durable pages.
3
+ * admitted-request.json first, invocation.json fallback. Missing page (ENOENT)
4
+ * → try next / undefined; damage and non-ENOENT IO failures propagate.
5
+ */
6
+ import { readFile } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+
9
+ function isEnoent(error: unknown): boolean {
10
+ return (
11
+ error instanceof Error &&
12
+ "code" in error &&
13
+ (error as NodeJS.ErrnoException).code === "ENOENT"
14
+ );
15
+ }
16
+
17
+ function ticketFromRecord(record: Record<string, unknown>): number | undefined {
18
+ const ticketNumber = record.ticketNumber;
19
+ if (
20
+ typeof ticketNumber === "number" &&
21
+ Number.isSafeInteger(ticketNumber) &&
22
+ ticketNumber >= 1
23
+ ) {
24
+ return ticketNumber;
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ async function readPageTicketNumber(
30
+ runDirectory: string,
31
+ page: "admitted-request.json" | "invocation.json",
32
+ ): Promise<number | undefined> {
33
+ try {
34
+ const raw = JSON.parse(await readFile(join(runDirectory, page), "utf8")) as unknown;
35
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
36
+ return undefined;
37
+ }
38
+ return ticketFromRecord(raw as Record<string, unknown>);
39
+ } catch (error) {
40
+ if (isEnoent(error)) return undefined;
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ /** Sole reader of ticketNumber from a retained run's admitted then invocation pages. */
46
+ export async function readRunTicketNumber(
47
+ runDirectory: string,
48
+ ): Promise<number | undefined> {
49
+ return (
50
+ (await readPageTicketNumber(runDirectory, "admitted-request.json")) ??
51
+ (await readPageTicketNumber(runDirectory, "invocation.json"))
52
+ );
53
+ }
@@ -56,7 +56,17 @@ function runIdentity(context: HostContext): string {
56
56
  throw new Error("提交账需要已受理的 run 身份");
57
57
  }
58
58
 
59
+ /**
60
+ * Court-turn attempt identity (#637 same-ticket re-summons).
61
+ * AK_ROLE_COURT_ATTEMPT is injected only for a new court turn on an already-retained
62
+ * run (forceContinuation / summons). Manual resume and first mint omit it so the
63
+ * session-stable id keeps sole-final + sealed-idempotent semantics.
64
+ */
65
+ export const COURT_ATTEMPT_ENV = "AK_ROLE_COURT_ATTEMPT" as const;
66
+
59
67
  function attemptIdentity(context: HostContext, runId: string): string {
68
+ const courtAttempt = process.env[COURT_ATTEMPT_ENV];
69
+ if (typeof courtAttempt === "string" && courtAttempt.length > 0) return courtAttempt;
60
70
  return context.sessionManager.getHeader?.()?.id ?? context.sessionManager.getLeafId?.() ?? `${runId}:initial`;
61
71
  }
62
72
 
@@ -103,15 +113,54 @@ async function readOwnedSubmissionRecords(cwd: string, runId: string, home?: str
103
113
  };
104
114
  }
105
115
 
106
- /** Settlement read seam: typed sealed projection only, never host session JSONL. */
116
+ /** Optional court-turn scope for settlement reads (#637). */
117
+ export type SubmissionLedgerReadScope = {
118
+ readonly home?: string;
119
+ /** When set, only this court attempt's ledger rows participate. */
120
+ readonly attemptId?: string;
121
+ };
122
+
123
+ function resolveReadScope(
124
+ homeOrScope?: string | SubmissionLedgerReadScope,
125
+ ): SubmissionLedgerReadScope {
126
+ if (homeOrScope === undefined) return {};
127
+ if (typeof homeOrScope === "string") return { home: homeOrScope };
128
+ return homeOrScope;
129
+ }
130
+
131
+ function recordAttemptId(record: { subject?: unknown; payload?: unknown }): string | undefined {
132
+ if (typeof record.subject === "object" && record.subject !== null) {
133
+ const fromSubject = (record.subject as { attemptId?: unknown }).attemptId;
134
+ if (typeof fromSubject === "string" && fromSubject.length > 0) return fromSubject;
135
+ }
136
+ if (typeof record.payload === "object" && record.payload !== null) {
137
+ const fromPayload = (record.payload as { attemptId?: unknown }).attemptId;
138
+ if (typeof fromPayload === "string" && fromPayload.length > 0) return fromPayload;
139
+ }
140
+ return undefined;
141
+ }
142
+
143
+ function recordsForAttempt<T extends { subject?: unknown; payload?: unknown }>(
144
+ owned: readonly T[],
145
+ attemptId: string | undefined,
146
+ ): readonly T[] {
147
+ if (attemptId === undefined) return owned;
148
+ return owned.filter((record) => recordAttemptId(record) === attemptId);
149
+ }
150
+
151
+ /** Settlement read seam: typed sealed projection only, never host session JSONL.
152
+ * Omit attemptId for run-scoped latest sealed (manual resume idempotent).
153
+ * Pass attemptId for the current court turn so a prior seal cannot wash this turn. */
107
154
  export async function readSealedSubmission(
108
155
  cwd: string,
109
156
  runId: string,
110
- home?: string,
157
+ homeOrScope?: string | SubmissionLedgerReadScope,
111
158
  ): Promise<SealedSubmissionProjection | undefined> {
112
- const { owned } = await readOwnedSubmissionRecords(cwd, runId, home);
113
- for (let index = owned.length - 1; index >= 0; index -= 1) {
114
- const record = owned[index];
159
+ const scope = resolveReadScope(homeOrScope);
160
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
161
+ const scoped = recordsForAttempt(owned, scope.attemptId);
162
+ for (let index = scoped.length - 1; index >= 0; index -= 1) {
163
+ const record = scoped[index];
115
164
  if (record?.kind === "post-seal-anomaly") return undefined;
116
165
  if (record?.kind !== "sealed") continue;
117
166
  const payload = record.payload as Partial<Extract<SubmissionLedgerEvent, { type: "sealed" }>> | undefined;
@@ -124,11 +173,13 @@ export async function readSealedSubmission(
124
173
  export async function readAuditEscalationSubmission(
125
174
  cwd: string,
126
175
  runId: string,
127
- home?: string,
176
+ homeOrScope?: string | SubmissionLedgerReadScope,
128
177
  ): Promise<AuditEscalationSubmissionProjection | undefined> {
129
- const { owned } = await readOwnedSubmissionRecords(cwd, runId, home);
130
- for (let index = owned.length - 1; index >= 0; index -= 1) {
131
- const record = owned[index];
178
+ const scope = resolveReadScope(homeOrScope);
179
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
180
+ const scoped = recordsForAttempt(owned, scope.attemptId);
181
+ for (let index = scoped.length - 1; index >= 0; index -= 1) {
182
+ const record = scoped[index];
132
183
  if (record?.kind !== "outcome") continue;
133
184
  const payload = record.payload as Partial<Extract<SubmissionLedgerEvent, { type: "outcome" }>> | undefined;
134
185
  if (payload?.type !== "outcome" || payload.outcome !== "audit-escalation") continue;
@@ -143,11 +194,13 @@ export type LatestSubmissionOutcome = Extract<SubmissionLedgerEvent, { type: "ou
143
194
  export async function readLatestSubmissionOutcome(
144
195
  cwd: string,
145
196
  runId: string,
146
- home?: string,
197
+ homeOrScope?: string | SubmissionLedgerReadScope,
147
198
  ): Promise<LatestSubmissionOutcome | undefined> {
148
- const { owned } = await readOwnedSubmissionRecords(cwd, runId, home);
149
- for (let index = owned.length - 1; index >= 0; index -= 1) {
150
- const record = owned[index];
199
+ const scope = resolveReadScope(homeOrScope);
200
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
201
+ const scoped = recordsForAttempt(owned, scope.attemptId);
202
+ for (let index = scoped.length - 1; index >= 0; index -= 1) {
203
+ const record = scoped[index];
151
204
  if (record?.kind !== "outcome") continue;
152
205
  const payload = record.payload as Partial<LatestSubmissionOutcome> | undefined;
153
206
  if (payload?.type === "outcome" && typeof payload.outcome === "string") {
@@ -157,14 +210,27 @@ export async function readLatestSubmissionOutcome(
157
210
  return undefined;
158
211
  }
159
212
 
160
- type LedgerState = { prior?: RecordPointer; sealed: boolean; sequence: number };
213
+ type LedgerState = { prior?: RecordPointer; sealedAttemptIds: Set<string>; sequence: number };
214
+
215
+ function sealedAttemptIdFromRecord(record: { kind: string; payload?: unknown }): string | undefined {
216
+ if (record.kind !== "sealed") return undefined;
217
+ const payload = record.payload as Partial<Extract<SubmissionLedgerEvent, { type: "sealed" }>> | undefined;
218
+ return payload?.type === "sealed" && typeof payload.attemptId === "string" && payload.attemptId.length > 0
219
+ ? payload.attemptId
220
+ : undefined;
221
+ }
161
222
 
162
223
  async function restoreState(cwd: string, runId: string, home?: string): Promise<LedgerState> {
163
224
  const { file, owned } = await readOwnedSubmissionRecords(cwd, runId, home);
164
225
  const last = owned.at(-1);
226
+ const sealedAttemptIds = new Set<string>();
227
+ for (const record of owned) {
228
+ const attemptId = sealedAttemptIdFromRecord(record);
229
+ if (attemptId !== undefined) sealedAttemptIds.add(attemptId);
230
+ }
165
231
  return {
166
232
  ...(last === undefined ? {} : { prior: { identity: last.identity, recordFile: file, kind: last.kind, level: last.level } }),
167
- sealed: owned.some((record) => record.kind === "sealed"),
233
+ sealedAttemptIds,
168
234
  sequence: owned.reduce((maximum, record) => {
169
235
  const payload = record.payload as Partial<SubmissionLedgerEvent> | undefined;
170
236
  return payload?.type === "candidate" && typeof payload.sequence === "number" ? Math.max(maximum, payload.sequence) : maximum;
@@ -220,7 +286,9 @@ export function createSubmissionLedgerHost(
220
286
  const runId = runIdentity(context);
221
287
  const attemptId = attemptIdentity(context, runId);
222
288
  const state = await stateFor(context, runId);
223
- if (state.sealed) appendFor(state, context, runId, attemptId, { type: "post-seal-anomaly", attemptId, toolCallId, toolName });
289
+ if (state.sealedAttemptIds.has(attemptId)) {
290
+ appendFor(state, context, runId, attemptId, { type: "post-seal-anomaly", attemptId, toolCallId, toolName });
291
+ }
224
292
  } catch (error) {
225
293
  failInfrastructure(error, context);
226
294
  }
@@ -261,7 +329,7 @@ export function createSubmissionLedgerHost(
261
329
  if (typeof status !== "string" || status.length === 0) throw new Error("提交账封账缺少 acceptedFacts.status");
262
330
  const projection: Extract<TerminalRoleOutcome, { kind: "accepted" }> = { kind: "accepted", role: candidate.role, status, decisiveFacts: details };
263
331
  appendFor(state, candidate.context, runId, attemptId, { type: "sealed", attemptId, toolCallId: candidate.toolCallId, accepted: candidate.result.details, projection });
264
- state.sealed = true;
332
+ state.sealedAttemptIds.add(attemptId);
265
333
  await projectClosure(projection, context);
266
334
  context.abort();
267
335
  } catch (error) {
@@ -281,7 +349,7 @@ export function createSubmissionLedgerHost(
281
349
  const attemptId = attemptIdentity(context, runId);
282
350
  const state = await stateFor(context, runId);
283
351
  const append = (event: SubmissionLedgerEvent) => appendFor(state, context, runId, attemptId, event);
284
- if (state.sealed) throw new Error("提交账已封账");
352
+ if (state.sealedAttemptIds.has(attemptId)) throw new Error("提交账已封账");
285
353
  // #541 / #575: shared infra-declaration fail lives on the ledger seam,
286
354
  // before any role execute or sole-final work (one owner for every seat).
287
355
  // #641 chain②: seats may opt into bouncing a machine-verified normal