@akagilnc/pi-workflow-roles 0.1.5483 → 0.1.5571
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +6312 -6376
- package/dist/analyst-gate-cycles-read.js +28 -78
- package/dist/analyst-ledger.js +26 -42
- package/dist/analyst-metric-families/acceptance-success-rework.js +25 -1
- package/dist/analyst-metric-families/round-timeline.js +2 -5
- package/dist/archivist-record-entry.js +4 -3
- package/dist/archivist-record-pointer.js +71 -36
- package/dist/atomic-write.js +2 -2
- package/dist/auditor-dossier-tool.js +11 -6
- package/dist/auditor-soul.js +5 -12
- package/dist/collector-config.js +6 -5
- package/dist/doctor-evidence.js +8 -4
- package/dist/engine-detour-usage.js +8 -22
- package/dist/headless-host/production-host.js +5969 -6011
- package/dist/host-descriptions.js +0 -4
- package/dist/host-session-record.js +266 -267
- package/dist/ledger-session-read.js +5 -0
- package/dist/migrate-book-topology.js +1369 -722
- package/dist/navigator-public-session.js +13 -17
- package/dist/notary-source-run.js +8 -16
- package/dist/packaged-role-registry.js +5 -56
- package/dist/pi/durable-principal.js +2 -3
- package/dist/pi/role-turn-host.js +79 -15
- package/dist/public-cli/auto-resume.js +8 -47
- package/dist/public-cli/instruction-seat-run.js +9 -7
- package/dist/public-cli/invocation.js +67 -100
- package/dist/public-cli/main.js +2689 -2532
- package/dist/public-cli/post-admission.js +96 -55
- package/dist/public-cli/run-lifecycle.js +44 -100
- package/dist/public-cli/settlement.js +184 -429
- package/dist/public-cli/terminal.js +0 -12
- package/dist/role-run-placement.js +53 -16
- package/dist/role-run-relocation.js +111 -52
- package/dist/run-dossier-files.js +30 -0
- package/dist/run-dossier.js +322 -0
- package/dist/run-terminal-artifacts.js +69 -202
- package/dist/run-ticket-number.js +49 -7
- package/dist/session-assistant-usage.js +9 -18
- package/dist/session-identity.js +49 -19
- package/dist/sitian-appender.js +29 -18
- package/dist/sitian-facade.js +11 -0
- package/dist/sitian-reader.js +44 -29
- package/dist/submission-gate.js +1 -1
- package/dist/submission-ledger.js +4 -4
- package/dist/ticket-provenance-contracts.js +2 -8
- package/dist/ticket-provenance.js +25 -9
- package/package.json +1 -1
- package/scripts/build-package.mjs +2 -0
- package/src/acp-host/production-host.ts +1 -1
- package/src/acp-host/role-turn-host.ts +11 -4
- package/src/analyst-gate-cycles-read.ts +31 -88
- package/src/analyst-ledger.ts +36 -51
- package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
- package/src/analyst-metric-families/gate-cycles.ts +1 -1
- package/src/analyst-metric-families/round-timeline.ts +2 -5
- package/src/analyst-page.ts +3 -3
- package/src/archivist-record-entry.ts +6 -5
- package/src/archivist-record-pointer.ts +92 -42
- package/src/atomic-write.ts +3 -1
- package/src/auditor-dossier-tool.ts +14 -8
- package/src/auditor-soul.ts +5 -10
- package/src/book-topology-migration-placement.ts +16 -3
- package/src/book-topology-record-class-migrators.ts +4 -2
- package/src/book-topology-runs-migrator.ts +6 -3
- package/src/collector-config.ts +5 -3
- package/src/doctor-contracts.ts +1 -1
- package/src/doctor-evidence.ts +7 -3
- package/src/engine-detour-tool.ts +1 -1
- package/src/engine-detour-usage.ts +12 -25
- package/src/external-host-turn-loop.ts +25 -0
- package/src/factory-board.ts +16 -81
- package/src/headless-host/production-host.ts +1 -4
- package/src/headless-host/role-turn-host.ts +56 -21
- package/src/host-descriptions.ts +0 -6
- package/src/host-session-record.ts +224 -140
- package/src/ledger-session-read.ts +6 -0
- package/src/navigator-public-session.ts +13 -18
- package/src/navigator-work-context.ts +3 -1
- package/src/notary-role.ts +2 -1
- package/src/notary-source-run.ts +8 -18
- package/src/packaged-role-registry.ts +1 -65
- package/src/pi/adapter.ts +50 -7
- package/src/pi/durable-principal.ts +2 -4
- package/src/pi/role-turn-host.ts +85 -14
- package/src/public-cli/auto-resume.ts +7 -43
- package/src/public-cli/instruction-seat-run.ts +13 -6
- package/src/public-cli/invocation.ts +90 -144
- package/src/public-cli/post-admission.ts +111 -72
- package/src/public-cli/run-lifecycle.ts +47 -115
- package/src/public-cli/settlement.ts +221 -589
- package/src/public-cli/terminal.ts +1 -61
- package/src/role-envelope.ts +4 -2
- package/src/role-run-placement.ts +65 -17
- package/src/role-run-relocation.ts +110 -70
- package/src/role-runtime.ts +46 -69
- package/src/run-dossier-files.ts +33 -0
- package/src/run-dossier.ts +357 -0
- package/src/run-terminal-artifacts.ts +84 -240
- package/src/run-ticket-number.ts +82 -14
- package/src/session-assistant-usage.ts +8 -20
- package/src/session-identity.ts +52 -21
- package/src/sitian-appender.ts +33 -26
- package/src/sitian-facade.ts +22 -0
- package/src/sitian-reader.ts +62 -32
- package/src/submission-gate.ts +1 -1
- package/src/submission-ledger.ts +4 -4
- package/src/ticket-provenance-contracts.ts +3 -6
- package/src/ticket-provenance.ts +23 -11
- package/src/ticket-snapshot.ts +3 -2
- package/src/ticket-trajectory.ts +95 -109
|
@@ -6,54 +6,28 @@
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
9
|
-
import {
|
|
10
|
-
import { readAnalystGateCyclesFromAuditorRoles, } from "../analyst-gate-cycles-read.js";
|
|
9
|
+
import { sessionFileOf } from "../role-run-placement.js";
|
|
11
10
|
import { sitianReport } from "../sitian-facade.js";
|
|
11
|
+
import { ATTEMPT_HISTORY_IDENTITY_FIELD, TERMINAL_COURT_ATTEMPT_FIELD, writeRunTerminal, } from "../run-terminal-artifacts.js";
|
|
12
12
|
import { readAttemptScopedSubmissionRows, readRecordedSubmissionRows, readRecordedSubmissions, } from "../submission-ledger.js";
|
|
13
13
|
import { CONTROLLED_FAILURE_CAUSES } from "../host-contracts.js";
|
|
14
14
|
import { childSignalDeathDiagnostic, processCancelDiagnostic, } from "./process-cancel.js";
|
|
15
|
+
import { readStrictPiSessionJsonl } from "../ledger-session-read.js";
|
|
15
16
|
import { serializeThrownValue } from "../serialize-thrown-value.js";
|
|
16
17
|
import { describeErrorIdentity } from "./run-lifecycle.js";
|
|
17
18
|
import { readEngineDetourToolUsage, readInvocationEngineMounted, runDirectoryFromSessionDirectory, sessionFileFromSessionDirectory, withEngineDetourToolUsageFact, } from "../engine-detour-usage.js";
|
|
18
19
|
import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "../dossier-resolution.js";
|
|
19
|
-
import {
|
|
20
|
+
import { homeFromRunDirectory, } from "./invocation.js";
|
|
20
21
|
import { SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY, SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, } from "../secretariat-contracts.js";
|
|
21
22
|
import { findLatestDurablePackagedRoleTerminal, NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY, } from "../navigator-invocation-identity.js";
|
|
22
|
-
import { packagedDurableOfficerEntry, packagedRoleAcceptedOutputTool, packagedRoleMetadata,
|
|
23
|
+
import { packagedDurableOfficerEntry, packagedRoleAcceptedOutputTool, packagedRoleMetadata, } from "../packaged-role-registry.js";
|
|
23
24
|
import { NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, noReceiptLifecycleFacts, parseNoReceiptLifecycleFacts, priorReceiptContinuation, receiptAttemptPointer, } from "../receipt-delivery-policy.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
25
|
+
import { readStateRowsSync, runCurrentPath } from "../run-dossier.js";
|
|
26
|
+
import { reportRunRecord } from "../sitian-facade.js";
|
|
26
27
|
/** Ledger reads use the run's machine home — not ambient process HOME (child write vs parent settle). */
|
|
27
28
|
function sealedLedgerHome(admitted) {
|
|
28
29
|
return homeFromRunDirectory(admitted.runDirectory);
|
|
29
30
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Hardened artifacts directory. A planted symlink at the run directory or the
|
|
32
|
-
* artifacts path must not receive a durable run artifact.
|
|
33
|
-
*/
|
|
34
|
-
export async function ensureRealArtifactsDirectory(runDirectory) {
|
|
35
|
-
const runStat = await lstat(runDirectory);
|
|
36
|
-
if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
|
|
37
|
-
throw new Error("run artifact retention: run directory is not a real directory");
|
|
38
|
-
}
|
|
39
|
-
const artifactsDir = roleRunArtifactsDirectory(runDirectory);
|
|
40
|
-
try {
|
|
41
|
-
const existing = await lstat(artifactsDir);
|
|
42
|
-
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
43
|
-
throw new Error("run artifact retention: artifacts path is not a real directory");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
if (!isEnoent(error))
|
|
48
|
-
throw error;
|
|
49
|
-
await mkdir(artifactsDir, { recursive: true });
|
|
50
|
-
const created = await lstat(artifactsDir);
|
|
51
|
-
if (created.isSymbolicLink() || !created.isDirectory()) {
|
|
52
|
-
throw new Error("run artifact retention: artifacts directory is not a real directory");
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return artifactsDir;
|
|
56
|
-
}
|
|
57
31
|
/**
|
|
58
32
|
* One retention path for a package fault beside an already chosen terminal.
|
|
59
33
|
* Session append is first when the caller has that channel. The artifact file
|
|
@@ -67,13 +41,14 @@ export async function retainPackageFault(input) {
|
|
|
67
41
|
...(Object.hasOwn(input, "error") ? { failure: projectThrownFailureLeaf(input.error) } : {}),
|
|
68
42
|
};
|
|
69
43
|
let retentionFailure;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
44
|
+
// The run's log is the fallback after the session append throws, and the only
|
|
45
|
+
// channel when the caller has none.
|
|
46
|
+
const writeLog = (appendFailure) => {
|
|
47
|
+
reportRunRecord(input.runDirectory, "post-admission-diagnostic", {
|
|
73
48
|
version: 1,
|
|
74
49
|
...payload,
|
|
75
50
|
...(appendFailure === undefined ? {} : { retentionFailure: appendFailure }),
|
|
76
|
-
});
|
|
51
|
+
}, "settlement");
|
|
77
52
|
};
|
|
78
53
|
if (input.appendSession !== undefined) {
|
|
79
54
|
try {
|
|
@@ -83,21 +58,21 @@ export async function retainPackageFault(input) {
|
|
|
83
58
|
retentionFailure =
|
|
84
59
|
`post-dispatch diagnostic session append failed (best-effort continue): dossier=${describeErrorIdentity(appendError)}`;
|
|
85
60
|
try {
|
|
86
|
-
|
|
61
|
+
writeLog(projectThrownFailureLeaf(appendError));
|
|
87
62
|
}
|
|
88
|
-
catch (
|
|
63
|
+
catch (logError) {
|
|
89
64
|
retentionFailure =
|
|
90
|
-
`post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)};
|
|
65
|
+
`post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; log=${describeErrorIdentity(logError)}`;
|
|
91
66
|
}
|
|
92
67
|
}
|
|
93
68
|
}
|
|
94
69
|
else {
|
|
95
70
|
try {
|
|
96
|
-
|
|
71
|
+
writeLog();
|
|
97
72
|
}
|
|
98
|
-
catch (
|
|
73
|
+
catch (logError) {
|
|
99
74
|
retentionFailure =
|
|
100
|
-
`post-dispatch diagnostic durable retention failed (best-effort continue):
|
|
75
|
+
`post-dispatch diagnostic durable retention failed (best-effort continue): log=${describeErrorIdentity(logError)}`;
|
|
101
76
|
}
|
|
102
77
|
}
|
|
103
78
|
const stderr = input.stderr ?? ((text) => {
|
|
@@ -123,7 +98,7 @@ export async function noteSettlementFault(runDirectory, scope, diagnostic) {
|
|
|
123
98
|
export function ledgerReadScope(admitted, scope) {
|
|
124
99
|
return {
|
|
125
100
|
home: sealedLedgerHome(admitted),
|
|
126
|
-
sessionParent:
|
|
101
|
+
sessionParent: sessionFileOf(admitted.runDirectory),
|
|
127
102
|
...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
|
|
128
103
|
? {}
|
|
129
104
|
: { attemptId: scope.courtAttemptId }),
|
|
@@ -292,17 +267,30 @@ async function settleNoReceiptTerminal(admitted, authority, scope, facts) {
|
|
|
292
267
|
kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt",
|
|
293
268
|
...facts, decisiveFacts: facts,
|
|
294
269
|
};
|
|
270
|
+
let attemptHistoryIdentity;
|
|
295
271
|
if (scope?.recordAttemptHistory === true) {
|
|
296
|
-
await appendRunAttemptHistory(
|
|
272
|
+
const pointer = await appendRunAttemptHistory(attemptHistorySource(admitted, coordinates), roleOutcome);
|
|
273
|
+
attemptHistoryIdentity = pointer.identity;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
attemptHistoryIdentity = await belongingAttemptHistoryIdentity(admitted.runDirectory, scope);
|
|
277
|
+
}
|
|
278
|
+
if (scope?.previewOnly !== true) {
|
|
279
|
+
// Belonging settlement only (#1161 甲 / R2): never inherit another court's
|
|
280
|
+
// latest terminal pointer, and never invent a history row on re-projection (N1).
|
|
281
|
+
writeRunTerminal(admitted.runDirectory, "no_receipt", {
|
|
282
|
+
role: admitted.role,
|
|
283
|
+
runId: admitted.runId,
|
|
284
|
+
outcome: roleOutcome,
|
|
285
|
+
...terminalBelongingFields(attemptHistoryIdentity, scope),
|
|
286
|
+
});
|
|
297
287
|
}
|
|
298
|
-
|
|
299
|
-
await clearOppositeTerminalArtifactFace(admitted.runDirectory);
|
|
300
|
-
return withOptionalGateProjection({
|
|
288
|
+
return attachEngineDetourToolUsage({
|
|
301
289
|
roleOutcome,
|
|
302
290
|
navigator: await extractNavigatorFactFromAdmittedSession(coordinates.sessionFile, admitted.runDirectory, scope),
|
|
303
291
|
artifacts: [],
|
|
304
292
|
runId: admitted.runId,
|
|
305
|
-
}, coordinates.sessionDirectory,
|
|
293
|
+
}, coordinates.sessionDirectory, detourUsageContext(admitted, scope));
|
|
306
294
|
}
|
|
307
295
|
/** Transitional host-session reads remain only for non-sealed failure and audit evidence. */
|
|
308
296
|
function coordinatesFromAdmitted(authority, admitted) {
|
|
@@ -638,12 +626,7 @@ export const NAVIGATOR_POST_ROLE_GRACE_MS = 10_000;
|
|
|
638
626
|
* terminal note it; they do not relabel it as a host session cause.
|
|
639
627
|
*/
|
|
640
628
|
export async function readBoundSessionEntries(sessionFile) {
|
|
641
|
-
|
|
642
|
-
const entries = [];
|
|
643
|
-
for (const line of text.trim().split("\n").filter(Boolean)) {
|
|
644
|
-
entries.push(JSON.parse(line));
|
|
645
|
-
}
|
|
646
|
-
return entries;
|
|
629
|
+
return await readStrictPiSessionJsonl(sessionFile);
|
|
647
630
|
}
|
|
648
631
|
/**
|
|
649
632
|
* #419 per-attempt process history. 史必追加,指针可覆盖;指针可以覆盖的前提是史已落。
|
|
@@ -652,9 +635,16 @@ export async function readBoundSessionEntries(sessionFile) {
|
|
|
652
635
|
* second ledger mechanism is introduced.
|
|
653
636
|
*/
|
|
654
637
|
export const ATTEMPT_HISTORY_ENTRY_TYPE = "ak_run_attempt_history";
|
|
638
|
+
function attemptHistorySource(admitted, coordinates) {
|
|
639
|
+
return {
|
|
640
|
+
role: admitted.role,
|
|
641
|
+
runId: admitted.runId,
|
|
642
|
+
sessionFile: coordinates.sessionFile,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
655
645
|
/** Append the complete attempt to the package ledger before overwriting pointer artifacts (#419). */
|
|
656
646
|
export async function appendRunAttemptHistory(source, outcome) {
|
|
657
|
-
sitianReport({
|
|
647
|
+
return sitianReport({
|
|
658
648
|
level: "event",
|
|
659
649
|
kind: "attempt-history",
|
|
660
650
|
subject: { runId: source.runId },
|
|
@@ -737,61 +727,6 @@ function parseNavigatorAttendanceDetails(details) {
|
|
|
737
727
|
reason: "Navigator attendance disposition is unparseable",
|
|
738
728
|
};
|
|
739
729
|
}
|
|
740
|
-
/**
|
|
741
|
-
* Project direct and historical paired gate rounds onto the public Terminal.
|
|
742
|
-
* actualSeats derive only from accepted receipts, never expected/missing seats.
|
|
743
|
-
*/
|
|
744
|
-
export function projectTerminalGateFact(rounds) {
|
|
745
|
-
if (rounds.length === 0)
|
|
746
|
-
return undefined;
|
|
747
|
-
const seen = new Set();
|
|
748
|
-
for (const round of rounds) {
|
|
749
|
-
if (round.origin.kind === "historical_dispatch")
|
|
750
|
-
seen.add("gatekeeper");
|
|
751
|
-
seen.add(round.officer);
|
|
752
|
-
}
|
|
753
|
-
const actualSeats = ["gatekeeper", "inspector", "notary"].filter((seat) => seen.has(seat));
|
|
754
|
-
return {
|
|
755
|
-
actualSeats,
|
|
756
|
-
rounds: rounds.map((round) => ({
|
|
757
|
-
roundIndex: round.roundIndex,
|
|
758
|
-
dispatch: round.origin.kind === "direct"
|
|
759
|
-
? { kind: "direct", officer: round.officer }
|
|
760
|
-
: {
|
|
761
|
-
kind: "historical_dispatch",
|
|
762
|
-
officer: round.officer,
|
|
763
|
-
...(round.origin.reason === undefined
|
|
764
|
-
? {}
|
|
765
|
-
: { reason: round.origin.reason }),
|
|
766
|
-
},
|
|
767
|
-
officer: {
|
|
768
|
-
seat: round.officer,
|
|
769
|
-
status: round.status,
|
|
770
|
-
findings: round.findings,
|
|
771
|
-
},
|
|
772
|
-
})),
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* Read gate facts from the run's session/auditor-roles nest (#446/#478).
|
|
777
|
-
* Missing directories → undefined (no-gate zero change).
|
|
778
|
-
* Damaged discovered volumes propagate — never wash to "no gate".
|
|
779
|
-
*/
|
|
780
|
-
export async function extractGateFactFromSessionDirectory(sessionDirectory, options = {}) {
|
|
781
|
-
const directories = [join(sessionDirectory, "auditor-roles")];
|
|
782
|
-
const parentSessionFile = options.parentSessionFile ?? join(sessionDirectory, "session.jsonl");
|
|
783
|
-
const rounds = await readAnalystGateCyclesFromAuditorRoles(directories, {
|
|
784
|
-
parentSessionFile,
|
|
785
|
-
});
|
|
786
|
-
return projectTerminalGateFact(rounds);
|
|
787
|
-
}
|
|
788
|
-
/**
|
|
789
|
-
* Attach optional gate projection onto a settled Terminal base.
|
|
790
|
-
* Shared by every settle path so auditor-roles is scanned once here only.
|
|
791
|
-
* `runId` is optional only for a batch projection that has no parent run.
|
|
792
|
-
* Gate read damage is a package note beside the host terminal. It does not
|
|
793
|
-
* replace that terminal and it does not omit the gate without a trace.
|
|
794
|
-
*/
|
|
795
730
|
/**
|
|
796
731
|
* #537: project this-invocation ak_engine_detour usage onto decisiveFacts.
|
|
797
732
|
* Absent when engine is not mounted; callCount 0 when mounted with zero calls.
|
|
@@ -800,27 +735,27 @@ export async function extractGateFactFromSessionDirectory(sessionDirectory, opti
|
|
|
800
735
|
* courtAttemptId and never Pi session.jsonl toolResult join. Session damage
|
|
801
736
|
* therefore cannot replace an already-formed roleOutcome (no_receipt / failure).
|
|
802
737
|
*/
|
|
803
|
-
async function attachEngineDetourToolUsage(base, sessionDirectory,
|
|
804
|
-
const runDirectory = typeof
|
|
805
|
-
?
|
|
738
|
+
async function attachEngineDetourToolUsage(base, sessionDirectory, detourContext = {}) {
|
|
739
|
+
const runDirectory = typeof detourContext.runDirectory === "string" && detourContext.runDirectory.length > 0
|
|
740
|
+
? detourContext.runDirectory
|
|
806
741
|
: runDirectoryFromSessionDirectory(sessionDirectory);
|
|
807
742
|
let engineMounted = false;
|
|
808
743
|
try {
|
|
809
744
|
engineMounted = await readInvocationEngineMounted(runDirectory);
|
|
810
745
|
}
|
|
811
746
|
catch (error) {
|
|
812
|
-
await noteSettlementFault(runDirectory,
|
|
747
|
+
await noteSettlementFault(runDirectory, detourContext.notePackageFault === undefined
|
|
813
748
|
? undefined
|
|
814
|
-
: { notePackageFault:
|
|
749
|
+
: { notePackageFault: detourContext.notePackageFault }, `engine mount read failed beside host terminal: ${describeErrorIdentity(error)}`);
|
|
815
750
|
return base;
|
|
816
751
|
}
|
|
817
752
|
if (!engineMounted)
|
|
818
753
|
return base;
|
|
819
754
|
// Public-invocation scope from the shared Host envelope (settlement scope), never
|
|
820
755
|
// courtAttemptId and never a detour sidecar file.
|
|
821
|
-
const invocationScopeId = typeof
|
|
822
|
-
|
|
823
|
-
?
|
|
756
|
+
const invocationScopeId = typeof detourContext.invocationScopeId === "string" &&
|
|
757
|
+
detourContext.invocationScopeId.length > 0
|
|
758
|
+
? detourContext.invocationScopeId
|
|
824
759
|
: undefined;
|
|
825
760
|
const sessionFile = sessionFileFromSessionDirectory(sessionDirectory);
|
|
826
761
|
let usage;
|
|
@@ -833,9 +768,9 @@ async function attachEngineDetourToolUsage(base, sessionDirectory, gateContext =
|
|
|
833
768
|
});
|
|
834
769
|
}
|
|
835
770
|
catch (error) {
|
|
836
|
-
await noteSettlementFault(runDirectory,
|
|
771
|
+
await noteSettlementFault(runDirectory, detourContext.notePackageFault === undefined
|
|
837
772
|
? undefined
|
|
838
|
-
: { notePackageFault:
|
|
773
|
+
: { notePackageFault: detourContext.notePackageFault }, `engine detour read failed beside host terminal: ${describeErrorIdentity(error)}`);
|
|
839
774
|
return base;
|
|
840
775
|
}
|
|
841
776
|
return {
|
|
@@ -843,45 +778,16 @@ async function attachEngineDetourToolUsage(base, sessionDirectory, gateContext =
|
|
|
843
778
|
roleOutcome: withEngineDetourToolUsageFact(base.roleOutcome, usage),
|
|
844
779
|
};
|
|
845
780
|
}
|
|
846
|
-
/**
|
|
847
|
-
function
|
|
781
|
+
/** Engine-detour usage context from admitted run + settlement scope. */
|
|
782
|
+
function detourUsageContext(admitted, scope) {
|
|
848
783
|
return {
|
|
849
784
|
runDirectory: admitted.runDirectory,
|
|
850
|
-
...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
|
|
851
|
-
? {}
|
|
852
|
-
: { courtAttemptId: scope.courtAttemptId }),
|
|
853
785
|
...(scope?.invocationScopeId === undefined || scope.invocationScopeId.length === 0
|
|
854
786
|
? {}
|
|
855
787
|
: { invocationScopeId: scope.invocationScopeId }),
|
|
856
788
|
...(scope?.notePackageFault === undefined ? {} : { notePackageFault: scope.notePackageFault }),
|
|
857
789
|
};
|
|
858
790
|
}
|
|
859
|
-
async function withOptionalGateProjection(base, sessionDirectory, gateContext = {}) {
|
|
860
|
-
// A gate transport failure is already represented by typed evidence and has no
|
|
861
|
-
// accepted gate cycle to project. Re-reading that rejected receipt as an
|
|
862
|
-
// accepted cycle would replace the original failure with a projection error.
|
|
863
|
-
const secondaryEvidence = base.roleOutcome.kind === "failure"
|
|
864
|
-
? base.roleOutcome.decisiveFacts.secondaryEvidence
|
|
865
|
-
: undefined;
|
|
866
|
-
const skipGate = isRecord(secondaryEvidence)
|
|
867
|
-
&& secondaryEvidence.kind === "role_infrastructure_failure"
|
|
868
|
-
&& packagedSkipsGateOnInfrastructureStage(secondaryEvidence.stage);
|
|
869
|
-
let next = base;
|
|
870
|
-
if (!skipGate) {
|
|
871
|
-
try {
|
|
872
|
-
const gate = await extractGateFactFromSessionDirectory(sessionDirectory, gateContext);
|
|
873
|
-
if (gate !== undefined)
|
|
874
|
-
next = { ...base, gate };
|
|
875
|
-
}
|
|
876
|
-
catch (error) {
|
|
877
|
-
const runDirectory = gateContext.runDirectory ?? runDirectoryFromSessionDirectory(sessionDirectory);
|
|
878
|
-
await noteSettlementFault(runDirectory, gateContext.notePackageFault === undefined
|
|
879
|
-
? undefined
|
|
880
|
-
: { notePackageFault: gateContext.notePackageFault }, `gate read failed beside host terminal: ${describeErrorIdentity(error)}`);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
return attachEngineDetourToolUsage(next, sessionDirectory, gateContext);
|
|
884
|
-
}
|
|
885
791
|
function routePlaybookFailureMessage(entries) {
|
|
886
792
|
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
887
793
|
const entry = entries[i];
|
|
@@ -956,85 +862,92 @@ async function extractNavigatorFactFromAdmittedSession(sessionFile, runDirectory
|
|
|
956
862
|
}
|
|
957
863
|
}
|
|
958
864
|
/**
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
865
|
+
* Sole success-terminal publisher (#953): replace the leg's terminal in
|
|
866
|
+
* current.json. The report carries the outcome's verdict facts; the role's
|
|
867
|
+
* submitted payloads live in history.jsonl. The terminal names the
|
|
868
|
+
* attempt-history row that belongs to this settlement — from this settle's
|
|
869
|
+
* append when recording, otherwise that court's prior terminal row in
|
|
870
|
+
* state.jsonl (#1161 甲 / R2). Never content-match payloads, never inherit the
|
|
871
|
+
* leg's latest terminal, never invent a history row on re-projection (N1).
|
|
962
872
|
*/
|
|
963
|
-
async function
|
|
964
|
-
|
|
965
|
-
|
|
873
|
+
async function publishAcceptedTerminal(admitted, roleOutcome, coordinates, scope, report) {
|
|
874
|
+
// #419: a dispatched turn appends before rewriting last-write-wins views;
|
|
875
|
+
// a later projection may refresh views but must not invent another attempt.
|
|
876
|
+
let attemptHistoryIdentity;
|
|
877
|
+
if (scope?.recordAttemptHistory === true) {
|
|
878
|
+
const pointer = await appendRunAttemptHistory(attemptHistorySource(admitted, coordinates), roleOutcome);
|
|
879
|
+
attemptHistoryIdentity = pointer.identity;
|
|
966
880
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
// (artifacts-as-file mid-path) — same absent-face semantics as the reader.
|
|
970
|
-
// Other errno stay loud (失败诚实).
|
|
971
|
-
if (isMissingPathError(error))
|
|
972
|
-
return;
|
|
973
|
-
throw error;
|
|
881
|
+
else {
|
|
882
|
+
attemptHistoryIdentity = await belongingAttemptHistoryIdentity(admitted.runDirectory, scope);
|
|
974
883
|
}
|
|
884
|
+
writeRunTerminal(admitted.runDirectory, "report", {
|
|
885
|
+
...report,
|
|
886
|
+
...terminalBelongingFields(attemptHistoryIdentity, scope),
|
|
887
|
+
});
|
|
888
|
+
return [{ kind: "report", path: runCurrentPath(admitted.runDirectory) }];
|
|
975
889
|
}
|
|
976
|
-
/**
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
* remove recursively. Unique ownership is listSeamOwnedUniqueErrorFacePaths:
|
|
988
|
-
* parent runs/ faces clear only when body.runId binds; unparseable runId → none.
|
|
989
|
-
* Path absence (ENOENT/ENOTDIR) is silent; other delete failures stay loud.
|
|
990
|
-
*/
|
|
991
|
-
async function clearOppositeTerminalArtifactFace(runDirectory) {
|
|
992
|
-
const artifactsDir = roleRunArtifactsDirectory(runDirectory);
|
|
993
|
-
for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
|
|
994
|
-
await removeFaceIfPresent(join(artifactsDir, file));
|
|
995
|
-
}
|
|
996
|
-
// Fixed fallbacks + unique clear too; subsequent publish writes the new
|
|
997
|
-
// durable path after this clear (conventional first when writable).
|
|
998
|
-
for (const relative of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
|
|
999
|
-
await removeFaceIfPresent(join(runDirectory, relative));
|
|
1000
|
-
}
|
|
1001
|
-
for (const path of await listSeamOwnedUniqueErrorFacePaths(runDirectory)) {
|
|
1002
|
-
await removeFaceIfPresent(path);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
async function ensureTerminalArtifactFace(runDirectory) {
|
|
1006
|
-
const artifactsDir = await ensureRunArtifactsDir(runDirectory);
|
|
1007
|
-
await clearOppositeTerminalArtifactFace(runDirectory);
|
|
1008
|
-
return artifactsDir;
|
|
1009
|
-
}
|
|
1010
|
-
function acceptedArtifactAttachmentRefs(attachments) {
|
|
1011
|
-
return attachments.map((a) => ({
|
|
1012
|
-
provenancePath: a.provenancePath,
|
|
1013
|
-
frozenPath: a.frozenPath,
|
|
1014
|
-
sha256: a.sha256,
|
|
1015
|
-
byteLength: a.byteLength,
|
|
1016
|
-
}));
|
|
890
|
+
/** 甲 pointer + court tag carried on the terminal body for later re-projection. */
|
|
891
|
+
function terminalBelongingFields(attemptHistoryIdentity, scope) {
|
|
892
|
+
const courtAttemptId = scope?.courtAttemptId;
|
|
893
|
+
return {
|
|
894
|
+
...(attemptHistoryIdentity === undefined
|
|
895
|
+
? {}
|
|
896
|
+
: { [ATTEMPT_HISTORY_IDENTITY_FIELD]: attemptHistoryIdentity }),
|
|
897
|
+
...(courtAttemptId === undefined || courtAttemptId.length === 0
|
|
898
|
+
? {}
|
|
899
|
+
: { [TERMINAL_COURT_ATTEMPT_FIELD]: courtAttemptId }),
|
|
900
|
+
};
|
|
1017
901
|
}
|
|
1018
902
|
/**
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
903
|
+
* Belonging attempt-history identity for a non-recording re-projection (#1161 甲 / R2).
|
|
904
|
+
* Court-tagged settle recovers from that court's own prior terminal row in
|
|
905
|
+
* state.jsonl. Untagged normal rewrite (bare resume after a sealed face, no open
|
|
906
|
+
* court) keeps only the face being rewritten — the last terminal row's own
|
|
907
|
+
* identity. If that face has none (approved gap), return undefined: never walk
|
|
908
|
+
* older terminals for a non-empty pointer, never invent-append (N1), never
|
|
909
|
+
* subject.attemptId on attempt-history. Bare resume therefore cannot restore
|
|
910
|
+
* court B's pointer while the current face is A (or A's gap). Court-tagged
|
|
911
|
+
* lookup that finds no row still returns undefined — the approved gap.
|
|
1022
912
|
*/
|
|
1023
|
-
async function
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
913
|
+
async function belongingAttemptHistoryIdentity(runDirectory, scope) {
|
|
914
|
+
const courtAttemptId = scope?.courtAttemptId;
|
|
915
|
+
let rows;
|
|
916
|
+
try {
|
|
917
|
+
rows = readStateRowsSync(runDirectory);
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
// Documented: history/state failure domain does not block the terminal face
|
|
921
|
+
// (#1161 / main history disposition). True cause must leave a trace.
|
|
922
|
+
await noteSettlementFault(runDirectory, scope, `terminal state read failed beside terminal projection: ${describeErrorIdentity(error)}`);
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
if (courtAttemptId !== undefined && courtAttemptId.length > 0) {
|
|
926
|
+
let found;
|
|
927
|
+
for (const row of rows) {
|
|
928
|
+
if (row.kind !== "terminal" || !isRecord(row.payload))
|
|
929
|
+
continue;
|
|
930
|
+
const body = row.payload.body;
|
|
931
|
+
if (!isRecord(body) || body[TERMINAL_COURT_ATTEMPT_FIELD] !== courtAttemptId)
|
|
932
|
+
continue;
|
|
933
|
+
const identity = body[ATTEMPT_HISTORY_IDENTITY_FIELD];
|
|
934
|
+
if (typeof identity === "string" && identity.length > 0)
|
|
935
|
+
found = identity;
|
|
936
|
+
}
|
|
937
|
+
return found;
|
|
938
|
+
}
|
|
939
|
+
// Untagged: only the current face (last terminal). Gap face stays a gap.
|
|
940
|
+
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
941
|
+
const row = rows[index];
|
|
942
|
+
if (row.kind !== "terminal" || !isRecord(row.payload))
|
|
943
|
+
continue;
|
|
944
|
+
const body = row.payload.body;
|
|
945
|
+
if (!isRecord(body))
|
|
946
|
+
return undefined;
|
|
947
|
+
const identity = body[ATTEMPT_HISTORY_IDENTITY_FIELD];
|
|
948
|
+
return typeof identity === "string" && identity.length > 0 ? identity : undefined;
|
|
949
|
+
}
|
|
950
|
+
return undefined;
|
|
1038
951
|
}
|
|
1039
952
|
/**
|
|
1040
953
|
* Read session entries for lawful settlement.
|
|
@@ -1070,13 +983,13 @@ async function settleSealedSeat(admitted, authority, scope, options) {
|
|
|
1070
983
|
async function finishLawfulSeat(admitted, coordinates, entries, roleOutcome, scope) {
|
|
1071
984
|
const artifacts = scope?.previewOnly === true
|
|
1072
985
|
? []
|
|
1073
|
-
: await
|
|
1074
|
-
const terminal = await
|
|
986
|
+
: await publishDeclaredSeatTerminal(admitted, roleOutcome, coordinates, entries, scope);
|
|
987
|
+
const terminal = await attachEngineDetourToolUsage({
|
|
1075
988
|
roleOutcome,
|
|
1076
989
|
navigator: extractNavigatorFact(entries),
|
|
1077
990
|
artifacts,
|
|
1078
991
|
runId: admitted.runId,
|
|
1079
|
-
}, coordinates.sessionDirectory,
|
|
992
|
+
}, coordinates.sessionDirectory, detourUsageContext(admitted, scope));
|
|
1080
993
|
return attachRecordedSubmissions(admitted, terminal, scope);
|
|
1081
994
|
}
|
|
1082
995
|
/**
|
|
@@ -1118,8 +1031,8 @@ function extractDoctorCandidateFacts(entries) {
|
|
|
1118
1031
|
}
|
|
1119
1032
|
return {};
|
|
1120
1033
|
}
|
|
1121
|
-
const EMPTY_ARTIFACT_FACE = {
|
|
1122
|
-
/**
|
|
1034
|
+
const EMPTY_ARTIFACT_FACE = {};
|
|
1035
|
+
/** Terminal face for one seat. Absent means the report carries only the shared facts. */
|
|
1123
1036
|
function seatArtifactFace(role) {
|
|
1124
1037
|
const record = packagedRoleMetadata(role);
|
|
1125
1038
|
if (record !== undefined && "artifactFace" in record && record.artifactFace !== undefined) {
|
|
@@ -1127,30 +1040,6 @@ function seatArtifactFace(role) {
|
|
|
1127
1040
|
}
|
|
1128
1041
|
return EMPTY_ARTIFACT_FACE;
|
|
1129
1042
|
}
|
|
1130
|
-
function readAdmittedPath(source, path) {
|
|
1131
|
-
let current = source;
|
|
1132
|
-
for (const part of path.split(".")) {
|
|
1133
|
-
if (typeof current !== "object" || current === null)
|
|
1134
|
-
return undefined;
|
|
1135
|
-
current = current[part];
|
|
1136
|
-
}
|
|
1137
|
-
return current;
|
|
1138
|
-
}
|
|
1139
|
-
function evidenceLeaves(admitted, leaves) {
|
|
1140
|
-
const evidence = {};
|
|
1141
|
-
for (const leaf of leaves) {
|
|
1142
|
-
if (leaf.callerProvenance === true) {
|
|
1143
|
-
if (!admitted.instructionEmpty)
|
|
1144
|
-
evidence[leaf.key] = admitted.instruction;
|
|
1145
|
-
continue;
|
|
1146
|
-
}
|
|
1147
|
-
const value = readAdmittedPath(admitted, leaf.from ?? leaf.key);
|
|
1148
|
-
if (value === undefined && leaf.omitUndefined === true)
|
|
1149
|
-
continue;
|
|
1150
|
-
evidence[leaf.key] = leaf.copyArray === true && Array.isArray(value) ? [...value] : value;
|
|
1151
|
-
}
|
|
1152
|
-
return evidence;
|
|
1153
|
-
}
|
|
1154
1043
|
function doctorReportFacts(face, roleOutcome, entries) {
|
|
1155
1044
|
if (face.doctorReportFacts !== true || roleOutcome.kind === "audit_escalation")
|
|
1156
1045
|
return {};
|
|
@@ -1160,28 +1049,19 @@ function doctorReportFacts(face, roleOutcome, entries) {
|
|
|
1160
1049
|
...(facts.auditNoReceipt === undefined ? {} : { auditNoReceipt: facts.auditNoReceipt }),
|
|
1161
1050
|
};
|
|
1162
1051
|
}
|
|
1163
|
-
async function
|
|
1052
|
+
async function publishDeclaredSeatTerminal(admitted, roleOutcome, coordinates, entries, scope) {
|
|
1164
1053
|
const face = seatArtifactFace(admitted.role);
|
|
1165
1054
|
const phase = face.reportPhase === true
|
|
1166
|
-
? { phase:
|
|
1055
|
+
? { phase: admitted.phase }
|
|
1167
1056
|
: {};
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
evidence: {
|
|
1177
|
-
runId: admitted.runId,
|
|
1178
|
-
...(face.evidenceRole === true ? { role: admitted.role } : {}),
|
|
1179
|
-
...evidenceLeaves(admitted, face.leaves),
|
|
1180
|
-
sessionDirectory: coordinates.sessionDirectory,
|
|
1181
|
-
sessionFile: coordinates.sessionFile,
|
|
1182
|
-
admittedRequestPath: admitted.admittedRequestPath,
|
|
1183
|
-
attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
|
|
1184
|
-
},
|
|
1057
|
+
// Payloads are the role's submitted words: they live in history.jsonl, not here.
|
|
1058
|
+
const { payloads: _payloads, ...verdict } = roleOutcome;
|
|
1059
|
+
return publishAcceptedTerminal(admitted, roleOutcome, coordinates, scope, {
|
|
1060
|
+
role: admitted.role,
|
|
1061
|
+
runId: admitted.runId,
|
|
1062
|
+
...phase,
|
|
1063
|
+
outcome: verdict,
|
|
1064
|
+
...doctorReportFacts(face, roleOutcome, entries),
|
|
1185
1065
|
});
|
|
1186
1066
|
}
|
|
1187
1067
|
/** Latest top-level user message index; 0 when the session has none (initial attempt). */
|
|
@@ -1317,154 +1197,40 @@ async function applySecretariatCountersignTerminal(admitted, authority, settled,
|
|
|
1317
1197
|
return settled;
|
|
1318
1198
|
}
|
|
1319
1199
|
/**
|
|
1320
|
-
* After the audit gate returns, attach
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1200
|
+
* After the audit gate returns, attach the secretariat officer fact onto the
|
|
1201
|
+
* terminal this turn already settled. Does not publish again — a second publish
|
|
1202
|
+
* would settle the same attempt twice.
|
|
1323
1203
|
*/
|
|
1324
|
-
export async function
|
|
1325
|
-
const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
|
|
1326
|
-
let gate;
|
|
1327
|
-
try {
|
|
1328
|
-
gate = await extractGateFactFromSessionDirectory(sessionDirectory, {
|
|
1329
|
-
runDirectory: admitted.runDirectory,
|
|
1330
|
-
parentSessionFile: sessionFile,
|
|
1331
|
-
});
|
|
1332
|
-
}
|
|
1333
|
-
catch (error) {
|
|
1334
|
-
await noteSettlementFault(admitted.runDirectory, scope, `gate read failed beside host terminal: ${describeErrorIdentity(error)}`);
|
|
1335
|
-
}
|
|
1336
|
-
const withGate = gate === undefined ? terminal : { ...terminal, gate };
|
|
1204
|
+
export async function attachPostAuditCountersignFact(admitted, authority, terminal, scope) {
|
|
1337
1205
|
if (admitted.role !== "secretariat")
|
|
1338
|
-
return
|
|
1339
|
-
return applySecretariatCountersignTerminal(admitted, authority,
|
|
1340
|
-
}
|
|
1341
|
-
function publicationAttemptFromError(path, error) {
|
|
1342
|
-
if (error instanceof Error) {
|
|
1343
|
-
return {
|
|
1344
|
-
path,
|
|
1345
|
-
diagnostic: error.message || error.name || "write failed",
|
|
1346
|
-
identity: thrownIdentity(error),
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
return { path, diagnostic: String(error) };
|
|
1350
|
-
}
|
|
1351
|
-
/**
|
|
1352
|
-
* Directories eligible for open-ended unique failure-artifact placement.
|
|
1353
|
-
* Always includes the ledger runs/ parent of the run directory so an
|
|
1354
|
-
* unwritable run tree cannot strand the original controlled failure.
|
|
1355
|
-
*/
|
|
1356
|
-
function uniqueFailureFallbackDirs(runDirectory, baseDir) {
|
|
1357
|
-
const dirs = [];
|
|
1358
|
-
for (const dir of [baseDir, runDirectory, dirname(runDirectory)]) {
|
|
1359
|
-
if (!dirs.includes(dir))
|
|
1360
|
-
dirs.push(dir);
|
|
1361
|
-
}
|
|
1362
|
-
return dirs;
|
|
1363
|
-
}
|
|
1364
|
-
/**
|
|
1365
|
-
* Resolve a writable artifacts base directory. If `artifacts/` cannot be created
|
|
1366
|
-
* (e.g. a file occupies that name), fall back to the run directory itself.
|
|
1367
|
-
*/
|
|
1368
|
-
async function resolveFailureArtifactsBase(runDirectory) {
|
|
1369
|
-
const artifactsDir = roleRunArtifactsDirectory(runDirectory);
|
|
1370
|
-
try {
|
|
1371
|
-
await ensureRunArtifactsDir(runDirectory);
|
|
1372
|
-
return { baseDir: artifactsDir };
|
|
1373
|
-
}
|
|
1374
|
-
catch (error) {
|
|
1375
|
-
return {
|
|
1376
|
-
baseDir: runDirectory,
|
|
1377
|
-
attempt: publicationAttemptFromError(artifactsDir, error),
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1206
|
+
return terminal;
|
|
1207
|
+
return applySecretariatCountersignTerminal(admitted, authority, terminal, scope);
|
|
1380
1208
|
}
|
|
1381
1209
|
/**
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
1384
|
-
* original controlled failure outside settlement.
|
|
1210
|
+
* Replace the leg's terminal with the controlled failure. A write failure
|
|
1211
|
+
* propagates: the caller notes it beside the host terminal.
|
|
1385
1212
|
*/
|
|
1386
|
-
async function
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const path = candidates[i];
|
|
1395
|
-
const payload = issues.length === 0
|
|
1396
|
-
? basePayload
|
|
1397
|
-
: { ...basePayload, publicationIssues: issues };
|
|
1398
|
-
try {
|
|
1399
|
-
await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
1400
|
-
return { path, issues };
|
|
1401
|
-
}
|
|
1402
|
-
catch (error) {
|
|
1403
|
-
issues.push(publicationAttemptFromError(path, error));
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
const last = issues.at(-1);
|
|
1407
|
-
const error = new Error(last?.diagnostic ?? "unable to write durable failure artifact");
|
|
1408
|
-
if (last?.identity?.name !== undefined && last.identity.name !== "") {
|
|
1409
|
-
error.name = last.identity.name;
|
|
1410
|
-
}
|
|
1411
|
-
if (last?.identity?.code !== undefined) {
|
|
1412
|
-
error.code = last.identity.code;
|
|
1413
|
-
}
|
|
1414
|
-
error.publicationAttempts = issues;
|
|
1415
|
-
throw error;
|
|
1416
|
-
}
|
|
1417
|
-
export async function publishFailureArtifacts(admitted, failure, authority, onErrorPublished, recordAttemptHistory = false) {
|
|
1418
|
-
const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
|
|
1419
|
-
const { baseDir, attempt: baseAttempt } = await resolveFailureArtifactsBase(admitted.runDirectory);
|
|
1420
|
-
const priorIssues = baseAttempt === undefined ? [] : [baseAttempt];
|
|
1421
|
-
// #419: a dispatched failure joins history before fixed-name views change.
|
|
1422
|
-
// Re-projecting an existing failure does not append. History write failure
|
|
1423
|
-
// rides publicationIssues rather than stranding the controlled failure.
|
|
1424
|
-
if (recordAttemptHistory) {
|
|
1213
|
+
export async function publishFailureTerminal(admitted, failure, coordinates, onErrorPublished, scope) {
|
|
1214
|
+
// #419: a dispatched failure joins history before the terminal changes. A history
|
|
1215
|
+
// write failure never strands the failure terminal; it is rethrown after it.
|
|
1216
|
+
let historyFailure;
|
|
1217
|
+
let historyFailed = false;
|
|
1218
|
+
const failureOutcome = { kind: "failure", role: admitted.role, ...failure };
|
|
1219
|
+
let attemptHistoryIdentity;
|
|
1220
|
+
if (scope?.recordAttemptHistory === true) {
|
|
1425
1221
|
try {
|
|
1426
|
-
await appendRunAttemptHistory(
|
|
1427
|
-
|
|
1428
|
-
});
|
|
1222
|
+
const pointer = await appendRunAttemptHistory(attemptHistorySource(admitted, coordinates), failureOutcome);
|
|
1223
|
+
attemptHistoryIdentity = pointer.identity;
|
|
1429
1224
|
}
|
|
1430
1225
|
catch (error) {
|
|
1431
|
-
|
|
1226
|
+
historyFailure = error;
|
|
1227
|
+
historyFailed = true;
|
|
1432
1228
|
}
|
|
1433
1229
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
try {
|
|
1437
|
-
await clearOppositeTerminalArtifactFace(admitted.runDirectory);
|
|
1230
|
+
else {
|
|
1231
|
+
attemptHistoryIdentity = await belongingAttemptHistoryIdentity(admitted.runDirectory, scope);
|
|
1438
1232
|
}
|
|
1439
|
-
|
|
1440
|
-
priorIssues.push(publicationAttemptFromError(roleRunArtifactsDirectory(admitted.runDirectory), error));
|
|
1441
|
-
}
|
|
1442
|
-
// Prefer conventional names; unique fallback dirs keep colliding fixed paths
|
|
1443
|
-
// from stranding the original failure outside settlement. Include the ledger
|
|
1444
|
-
// runs/ parent so a locked run directory (EACCES) cannot exhaust durability.
|
|
1445
|
-
const underArtifacts = baseDir === roleRunArtifactsDirectory(admitted.runDirectory);
|
|
1446
|
-
const uniqueFallbackDirs = uniqueFailureFallbackDirs(admitted.runDirectory, baseDir);
|
|
1447
|
-
const errorCandidates = underArtifacts
|
|
1448
|
-
? [
|
|
1449
|
-
join(baseDir, RUN_TERMINAL_ERROR_FILE),
|
|
1450
|
-
join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
|
|
1451
|
-
join(admitted.runDirectory, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
|
|
1452
|
-
]
|
|
1453
|
-
: [
|
|
1454
|
-
join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
|
|
1455
|
-
join(baseDir, RUN_TERMINAL_ERROR_FILE),
|
|
1456
|
-
];
|
|
1457
|
-
const evidenceCandidates = underArtifacts
|
|
1458
|
-
? [
|
|
1459
|
-
join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
|
|
1460
|
-
join(baseDir, "evidence.settlement.json"),
|
|
1461
|
-
join(admitted.runDirectory, "evidence.settlement.json"),
|
|
1462
|
-
]
|
|
1463
|
-
: [
|
|
1464
|
-
join(baseDir, "evidence.settlement.json"),
|
|
1465
|
-
join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
|
|
1466
|
-
];
|
|
1467
|
-
const errorPayloadBase = {
|
|
1233
|
+
writeRunTerminal(admitted.runDirectory, "error", {
|
|
1468
1234
|
kind: "error",
|
|
1469
1235
|
role: admitted.role,
|
|
1470
1236
|
runId: admitted.runId,
|
|
@@ -1475,24 +1241,13 @@ export async function publishFailureArtifacts(admitted, failure, authority, onEr
|
|
|
1475
1241
|
// This package's own facts, kept beside the host's report.
|
|
1476
1242
|
...(failure.packageFact === undefined ? {} : { packageFact: failure.packageFact }),
|
|
1477
1243
|
...(failure.stderr === undefined ? {} : { stderr: failure.stderr }),
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
admittedRequestPath: admitted.admittedRequestPath,
|
|
1486
|
-
attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
|
|
1487
|
-
...(failure.cause === undefined ? {} : { failureCause: failure.cause }),
|
|
1488
|
-
};
|
|
1489
|
-
const evidenceWrite = await writeFailureJsonRetainingCause(evidenceCandidates, uniqueFallbackDirs, "evidence", evidencePayload,
|
|
1490
|
-
// Evidence records the same publication collisions observed placing the error body.
|
|
1491
|
-
errorWrite.issues);
|
|
1492
|
-
return [
|
|
1493
|
-
{ kind: "error", path: errorWrite.path },
|
|
1494
|
-
{ kind: "evidence", path: evidenceWrite.path },
|
|
1495
|
-
];
|
|
1244
|
+
...terminalBelongingFields(attemptHistoryIdentity, scope),
|
|
1245
|
+
});
|
|
1246
|
+
const path = runCurrentPath(admitted.runDirectory);
|
|
1247
|
+
onErrorPublished?.(path);
|
|
1248
|
+
if (historyFailed)
|
|
1249
|
+
throw historyFailure;
|
|
1250
|
+
return [{ kind: "error", path }];
|
|
1496
1251
|
}
|
|
1497
1252
|
/**
|
|
1498
1253
|
* Shared controlled-failure Terminal settlement (#107 ownership).
|
|
@@ -1506,7 +1261,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
|
|
|
1506
1261
|
let artifacts = [];
|
|
1507
1262
|
if (options.previewOnly !== true) {
|
|
1508
1263
|
try {
|
|
1509
|
-
artifacts = await
|
|
1264
|
+
artifacts = await publishFailureTerminal(admitted, failure, coordinates, options.onErrorPublished, options);
|
|
1510
1265
|
}
|
|
1511
1266
|
catch (error) {
|
|
1512
1267
|
await noteSettlementFault(admitted.runDirectory, options, `failure artifact publication failed beside host terminal: ${describeErrorIdentity(error)}`);
|
|
@@ -1538,7 +1293,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
|
|
|
1538
1293
|
diagnostic: failure.diagnostic,
|
|
1539
1294
|
decisiveFacts,
|
|
1540
1295
|
};
|
|
1541
|
-
return
|
|
1296
|
+
return attachEngineDetourToolUsage({ roleOutcome, navigator, artifacts, runId: admitted.runId }, sessionDirectory, detourUsageContext(admitted, options));
|
|
1542
1297
|
}
|
|
1543
1298
|
/**
|
|
1544
1299
|
* Emit one complete failure Terminal on stdout and one concise stderr diagnostic.
|