@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.
- package/CLAUDE.md +2 -0
- package/dist/evidence-child-executor.js +2 -11
- package/dist/grok/production-host.js +196 -205
- package/dist/pi/in-process-session.js +4 -0
- package/dist/public-cli/main.js +998 -920
- package/package.json +1 -1
- package/scripts/build-package.mjs +1 -3
- package/src/analyst-gate-cycles-read.ts +3 -21
- package/src/analyst-ledger.ts +11 -42
- package/src/archivist-record-entry.ts +5 -6
- package/src/evidence-child-executor.ts +5 -15
- package/src/grok/production-host.ts +3 -32
- package/src/grok/role-envelope.ts +10 -4
- package/src/host-contracts.ts +4 -11
- package/src/host-transition-prior-native.ts +3 -7
- package/src/ledger-session-read.ts +1 -17
- package/src/pi/in-process-session.ts +6 -2
- package/src/pi/role-turn-host.ts +9 -0
- package/src/public-cli/collector-run.ts +4 -4
- package/src/public-cli/countersign-run.ts +90 -20
- package/src/public-cli/doctor-run.ts +4 -4
- package/src/public-cli/gleaner-left-run.ts +9 -6
- package/src/public-cli/inspector-run.ts +95 -32
- package/src/public-cli/instruction-seat-run.ts +7 -6
- package/src/public-cli/invocation.ts +14 -8
- package/src/public-cli/judge-run.ts +4 -4
- package/src/public-cli/notary-run.ts +91 -30
- package/src/public-cli/post-admission.ts +424 -360
- package/src/public-cli/run-lifecycle.ts +205 -0
- package/src/public-cli/seat-ticket-binding.ts +136 -16
- package/src/public-cli/settlement.ts +74 -52
- package/src/run-ticket-number.ts +53 -0
- package/src/submission-ledger.ts +86 -18
- package/dist/ledger-session-read.js +0 -241
- package/dist/ticket-seat-memory.js +0 -244
- 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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
1047
|
-
//
|
|
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
|
|
2370
|
-
*
|
|
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
|
|
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
|
+
}
|
package/src/submission-ledger.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
157
|
+
homeOrScope?: string | SubmissionLedgerReadScope,
|
|
111
158
|
): Promise<SealedSubmissionProjection | undefined> {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
176
|
+
homeOrScope?: string | SubmissionLedgerReadScope,
|
|
128
177
|
): Promise<AuditEscalationSubmissionProjection | undefined> {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
197
|
+
homeOrScope?: string | SubmissionLedgerReadScope,
|
|
147
198
|
): Promise<LatestSubmissionOutcome | undefined> {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
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;
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|