@akagilnc/pi-workflow-roles 0.1.4062 → 0.1.4196
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/dist/acp-host/production-host.js +2075 -1088
- package/dist/archivist-record-entry.js +204 -20
- package/dist/archivist-record-topology.js +36 -16
- package/dist/auditor-dossier-tool.js +2 -1
- package/dist/diarist-contracts.js +96 -0
- package/dist/doctor-evidence.js +3 -2
- package/dist/headless-host/production-host.js +2215 -1228
- package/dist/migrate-book-topology.js +278917 -0
- package/dist/navigator-public-session.js +22 -25
- package/dist/notary-source-run.js +7 -4
- package/dist/pi/durable-principal.js +9 -6
- package/dist/public-cli/auto-resume.js +2 -5
- package/dist/public-cli/diarist-run.js +27 -25
- package/dist/public-cli/instruction-seat-run.js +8 -11
- package/dist/public-cli/invocation.js +127 -63
- package/dist/public-cli/main.js +1507 -645
- package/dist/public-cli/option-definitions.js +2 -2
- package/dist/public-cli/post-admission.js +47 -12
- package/dist/public-cli/run-lifecycle.js +206 -58
- package/dist/public-cli/seat-ticket-binding.js +3 -6
- package/dist/public-cli/settlement.js +5 -3
- package/dist/role-run-placement.js +84 -0
- package/dist/role-run-relocation.js +358 -0
- package/dist/run-terminal-artifacts.js +2 -1
- package/dist/run-ticket-number.js +51 -32
- package/dist/sitian-appender.js +11 -26
- package/dist/submission-ledger.js +78 -19
- package/dist/ticket-provenance.js +3 -1
- package/package.json +3 -2
- package/scripts/build-package.mjs +34 -2
- package/souls/countersign.md +2 -2
- package/souls/fixer.md +3 -2
- package/src/analyst-ledger.ts +14 -15
- package/src/archivist-record-entry.ts +285 -38
- package/src/archivist-record-topology.ts +43 -18
- package/src/auditor-dossier-tool.ts +2 -1
- package/src/book-topology-migration-placement.ts +493 -0
- package/src/book-topology-migration.ts +220 -0
- package/src/book-topology-migrators-t11.ts +856 -0
- package/src/book-topology-mixed-volume-migrators.ts +470 -0
- package/src/book-topology-partition-migrators.ts +25 -0
- package/src/book-topology-record-class-migrators.ts +779 -0
- package/src/book-topology-runs-migrator.ts +244 -0
- package/src/diarist-contracts.ts +116 -0
- package/src/diarist-role.ts +6 -0
- package/src/diarist.ts +2 -0
- package/src/doctor-evidence.ts +4 -1
- package/src/doctor-role.ts +1 -1
- package/src/navigator-public-session.ts +33 -30
- package/src/notary-source-run.ts +9 -4
- package/src/pi/durable-principal.ts +9 -13
- package/src/public-cli/auto-resume.ts +2 -6
- package/src/public-cli/countersign-run.ts +182 -32
- package/src/public-cli/diarist-run.ts +34 -26
- package/src/public-cli/instruction-seat-run.ts +7 -11
- package/src/public-cli/invocation.ts +192 -58
- package/src/public-cli/option-definitions.ts +2 -2
- package/src/public-cli/post-admission.ts +73 -13
- package/src/public-cli/run-lifecycle.ts +255 -65
- package/src/public-cli/seat-ticket-binding.ts +3 -6
- package/src/public-cli/settlement.ts +6 -4
- package/src/role-run-placement.ts +128 -0
- package/src/role-run-relocation.ts +491 -0
- package/src/role-runtime.ts +11 -5
- package/src/run-terminal-artifacts.ts +3 -1
- package/src/run-ticket-number.ts +80 -20
- package/src/sitian-appender.ts +25 -28
- package/src/submission-ledger.ts +102 -19
- package/src/ticket-provenance.ts +4 -1
- package/src/ticket-trajectory.ts +22 -35
- package/src/worker-role.ts +4 -4
- package/src/worker-submission-gates.ts +6 -6
|
@@ -529,8 +529,8 @@ const DOCTOR_OPTIONS = [
|
|
|
529
529
|
repeatable: false,
|
|
530
530
|
form: "option",
|
|
531
531
|
description: {
|
|
532
|
-
en: "Optional project-relative .ak-roles/books/<book
|
|
533
|
-
zh: "可选项目相对 .ak-roles/books/<book
|
|
532
|
+
en: "Optional project-relative .ak-roles/books/<book>/<n>/runs override matching --issue.",
|
|
533
|
+
zh: "可选项目相对 .ak-roles/books/<book>/<n>/runs 覆盖,且须匹配 --issue。",
|
|
534
534
|
},
|
|
535
535
|
},
|
|
536
536
|
];
|
|
@@ -62,10 +62,10 @@ function withOnceSuccessfulBeforeDispatch(adapters) {
|
|
|
62
62
|
let succeeded = false;
|
|
63
63
|
return {
|
|
64
64
|
...adapters,
|
|
65
|
-
beforeDispatch: async (admitted) => {
|
|
65
|
+
beforeDispatch: async (admitted, lease) => {
|
|
66
66
|
if (succeeded)
|
|
67
67
|
return;
|
|
68
|
-
await hook(admitted);
|
|
68
|
+
await hook(admitted, lease);
|
|
69
69
|
succeeded = true;
|
|
70
70
|
},
|
|
71
71
|
};
|
|
@@ -259,6 +259,41 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
259
259
|
const persistRunState = input.persistRunState !== false;
|
|
260
260
|
const deferredPersist = persistRunState ? {} : { needsPersist: true };
|
|
261
261
|
const shouldPresent = adapters.shouldPresentSettled ?? ((terminal) => isLawfulTypedTerminalOutcome(terminal.roleOutcome));
|
|
262
|
+
/**
|
|
263
|
+
* Once-only post-host-turn hook (Diarist bind/relocate, etc.) under the still-held
|
|
264
|
+
* writer lease. Every exit after the host turn has started must pass here before
|
|
265
|
+
* lease release — success and failure alike. When a primary failure terminal already
|
|
266
|
+
* exists, keep that cause and leave relocate failure on the shared diagnostic channel.
|
|
267
|
+
*/
|
|
268
|
+
let afterDispatchApplied = false;
|
|
269
|
+
const finishAfterTurn = async (result) => {
|
|
270
|
+
if (adapters.afterDispatch === undefined || afterDispatchApplied)
|
|
271
|
+
return result;
|
|
272
|
+
afterDispatchApplied = true;
|
|
273
|
+
try {
|
|
274
|
+
await adapters.afterDispatch(admitted, lease);
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
const primaryFailure = result.terminal !== undefined
|
|
279
|
+
&& !isLawfulTypedTerminalOutcome(result.terminal.roleOutcome);
|
|
280
|
+
if (primaryFailure) {
|
|
281
|
+
await recordBestEffortPostDispatchDiagnostic(admitted, env, `afterDispatch failed beside primary terminal (best-effort continue): ${describeErrorIdentity(error)}`, io);
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
...(await presentControlledFailure(admitted, {
|
|
286
|
+
timedOut: false,
|
|
287
|
+
code: null,
|
|
288
|
+
stderr: "",
|
|
289
|
+
thrown: error,
|
|
290
|
+
}, adapters, env.principalAuthority, io, persistRunState)),
|
|
291
|
+
...(result.turnDispatched === true ? { turnDispatched: true } : {}),
|
|
292
|
+
...(result.skipAutoResume === true ? { skipAutoResume: true } : {}),
|
|
293
|
+
...deferredPersist,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
};
|
|
262
297
|
try {
|
|
263
298
|
const missingCredential = missingCredentialPreDispatchFailure(env.model, env.credentials);
|
|
264
299
|
if (missingCredential !== undefined) {
|
|
@@ -315,7 +350,7 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
315
350
|
// (#840 父子不层叠).
|
|
316
351
|
if (adapters.beforeDispatch !== undefined) {
|
|
317
352
|
try {
|
|
318
|
-
await adapters.beforeDispatch(admitted);
|
|
353
|
+
await adapters.beforeDispatch(admitted, lease);
|
|
319
354
|
}
|
|
320
355
|
catch (error) {
|
|
321
356
|
const settled = (await presentControlledFailure(admitted, {
|
|
@@ -384,7 +419,7 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
384
419
|
stderr: "",
|
|
385
420
|
thrown: error,
|
|
386
421
|
}, adapters, env.principalAuthority, io, persistRunState);
|
|
387
|
-
return { ...settled, turnDispatched: true, ...deferredPersist };
|
|
422
|
+
return await finishAfterTurn({ ...settled, turnDispatched: true, ...deferredPersist });
|
|
388
423
|
}
|
|
389
424
|
// `result.stderr` stays live in memory for the real classification below
|
|
390
425
|
// regardless of whether this durable mirror write succeeds. A write
|
|
@@ -551,7 +586,7 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
551
586
|
stderr: result.stderr,
|
|
552
587
|
thrown: error,
|
|
553
588
|
}, adapters, env.principalAuthority, io, persistRunState);
|
|
554
|
-
return { ...settledFailure, turnDispatched: true, ...deferredPersist };
|
|
589
|
+
return await finishAfterTurn({ ...settledFailure, turnDispatched: true, ...deferredPersist });
|
|
555
590
|
}
|
|
556
591
|
if (settledOutcome !== undefined) {
|
|
557
592
|
if (persistRunState) {
|
|
@@ -576,12 +611,12 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
576
611
|
thrown: error,
|
|
577
612
|
skipRunStateWrite: true,
|
|
578
613
|
}, adapters, env.principalAuthority, io, persistRunState);
|
|
579
|
-
return { ...failed, turnDispatched: true, ...deferredPersist };
|
|
614
|
+
return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
|
|
580
615
|
}
|
|
581
616
|
}
|
|
582
617
|
// Lawful persist + present is the caller's stop seam (auto-resume loop /
|
|
583
618
|
// manual resume), not this retried host-turn function.
|
|
584
|
-
return { ...settledOutcome, ...deferredPersist };
|
|
619
|
+
return await finishAfterTurn({ ...settledOutcome, ...deferredPersist });
|
|
585
620
|
}
|
|
586
621
|
// Host/runner true failure coexists with already-recorded payloads — never wash as accepted.
|
|
587
622
|
if (hostSignalFailed) {
|
|
@@ -608,7 +643,7 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
608
643
|
}
|
|
609
644
|
: { knownDetails: stderrLogWriteDetails }),
|
|
610
645
|
}, adapters, env.principalAuthority, io, persistRunState);
|
|
611
|
-
return { ...failed, turnDispatched: true, ...deferredPersist };
|
|
646
|
+
return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
|
|
612
647
|
}
|
|
613
648
|
// Nothing else was wrong, but the durable stderr mirror itself failed to
|
|
614
649
|
// write — that is the real (infrastructure) problem in this case, not a
|
|
@@ -620,7 +655,7 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
620
655
|
stderr: result.stderr,
|
|
621
656
|
thrown: stderrLogWriteFailure,
|
|
622
657
|
}, adapters, env.principalAuthority, io, persistRunState);
|
|
623
|
-
return { ...failed, turnDispatched: true, ...deferredPersist };
|
|
658
|
+
return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
|
|
624
659
|
}
|
|
625
660
|
const noReceipt = await attachRecordedSubmissions(admitted, await settleHostEndedNoReceipt(admitted, env.principalAuthority), courtScope);
|
|
626
661
|
if (persistRunState) {
|
|
@@ -640,16 +675,16 @@ export async function dispatchPostAdmissionTurn(input) {
|
|
|
640
675
|
thrown: error,
|
|
641
676
|
skipRunStateWrite: true,
|
|
642
677
|
}, adapters, env.principalAuthority, io, persistRunState);
|
|
643
|
-
return { ...failed, turnDispatched: true, ...deferredPersist };
|
|
678
|
+
return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
|
|
644
679
|
}
|
|
645
680
|
}
|
|
646
|
-
return {
|
|
681
|
+
return await finishAfterTurn({
|
|
647
682
|
exitCode: exitCodeForTerminalOutcome(noReceipt.roleOutcome),
|
|
648
683
|
admitted,
|
|
649
684
|
terminal: noReceipt,
|
|
650
685
|
turnDispatched: true,
|
|
651
686
|
...deferredPersist,
|
|
652
|
-
};
|
|
687
|
+
});
|
|
653
688
|
}
|
|
654
689
|
finally {
|
|
655
690
|
try {
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* any existing run with an available Pi session principal may be resumed; caller decides.
|
|
6
6
|
* Prose is never regex-classified as quota evidence.
|
|
7
7
|
*/
|
|
8
|
-
import { chmod, open, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
9
|
-
import { isAbsolute, join } from "node:path";
|
|
8
|
+
import { chmod, lstat, open, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
9
|
+
import { basename, isAbsolute, join } from "node:path";
|
|
10
10
|
import { activationBookDirectory, resolveActivationLedgerHome, } from "../activation-ledger-topology.js";
|
|
11
|
-
import {
|
|
11
|
+
import { listBookRunDirectories } from "../role-run-placement.js";
|
|
12
|
+
import { isSafePositiveTicketNumber, readBoardTicketNumber, } from "../run-ticket-number.js";
|
|
12
13
|
import { CliUsageError } from "./cli-errors.js";
|
|
13
14
|
import { readLatestTypedProviderHttpObservation, } from "../typed-provider-http.js";
|
|
14
15
|
export { clearTypedProviderHttpObservation, recordTypedProviderHttpStatus, readLatestTypedProviderHttpObservation, } from "../typed-provider-http.js";
|
|
15
16
|
import { parseCollectorRepository } from "../collector-config.js";
|
|
17
|
+
import { interpretDurableCourtTicketNumbers, sameCourtTicketNumbers, } from "../diarist-contracts.js";
|
|
16
18
|
import { appendEngineSessionMaterial, engineSessionMaterialFromOptions, pickEngineAxis, } from "../package-resources/engine-material.js";
|
|
17
19
|
import { recordEffectiveInvocationModel, requireAuthorityRef, } from "./invocation.js";
|
|
18
20
|
/** Providers eligible for v1 typed-429 resume (Codex / xAI only).
|
|
@@ -477,14 +479,11 @@ function isProcessAlive(pid) {
|
|
|
477
479
|
}
|
|
478
480
|
}
|
|
479
481
|
/**
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
* authorizes reclaim; only a "dead" verdict does. A non-ENOENT read failure
|
|
484
|
-
* still decides "absent" but rides along as readFailure so the true cause can
|
|
485
|
-
* land in the cleanup sink instead of being laundered away.
|
|
482
|
+
* The writer lease holder is the shared authority for current run activity.
|
|
483
|
+
* Only ENOENT proves no holder; malformed or unreadable locks remain unknown
|
|
484
|
+
* because they can be observed while a live creator is writing its pid.
|
|
486
485
|
*/
|
|
487
|
-
async function autopsyWriterLock(lockPath) {
|
|
486
|
+
export async function autopsyWriterLock(lockPath) {
|
|
488
487
|
let content;
|
|
489
488
|
try {
|
|
490
489
|
content = await readFile(lockPath, "utf8");
|
|
@@ -492,14 +491,16 @@ async function autopsyWriterLock(lockPath) {
|
|
|
492
491
|
catch (error) {
|
|
493
492
|
if (errorCodeOf(error) === "ENOENT")
|
|
494
493
|
return { verdict: "absent" };
|
|
495
|
-
return { verdict: "
|
|
494
|
+
return { verdict: "unknown", reason: "unreadable", readFailure: error };
|
|
496
495
|
}
|
|
497
496
|
const normalized = content.trim();
|
|
498
|
-
if (!/^[1-9]\d*$/.test(normalized))
|
|
499
|
-
return { verdict: "
|
|
497
|
+
if (!/^[1-9]\d*$/.test(normalized)) {
|
|
498
|
+
return { verdict: "unknown", reason: "unparseable", content };
|
|
499
|
+
}
|
|
500
500
|
const pid = Number.parseInt(normalized, 10);
|
|
501
|
-
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
502
|
-
return { verdict: "
|
|
501
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
502
|
+
return { verdict: "unknown", reason: "unparseable", content };
|
|
503
|
+
}
|
|
503
504
|
return isProcessAlive(pid) ? { verdict: "alive", pid } : { verdict: "dead", pid };
|
|
504
505
|
}
|
|
505
506
|
function describeAutopsy(autopsy) {
|
|
@@ -509,9 +510,9 @@ function describeAutopsy(autopsy) {
|
|
|
509
510
|
case "dead":
|
|
510
511
|
return `dead pid ${autopsy.pid}`;
|
|
511
512
|
case "absent":
|
|
512
|
-
return
|
|
513
|
-
|
|
514
|
-
|
|
513
|
+
return "absent holder";
|
|
514
|
+
case "unknown":
|
|
515
|
+
return autopsy.reason === "unreadable" ? "unreadable lock" : "unparseable holder";
|
|
515
516
|
}
|
|
516
517
|
}
|
|
517
518
|
/**
|
|
@@ -565,28 +566,36 @@ async function createWriterLease(lockPath, runDirectory, reportCleanupFailure) {
|
|
|
565
566
|
throw error;
|
|
566
567
|
}
|
|
567
568
|
let released = false;
|
|
569
|
+
let currentRunDirectory = runDirectory;
|
|
570
|
+
let currentLockPath = lockPath;
|
|
568
571
|
return {
|
|
569
|
-
lockPath
|
|
572
|
+
get lockPath() {
|
|
573
|
+
return currentLockPath;
|
|
574
|
+
},
|
|
575
|
+
relocate(nextRunDirectory) {
|
|
576
|
+
currentRunDirectory = nextRunDirectory;
|
|
577
|
+
currentLockPath = join(nextRunDirectory, WRITER_LOCK_FILE);
|
|
578
|
+
},
|
|
570
579
|
async release() {
|
|
571
580
|
if (released)
|
|
572
581
|
return;
|
|
573
582
|
released = true;
|
|
574
583
|
await handle.close().catch(() => undefined);
|
|
575
584
|
try {
|
|
576
|
-
await unlink(
|
|
585
|
+
await unlink(currentLockPath);
|
|
577
586
|
}
|
|
578
587
|
catch (error) {
|
|
579
588
|
if (errorCodeOf(error) === "EACCES") {
|
|
580
589
|
try {
|
|
581
|
-
await chmod(
|
|
582
|
-
await unlink(
|
|
590
|
+
await chmod(currentRunDirectory, 0o755);
|
|
591
|
+
await unlink(currentLockPath);
|
|
583
592
|
}
|
|
584
593
|
catch (retryError) {
|
|
585
|
-
reportCleanupFailure(retryError);
|
|
594
|
+
reportCleanupFailure(retryError, currentLockPath);
|
|
586
595
|
}
|
|
587
596
|
}
|
|
588
597
|
else {
|
|
589
|
-
reportCleanupFailure(error);
|
|
598
|
+
reportCleanupFailure(error, currentLockPath);
|
|
590
599
|
}
|
|
591
600
|
}
|
|
592
601
|
},
|
|
@@ -640,8 +649,8 @@ export async function acquireRunWriterLease(runDirectory, onCleanupFailure) {
|
|
|
640
649
|
* acquire reads it as live and rejects; nothing here may promise that the
|
|
641
650
|
* next acquire reclaims it.
|
|
642
651
|
*/
|
|
643
|
-
const reportCleanupFailure = (error) => {
|
|
644
|
-
reportDiagnostic(`writer lease lock cleanup failed (release is best-effort; residual lock left in place) at ${
|
|
652
|
+
const reportCleanupFailure = (error, lockPath) => {
|
|
653
|
+
reportDiagnostic(`writer lease lock cleanup failed (release is best-effort; residual lock left in place) at ${lockPath}: ${describeErrorIdentity(error)}`);
|
|
645
654
|
};
|
|
646
655
|
/** Contested-lock read failure: nothing was cleaned up; the lock stays exactly where it is. */
|
|
647
656
|
const reportReadFailure = (error) => {
|
|
@@ -659,17 +668,20 @@ export async function acquireRunWriterLease(runDirectory, onCleanupFailure) {
|
|
|
659
668
|
throw error;
|
|
660
669
|
}
|
|
661
670
|
lastAutopsy = await autopsyWriterLock(lockPath);
|
|
662
|
-
if (lastAutopsy.verdict === "
|
|
671
|
+
if (lastAutopsy.verdict === "unknown" && lastAutopsy.reason === "unreadable") {
|
|
663
672
|
reportReadFailure(lastAutopsy.readFailure);
|
|
664
673
|
}
|
|
665
674
|
if (lastAutopsy.verdict === "alive") {
|
|
666
675
|
throw new RunWriterLeaseHeldError(`role run writer lease is already held by live pid ${lastAutopsy.pid} at ${lockPath}`);
|
|
667
676
|
}
|
|
668
|
-
if (lastAutopsy.verdict === "
|
|
669
|
-
throw new RunWriterLeaseHeldError(lastAutopsy.
|
|
677
|
+
if (lastAutopsy.verdict === "unknown") {
|
|
678
|
+
throw new RunWriterLeaseHeldError(lastAutopsy.reason === "unreadable"
|
|
670
679
|
? `role run writer lease lock is unreadable at ${lockPath}: ${describeErrorIdentity(lastAutopsy.readFailure)}; holder liveness unverifiable, lock left in place`
|
|
671
680
|
: `role run writer lease lock at ${lockPath} has no verifiable holder pid (empty or unparseable); holder liveness unverifiable, lock left in place`);
|
|
672
681
|
}
|
|
682
|
+
if (lastAutopsy.verdict === "absent") {
|
|
683
|
+
throw new RunWriterLeaseHeldError(`role run writer lease disappeared before holder autopsy at ${lockPath}`);
|
|
684
|
+
}
|
|
673
685
|
if (reclaimsLeft <= 0)
|
|
674
686
|
break;
|
|
675
687
|
let reclaimed = false;
|
|
@@ -694,8 +706,11 @@ export async function acquireRunWriterLease(runDirectory, onCleanupFailure) {
|
|
|
694
706
|
/**
|
|
695
707
|
* Locate a Role run directory by run ID under the ledger books home.
|
|
696
708
|
* Returns undefined when the ID is unknown.
|
|
709
|
+
* Walk surface = listBookRunDirectories (flat legacy + subject-tree).
|
|
710
|
+
* Collects every match under the supplied book/role filters: zero → undefined,
|
|
711
|
+
* one → path, many → loud ambiguity (never readdir-first).
|
|
697
712
|
*/
|
|
698
|
-
export async function findRunDirectoryById(home, runId) {
|
|
713
|
+
export async function findRunDirectoryById(home, runId, onlyBookKey, onlyRole) {
|
|
699
714
|
if (runId.trim() === "")
|
|
700
715
|
return undefined;
|
|
701
716
|
const ledgerHome = resolveActivationLedgerHome(home);
|
|
@@ -704,25 +719,39 @@ export async function findRunDirectoryById(home, runId) {
|
|
|
704
719
|
try {
|
|
705
720
|
bookKeys = await readdir(booksRoot);
|
|
706
721
|
}
|
|
707
|
-
catch {
|
|
708
|
-
|
|
722
|
+
catch (error) {
|
|
723
|
+
// Only a missing books root is "unknown id"; permission/IO damage propagates.
|
|
724
|
+
if (errorCodeOf(error) === "ENOENT")
|
|
725
|
+
return undefined;
|
|
726
|
+
throw error;
|
|
709
727
|
}
|
|
728
|
+
const matches = [];
|
|
710
729
|
for (const bookKey of bookKeys) {
|
|
711
|
-
|
|
712
|
-
|
|
730
|
+
if (onlyBookKey !== undefined && bookKey !== onlyBookKey)
|
|
731
|
+
continue;
|
|
732
|
+
const bookDir = activationBookDirectory(ledgerHome, bookKey);
|
|
733
|
+
let runDirectories;
|
|
713
734
|
try {
|
|
714
|
-
|
|
735
|
+
runDirectories = await listBookRunDirectories(bookDir);
|
|
715
736
|
}
|
|
716
|
-
catch {
|
|
717
|
-
|
|
737
|
+
catch (error) {
|
|
738
|
+
if (errorCodeOf(error) === "ENOENT")
|
|
739
|
+
continue;
|
|
740
|
+
throw error;
|
|
718
741
|
}
|
|
719
|
-
for (const
|
|
720
|
-
|
|
721
|
-
|
|
742
|
+
for (const runDirectory of runDirectories) {
|
|
743
|
+
const entry = basename(runDirectory);
|
|
744
|
+
if ((onlyRole === undefined && (entry === `${runId}@judge` || entry.startsWith(`${runId}@`))) ||
|
|
745
|
+
entry === `${runId}@${onlyRole}`) {
|
|
746
|
+
matches.push(runDirectory);
|
|
722
747
|
}
|
|
723
748
|
}
|
|
724
749
|
}
|
|
725
|
-
|
|
750
|
+
if (matches.length === 0)
|
|
751
|
+
return undefined;
|
|
752
|
+
if (matches.length === 1)
|
|
753
|
+
return matches[0];
|
|
754
|
+
throw new Error(`ambiguous role run id ${runId}: ${matches.join(", ")}`);
|
|
726
755
|
}
|
|
727
756
|
/** Code-owned gate inspector summons prefix (public-role-summons / #747). */
|
|
728
757
|
export const GATE_DOSSIER_POINTER_PREFIX = "卷宗指针:";
|
|
@@ -760,19 +789,49 @@ export async function readRunParentPath(runDirectory) {
|
|
|
760
789
|
}
|
|
761
790
|
return undefined;
|
|
762
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* True when durable run-state names a session principal that has actually formed
|
|
794
|
+
* (session file present as a real file). Provisional runs relocated+terminalized
|
|
795
|
+
* before dispatch never form one — same-ticket lookup must skip them so a newer
|
|
796
|
+
* abandoned mint cannot eclipse an older resumable principal (#859).
|
|
797
|
+
* Not a terminal-state gate: ADR/#416 allows terminal resume when principal exists.
|
|
798
|
+
*/
|
|
799
|
+
async function runHasFormedSessionPrincipal(runDirectory) {
|
|
800
|
+
const disk = await readRoleRunStateDisk(runDirectory);
|
|
801
|
+
if (disk === undefined)
|
|
802
|
+
return false;
|
|
803
|
+
const sessionFile = typeof disk.principalWire.sessionFile === "string" &&
|
|
804
|
+
disk.principalWire.sessionFile.trim() !== ""
|
|
805
|
+
? disk.principalWire.sessionFile
|
|
806
|
+
: join(disk.principalWire.sessionDirectory, "session.jsonl");
|
|
807
|
+
try {
|
|
808
|
+
const stat = await lstat(sessionFile);
|
|
809
|
+
return stat.isFile() && !stat.isSymbolicLink();
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
// Absent session file → not formed; permission/IO/damage keeps identity.
|
|
813
|
+
if (errorCodeOf(error) === "ENOENT")
|
|
814
|
+
return false;
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
763
818
|
/**
|
|
764
819
|
* Locate the latest retained run for one seat under a book (#637 / #747).
|
|
765
|
-
* Same walk surface as findRunDirectoryById. Match by
|
|
766
|
-
* seats, #747) or by ticket number (countersign /
|
|
767
|
-
* runId is UUIDv7 — lexicographic max is latest
|
|
768
|
-
*
|
|
820
|
+
* Same walk surface as findRunDirectoryById (listBookRunDirectories). Match by
|
|
821
|
+
* parent run path (officer seats, #747) or by ticket number (countersign /
|
|
822
|
+
* diarist principal). runId is UUIDv7 — lexicographic max is latest among runs
|
|
823
|
+
* that formed a session principal. No parallel index.
|
|
824
|
+
* Only a truly missing book directory means no history; damage/permission errors propagate.
|
|
769
825
|
*/
|
|
770
826
|
export async function findLatestRunIdForSeatTicket(input) {
|
|
827
|
+
if (input.ticketNumber === undefined && input.parentRunPath === undefined) {
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
771
830
|
const ledgerHome = resolveActivationLedgerHome(input.home);
|
|
772
|
-
const
|
|
773
|
-
let
|
|
831
|
+
const bookDir = activationBookDirectory(ledgerHome, input.bookKey);
|
|
832
|
+
let runDirectories;
|
|
774
833
|
try {
|
|
775
|
-
|
|
834
|
+
runDirectories = await listBookRunDirectories(bookDir);
|
|
776
835
|
}
|
|
777
836
|
catch (error) {
|
|
778
837
|
if (errorCodeOf(error) === "ENOENT")
|
|
@@ -781,39 +840,38 @@ export async function findLatestRunIdForSeatTicket(input) {
|
|
|
781
840
|
}
|
|
782
841
|
const suffix = `@${input.role}`;
|
|
783
842
|
let best;
|
|
784
|
-
for (const
|
|
843
|
+
for (const runDirectory of runDirectories) {
|
|
844
|
+
const entry = basename(runDirectory);
|
|
785
845
|
if (!entry.endsWith(suffix))
|
|
786
846
|
continue;
|
|
787
847
|
const runId = entry.slice(0, entry.length - suffix.length);
|
|
788
848
|
if (runId.length === 0)
|
|
789
849
|
continue;
|
|
790
|
-
const runDirectory = join(runsDir, entry);
|
|
791
850
|
if (input.parentRunPath !== undefined) {
|
|
792
851
|
const parentPath = await readRunParentPath(runDirectory);
|
|
793
852
|
if (parentPath !== input.parentRunPath)
|
|
794
853
|
continue;
|
|
795
854
|
}
|
|
796
855
|
else if (input.ticketNumber !== undefined) {
|
|
797
|
-
|
|
856
|
+
// Same-ticket resume is board identity only — never migration-derived.
|
|
857
|
+
const ticketNumber = await readBoardTicketNumber(runDirectory);
|
|
798
858
|
if (ticketNumber !== input.ticketNumber)
|
|
799
859
|
continue;
|
|
800
860
|
}
|
|
801
|
-
|
|
861
|
+
// Durable fact: never resume-select a provisional that never formed principal.
|
|
862
|
+
if (!(await runHasFormedSessionPrincipal(runDirectory)))
|
|
802
863
|
continue;
|
|
803
|
-
}
|
|
804
864
|
if (best === undefined || runId > best)
|
|
805
865
|
best = runId;
|
|
806
866
|
}
|
|
807
867
|
return best;
|
|
808
868
|
}
|
|
809
|
-
/** Restore optional correlation + typed ticket identity from a durable
|
|
869
|
+
/** Restore optional correlation + typed main-ticket identity from a durable page. */
|
|
810
870
|
function parsePersistedTicketIdentity(record) {
|
|
811
871
|
const correlationId = typeof record.correlationId === "string" && record.correlationId.trim() !== ""
|
|
812
872
|
? record.correlationId
|
|
813
873
|
: undefined;
|
|
814
|
-
const ticketNumber =
|
|
815
|
-
Number.isSafeInteger(record.ticketNumber) &&
|
|
816
|
-
record.ticketNumber >= 1
|
|
874
|
+
const ticketNumber = isSafePositiveTicketNumber(record.ticketNumber)
|
|
817
875
|
? record.ticketNumber
|
|
818
876
|
: undefined;
|
|
819
877
|
return {
|
|
@@ -821,10 +879,64 @@ function parsePersistedTicketIdentity(record) {
|
|
|
821
879
|
...(ticketNumber === undefined ? {} : { ticketNumber }),
|
|
822
880
|
};
|
|
823
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* #871 durable set from one run page.
|
|
884
|
+
* Absent → absent. Present damage keeps the original reason (no throw here).
|
|
885
|
+
*/
|
|
886
|
+
function readDurableCourtTicketNumbersPage(record, principalTicket, pageLabel) {
|
|
887
|
+
const interpreted = interpretDurableCourtTicketNumbers(record, {
|
|
888
|
+
...(principalTicket === undefined ? {} : { principalTicket }),
|
|
889
|
+
});
|
|
890
|
+
if (interpreted.kind === "damage") {
|
|
891
|
+
return {
|
|
892
|
+
kind: "damage",
|
|
893
|
+
reason: `role run ${pageLabel} courtTicketNumbers is damaged: ${interpreted.reason}`,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
if (interpreted.kind === "ok") {
|
|
897
|
+
return { kind: "ok", tickets: interpreted.tickets };
|
|
898
|
+
}
|
|
899
|
+
return { kind: "absent" };
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Merge admitted + invocation #871 sets.
|
|
903
|
+
* Both present must match; one present wins; both absent stays legacy.
|
|
904
|
+
* Damage / cross-page mismatch return a diagnostic — countersign resume settles
|
|
905
|
+
* controlled failure; load itself does not structural-reject.
|
|
906
|
+
*/
|
|
907
|
+
function mergeDurableCourtTicketNumbers(input) {
|
|
908
|
+
const fromAdmitted = input.admittedRecord === undefined
|
|
909
|
+
? { kind: "absent" }
|
|
910
|
+
: readDurableCourtTicketNumbersPage(input.admittedRecord, input.principalTicket, "admitted-request");
|
|
911
|
+
const fromInvocation = input.invocationRecord === undefined
|
|
912
|
+
? { kind: "absent" }
|
|
913
|
+
: readDurableCourtTicketNumbersPage(input.invocationRecord, input.principalTicket, "invocation");
|
|
914
|
+
if (fromAdmitted.kind === "damage")
|
|
915
|
+
return fromAdmitted;
|
|
916
|
+
if (fromInvocation.kind === "damage")
|
|
917
|
+
return fromInvocation;
|
|
918
|
+
if (fromAdmitted.kind === "ok" && fromInvocation.kind === "ok") {
|
|
919
|
+
if (!sameCourtTicketNumbers(fromAdmitted.tickets, fromInvocation.tickets)) {
|
|
920
|
+
return {
|
|
921
|
+
kind: "damage",
|
|
922
|
+
reason: "role run courtTicketNumbers differs between admitted-request and invocation",
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
return { kind: "ok", tickets: fromAdmitted.tickets };
|
|
926
|
+
}
|
|
927
|
+
if (fromAdmitted.kind === "ok")
|
|
928
|
+
return { kind: "ok", tickets: fromAdmitted.tickets };
|
|
929
|
+
if (fromInvocation.kind === "ok")
|
|
930
|
+
return { kind: "ok", tickets: fromInvocation.tickets };
|
|
931
|
+
return { kind: "ok" };
|
|
932
|
+
}
|
|
824
933
|
function restoredTicketFields(fields) {
|
|
825
934
|
return {
|
|
826
935
|
...(fields.correlationId === undefined ? {} : { correlationId: fields.correlationId }),
|
|
827
936
|
...(fields.ticketNumber === undefined ? {} : { ticketNumber: fields.ticketNumber }),
|
|
937
|
+
...(fields.courtTicketNumbers === undefined
|
|
938
|
+
? {}
|
|
939
|
+
: { courtTicketNumbers: fields.courtTicketNumbers }),
|
|
828
940
|
};
|
|
829
941
|
}
|
|
830
942
|
async function loadResumableRunRecord(home, runId, authority) {
|
|
@@ -862,6 +974,10 @@ async function loadResumableRunRecord(home, runId, authority) {
|
|
|
862
974
|
let derived;
|
|
863
975
|
let correlationId;
|
|
864
976
|
let ticketNumber;
|
|
977
|
+
let courtTicketNumbers;
|
|
978
|
+
let courtTicketNumbersDamage;
|
|
979
|
+
let admittedIdentityRecord;
|
|
980
|
+
let invocationIdentityRecord;
|
|
865
981
|
let prNumber;
|
|
866
982
|
let repository;
|
|
867
983
|
let repositoryDisplay;
|
|
@@ -996,6 +1112,7 @@ async function loadResumableRunRecord(home, runId, authority) {
|
|
|
996
1112
|
};
|
|
997
1113
|
}
|
|
998
1114
|
}
|
|
1115
|
+
admittedIdentityRecord = record;
|
|
999
1116
|
const fromAdmitted = parsePersistedTicketIdentity(record);
|
|
1000
1117
|
correlationId = fromAdmitted.correlationId;
|
|
1001
1118
|
ticketNumber = fromAdmitted.ticketNumber;
|
|
@@ -1014,6 +1131,7 @@ async function loadResumableRunRecord(home, runId, authority) {
|
|
|
1014
1131
|
typeof invocationRaw === "object" &&
|
|
1015
1132
|
!Array.isArray(invocationRaw)) {
|
|
1016
1133
|
const rec = invocationRaw;
|
|
1134
|
+
invocationIdentityRecord = rec;
|
|
1017
1135
|
if (typeof rec.provider === "string" && typeof rec.model === "string") {
|
|
1018
1136
|
model = {
|
|
1019
1137
|
provider: rec.provider,
|
|
@@ -1033,12 +1151,29 @@ async function loadResumableRunRecord(home, runId, authority) {
|
|
|
1033
1151
|
}
|
|
1034
1152
|
}
|
|
1035
1153
|
catch (error) {
|
|
1154
|
+
if (error instanceof CliUsageError) {
|
|
1155
|
+
throw error;
|
|
1156
|
+
}
|
|
1036
1157
|
if (!(error instanceof Error &&
|
|
1037
1158
|
"code" in error &&
|
|
1038
1159
|
error.code === "ENOENT")) {
|
|
1039
1160
|
throw new CliUsageError(`role run invocation identity is unreadable: ${runId}`, { cause: error });
|
|
1040
1161
|
}
|
|
1041
1162
|
}
|
|
1163
|
+
// #871: resolve set after main ticket so principal membership can be checked.
|
|
1164
|
+
// Present-but-damaged / cross-page mismatch keep diagnostic for countersign
|
|
1165
|
+
// controlled-failure; both-absent stays legacy; single lawful page restores.
|
|
1166
|
+
const mergedSet = mergeDurableCourtTicketNumbers({
|
|
1167
|
+
admittedRecord: admittedIdentityRecord,
|
|
1168
|
+
invocationRecord: invocationIdentityRecord,
|
|
1169
|
+
principalTicket: ticketNumber,
|
|
1170
|
+
});
|
|
1171
|
+
if (mergedSet.kind === "damage") {
|
|
1172
|
+
courtTicketNumbersDamage = mergedSet.reason;
|
|
1173
|
+
}
|
|
1174
|
+
else if (mergedSet.tickets !== undefined) {
|
|
1175
|
+
courtTicketNumbers = mergedSet.tickets;
|
|
1176
|
+
}
|
|
1042
1177
|
return {
|
|
1043
1178
|
run,
|
|
1044
1179
|
principal,
|
|
@@ -1058,6 +1193,10 @@ async function loadResumableRunRecord(home, runId, authority) {
|
|
|
1058
1193
|
...(derived === undefined ? {} : { derived }),
|
|
1059
1194
|
...(correlationId === undefined ? {} : { correlationId }),
|
|
1060
1195
|
...(ticketNumber === undefined ? {} : { ticketNumber }),
|
|
1196
|
+
...(courtTicketNumbers === undefined ? {} : { courtTicketNumbers }),
|
|
1197
|
+
...(courtTicketNumbersDamage === undefined
|
|
1198
|
+
? {}
|
|
1199
|
+
: { courtTicketNumbersDamage }),
|
|
1061
1200
|
...(prNumber === undefined ? {} : { prNumber }),
|
|
1062
1201
|
...(repository === undefined ? {} : { repository }),
|
|
1063
1202
|
...(repositoryDisplay === undefined ? {} : { repositoryDisplay }),
|
|
@@ -1213,9 +1352,18 @@ export async function loadResumableCountersignRun(home, runId, authority) {
|
|
|
1213
1352
|
if (loaded.run.role !== "countersign") {
|
|
1214
1353
|
throw new CliUsageError(`role run ${runId} belongs to ${loaded.run.role}, not countersign`);
|
|
1215
1354
|
}
|
|
1355
|
+
const base = resumedBaseAdmitted(loaded);
|
|
1216
1356
|
const admitted = {
|
|
1217
1357
|
role: "countersign",
|
|
1218
|
-
...
|
|
1358
|
+
...base,
|
|
1359
|
+
...(base.courtTicketNumbers === undefined
|
|
1360
|
+
? {}
|
|
1361
|
+
: { courtTicketNumbers: base.courtTicketNumbers }),
|
|
1362
|
+
...(loaded.admittedFields.courtTicketNumbersDamage === undefined
|
|
1363
|
+
? {}
|
|
1364
|
+
: {
|
|
1365
|
+
courtTicketNumbersDamage: loaded.admittedFields.courtTicketNumbersDamage,
|
|
1366
|
+
}),
|
|
1219
1367
|
};
|
|
1220
1368
|
return seatLoadedResult(loaded, admitted);
|
|
1221
1369
|
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared ticket identity seam for public court seats (#635 / #637 / #709 / #747 / #771).
|
|
3
3
|
*
|
|
4
|
-
* ADR 0075 `diarist-resolves-ticket-llm-layer`: the
|
|
5
|
-
* target
|
|
6
|
-
*
|
|
7
|
-
* judges which ticket the summons is about — no prose harvest, no first-#N
|
|
8
|
-
* position pick, no matching book-known numbers against instruction text
|
|
4
|
+
* ADR 0075 `diarist-resolves-ticket-llm-layer`: the diarist LLM recognizes the
|
|
5
|
+
* court target; recognized ticket → provenance, truly unbound → no provenance,
|
|
6
|
+
* or escalate when it cannot recognize one. Code does not re-judge the ticket
|
|
9
7
|
* (锚定宪法; owner 2026-09-08: 代码不准做判断). Other seats reuse a typed
|
|
10
8
|
* identity already handed over (起居郎 assertion / source-run / already-bound
|
|
11
9
|
* resume) — they do not re-recognize from instruction. No CLI --ticket, no
|
|
12
10
|
* attachment frontmatter. #747: officer same-parent resume also lives here.
|
|
13
11
|
*
|
|
14
|
-
* Mechanical verify helpers live on diarist.ts / invocation.ts (sole owners).
|
|
15
12
|
* This module owns same-ticket / same-parent resume only.
|
|
16
13
|
*/
|
|
17
14
|
import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
|