@akagilnc/pi-workflow-roles 0.1.5382 → 0.1.5462
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 +4 -2
- package/README.zh-CN.md +4 -2
- package/dist/acp-host/production-host.js +8843 -9012
- package/dist/archivist-record-pointer.js +38 -0
- package/dist/compliance-transport.js +7 -5
- package/dist/doctor-auditor.js +1 -0
- package/dist/engine-detour-usage.js +9 -36
- package/dist/gatekeeper-role.js +4 -18
- package/dist/headless-host/description.js +0 -209
- package/dist/headless-host/production-host.js +7828 -8247
- package/dist/host-contracts.js +8 -0
- package/dist/judge-role.js +14 -1
- package/dist/migrate-book-topology.js +215 -96
- package/dist/navigator-attendance.js +10 -9
- package/dist/navigator-invocation-identity.js +18 -0
- package/dist/packaged-role-registry.js +1 -9
- package/dist/pi/role-turn-host.js +46 -19
- package/dist/public-cli/auto-resume.js +29 -66
- package/dist/public-cli/cli.js +1 -0
- package/dist/public-cli/countersign-run.js +4 -18
- package/dist/public-cli/instruction-seat-run.js +258 -69
- package/dist/public-cli/main.js +7488 -7758
- package/dist/public-cli/post-admission.js +451 -349
- package/dist/public-cli/process-cancel.js +8 -0
- package/dist/public-cli/run-lifecycle.js +24 -94
- package/dist/public-cli/settlement.js +624 -1471
- package/dist/public-cli/terminal.js +34 -53
- package/dist/public-cli/turn-request.js +3 -0
- package/dist/public-role-summons.js +3 -8
- package/dist/receipt-delivery-policy.js +162 -28
- package/dist/run-terminal-artifacts.js +19 -14
- package/dist/serialize-thrown-value.js +7 -1
- package/dist/submission-errors.js +11 -1
- package/dist/submission-gate.js +27 -4
- package/dist/submission-ledger.js +21 -89
- package/package.json +1 -1
- package/scripts/build-package.mjs +0 -2
- package/src/acp-host/role-turn-host.ts +168 -75
- package/src/archivist-record-pointer.ts +48 -0
- package/src/compliance-transport.ts +13 -8
- package/src/doctor-auditor.ts +3 -0
- package/src/engine-detour-usage.ts +10 -39
- package/src/external-host-turn-loop.ts +100 -43
- package/src/gatekeeper-role.ts +7 -22
- package/src/headless-host/description.ts +4 -214
- package/src/headless-host/production-host.ts +2 -1
- package/src/headless-host/role-turn-host.ts +153 -186
- package/src/host-contracts.ts +20 -10
- package/src/judge-role.ts +24 -5
- package/src/navigator-attendance.ts +17 -12
- package/src/navigator-invocation-identity.ts +31 -0
- package/src/navigator-public-session.ts +1 -1
- package/src/navigator-session-contracts.ts +1 -1
- package/src/packaged-role-registry.ts +2 -10
- package/src/pi/adapter.ts +33 -11
- package/src/pi/role-turn-host.ts +49 -28
- package/src/prepared-role-turn.ts +2 -0
- package/src/public-cli/auto-resume.ts +27 -66
- package/src/public-cli/cli.ts +1 -0
- package/src/public-cli/countersign-run.ts +4 -22
- package/src/public-cli/instruction-seat-run.ts +307 -74
- package/src/public-cli/post-admission.ts +615 -459
- package/src/public-cli/process-cancel.ts +9 -0
- package/src/public-cli/run-lifecycle.ts +25 -126
- package/src/public-cli/settlement.ts +801 -1859
- package/src/public-cli/terminal.ts +42 -70
- package/src/public-cli/turn-request.ts +10 -1
- package/src/public-role-summons.ts +3 -8
- package/src/receipt-delivery-policy.ts +219 -28
- package/src/role-envelope.ts +21 -10
- package/src/role-runtime-dependencies.ts +1 -0
- package/src/role-runtime.ts +79 -102
- package/src/run-terminal-artifacts.ts +21 -17
- package/src/serialize-thrown-value.ts +7 -1
- package/src/submission-correctable-error.ts +5 -2
- package/src/submission-errors.ts +12 -1
- package/src/submission-gate.ts +34 -4
- package/src/submission-ledger.ts +30 -116
- package/src/worker-role.ts +19 -7
- package/src/worker-submission-gates.ts +29 -7
- package/dist/audit-escalation.js +0 -7
- package/dist/pi/known-failure.js +0 -51
- package/dist/typed-provider-http.js +0 -57
- package/dist/upstream-error-testimony.js +0 -19
- package/src/pi/known-failure.ts +0 -76
- package/src/typed-provider-http.ts +0 -89
- package/src/upstream-error-testimony.ts +0 -45
|
@@ -4,97 +4,67 @@
|
|
|
4
4
|
* Controlled failures and audit human decisions settle here without washing causes.
|
|
5
5
|
*/
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { appendFile, lstat, readFile, readdir, rm, unlink, writeFile } from "node:fs/promises";
|
|
8
7
|
import { dirname, join } from "node:path";
|
|
8
|
+
import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
9
9
|
|
|
10
|
+
import { writeHardenedArtifactFile } from "./auto-resume.ts";
|
|
10
11
|
import {
|
|
11
12
|
readAnalystGateCyclesFromAuditorRoles,
|
|
12
13
|
type AnalystGateCycleRound,
|
|
13
14
|
} from "../analyst-gate-cycles-read.ts";
|
|
14
|
-
import {
|
|
15
|
+
import { sitianReport } from "../sitian-facade.ts";
|
|
15
16
|
|
|
16
17
|
import {
|
|
17
|
-
hasFreshAttemptSubmission,
|
|
18
18
|
readAttemptScopedSubmissionRows,
|
|
19
19
|
readRecordedSubmissionRows,
|
|
20
20
|
readRecordedSubmissions,
|
|
21
21
|
} from "../submission-ledger.ts";
|
|
22
22
|
|
|
23
|
-
import {
|
|
24
|
-
import { AUDITOR_SOUL_ROLES } from "../auditor-soul.ts";
|
|
25
|
-
import type { RoleTurnKnownFailure } from "../host-contracts.ts";
|
|
26
|
-
import { knownFailureFromProviderStop } from "../pi/known-failure.ts";
|
|
27
|
-
import { serializeThrownValue } from "../serialize-thrown-value.ts";
|
|
28
|
-
import {
|
|
29
|
-
isV1ResumableProvider,
|
|
30
|
-
readLatestTypedProviderHttpObservation,
|
|
31
|
-
readTypedHttp429Observation,
|
|
32
|
-
type TypedHttp429Observation,
|
|
33
|
-
type TypedProviderHttpObservation,
|
|
34
|
-
} from "./run-lifecycle.ts";
|
|
23
|
+
import { CONTROLLED_FAILURE_CAUSES, type RoleTurnKnownFailure } from "../host-contracts.ts";
|
|
35
24
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} from "
|
|
42
|
-
// COMPLIANCE_RESPONSE_ENTRY_TYPE remains for boundRetainedAuditResponse (call/result
|
|
43
|
-
// interval binding on historical session bytes). Provider-stop retain authority is Sitian.
|
|
44
|
-
import {
|
|
45
|
-
ENGINE_DETOUR_TOOL_NAME,
|
|
46
|
-
} from "../engine-detour.ts";
|
|
25
|
+
childSignalDeathDiagnostic,
|
|
26
|
+
processCancelDiagnostic,
|
|
27
|
+
type CatchableProcessSignal,
|
|
28
|
+
} from "./process-cancel.ts";
|
|
29
|
+
import { serializeThrownValue } from "../serialize-thrown-value.ts";
|
|
30
|
+
import { describeErrorIdentity } from "./run-lifecycle.ts";
|
|
47
31
|
import {
|
|
48
|
-
projectEngineDetourToolUsageForPublicTerminal,
|
|
49
32
|
readEngineDetourToolUsage,
|
|
50
33
|
readInvocationEngineMounted,
|
|
51
34
|
runDirectoryFromSessionDirectory,
|
|
52
35
|
sessionFileFromSessionDirectory,
|
|
53
36
|
withEngineDetourToolUsageFact,
|
|
54
37
|
} from "../engine-detour-usage.ts";
|
|
55
|
-
import {
|
|
56
|
-
JUDGE_OUTPUT_TOOL_NAME,
|
|
57
|
-
type JudgeVerdict,
|
|
58
|
-
} from "../package-contracts/judge-output.ts";
|
|
59
|
-
import {
|
|
60
|
-
CODER_OUTPUT_TOOL_NAME,
|
|
61
|
-
FIXER_OUTPUT_TOOL_NAME,
|
|
62
|
-
} from "../package-contracts/worker-output.ts";
|
|
63
38
|
|
|
64
|
-
import {
|
|
65
|
-
DOCTOR_OUTPUT_TOOL_NAME,
|
|
66
|
-
type DoctorCaseCost,
|
|
67
|
-
} from "../doctor-contracts.ts";
|
|
39
|
+
import type { DoctorCaseCost } from "../doctor-contracts.ts";
|
|
68
40
|
import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "../dossier-resolution.ts";
|
|
69
41
|
import {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
packagedDurableOfficerEntry,
|
|
75
|
-
packagedRoleAcceptedOutputTool,
|
|
76
|
-
packagedRoleMetadata,
|
|
77
|
-
packagedSkipsGateOnInfrastructureStage,
|
|
78
|
-
type PackagedArtifactFace,
|
|
79
|
-
type PackagedArtifactLeaf,
|
|
80
|
-
type PackagedRole,
|
|
81
|
-
} from "../packaged-role-registry.ts";
|
|
42
|
+
ensureRunArtifactsDir,
|
|
43
|
+
homeFromRunDirectory,
|
|
44
|
+
type AdmittedRoleInvocation,
|
|
45
|
+
} from "./invocation.ts";
|
|
82
46
|
import {
|
|
83
47
|
SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY,
|
|
84
48
|
SECRETARIAT_GATE_OFFICER_ENTRY_TYPE,
|
|
85
49
|
} from "../secretariat-contracts.ts";
|
|
86
50
|
import {
|
|
87
|
-
classifyPackagedRoleTerminalResult,
|
|
88
51
|
findLatestDurablePackagedRoleTerminal,
|
|
89
|
-
hasNavigatorInfrastructureFailureBase,
|
|
90
|
-
isAcceptedPackagedRoleTerminalResult,
|
|
91
52
|
NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY,
|
|
92
53
|
} from "../navigator-invocation-identity.ts";
|
|
54
|
+
import {
|
|
55
|
+
packagedDurableOfficerEntry,
|
|
56
|
+
packagedRoleAcceptedOutputTool,
|
|
57
|
+
packagedRoleMetadata,
|
|
58
|
+
packagedSkipsGateOnInfrastructureStage,
|
|
59
|
+
type PackagedArtifactFace,
|
|
60
|
+
type PackagedArtifactLeaf,
|
|
61
|
+
} from "../packaged-role-registry.ts";
|
|
93
62
|
import {
|
|
94
63
|
NO_RECEIPT_LIFECYCLE_ENTRY_TYPE,
|
|
95
|
-
RECEIPT_DELIVERY_TURN_LIMIT,
|
|
96
64
|
noReceiptLifecycleFacts,
|
|
97
65
|
parseNoReceiptLifecycleFacts,
|
|
66
|
+
priorReceiptContinuation,
|
|
67
|
+
receiptAttemptPointer,
|
|
98
68
|
type NoReceiptLifecycleFacts,
|
|
99
69
|
} from "../receipt-delivery-policy.ts";
|
|
100
70
|
import type {
|
|
@@ -103,18 +73,17 @@ import type {
|
|
|
103
73
|
DurablePrincipalCoordinates,
|
|
104
74
|
} from "../host-contracts.ts";
|
|
105
75
|
import { roleRunArtifactsDirectory } from "../role-run-placement.ts";
|
|
106
|
-
import { rewriteRunDirectoryPathValue } from "../role-run-relocation.ts";
|
|
107
76
|
import {
|
|
108
77
|
listSeamOwnedUniqueErrorFacePaths,
|
|
78
|
+
uniqueErrorFallbackName,
|
|
79
|
+
UNIQUE_ERROR_FALLBACK_STEM,
|
|
109
80
|
RUN_TERMINAL_ARTIFACT_FILES,
|
|
110
81
|
RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS,
|
|
82
|
+
RUN_TERMINAL_REPORT_FILE,
|
|
83
|
+
RUN_TERMINAL_ERROR_FILE,
|
|
84
|
+
RUN_TERMINAL_EVIDENCE_FILE,
|
|
85
|
+
RUN_TERMINAL_ERROR_SETTLEMENT_FILE,
|
|
111
86
|
} from "../run-terminal-artifacts.ts";
|
|
112
|
-
import {
|
|
113
|
-
ensureRunArtifactsDir,
|
|
114
|
-
homeFromRunDirectory,
|
|
115
|
-
type AdmittedJudgeInvocation,
|
|
116
|
-
type AdmittedRoleInvocation,
|
|
117
|
-
} from "./invocation.ts";
|
|
118
87
|
|
|
119
88
|
/** Ledger reads use the run's machine home — not ambient process HOME (child write vs parent settle). */
|
|
120
89
|
function sealedLedgerHome(admitted: Pick<AdmittedRoleInvocation, "runDirectory">): string {
|
|
@@ -126,12 +95,123 @@ function sealedLedgerHome(admitted: Pick<AdmittedRoleInvocation, "runDirectory">
|
|
|
126
95
|
* courtAttemptId tags the new court; recorded payloads stay run-scoped (#836).
|
|
127
96
|
*/
|
|
128
97
|
export type SettlementCourtScope = {
|
|
98
|
+
/** Only the dispatched host turn may record this attempt; later reads are projections. */
|
|
99
|
+
readonly recordAttemptHistory?: true;
|
|
100
|
+
/** Inspect a candidate without rewriting history or terminal artifact faces. */
|
|
101
|
+
readonly previewOnly?: true;
|
|
129
102
|
readonly courtAttemptId?: string;
|
|
130
103
|
/** Public-invocation scope from the shared Host envelope (#537). */
|
|
131
104
|
readonly invocationScopeId?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Package fault beside an already chosen terminal. The host report stays.
|
|
107
|
+
* Bound by the public court to the one post-dispatch receiver.
|
|
108
|
+
*/
|
|
109
|
+
readonly notePackageFault?: (diagnostic: string) => void | Promise<void>;
|
|
132
110
|
};
|
|
133
111
|
|
|
134
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Hardened artifacts directory. A planted symlink at the run directory or the
|
|
114
|
+
* artifacts path must not receive a durable run artifact.
|
|
115
|
+
*/
|
|
116
|
+
export async function ensureRealArtifactsDirectory(runDirectory: string): Promise<string> {
|
|
117
|
+
const runStat = await lstat(runDirectory);
|
|
118
|
+
if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
|
|
119
|
+
throw new Error("run artifact retention: run directory is not a real directory");
|
|
120
|
+
}
|
|
121
|
+
const artifactsDir = roleRunArtifactsDirectory(runDirectory);
|
|
122
|
+
try {
|
|
123
|
+
const existing = await lstat(artifactsDir);
|
|
124
|
+
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
125
|
+
throw new Error("run artifact retention: artifacts path is not a real directory");
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (!isEnoent(error)) throw error;
|
|
129
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
130
|
+
const created = await lstat(artifactsDir);
|
|
131
|
+
if (created.isSymbolicLink() || !created.isDirectory()) {
|
|
132
|
+
throw new Error("run artifact retention: artifacts directory is not a real directory");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return artifactsDir;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* One retention path for a package fault beside an already chosen terminal.
|
|
140
|
+
* Session append is first when the caller has that channel. The artifact file
|
|
141
|
+
* is only the fallback after that append throws. Either failure is reported;
|
|
142
|
+
* neither replaces the terminal.
|
|
143
|
+
*/
|
|
144
|
+
export async function retainPackageFault(input: {
|
|
145
|
+
readonly runDirectory: string;
|
|
146
|
+
readonly diagnostic: string;
|
|
147
|
+
/** Original package exception, independent of the host report. */
|
|
148
|
+
readonly error?: unknown;
|
|
149
|
+
readonly appendSession?: (payload: {
|
|
150
|
+
readonly diagnostic: string;
|
|
151
|
+
readonly recordedAt: string;
|
|
152
|
+
}) => Promise<void>;
|
|
153
|
+
readonly stderr?: (text: string) => void;
|
|
154
|
+
}): Promise<void> {
|
|
155
|
+
const payload = {
|
|
156
|
+
diagnostic: input.diagnostic,
|
|
157
|
+
recordedAt: new Date().toISOString(),
|
|
158
|
+
...(Object.hasOwn(input, "error") ? { failure: projectThrownFailureLeaf(input.error) } : {}),
|
|
159
|
+
};
|
|
160
|
+
let retentionFailure: string | undefined;
|
|
161
|
+
const writeArtifact = async (appendFailure?: ControlledFailure): Promise<void> => {
|
|
162
|
+
const artifactsDir = await ensureRealArtifactsDirectory(input.runDirectory);
|
|
163
|
+
await writeHardenedArtifactFile(artifactsDir, "post-admission-diagnostic", {
|
|
164
|
+
version: 1,
|
|
165
|
+
...payload,
|
|
166
|
+
...(appendFailure === undefined ? {} : { retentionFailure: appendFailure }),
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
if (input.appendSession !== undefined) {
|
|
170
|
+
try {
|
|
171
|
+
await input.appendSession(payload);
|
|
172
|
+
} catch (appendError) {
|
|
173
|
+
retentionFailure =
|
|
174
|
+
`post-dispatch diagnostic session append failed (best-effort continue): dossier=${describeErrorIdentity(appendError)}`;
|
|
175
|
+
try {
|
|
176
|
+
await writeArtifact(projectThrownFailureLeaf(appendError));
|
|
177
|
+
} catch (artifactError) {
|
|
178
|
+
retentionFailure =
|
|
179
|
+
`post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; artifact=${describeErrorIdentity(artifactError)}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
try {
|
|
184
|
+
await writeArtifact();
|
|
185
|
+
} catch (artifactError) {
|
|
186
|
+
retentionFailure =
|
|
187
|
+
`post-dispatch diagnostic durable retention failed (best-effort continue): artifact=${describeErrorIdentity(artifactError)}`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const stderr = input.stderr ?? ((text: string) => {
|
|
191
|
+
process.stderr.write(text);
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
stderr(formatCliDiagnostic(input.diagnostic));
|
|
195
|
+
if (retentionFailure !== undefined) stderr(formatCliDiagnostic(retentionFailure));
|
|
196
|
+
} catch {
|
|
197
|
+
// Presentation is best-effort beside an already-formed host terminal.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Record a package fault without letting the note replace the terminal. */
|
|
202
|
+
export async function noteSettlementFault(
|
|
203
|
+
runDirectory: string,
|
|
204
|
+
scope: SettlementCourtScope | undefined,
|
|
205
|
+
diagnostic: string,
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
if (scope?.notePackageFault !== undefined) {
|
|
208
|
+
await scope.notePackageFault(diagnostic);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
await retainPackageFault({ runDirectory, diagnostic });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function ledgerReadScope(
|
|
135
215
|
admitted: Pick<AdmittedRoleInvocation, "runDirectory">,
|
|
136
216
|
scope?: SettlementCourtScope,
|
|
137
217
|
): { home: string; sessionParent: string; attemptId?: string } {
|
|
@@ -207,29 +287,6 @@ async function sealedLedgerOutcome(
|
|
|
207
287
|
return roleOutcomeFromRows(role, rows);
|
|
208
288
|
}
|
|
209
289
|
|
|
210
|
-
/**
|
|
211
|
-
* True when this court/attempt itself already produced a sealed or
|
|
212
|
-
* audit-escalation submission (#836 r12 class 2). Presentation
|
|
213
|
-
* (submissions/payloads) always stays run-scoped and unfiltered — this is a
|
|
214
|
-
* control-flow signal only, consumed to keep a stale prior-attempt
|
|
215
|
-
* acceptance from outranking this attempt's own real host-turn failure.
|
|
216
|
-
* Absent a courtAttemptId scope there is no distinct prior attempt to stale
|
|
217
|
-
* against, so this defaults true (unchanged behavior for ordinary,
|
|
218
|
-
* non-court dispatches).
|
|
219
|
-
*/
|
|
220
|
-
export async function attemptProducedFreshSubmission(
|
|
221
|
-
admitted: AdmittedRoleInvocation,
|
|
222
|
-
scope?: SettlementCourtScope,
|
|
223
|
-
): Promise<boolean> {
|
|
224
|
-
if (scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0) return true;
|
|
225
|
-
return hasFreshAttemptSubmission(
|
|
226
|
-
admitted.projectRoot,
|
|
227
|
-
admitted.runId,
|
|
228
|
-
scope.courtAttemptId,
|
|
229
|
-
ledgerReadScope(admitted, scope),
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
290
|
/** #836: every raw role payload in settle scope (调几次记几次). */
|
|
234
291
|
export async function recordedSubmissionPayloads(
|
|
235
292
|
admitted: Pick<AdmittedRoleInvocation, "projectRoot" | "runId" | "runDirectory">,
|
|
@@ -247,23 +304,8 @@ export function withSubmissions<T extends TerminalResult>(
|
|
|
247
304
|
submissions: readonly unknown[],
|
|
248
305
|
): T {
|
|
249
306
|
if (submissions.length === 0) return terminal;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// #879: accepted/audit_escalation may still fill a missing this-court
|
|
253
|
-
// payloads face from that history. #953: failure must not — prior receipts
|
|
254
|
-
// stay on the historical carrier only, never as the current failure's
|
|
255
|
-
// ordinary payloads/receipt face.
|
|
256
|
-
if (roleOutcome.kind === "failure") {
|
|
257
|
-
return { ...terminal, submissions };
|
|
258
|
-
}
|
|
259
|
-
const withPayloads =
|
|
260
|
-
roleOutcome.kind === "accepted" || roleOutcome.kind === "audit_escalation"
|
|
261
|
-
? {
|
|
262
|
-
...roleOutcome,
|
|
263
|
-
payloads: coalesceSubmissionRows(roleOutcome.payloads, submissions),
|
|
264
|
-
}
|
|
265
|
-
: roleOutcome;
|
|
266
|
-
return { ...terminal, roleOutcome: withPayloads, submissions };
|
|
307
|
+
// Run history is presentation, never a substitute for this court's reply (#879).
|
|
308
|
+
return { ...terminal, submissions };
|
|
267
309
|
}
|
|
268
310
|
|
|
269
311
|
/** Attach full ledger submissions onto any settled terminal (#836). */
|
|
@@ -276,13 +318,19 @@ export async function attachRecordedSubmissions<T extends TerminalResult>(
|
|
|
276
318
|
// pass courtAttemptId here — roleOutcome already carries this-court payloads
|
|
277
319
|
// from sealedLedgerOutcome when scoped; withSubmissions keeps them.
|
|
278
320
|
void scope;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
321
|
+
try {
|
|
322
|
+
return withSubmissions(
|
|
323
|
+
terminal,
|
|
324
|
+
await recordedSubmissionPayloads(admitted, undefined),
|
|
325
|
+
);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
await noteSettlementFault(
|
|
328
|
+
admitted.runDirectory,
|
|
329
|
+
scope,
|
|
330
|
+
`submission history read failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
331
|
+
);
|
|
332
|
+
return terminal;
|
|
333
|
+
}
|
|
286
334
|
}
|
|
287
335
|
|
|
288
336
|
/**
|
|
@@ -294,32 +342,143 @@ export async function settleHostEndedNoReceipt(
|
|
|
294
342
|
admitted: AdmittedRoleInvocation,
|
|
295
343
|
authority: DurablePrincipalAuthority,
|
|
296
344
|
scope?: SettlementCourtScope,
|
|
345
|
+
/**
|
|
346
|
+
* #1132: delivery requests this run actually issued. Zero stays zero when
|
|
347
|
+
* nothing was sent and this attempt has no lifecycle fact. A fact already
|
|
348
|
+
* written for this run and attempt supplies the count when it is larger.
|
|
349
|
+
* The budget is never the count, and another loop's resumes are not催交.
|
|
350
|
+
*/
|
|
351
|
+
issuedDeliveryRequests = 0,
|
|
297
352
|
): Promise<TerminalResult> {
|
|
298
|
-
const facts = noReceiptLifecycleFacts({
|
|
299
|
-
terminalToolCalled: false,
|
|
300
|
-
rejectedReceipts: [],
|
|
301
|
-
deliveryTurns: RECEIPT_DELIVERY_TURN_LIMIT,
|
|
302
|
-
runPointer: admitted.runDirectory,
|
|
303
|
-
attemptPointer: `current:${admitted.runDirectory}`,
|
|
304
|
-
});
|
|
305
353
|
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
354
|
+
const binding = {
|
|
355
|
+
runPointer: admitted.runDirectory,
|
|
356
|
+
attemptPointer: receiptAttemptPointer(admitted.runDirectory, scope?.invocationScopeId),
|
|
357
|
+
};
|
|
358
|
+
// The delivery owner records what actually happened this attempt — whether the
|
|
359
|
+
// terminal tool was called and what was rejected. Present those facts; a fixed
|
|
360
|
+
// `false` / `[]` would assert an empty delivery that never happened
|
|
361
|
+
// (#1032: 无回执如实呈 no_receipt; 单一真源).
|
|
362
|
+
const recorded = await readRecordedNoReceiptFacts(coordinates.sessionFile, binding, scope?.invocationScopeId);
|
|
363
|
+
if (recorded instanceof LifecycleReadFailure) {
|
|
364
|
+
// A present record that cannot be read is not an empty delivery. Omit the
|
|
365
|
+
// facts and keep the real error beside the no_receipt terminal.
|
|
366
|
+
await noteSettlementFault(
|
|
367
|
+
admitted.runDirectory,
|
|
368
|
+
scope,
|
|
369
|
+
`no-receipt lifecycle record could not be read: ${describeErrorIdentity(recorded.error ?? recorded)}`,
|
|
370
|
+
);
|
|
371
|
+
return settleNoReceiptTerminal(admitted, authority, scope, undefined);
|
|
372
|
+
}
|
|
373
|
+
return settleNoReceiptTerminal(
|
|
374
|
+
admitted,
|
|
375
|
+
authority,
|
|
376
|
+
scope,
|
|
377
|
+
noReceiptLifecycleFacts({
|
|
378
|
+
terminalToolCalled: recorded?.terminalToolCalled ?? false,
|
|
379
|
+
rejectedReceipts: recorded?.rejectedReceipts ?? [],
|
|
380
|
+
deliveryTurns: Math.max(issuedDeliveryRequests, recorded?.deliveryTurns ?? 0),
|
|
381
|
+
...binding,
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** A lifecycle record that exists but could not be read or parsed. */
|
|
387
|
+
export class LifecycleReadFailure extends Error {
|
|
388
|
+
/** The underlying read or parse failure, kept whole. */
|
|
389
|
+
readonly error: unknown;
|
|
390
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
391
|
+
super(message);
|
|
392
|
+
this.name = "LifecycleReadFailure";
|
|
393
|
+
this.error = options?.cause;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The delivery owner's own lifecycle record for this attempt.
|
|
399
|
+
* - undefined: the owner wrote no record (genuine absence).
|
|
400
|
+
* - LifecycleReadFailure: a record exists but could not be read or parsed.
|
|
401
|
+
*/
|
|
402
|
+
async function readRecordedNoReceiptFacts(
|
|
403
|
+
sessionFile: string,
|
|
404
|
+
binding: { runPointer: string; attemptPointer: string },
|
|
405
|
+
invocationScopeId?: string,
|
|
406
|
+
): Promise<NoReceiptLifecycleFacts | undefined | LifecycleReadFailure> {
|
|
407
|
+
let entries: readonly SessionEntry[];
|
|
408
|
+
try {
|
|
409
|
+
entries = await readBoundSessionEntries(sessionFile);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
// A session that was never written is genuine absence; anything else is a
|
|
412
|
+
// real read failure about this run.
|
|
413
|
+
if (isEnoent(error)) return undefined;
|
|
414
|
+
return new LifecycleReadFailure(
|
|
415
|
+
`session transcript unreadable: ${errorText(error)}`,
|
|
416
|
+
{ cause: error },
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const scoped = invocationScopeId !== undefined && invocationScopeId.trim() !== "";
|
|
420
|
+
const entry = (scoped ? entries : entries.slice(currentAttemptStartIndex(entries))).slice().reverse().find(
|
|
421
|
+
(item: SessionEntry) => {
|
|
422
|
+
if (item.customType !== NO_RECEIPT_LIFECYCLE_ENTRY_TYPE
|
|
423
|
+
&& item.message?.customType !== NO_RECEIPT_LIFECYCLE_ENTRY_TYPE) return false;
|
|
424
|
+
const raw = item.data ?? item.message?.details;
|
|
425
|
+
return !scoped || !isRecord(raw) || raw.attemptPointer === undefined
|
|
426
|
+
|| raw.attemptPointer === binding.attemptPointer;
|
|
319
427
|
},
|
|
320
|
-
coordinates.sessionDirectory,
|
|
321
|
-
detourGateContext(admitted, scope),
|
|
322
428
|
);
|
|
429
|
+
const raw = entry?.data ?? entry?.message?.details;
|
|
430
|
+
if (raw === undefined) {
|
|
431
|
+
if (!scoped) return undefined;
|
|
432
|
+
const prior = priorReceiptContinuation(entries, invocationScopeId!);
|
|
433
|
+
return noReceiptLifecycleFacts({ ...prior, ...binding });
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const facts = parseNoReceiptLifecycleFacts(raw);
|
|
437
|
+
return facts.runPointer === binding.runPointer
|
|
438
|
+
&& facts.attemptPointer === binding.attemptPointer
|
|
439
|
+
? (scoped ? noReceiptLifecycleFacts({ ...priorReceiptContinuation(entries, invocationScopeId!), ...binding }) : facts)
|
|
440
|
+
: undefined;
|
|
441
|
+
} catch (error) {
|
|
442
|
+
return new LifecycleReadFailure(
|
|
443
|
+
`no-receipt lifecycle record is malformed: ${errorText(error)}`,
|
|
444
|
+
{ cause: error },
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function settleNoReceiptTerminal(
|
|
450
|
+
admitted: AdmittedRoleInvocation,
|
|
451
|
+
authority: DurablePrincipalAuthority,
|
|
452
|
+
scope: SettlementCourtScope | undefined,
|
|
453
|
+
facts: NoReceiptLifecycleFacts | undefined,
|
|
454
|
+
): Promise<TerminalResult> {
|
|
455
|
+
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
456
|
+
const roleOutcome: TerminalRoleOutcome = facts === undefined
|
|
457
|
+
? {
|
|
458
|
+
kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt",
|
|
459
|
+
decisiveFacts: {},
|
|
460
|
+
}
|
|
461
|
+
: {
|
|
462
|
+
kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt",
|
|
463
|
+
...facts, decisiveFacts: facts,
|
|
464
|
+
};
|
|
465
|
+
if (scope?.recordAttemptHistory === true) {
|
|
466
|
+
await appendRunAttemptHistory(
|
|
467
|
+
{ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile },
|
|
468
|
+
roleOutcome,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
if (scope?.previewOnly !== true) await clearOppositeTerminalArtifactFace(admitted.runDirectory);
|
|
472
|
+
return withOptionalGateProjection({
|
|
473
|
+
roleOutcome,
|
|
474
|
+
navigator: await extractNavigatorFactFromAdmittedSession(
|
|
475
|
+
coordinates.sessionFile,
|
|
476
|
+
admitted.runDirectory,
|
|
477
|
+
scope,
|
|
478
|
+
),
|
|
479
|
+
artifacts: [],
|
|
480
|
+
runId: admitted.runId,
|
|
481
|
+
}, coordinates.sessionDirectory, detourGateContext(admitted, scope));
|
|
323
482
|
}
|
|
324
483
|
|
|
325
484
|
/** Transitional host-session reads remain only for non-sealed failure and audit evidence. */
|
|
@@ -330,7 +489,6 @@ function coordinatesFromAdmitted(
|
|
|
330
489
|
return authority.decode(admitted.principal);
|
|
331
490
|
}
|
|
332
491
|
import {
|
|
333
|
-
coalesceSubmissionRows,
|
|
334
492
|
exitCodeForTerminalOutcome,
|
|
335
493
|
formatTerminalResult,
|
|
336
494
|
isLawfulTypedTerminalOutcome,
|
|
@@ -341,12 +499,11 @@ import {
|
|
|
341
499
|
type TerminalGateSeat,
|
|
342
500
|
type TerminalNavigatorFact,
|
|
343
501
|
type TerminalResult,
|
|
344
|
-
type TerminalResume,
|
|
345
502
|
type TerminalRoleName,
|
|
346
503
|
type TerminalRoleOutcome,
|
|
347
504
|
} from "./terminal.ts";
|
|
348
505
|
|
|
349
|
-
import { isEnoent, isMissingPathError, isRecord } from "../unknown-value.ts";
|
|
506
|
+
import { isEnoent, isMissingPathError, isRecord, errorText } from "../unknown-value.ts";
|
|
350
507
|
|
|
351
508
|
export type { ControlledFailureCause };
|
|
352
509
|
|
|
@@ -356,39 +513,55 @@ export {
|
|
|
356
513
|
isLawfulTypedTerminalOutcome,
|
|
357
514
|
};
|
|
358
515
|
|
|
359
|
-
/**
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Preserved post-admission failure (not a role Receipt). The host contract owns
|
|
518
|
+
* the shape; the only difference settlement requires is that a settled failure
|
|
519
|
+
* always carries a diagnostic to present, so `cause` and `identity` stay
|
|
520
|
+
* omitted when no typed fact confirms them (#881 — no fabricated label).
|
|
521
|
+
*/
|
|
522
|
+
export type ControlledFailure = RoleTurnKnownFailure & {
|
|
363
523
|
readonly diagnostic: string;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
524
|
+
/**
|
|
525
|
+
* Real facts this package observed while handling the call, kept beside the
|
|
526
|
+
* host's report rather than inside its open `details`. `details` belongs to
|
|
527
|
+
* the host: a key written there can collide with a key the host itself
|
|
528
|
+
* carries, and the host's value would be the one lost (host-contracts.ts:41
|
|
529
|
+
* — an open record with no reserved keys).
|
|
530
|
+
*/
|
|
531
|
+
readonly packageFact?: PackageSideFact;
|
|
532
|
+
/**
|
|
533
|
+
* Host stderr that is not already this failure's diagnostic. A stderr.log
|
|
534
|
+
* or artifact write can fail; the original bytes stay on this object.
|
|
535
|
+
*/
|
|
536
|
+
readonly stderr?: string;
|
|
369
537
|
};
|
|
370
538
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
export
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
539
|
+
/**
|
|
540
|
+
* Facts the package itself established, each kept whole. None of them is ever
|
|
541
|
+
* promoted into the host's cause, diagnostic or details.
|
|
542
|
+
*/
|
|
543
|
+
export type PackageSideFact = {
|
|
544
|
+
/**
|
|
545
|
+
* This package's own view of the host's exit for this call. Kept beside the
|
|
546
|
+
* host's `details`, never inside it.
|
|
547
|
+
*/
|
|
548
|
+
readonly exitCode?: number | null;
|
|
549
|
+
readonly timedOut?: boolean;
|
|
550
|
+
/**
|
|
551
|
+
* The signal that ended the host child on this call, when the host reported
|
|
552
|
+
* one. Beside the host's details, never inside them.
|
|
553
|
+
*/
|
|
554
|
+
readonly signal?: string;
|
|
555
|
+
/**
|
|
556
|
+
* Catchable process signal this call actually received. Beside the host's
|
|
557
|
+
* report, never a replacement for it.
|
|
558
|
+
*/
|
|
559
|
+
readonly cancelName?: string;
|
|
560
|
+
/** An exception caught after the host already reported this call. */
|
|
561
|
+
readonly thrown?: RoleTurnKnownFailure & { readonly diagnostic: string };
|
|
562
|
+
/** A durable stderr.log write that failed while handling this call. */
|
|
563
|
+
readonly stderrLogWriteFailure?: unknown;
|
|
564
|
+
};
|
|
392
565
|
|
|
393
566
|
/**
|
|
394
567
|
* Host stderr as recorded — full bytes, no flood filter, no char clip (#836).
|
|
@@ -405,14 +578,13 @@ export function formatCliDiagnostic(message: string): string {
|
|
|
405
578
|
return `ak-role: ${message}\n`;
|
|
406
579
|
}
|
|
407
580
|
|
|
408
|
-
/**
|
|
409
|
-
* One concise stderr line for humans. Durable Error Artifact / Terminal keep the
|
|
410
|
-
* full original diagnostic — presentation collapses newlines and flood frames.
|
|
411
|
-
*/
|
|
412
581
|
/** #836: full diagnostic on stderr — no first-line clip / flood filter. */
|
|
413
582
|
export function formatFailureStderrDiagnostic(failure: ControlledFailure): string {
|
|
414
583
|
const text = failure.diagnostic.trim().length > 0 ? failure.diagnostic : "failure";
|
|
415
|
-
|
|
584
|
+
const head = formatCliDiagnostic(text);
|
|
585
|
+
const beside = failure.stderr;
|
|
586
|
+
if (beside === undefined || beside.length === 0 || beside === failure.diagnostic) return head;
|
|
587
|
+
return beside.endsWith("\n") ? `${head}${beside}` : `${head}${beside}\n`;
|
|
416
588
|
}
|
|
417
589
|
|
|
418
590
|
/**
|
|
@@ -456,42 +628,27 @@ export function presentControlledFailure(
|
|
|
456
628
|
io.stderr(formatFailureStderrDiagnostic(failure));
|
|
457
629
|
}
|
|
458
630
|
|
|
459
|
-
/**
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
export
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
await readFile(sessionFile, "utf8");
|
|
470
|
-
return { state: "present" };
|
|
471
|
-
} catch (error) {
|
|
472
|
-
if (isEnoent(error)) return { state: "missing" };
|
|
473
|
-
return {
|
|
474
|
-
state: "unreadable",
|
|
475
|
-
diagnostic:
|
|
476
|
-
error instanceof Error
|
|
477
|
-
? error.message || error.name
|
|
478
|
-
: String(error),
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
}
|
|
631
|
+
/**
|
|
632
|
+
* A caught error's identity, read structurally off the thrown value: its name
|
|
633
|
+
* and its `code` when it carries one (an OS errno, typically). The errno
|
|
634
|
+
* travels as data so a consumer asserts the identity instead of recognising
|
|
635
|
+
* the system error by its message wording (质量法: 机器只咬契约,不咬呈现).
|
|
636
|
+
*/
|
|
637
|
+
export type ThrownErrorIdentity = {
|
|
638
|
+
readonly name: string;
|
|
639
|
+
readonly code?: string | number;
|
|
640
|
+
};
|
|
482
641
|
|
|
483
|
-
function thrownIdentity(error: Error): {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
} {
|
|
487
|
-
const identity: { name?: string; code?: string | number } = {
|
|
642
|
+
function thrownIdentity(error: Error): ThrownErrorIdentity {
|
|
643
|
+
const code = (error as { code?: unknown }).code;
|
|
644
|
+
return {
|
|
488
645
|
name: error.name,
|
|
646
|
+
...(typeof code === "string" || typeof code === "number" ? { code } : {}),
|
|
489
647
|
};
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return identity;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function isControlledFailureCause(cause: unknown): cause is ControlledFailureCause {
|
|
651
|
+
return CONTROLLED_FAILURE_CAUSES.some((known) => known === cause);
|
|
495
652
|
}
|
|
496
653
|
|
|
497
654
|
/** Production-owned typed thrown failure (explicit-internal channel). */
|
|
@@ -504,48 +661,43 @@ function isTypedActivationError(
|
|
|
504
661
|
} {
|
|
505
662
|
if (!(error instanceof Error)) return false;
|
|
506
663
|
const cause = (error as { knownCause?: unknown }).knownCause;
|
|
507
|
-
return (
|
|
508
|
-
cause === "provider" ||
|
|
509
|
-
cause === "activation" ||
|
|
510
|
-
cause === "session" ||
|
|
511
|
-
cause === "output" ||
|
|
512
|
-
cause === "timeout"
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/** Flatten nested AggregateError leaves; non-aggregate values stay as one fact. */
|
|
517
|
-
function flattenThrownFailureLeaves(error: unknown): unknown[] {
|
|
518
|
-
if (!(error instanceof AggregateError)) {
|
|
519
|
-
return [error];
|
|
520
|
-
}
|
|
521
|
-
const leaves: unknown[] = [];
|
|
522
|
-
for (const item of error.errors) {
|
|
523
|
-
leaves.push(...flattenThrownFailureLeaves(item));
|
|
524
|
-
}
|
|
525
|
-
return leaves;
|
|
664
|
+
return isControlledFailureCause(cause);
|
|
526
665
|
}
|
|
527
666
|
|
|
528
667
|
/**
|
|
529
668
|
* Project one thrown value into a ControlledFailure leaf.
|
|
530
669
|
* Sole owner for thrown-leaf identity/diagnostic mapping.
|
|
531
|
-
*
|
|
670
|
+
* Aggregate shells and their direct children remain distinct facts.
|
|
532
671
|
*/
|
|
533
672
|
export function projectThrownFailureLeaf(error: unknown): ControlledFailure {
|
|
673
|
+
const originalDetails = error !== null && typeof error === "object"
|
|
674
|
+
? (error as { details?: Readonly<Record<string, unknown>> }).details
|
|
675
|
+
: undefined;
|
|
676
|
+
const testimony = {
|
|
677
|
+
error: serializeThrownValue(error),
|
|
678
|
+
...(error instanceof AggregateError
|
|
679
|
+
? { concurrentFailures: error.errors.map(projectThrownFailureLeaf) }
|
|
680
|
+
: {}),
|
|
681
|
+
};
|
|
682
|
+
const details = originalDetails === undefined ? testimony : originalDetails;
|
|
683
|
+
const packageFact = originalDetails === undefined ? {} : {
|
|
684
|
+
packageFact: {
|
|
685
|
+
thrown: {
|
|
686
|
+
diagnostic: error instanceof Error ? error.message || error.name || "exception" : "non-Error throw",
|
|
687
|
+
details: testimony,
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
};
|
|
534
691
|
if (isTypedActivationError(error)) {
|
|
535
692
|
const identity = thrownIdentity(error);
|
|
536
|
-
if (error.failureCode !== undefined && identity.code === undefined) {
|
|
537
|
-
identity.code = error.failureCode;
|
|
538
|
-
}
|
|
539
693
|
return {
|
|
540
694
|
cause: error.knownCause,
|
|
541
695
|
diagnostic: error.message || error.name || "exception",
|
|
542
|
-
identity
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
error: serializeThrownValue(error),
|
|
548
|
-
},
|
|
696
|
+
identity: error.failureCode !== undefined && identity.code === undefined
|
|
697
|
+
? { ...identity, code: error.failureCode }
|
|
698
|
+
: identity,
|
|
699
|
+
details,
|
|
700
|
+
...packageFact,
|
|
549
701
|
};
|
|
550
702
|
}
|
|
551
703
|
if (error instanceof Error) {
|
|
@@ -554,72 +706,39 @@ export function projectThrownFailureLeaf(error: unknown): ControlledFailure {
|
|
|
554
706
|
return {
|
|
555
707
|
diagnostic: error.message || error.name || "exception",
|
|
556
708
|
identity,
|
|
557
|
-
details
|
|
709
|
+
details,
|
|
710
|
+
...packageFact,
|
|
558
711
|
};
|
|
559
712
|
}
|
|
560
713
|
return {
|
|
561
714
|
diagnostic: error !== null && typeof error === "object" ? "non-Error throw" : String(error),
|
|
562
|
-
details
|
|
715
|
+
details,
|
|
716
|
+
...packageFact,
|
|
563
717
|
};
|
|
564
718
|
}
|
|
565
719
|
|
|
566
720
|
/**
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
721
|
+
* Merge caller-owned secondary evidence into a classified failure without
|
|
722
|
+
* washing path facts. The package's own facts ride along on every branch, so
|
|
723
|
+
* no fallback path silently drops them.
|
|
570
724
|
*/
|
|
571
|
-
function classifyThrownFailure(error: unknown): ControlledFailure {
|
|
572
|
-
if (!(error instanceof AggregateError)) {
|
|
573
|
-
return projectThrownFailureLeaf(error);
|
|
574
|
-
}
|
|
575
|
-
const leaves = flattenThrownFailureLeaves(error);
|
|
576
|
-
if (leaves.length === 0) {
|
|
577
|
-
// Empty aggregate — retain the aggregate shell rather than invent a cause.
|
|
578
|
-
return projectThrownFailureLeaf(error);
|
|
579
|
-
}
|
|
580
|
-
const primary = projectThrownFailureLeaf(leaves[0]);
|
|
581
|
-
if (leaves.length === 1) {
|
|
582
|
-
return primary;
|
|
583
|
-
}
|
|
584
|
-
const priorConcurrent = Array.isArray(primary.details?.concurrentFailures)
|
|
585
|
-
? primary.details.concurrentFailures
|
|
586
|
-
: [];
|
|
587
|
-
const concurrentFailures = [
|
|
588
|
-
...priorConcurrent,
|
|
589
|
-
...leaves.slice(1).map((leaf) => {
|
|
590
|
-
const secondary = projectThrownFailureLeaf(leaf);
|
|
591
|
-
return {
|
|
592
|
-
...(secondary.cause === undefined ? {} : { cause: secondary.cause }),
|
|
593
|
-
diagnostic: secondary.diagnostic,
|
|
594
|
-
...(secondary.identity === undefined ? {} : { identity: secondary.identity }),
|
|
595
|
-
...(secondary.details === undefined ? {} : { details: secondary.details }),
|
|
596
|
-
};
|
|
597
|
-
}),
|
|
598
|
-
];
|
|
599
|
-
return {
|
|
600
|
-
...(primary.cause === undefined ? {} : { cause: primary.cause }),
|
|
601
|
-
diagnostic: primary.diagnostic,
|
|
602
|
-
...(primary.identity === undefined ? {} : { identity: primary.identity }),
|
|
603
|
-
details: {
|
|
604
|
-
...(primary.details ?? {}),
|
|
605
|
-
concurrentFailures,
|
|
606
|
-
},
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/** Merge caller-owned secondary evidence into a classified failure without washing path facts. */
|
|
611
725
|
function withKnownDetails(
|
|
612
726
|
failure: ControlledFailure,
|
|
613
727
|
knownDetails: Readonly<Record<string, unknown>> | undefined,
|
|
728
|
+
packageFact?: PackageSideFact,
|
|
614
729
|
): ControlledFailure {
|
|
615
|
-
|
|
616
|
-
|
|
730
|
+
const facts = packageFact === undefined || Object.keys(packageFact).length === 0
|
|
731
|
+
? undefined
|
|
732
|
+
: packageFact;
|
|
733
|
+
// The host's own details win outright: this helper must not displace a key the
|
|
734
|
+
// host carried. Only when the host supplied no record at all does the
|
|
735
|
+
// classified failure keep the details it derived from the host's exit.
|
|
617
736
|
return {
|
|
618
737
|
...failure,
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
},
|
|
738
|
+
...(facts === undefined ? {} : { packageFact: facts }),
|
|
739
|
+
// The host's own record replaces the derived one when it supplied one; when
|
|
740
|
+
// it supplied none, the failure keeps what it derived from the host's exit.
|
|
741
|
+
...(knownDetails === undefined ? {} : { details: knownDetails }),
|
|
623
742
|
};
|
|
624
743
|
}
|
|
625
744
|
|
|
@@ -627,11 +746,49 @@ function withKnownDetails(
|
|
|
627
746
|
* Classify a controlled post-admission failure without washing original identities.
|
|
628
747
|
* Cause classes are closed; diagnostic text retains the original identity when known.
|
|
629
748
|
*
|
|
630
|
-
* Order: thrown → knownCause → timeout → activation (nonzero) →
|
|
749
|
+
* Order: thrown → knownCause → timeout → activation (nonzero) → clean exit.
|
|
631
750
|
* knownCause precedes timeout so a co-present typed provider/session identity is not
|
|
632
751
|
* washed when the child also timed out. Cause is never inferred from stderr wording.
|
|
633
|
-
* AggregateError
|
|
752
|
+
* AggregateError shells, direct children and raw testimony stay distinct.
|
|
634
753
|
*/
|
|
754
|
+
/**
|
|
755
|
+
* Wording for a failure the host reported without a diagnostic of its own. The
|
|
756
|
+
* host's signal or cancellation is named when it gave one; otherwise the exit
|
|
757
|
+
* is. Only ever a fallback — a host that reported a diagnostic keeps it
|
|
758
|
+
* (owner 4743ade7: 代码凭什么要去决定cli的失败原因?).
|
|
759
|
+
*/
|
|
760
|
+
function hostReportedFallbackOf(input: {
|
|
761
|
+
readonly timedOut?: boolean;
|
|
762
|
+
readonly code?: number | null;
|
|
763
|
+
readonly signal?: string;
|
|
764
|
+
readonly cancelName?: string;
|
|
765
|
+
}): string {
|
|
766
|
+
if (input.cancelName !== undefined) return processCancelDiagnostic(input.cancelName as CatchableProcessSignal);
|
|
767
|
+
if (input.signal !== undefined) return childSignalDeathDiagnostic(input.signal);
|
|
768
|
+
if (input.timedOut === true) return "role run timed out";
|
|
769
|
+
return `role run failed with exit ${input.code ?? "null"}`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** This call's package-side facts, or undefined when there are none. */
|
|
773
|
+
function packageFactsOf(input: {
|
|
774
|
+
readonly packageFact?: PackageSideFact;
|
|
775
|
+
readonly code?: number | null;
|
|
776
|
+
readonly timedOut?: boolean;
|
|
777
|
+
readonly signal?: string;
|
|
778
|
+
readonly cancelName?: string;
|
|
779
|
+
}): PackageSideFact | undefined {
|
|
780
|
+
const facts: PackageSideFact = {
|
|
781
|
+
...(input.code === undefined ? {} : { exitCode: input.code }),
|
|
782
|
+
...(input.timedOut === true ? { timedOut: true } : {}),
|
|
783
|
+
...(input.signal === undefined || input.signal.length === 0 ? {} : { signal: input.signal }),
|
|
784
|
+
...(input.cancelName === undefined || input.cancelName.length === 0
|
|
785
|
+
? {}
|
|
786
|
+
: { cancelName: input.cancelName }),
|
|
787
|
+
...(input.packageFact ?? {}),
|
|
788
|
+
};
|
|
789
|
+
return Object.keys(facts).length === 0 ? undefined : facts;
|
|
790
|
+
}
|
|
791
|
+
|
|
635
792
|
export function classifyPostAdmissionFailure(input: {
|
|
636
793
|
timedOut: boolean;
|
|
637
794
|
code: number | null;
|
|
@@ -642,7 +799,6 @@ export function classifyPostAdmissionFailure(input: {
|
|
|
642
799
|
* being washed into activation/null-exit paths that treat missing thrown as absence.
|
|
643
800
|
*/
|
|
644
801
|
thrown?: unknown;
|
|
645
|
-
session?: SessionReadiness;
|
|
646
802
|
/** Upstream-typed cause when the failure origin is already known. */
|
|
647
803
|
knownCause?: ControlledFailureCause;
|
|
648
804
|
/** Optional identity paired with knownCause (production channel). */
|
|
@@ -657,71 +813,112 @@ export function classifyPostAdmissionFailure(input: {
|
|
|
657
813
|
knownDiagnostic?: string;
|
|
658
814
|
/** Secondary evidence already carried by the typed production failure. */
|
|
659
815
|
knownDetails?: Readonly<Record<string, unknown>>;
|
|
816
|
+
/** The signal that killed the host child, when one did (a non-normal exit). */
|
|
817
|
+
signal?: string;
|
|
818
|
+
/**
|
|
819
|
+
* A catchable process-signal cancellation the host reported. Like `signal`,
|
|
820
|
+
* it only supplies fallback wording: the host's own diagnostic always wins.
|
|
821
|
+
*/
|
|
822
|
+
readonly cancelName?: string;
|
|
823
|
+
/** Further package-side facts merged under `packageFact`, never the cause. */
|
|
824
|
+
packageFact?: PackageSideFact;
|
|
660
825
|
}): ControlledFailure {
|
|
661
826
|
// Own-key presence, not value: `throw undefined` is a real caught exception.
|
|
827
|
+
// An exception caught after the host already reported its own failure carries
|
|
828
|
+
// both real facts: the host's report stays the cause, and the later exception
|
|
829
|
+
// is kept beside it (失败诚实宪法:接住可以,洗白不行).
|
|
662
830
|
if (Object.hasOwn(input, "thrown")) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
831
|
+
const thrown = projectThrownFailureLeaf(input.thrown);
|
|
832
|
+
// A host that carried a details record reported *something* for this call,
|
|
833
|
+
// even with no typed class: that record is its own and must not be
|
|
834
|
+
// displaced by the exception.
|
|
835
|
+
const hostReported = input.knownCause !== undefined
|
|
836
|
+
|| (input.knownDiagnostic !== undefined && input.knownDiagnostic.trim() !== "")
|
|
837
|
+
|| input.knownIdentity !== undefined
|
|
838
|
+
|| input.knownDetails !== undefined;
|
|
839
|
+
// The host reported no *typed* class, but it did report this call: a
|
|
840
|
+
// nonzero exit, a timeout, or a signal death are its own facts. A later
|
|
841
|
+
// settlement exception does not replace them — it is a different failure
|
|
842
|
+
// and rides beside them (owner 4743ade7: 代码凭什么要去决定cli的失败原因?).
|
|
843
|
+
if (!hostReported) {
|
|
844
|
+
// A number is the host having reported an exit. A null code with a signal
|
|
845
|
+
// is its native signal-death report. Anything else means the host produced
|
|
846
|
+
// no result at all, and then the exception really is the only fact there
|
|
847
|
+
// is — there is nothing of the host's to preserve over it.
|
|
848
|
+
const hostFailed = input.timedOut
|
|
849
|
+
|| input.signal !== undefined
|
|
850
|
+
|| (typeof input.code === "number" && input.code !== 0);
|
|
851
|
+
if (!hostFailed) {
|
|
852
|
+
const facts = packageFactsOf(input);
|
|
853
|
+
if (facts?.cancelName === undefined && facts?.signal === undefined) return thrown;
|
|
854
|
+
return {
|
|
855
|
+
...thrown,
|
|
856
|
+
packageFact: { ...facts, ...(thrown.packageFact ?? {}) },
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
const facts = packageFactsOf(input);
|
|
860
|
+
const packageFact = facts === undefined ? { thrown } : { ...facts, thrown };
|
|
861
|
+
return {
|
|
862
|
+
...(input.timedOut ? { cause: "timeout" as const } : {}),
|
|
863
|
+
...(input.signal === undefined ? {} : { identity: { name: "ChildSignalDeath", code: input.signal } }),
|
|
864
|
+
diagnostic: conciseChildDiagnostic(
|
|
865
|
+
input.stderr,
|
|
866
|
+
hostReportedFallbackOf(input),
|
|
867
|
+
),
|
|
868
|
+
// The host's record is untouched; this package's view of the exit and
|
|
869
|
+
// the caught exception both ride under `packageFact`.
|
|
870
|
+
packageFact,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
// The host's report is presented exactly as it gave it — a field it left out
|
|
874
|
+
// is not filled in from the exception, which is a different failure. The
|
|
875
|
+
// exception is kept whole beside it under its own key, so neither erases
|
|
876
|
+
// the other (失败诚实宪法:接住可以,洗白不行).
|
|
681
877
|
return {
|
|
682
|
-
cause: input.knownCause,
|
|
683
|
-
diagnostic
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
878
|
+
...(input.knownCause === undefined ? {} : { cause: input.knownCause }),
|
|
879
|
+
// A settled failure always presents a diagnostic. The host's own is used
|
|
880
|
+
// when it gave one; when it gave none, this call's own stderr supplies the
|
|
881
|
+
// text. The exception is a different failure and never fills this field —
|
|
882
|
+
// it rides whole under `packageFact` (owner 4743ade7: 代码凭什么要去决定cli的失败原因?).
|
|
883
|
+
diagnostic: input.knownDiagnostic !== undefined && input.knownDiagnostic.trim() !== ""
|
|
884
|
+
? input.knownDiagnostic
|
|
885
|
+
: conciseChildDiagnostic(input.stderr, hostReportedFallbackOf(input)),
|
|
690
886
|
...(input.knownIdentity === undefined
|
|
691
887
|
? {}
|
|
692
888
|
: { identity: input.knownIdentity }),
|
|
889
|
+
// The host's details are handed back exactly as given.
|
|
890
|
+
...(input.knownDetails === undefined ? {} : { details: input.knownDetails }),
|
|
891
|
+
// Every fact this package captured on this call — the caught exception and
|
|
892
|
+
// any earlier auxiliary read failure — is reported, none of them dropped.
|
|
893
|
+
packageFact: { ...packageFactsOf(input), thrown },
|
|
693
894
|
};
|
|
694
895
|
}
|
|
695
|
-
// #881: original
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
// not a stand-in primary failure record.
|
|
699
|
-
if (
|
|
896
|
+
// #881: untyped original testimony retains diagnostic/identity, not a
|
|
897
|
+
// fabricated activation/output cause. Bare details remain secondary evidence.
|
|
898
|
+
if (input.knownCause !== undefined ||
|
|
700
899
|
(input.knownDiagnostic !== undefined && input.knownDiagnostic.trim() !== "") ||
|
|
701
|
-
input.knownIdentity !== undefined
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
input.
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
undefined,
|
|
724
|
-
);
|
|
900
|
+
input.knownIdentity !== undefined) {
|
|
901
|
+
const fallback =
|
|
902
|
+
input.knownCause === "provider" ? "provider failure"
|
|
903
|
+
: input.knownCause === "session" ? "session unreadable"
|
|
904
|
+
: input.knownCause === "output" ? "role run completed without a lawful typed terminal result"
|
|
905
|
+
: input.knownCause === undefined ? "role run failed"
|
|
906
|
+
: `role run failed (${input.knownCause})`;
|
|
907
|
+
const diagnostic = input.knownDiagnostic !== undefined && input.knownDiagnostic.trim() !== ""
|
|
908
|
+
? input.knownDiagnostic
|
|
909
|
+
: conciseChildDiagnostic(input.stderr, fallback);
|
|
910
|
+
const packageFact = packageFactsOf(input);
|
|
911
|
+
return {
|
|
912
|
+
...(input.knownCause === undefined ? {} : { cause: input.knownCause }),
|
|
913
|
+
diagnostic,
|
|
914
|
+
// The host's details are handed back byte-for-byte. This package's own
|
|
915
|
+
// view of the exit rides beside them — writing `exitCode` in there would
|
|
916
|
+
// overwrite a value the host itself carried (host-contracts.ts:41: an
|
|
917
|
+
// open read-only record with no reserved keys).
|
|
918
|
+
...(input.knownDetails === undefined ? {} : { details: input.knownDetails }),
|
|
919
|
+
...(packageFact === undefined ? {} : { packageFact }),
|
|
920
|
+
...(input.knownIdentity === undefined ? {} : { identity: input.knownIdentity }),
|
|
921
|
+
};
|
|
725
922
|
}
|
|
726
923
|
if (input.timedOut) {
|
|
727
924
|
return withKnownDetails(
|
|
@@ -731,46 +928,38 @@ export function classifyPostAdmissionFailure(input: {
|
|
|
731
928
|
details: { timedOut: true, exitCode: input.code },
|
|
732
929
|
},
|
|
733
930
|
input.knownDetails,
|
|
931
|
+
packageFactsOf(input),
|
|
734
932
|
);
|
|
735
933
|
}
|
|
736
934
|
if (input.code !== 0) {
|
|
737
|
-
|
|
935
|
+
// A nonzero exit says the run failed, not why. No typed fact confirmed a
|
|
936
|
+
// class, so `cause` stays absent and the CLI's own diagnostic and exit code
|
|
937
|
+
// are what remains (host contract: omit cause, keep the original). A signal
|
|
938
|
+
// or cancellation the host did report is named, not restated as an exit.
|
|
939
|
+
const fallback = hostReportedFallbackOf(input);
|
|
738
940
|
return withKnownDetails(
|
|
739
941
|
{
|
|
740
|
-
cause: "activation",
|
|
741
942
|
diagnostic: conciseChildDiagnostic(input.stderr, fallback),
|
|
742
943
|
details: { exitCode: input.code },
|
|
743
944
|
},
|
|
744
945
|
input.knownDetails,
|
|
946
|
+
packageFactsOf(input),
|
|
745
947
|
);
|
|
746
948
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
details: { exitCode: input.code, session: "missing" },
|
|
753
|
-
},
|
|
754
|
-
input.knownDetails,
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
|
-
if (input.session?.state === "unreadable") {
|
|
758
|
-
return withKnownDetails(
|
|
759
|
-
{
|
|
760
|
-
cause: "session",
|
|
761
|
-
diagnostic: input.session.diagnostic,
|
|
762
|
-
details: { exitCode: input.code, session: "unreadable" },
|
|
763
|
-
},
|
|
764
|
-
input.knownDetails,
|
|
765
|
-
);
|
|
766
|
-
}
|
|
949
|
+
// No typed fact confirmed a class, so none is asserted: the host exited
|
|
950
|
+
// cleanly, and the package has nothing to add (host-contracts.ts:19–42: omit
|
|
951
|
+
// cause and keep the original when no typed confirmation exists). A caller
|
|
952
|
+
// that must present a failure for a clean run settles it on the no_receipt
|
|
953
|
+
// path, not by minting a class here.
|
|
767
954
|
return withKnownDetails(
|
|
768
955
|
{
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
956
|
+
diagnostic: conciseChildDiagnostic(
|
|
957
|
+
input.stderr,
|
|
958
|
+
"role run completed without a lawful typed terminal result",
|
|
959
|
+
),
|
|
772
960
|
},
|
|
773
961
|
input.knownDetails,
|
|
962
|
+
packageFactsOf(input),
|
|
774
963
|
);
|
|
775
964
|
}
|
|
776
965
|
|
|
@@ -831,996 +1020,26 @@ type SessionEntry = {
|
|
|
831
1020
|
/** Session principal id from the durable header entry. */
|
|
832
1021
|
id?: string;
|
|
833
1022
|
/** Session cwd from the durable header entry. */
|
|
834
|
-
cwd?: string;
|
|
835
|
-
/** Parent session principal on durable child session headers. */
|
|
836
|
-
parentSession?: string;
|
|
837
|
-
};
|
|
838
|
-
|
|
839
|
-
/**
|
|
840
|
-
* Preserve session-read failure identity as a typed session cause.
|
|
841
|
-
* SyntaxError keeps its name so durable settlement does not wash malformed JSONL
|
|
842
|
-
* into generic output absence.
|
|
843
|
-
*/
|
|
844
|
-
function sessionReadFailure(
|
|
845
|
-
error: unknown,
|
|
846
|
-
fallbackMessage: string,
|
|
847
|
-
): Error & {
|
|
848
|
-
knownCause: ControlledFailureCause;
|
|
849
|
-
failureCode?: string | number;
|
|
850
|
-
} {
|
|
851
|
-
if (error instanceof SyntaxError) {
|
|
852
|
-
const failed = new SyntaxError(
|
|
853
|
-
error.message || fallbackMessage,
|
|
854
|
-
) as SyntaxError & {
|
|
855
|
-
knownCause: ControlledFailureCause;
|
|
856
|
-
failureCode?: string | number;
|
|
857
|
-
};
|
|
858
|
-
failed.knownCause = "session";
|
|
859
|
-
return failed;
|
|
860
|
-
}
|
|
861
|
-
if (error instanceof Error) {
|
|
862
|
-
const failed = new Error(
|
|
863
|
-
error.message || error.name || fallbackMessage,
|
|
864
|
-
) as Error & {
|
|
865
|
-
knownCause: ControlledFailureCause;
|
|
866
|
-
failureCode?: string | number;
|
|
867
|
-
code?: string | number;
|
|
868
|
-
};
|
|
869
|
-
failed.name = error.name || "Error";
|
|
870
|
-
failed.knownCause = "session";
|
|
871
|
-
const code = (error as { code?: unknown }).code;
|
|
872
|
-
if (typeof code === "string" || typeof code === "number") {
|
|
873
|
-
failed.failureCode = code;
|
|
874
|
-
failed.code = code;
|
|
875
|
-
}
|
|
876
|
-
return failed;
|
|
877
|
-
}
|
|
878
|
-
const failed = new Error(String(error)) as Error & {
|
|
879
|
-
knownCause: ControlledFailureCause;
|
|
880
|
-
failureCode?: string | number;
|
|
881
|
-
};
|
|
882
|
-
failed.knownCause = "session";
|
|
883
|
-
return failed;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* Read the exact bound Pi session file principal.
|
|
888
|
-
* Does not scan the session directory for "latest" — resume identity is the file.
|
|
889
|
-
*/
|
|
890
|
-
export async function readBoundSessionEntries(
|
|
891
|
-
sessionFile: string,
|
|
892
|
-
): Promise<SessionEntry[]> {
|
|
893
|
-
const text = await readFile(sessionFile, "utf8");
|
|
894
|
-
const entries: SessionEntry[] = [];
|
|
895
|
-
for (const line of text.trim().split("\n").filter(Boolean)) {
|
|
896
|
-
try {
|
|
897
|
-
entries.push(JSON.parse(line) as SessionEntry);
|
|
898
|
-
} catch (error) {
|
|
899
|
-
throw sessionReadFailure(error, "malformed session JSONL");
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
return entries;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* Latest native assistant provider-stop in a session (stopReason error|aborted).
|
|
907
|
-
* Only the final assistant turn decides terminality — an older error followed by a
|
|
908
|
-
* later non-error stop is not a provider failure (would wash a no-lawful-output path).
|
|
909
|
-
* Typed production source for provider cause — not child stderr prose.
|
|
910
|
-
*/
|
|
911
|
-
type SessionProviderStop = {
|
|
912
|
-
stopReason: "error" | "aborted";
|
|
913
|
-
errorMessage?: string;
|
|
914
|
-
provider?: string;
|
|
915
|
-
model?: string;
|
|
916
|
-
api?: string;
|
|
917
|
-
rawStopReason?: string;
|
|
918
|
-
diagnostics?: unknown;
|
|
919
|
-
httpStatus?: number;
|
|
920
|
-
body?: unknown;
|
|
921
|
-
code?: unknown;
|
|
922
|
-
errno?: unknown;
|
|
923
|
-
};
|
|
924
|
-
|
|
925
|
-
function typedHttpStatusFromMessage(message: SessionMessage): number | undefined {
|
|
926
|
-
for (const candidate of [message.httpStatus, message.statusCode, message.status]) {
|
|
927
|
-
if (typeof candidate === "number" && (candidate < 200 || candidate >= 300)) return candidate;
|
|
928
|
-
}
|
|
929
|
-
return undefined;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function sessionProviderStopFromAssistant(message: SessionMessage | undefined): SessionProviderStop | undefined {
|
|
933
|
-
if (message?.role !== "assistant") return undefined;
|
|
934
|
-
if (message.stopReason !== "error" && message.stopReason !== "aborted") return undefined;
|
|
935
|
-
const httpStatus = typedHttpStatusFromMessage(message);
|
|
936
|
-
return {
|
|
937
|
-
stopReason: message.stopReason,
|
|
938
|
-
// Preserve held errorMessage bytes — emptiness check must not rewrite.
|
|
939
|
-
...(typeof message.errorMessage === "string" && message.errorMessage.trim() !== ""
|
|
940
|
-
? { errorMessage: message.errorMessage }
|
|
941
|
-
: {}),
|
|
942
|
-
...(typeof message.provider === "string" && message.provider.trim() !== ""
|
|
943
|
-
? { provider: message.provider }
|
|
944
|
-
: {}),
|
|
945
|
-
...(typeof message.model === "string" && message.model.trim() !== ""
|
|
946
|
-
? { model: message.model }
|
|
947
|
-
: {}),
|
|
948
|
-
...(typeof message.api === "string" && message.api.trim() !== ""
|
|
949
|
-
? { api: message.api }
|
|
950
|
-
: {}),
|
|
951
|
-
...(typeof message.rawStopReason === "string" && message.rawStopReason.trim() !== ""
|
|
952
|
-
? { rawStopReason: message.rawStopReason }
|
|
953
|
-
: {}),
|
|
954
|
-
...(message.diagnostics === undefined ? {} : { diagnostics: message.diagnostics }),
|
|
955
|
-
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
956
|
-
...(message.body === undefined ? {} : { body: message.body }),
|
|
957
|
-
...(message.code === undefined ? {} : { code: message.code }),
|
|
958
|
-
...(message.errno === undefined ? {} : { errno: message.errno }),
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
export function extractSessionProviderStop(
|
|
963
|
-
entries: readonly SessionEntry[],
|
|
964
|
-
): SessionProviderStop | undefined {
|
|
965
|
-
// A resumed dispatch appends a typed top-level user turn to the same session.
|
|
966
|
-
// Older attempt native stops must not replace the newer attempt's stop.
|
|
967
|
-
// Sessions without a user turn are the initial attempt.
|
|
968
|
-
// Auditor retained responses live in Sitian (kind=auditor); see readSessionProviderStop.
|
|
969
|
-
let attemptStart = 0;
|
|
970
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
971
|
-
const entry = entries[i];
|
|
972
|
-
if (entry?.type === "message" && entry.message?.role === "user") {
|
|
973
|
-
attemptStart = i;
|
|
974
|
-
break;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
for (let i = entries.length - 1; i >= attemptStart; i -= 1) {
|
|
979
|
-
const entry = entries[i];
|
|
980
|
-
if (entry?.type !== "message") continue;
|
|
981
|
-
const message = entry.message;
|
|
982
|
-
if (message?.role !== "assistant") continue;
|
|
983
|
-
// Latest assistant in the current attempt only.
|
|
984
|
-
return sessionProviderStopFromAssistant(message);
|
|
985
|
-
}
|
|
986
|
-
return undefined;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
/**
|
|
990
|
-
* Latest Sitian-retained auditor response stop for this parent session principal.
|
|
991
|
-
* Writer: retainComplianceResponse → sitianReport(kind=auditor, payload={version,response}).
|
|
992
|
-
* Payload stopReason is preserved as retained — aborted stays aborted; no 500/error wash here.
|
|
993
|
-
*/
|
|
994
|
-
async function readSitianRetainedAuditorProviderStop(
|
|
995
|
-
sessionFile: string,
|
|
996
|
-
): Promise<SessionProviderStop | undefined> {
|
|
997
|
-
try {
|
|
998
|
-
const { recordFile } = resolveSitianRecordPath({
|
|
999
|
-
level: "event",
|
|
1000
|
-
kind: "auditor",
|
|
1001
|
-
sessionParent: sessionFile,
|
|
1002
|
-
// Path is driven by sessionParent when under ledger home; cwd is a fallback only.
|
|
1003
|
-
cwd: dirname(sessionFile),
|
|
1004
|
-
});
|
|
1005
|
-
const { records } = await readSitianRecords(recordFile);
|
|
1006
|
-
for (let i = records.length - 1; i >= 0; i -= 1) {
|
|
1007
|
-
const payload = records[i]?.payload;
|
|
1008
|
-
if (!isRecord(payload) || !isRecord(payload.response)) continue;
|
|
1009
|
-
// Lifecycle events carry `type` (binding / compliance_failure); retain does not.
|
|
1010
|
-
if (typeof payload.type === "string") continue;
|
|
1011
|
-
const stop = sessionProviderStopFromAssistant(payload.response as SessionMessage);
|
|
1012
|
-
if (stop !== undefined) return stop;
|
|
1013
|
-
// Latest retain exists but is not a provider-stop — do not scan older retains
|
|
1014
|
-
// (mirrors former session COMPLIANCE_RESPONSE preference break).
|
|
1015
|
-
break;
|
|
1016
|
-
}
|
|
1017
|
-
} catch {
|
|
1018
|
-
// Missing volume or unreadable path is absence, not a settlement failure.
|
|
1019
|
-
}
|
|
1020
|
-
return undefined;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/** Read retained auditor stop (Sitian) then native session assistant stop, if any. */
|
|
1024
|
-
export async function readSessionProviderStop(
|
|
1025
|
-
sessionFile: string,
|
|
1026
|
-
): Promise<SessionProviderStop | undefined> {
|
|
1027
|
-
const retained = await readSitianRetainedAuditorProviderStop(sessionFile);
|
|
1028
|
-
if (retained !== undefined) return retained;
|
|
1029
|
-
try {
|
|
1030
|
-
const entries = await readBoundSessionEntries(sessionFile);
|
|
1031
|
-
return extractSessionProviderStop(entries);
|
|
1032
|
-
} catch {
|
|
1033
|
-
return undefined;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
type BoundAuditorVolume = {
|
|
1038
|
-
readonly entries: SessionEntry[];
|
|
1039
|
-
readonly attemptEntryId?: string;
|
|
1040
|
-
readonly parentId: string;
|
|
1041
|
-
readonly sessionFile: string;
|
|
1042
|
-
};
|
|
1043
|
-
|
|
1044
|
-
async function loadBoundAuditorVolumes(
|
|
1045
|
-
sessionFile: string,
|
|
1046
|
-
): Promise<readonly BoundAuditorVolume[] | undefined> {
|
|
1047
|
-
let parentEntries: SessionEntry[];
|
|
1048
|
-
try {
|
|
1049
|
-
parentEntries = await readBoundSessionEntries(sessionFile);
|
|
1050
|
-
} catch (error) {
|
|
1051
|
-
if (isEnoent(error)) return undefined;
|
|
1052
|
-
throw sessionReadFailure(error, "failed to read parent session for auditor binding");
|
|
1053
|
-
}
|
|
1054
|
-
const parentId = parentEntries.find((entry) => entry.type === "session")?.id;
|
|
1055
|
-
if (parentId === undefined) return undefined;
|
|
1056
|
-
const childDirectories = [join(dirname(sessionFile), "auditor-roles")];
|
|
1057
|
-
const valid: BoundAuditorVolume[] = [];
|
|
1058
|
-
let sawAnyDirectory = false;
|
|
1059
|
-
for (const childDirectory of childDirectories) {
|
|
1060
|
-
let names: string[];
|
|
1061
|
-
try {
|
|
1062
|
-
names = await readdir(childDirectory);
|
|
1063
|
-
sawAnyDirectory = true;
|
|
1064
|
-
} catch (error) {
|
|
1065
|
-
if (isEnoent(error)) continue;
|
|
1066
|
-
throw sessionReadFailure(error, "failed to read bound auditor session directory");
|
|
1067
|
-
}
|
|
1068
|
-
for (const file of names.filter((name) => name.endsWith(".jsonl")).sort().reverse()) {
|
|
1069
|
-
let entries: SessionEntry[];
|
|
1070
|
-
try {
|
|
1071
|
-
entries = await readBoundSessionEntries(join(childDirectory, file));
|
|
1072
|
-
} catch (error) {
|
|
1073
|
-
throw sessionReadFailure(error, "failed to read discovered auditor session");
|
|
1074
|
-
}
|
|
1075
|
-
const header = entries.find((entry) => entry.type === "session");
|
|
1076
|
-
if (!isRecord(header)) continue;
|
|
1077
|
-
// Parent-attempt binding owns its interval on multi-attempt volumes
|
|
1078
|
-
// (never whole-volume provider/compliance).
|
|
1079
|
-
const bindingIndexes: number[] = [];
|
|
1080
|
-
for (let i = 0; i < entries.length; i += 1) {
|
|
1081
|
-
const entry = entries[i];
|
|
1082
|
-
if (entry?.type === "custom" && entry.customType === AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE) {
|
|
1083
|
-
bindingIndexes.push(i);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
const bindingPasses: Array<{ entry: SessionEntry | undefined; start: number; end: number }> =
|
|
1087
|
-
bindingIndexes.length > 0
|
|
1088
|
-
? bindingIndexes.map((start, idx) => ({
|
|
1089
|
-
entry: entries[start],
|
|
1090
|
-
start,
|
|
1091
|
-
end: idx + 1 < bindingIndexes.length ? bindingIndexes[idx + 1]! : entries.length,
|
|
1092
|
-
}))
|
|
1093
|
-
: [{ entry: undefined, start: 0, end: entries.length }];
|
|
1094
|
-
for (const { entry: bindingEntry, start, end } of bindingPasses) {
|
|
1095
|
-
const bindingParent =
|
|
1096
|
-
bindingEntry !== undefined &&
|
|
1097
|
-
isRecord(bindingEntry.data) &&
|
|
1098
|
-
isRecord(bindingEntry.data.parent)
|
|
1099
|
-
? bindingEntry.data.parent
|
|
1100
|
-
: undefined;
|
|
1101
|
-
const attemptEntryId =
|
|
1102
|
-
typeof bindingParent?.attemptEntryId === "string"
|
|
1103
|
-
? bindingParent.attemptEntryId
|
|
1104
|
-
: undefined;
|
|
1105
|
-
const boundSessionFile =
|
|
1106
|
-
typeof bindingParent?.sessionFile === "string"
|
|
1107
|
-
? bindingParent.sessionFile
|
|
1108
|
-
: typeof header.parentSession === "string"
|
|
1109
|
-
? header.parentSession
|
|
1110
|
-
: undefined;
|
|
1111
|
-
if (boundSessionFile !== sessionFile) continue;
|
|
1112
|
-
if (bindingParent !== undefined && bindingParent.sessionId !== parentId) continue;
|
|
1113
|
-
if (bindingParent === undefined && header.parentSession !== sessionFile) continue;
|
|
1114
|
-
valid.push({
|
|
1115
|
-
entries: entries.slice(start, end),
|
|
1116
|
-
parentId,
|
|
1117
|
-
sessionFile,
|
|
1118
|
-
...(attemptEntryId === undefined ? {} : { attemptEntryId }),
|
|
1119
|
-
});
|
|
1120
|
-
// Keep every interval bound to this parent. Auditor payload is relayed as
|
|
1121
|
-
// recorded; code does not expire it from later user-message shape (#858).
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
if (!sawAnyDirectory && valid.length === 0) return undefined;
|
|
1126
|
-
return valid;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
function complianceFailureFromAuditorVolumes(
|
|
1130
|
-
volumes: readonly BoundAuditorVolume[],
|
|
1131
|
-
): RoleTurnKnownFailure | undefined {
|
|
1132
|
-
for (const { entries, attemptEntryId, parentId, sessionFile } of volumes) {
|
|
1133
|
-
const stop = extractSessionProviderStop(entries);
|
|
1134
|
-
if (stop === undefined) continue;
|
|
1135
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1136
|
-
const entry = entries[i];
|
|
1137
|
-
if (entry?.type !== "custom" || entry.customType !== AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE || !isRecord(entry.data)) continue;
|
|
1138
|
-
const parent = isRecord(entry.data.parent) ? entry.data.parent : undefined;
|
|
1139
|
-
const failure = isRecord(entry.data.failure) ? entry.data.failure : undefined;
|
|
1140
|
-
if (parent?.sessionId !== parentId || parent.sessionFile !== sessionFile || parent.attemptEntryId !== attemptEntryId) continue;
|
|
1141
|
-
// #881: keep the recorded failure as written — typed cause when present, else raw diagnostic only.
|
|
1142
|
-
if (failure === undefined) continue;
|
|
1143
|
-
const identity = isRecord(failure.identity) ? failure.identity : undefined;
|
|
1144
|
-
const typedCause =
|
|
1145
|
-
failure.cause === "provider" ||
|
|
1146
|
-
failure.cause === "activation" ||
|
|
1147
|
-
failure.cause === "session" ||
|
|
1148
|
-
failure.cause === "output" ||
|
|
1149
|
-
failure.cause === "timeout"
|
|
1150
|
-
? (failure.cause as ControlledFailureCause)
|
|
1151
|
-
: undefined;
|
|
1152
|
-
return {
|
|
1153
|
-
...(typedCause === undefined ? {} : { cause: typedCause }),
|
|
1154
|
-
...(identity === undefined ? {} : { identity: {
|
|
1155
|
-
...(typeof identity.name === "string" ? { name: identity.name } : {}),
|
|
1156
|
-
...(typeof identity.code === "string" || typeof identity.code === "number" ? { code: identity.code } : {}),
|
|
1157
|
-
} }),
|
|
1158
|
-
...(typeof failure.diagnostic === "string" ? { diagnostic: failure.diagnostic } : {}),
|
|
1159
|
-
...(isRecord(failure.details) ? { details: failure.details } : {}),
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
return undefined;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function providerStopFallbackFromAuditorVolumes(
|
|
1167
|
-
volumes: readonly BoundAuditorVolume[],
|
|
1168
|
-
): RoleTurnKnownFailure | undefined {
|
|
1169
|
-
for (const { entries } of volumes) {
|
|
1170
|
-
const stop = extractSessionProviderStop(entries);
|
|
1171
|
-
if (stop === undefined) continue;
|
|
1172
|
-
const primary = knownFailureFromProviderStop(stop)!;
|
|
1173
|
-
return {
|
|
1174
|
-
...primary,
|
|
1175
|
-
details: {
|
|
1176
|
-
...(primary.details ?? {}),
|
|
1177
|
-
secondaryEvidence: "unavailable",
|
|
1178
|
-
},
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
return undefined;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
/** Strong auditor tier only — retained compliance-failure entries, no provider-stop fallback. */
|
|
1185
|
-
async function readBoundAuditorComplianceFailure(
|
|
1186
|
-
sessionFile: string,
|
|
1187
|
-
): Promise<RoleTurnKnownFailure | undefined> {
|
|
1188
|
-
const volumes = await loadBoundAuditorVolumes(sessionFile);
|
|
1189
|
-
if (volumes === undefined) return undefined;
|
|
1190
|
-
return complianceFailureFromAuditorVolumes(volumes);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
/** Weaker auditor tier: provider stop without a retained compliance-failure entry. */
|
|
1194
|
-
async function readBoundAuditorProviderStopFallback(
|
|
1195
|
-
sessionFile: string,
|
|
1196
|
-
): Promise<RoleTurnKnownFailure | undefined> {
|
|
1197
|
-
const volumes = await loadBoundAuditorVolumes(sessionFile);
|
|
1198
|
-
if (volumes === undefined) return undefined;
|
|
1199
|
-
return providerStopFallbackFromAuditorVolumes(volumes);
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
function typedFailedTerminatingToolKnownFailure(
|
|
1203
|
-
entries: readonly SessionEntry[],
|
|
1204
|
-
): RoleTurnKnownFailure | undefined {
|
|
1205
|
-
let attemptStart = 0;
|
|
1206
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1207
|
-
if (entries[i]?.type === "message" && entries[i]?.message?.role === "user") {
|
|
1208
|
-
attemptStart = i;
|
|
1209
|
-
break;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
const attemptEntries = entries.slice(attemptStart);
|
|
1213
|
-
for (let i = attemptEntries.length - 1; i >= 0; i -= 1) {
|
|
1214
|
-
const message = attemptEntries[i]?.message;
|
|
1215
|
-
if (attemptEntries[i]?.type !== "message" || message?.role !== "toolResult") continue;
|
|
1216
|
-
const classification = classifyPackagedRoleTerminalResult(message);
|
|
1217
|
-
if (classification.kind !== "infrastructure") continue;
|
|
1218
|
-
if (typeof message.toolCallId !== "string" || typeof message.toolName !== "string") continue;
|
|
1219
|
-
if (boundRoleToolCallForResult(attemptEntries, i, message, message.toolName) === undefined) continue;
|
|
1220
|
-
const textPart = Array.isArray(message.content)
|
|
1221
|
-
? message.content.find((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
|
|
1222
|
-
: undefined;
|
|
1223
|
-
const diagnostic = isRecord(textPart) ? textPart.text : undefined;
|
|
1224
|
-
// Durable details already carry fact + typed evidence from envelope one-shot projection (#475).
|
|
1225
|
-
// Do not re-parse retained compliance responses here.
|
|
1226
|
-
// Host infrastructure must NOT map to cause=output — that cause is reserved for
|
|
1227
|
-
// role-output failures (isError residual / no sealed receipt), not host infra.
|
|
1228
|
-
const details = isRecord(message.details) ? message.details : classification.fact;
|
|
1229
|
-
return {
|
|
1230
|
-
cause: "activation",
|
|
1231
|
-
identity: { name: message.toolName, code: message.toolCallId },
|
|
1232
|
-
...(typeof diagnostic === "string" && diagnostic.trim() !== "" ? { diagnostic } : {}),
|
|
1233
|
-
details,
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
return undefined;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
/**
|
|
1240
|
-
* One audited-runner resolution: knownFailure plus the typed-HTTP sidecar outcome
|
|
1241
|
-
* from the same read. Callers that also decide v1 resume must consume this once —
|
|
1242
|
-
* never re-read the sidecar in presentControlledFailure.
|
|
1243
|
-
*/
|
|
1244
|
-
export type AuditedRunnerFailureResolution = {
|
|
1245
|
-
readonly knownFailure?: RoleTurnKnownFailure;
|
|
1246
|
-
/** Successful sidecar read (not absence). */
|
|
1247
|
-
readonly typedHttpObservation?: TypedProviderHttpObservation;
|
|
1248
|
-
/**
|
|
1249
|
-
* True when this resolution already performed the typed-HTTP sidecar read
|
|
1250
|
-
* (success, absence, or non-absence failure folded into knownFailure).
|
|
1251
|
-
* False when an earlier evidence tier short-circuited before the sidecar.
|
|
1252
|
-
*/
|
|
1253
|
-
readonly typedHttpObservationSettled: boolean;
|
|
1254
|
-
};
|
|
1255
|
-
|
|
1256
|
-
function resolutionOf(
|
|
1257
|
-
knownFailure: RoleTurnKnownFailure | undefined,
|
|
1258
|
-
typedHttp: {
|
|
1259
|
-
readonly settled: boolean;
|
|
1260
|
-
readonly observation?: TypedProviderHttpObservation;
|
|
1261
|
-
} = { settled: false },
|
|
1262
|
-
): AuditedRunnerFailureResolution {
|
|
1263
|
-
return {
|
|
1264
|
-
...(knownFailure === undefined ? {} : { knownFailure }),
|
|
1265
|
-
...(typedHttp.observation === undefined ? {} : { typedHttpObservation: typedHttp.observation }),
|
|
1266
|
-
typedHttpObservationSettled: typedHttp.settled,
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
/** Sole evidence-priority owner for public runners with Soul auditors. */
|
|
1271
|
-
export async function resolveAuditedRunnerFailureResolution(input: {
|
|
1272
|
-
runner: RoleTurnKnownFailure | undefined;
|
|
1273
|
-
sessionFile: string;
|
|
1274
|
-
credential: RoleTurnKnownFailure | undefined;
|
|
1275
|
-
/** Optional run directory for typed provider HTTP observation (resume/429). */
|
|
1276
|
-
runDirectory?: string;
|
|
1277
|
-
}): Promise<AuditedRunnerFailureResolution> {
|
|
1278
|
-
if (input.runner !== undefined) return resolutionOf(input.runner);
|
|
1279
|
-
// Bound auditor compliance-failure retention outranks a parent failure that the
|
|
1280
|
-
// auditor path itself caused (retention EISDIR race). A typed terminating-tool
|
|
1281
|
-
// host failure is next — it outranks weaker auditor provider-stop fallback so
|
|
1282
|
-
// parent failInfrastructure abort pollution cannot wash a real diagnostic (#475).
|
|
1283
|
-
try {
|
|
1284
|
-
const auditorCompliance = await readBoundAuditorComplianceFailure(input.sessionFile);
|
|
1285
|
-
if (auditorCompliance !== undefined) return resolutionOf(auditorCompliance);
|
|
1286
|
-
} catch (error) {
|
|
1287
|
-
const failure = sessionReadFailure(error, "failed to recover bound auditor failure");
|
|
1288
|
-
return resolutionOf({
|
|
1289
|
-
cause: "session",
|
|
1290
|
-
identity: thrownIdentity(failure),
|
|
1291
|
-
diagnostic: failure.message || failure.name,
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
try {
|
|
1295
|
-
const terminatingFailure = typedFailedTerminatingToolKnownFailure(
|
|
1296
|
-
await readBoundSessionEntries(input.sessionFile),
|
|
1297
|
-
);
|
|
1298
|
-
if (terminatingFailure !== undefined) return resolutionOf(terminatingFailure);
|
|
1299
|
-
} catch (error) {
|
|
1300
|
-
if (!isEnoent(error)) {
|
|
1301
|
-
const failure = sessionReadFailure(error, "failed to recover typed terminating-tool failure");
|
|
1302
|
-
return resolutionOf({
|
|
1303
|
-
cause: "session",
|
|
1304
|
-
identity: thrownIdentity(failure),
|
|
1305
|
-
diagnostic: failure.message || failure.name,
|
|
1306
|
-
});
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
try {
|
|
1310
|
-
const auditorStop = await readBoundAuditorProviderStopFallback(input.sessionFile);
|
|
1311
|
-
if (auditorStop !== undefined) return resolutionOf(auditorStop);
|
|
1312
|
-
} catch (error) {
|
|
1313
|
-
const failure = sessionReadFailure(error, "failed to recover bound auditor provider stop");
|
|
1314
|
-
return resolutionOf({
|
|
1315
|
-
cause: "session",
|
|
1316
|
-
identity: thrownIdentity(failure),
|
|
1317
|
-
diagnostic: failure.message || failure.name,
|
|
1318
|
-
});
|
|
1319
|
-
}
|
|
1320
|
-
// Parent session provider-stop is next; credential is last.
|
|
1321
|
-
const parentStop = await readSessionProviderStop(input.sessionFile);
|
|
1322
|
-
// Typed HTTP observation: ENOENT=absence; other read/parse/shape failures keep real cause.
|
|
1323
|
-
// This is the single sidecar read for both knownFailure projection and v1 resume.
|
|
1324
|
-
let httpObservation: TypedProviderHttpObservation | undefined;
|
|
1325
|
-
if (input.runDirectory !== undefined) {
|
|
1326
|
-
try {
|
|
1327
|
-
httpObservation = await readLatestTypedProviderHttpObservation(input.runDirectory);
|
|
1328
|
-
} catch (error) {
|
|
1329
|
-
const failure = error instanceof Error ? error : new Error(String(error));
|
|
1330
|
-
return resolutionOf(
|
|
1331
|
-
{
|
|
1332
|
-
cause: "session",
|
|
1333
|
-
identity: thrownIdentity(failure),
|
|
1334
|
-
diagnostic: failure.message || failure.name,
|
|
1335
|
-
},
|
|
1336
|
-
{ settled: true },
|
|
1337
|
-
);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
const typedHttp = {
|
|
1341
|
-
settled: input.runDirectory !== undefined,
|
|
1342
|
-
...(httpObservation === undefined ? {} : { observation: httpObservation }),
|
|
1343
|
-
};
|
|
1344
|
-
if (parentStop === undefined) {
|
|
1345
|
-
if (input.credential !== undefined) return resolutionOf(input.credential, typedHttp);
|
|
1346
|
-
if (httpObservation === undefined) return resolutionOf(undefined, typedHttp);
|
|
1347
|
-
// Project the HTTP observation's status + provider/source association.
|
|
1348
|
-
return resolutionOf(
|
|
1349
|
-
knownFailureFromProviderStop({
|
|
1350
|
-
stopReason: "error",
|
|
1351
|
-
httpStatus: httpObservation.httpStatus,
|
|
1352
|
-
provider: httpObservation.provider,
|
|
1353
|
-
}),
|
|
1354
|
-
typedHttp,
|
|
1355
|
-
);
|
|
1356
|
-
}
|
|
1357
|
-
return resolutionOf(
|
|
1358
|
-
knownFailureFromProviderStop({
|
|
1359
|
-
...parentStop,
|
|
1360
|
-
...(httpObservation === undefined
|
|
1361
|
-
? {}
|
|
1362
|
-
: {
|
|
1363
|
-
httpStatus: httpObservation.httpStatus,
|
|
1364
|
-
// Observation association outranks session-configured provider name alone.
|
|
1365
|
-
provider: httpObservation.provider,
|
|
1366
|
-
}),
|
|
1367
|
-
}),
|
|
1368
|
-
typedHttp,
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
/** Sole evidence-priority owner for public runners with Soul auditors. */
|
|
1373
|
-
export async function resolveAuditedRunnerKnownFailure(input: {
|
|
1374
|
-
runner: RoleTurnKnownFailure | undefined;
|
|
1375
|
-
sessionFile: string;
|
|
1376
|
-
credential: RoleTurnKnownFailure | undefined;
|
|
1377
|
-
/** Optional run directory for typed provider HTTP observation (resume/429). */
|
|
1378
|
-
runDirectory?: string;
|
|
1379
|
-
}): Promise<RoleTurnKnownFailure | undefined> {
|
|
1380
|
-
return (await resolveAuditedRunnerFailureResolution(input)).knownFailure;
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
/**
|
|
1384
|
-
* v1 resume observation for controlled-failure settlement — at most one sidecar read.
|
|
1385
|
-
* Prefer the pre-resolved outcome from resolveAuditedRunnerFailureResolution.
|
|
1386
|
-
* Non-absence failures never throw: they return observationReadFailure for the
|
|
1387
|
-
* existing controlled-failure → error.json chain.
|
|
1388
|
-
*/
|
|
1389
|
-
export async function resolveControlledFailureResumeObservation(input: {
|
|
1390
|
-
readonly runDirectory: string;
|
|
1391
|
-
readonly typedHttpObservationSettled?: boolean;
|
|
1392
|
-
readonly typedHttpObservation?: TypedProviderHttpObservation;
|
|
1393
|
-
}): Promise<{
|
|
1394
|
-
readonly typedHttp429?: TypedHttp429Observation;
|
|
1395
|
-
readonly observationReadFailure?: RoleTurnKnownFailure;
|
|
1396
|
-
}> {
|
|
1397
|
-
if (input.typedHttpObservationSettled === true) {
|
|
1398
|
-
const observation = input.typedHttpObservation;
|
|
1399
|
-
if (
|
|
1400
|
-
observation !== undefined &&
|
|
1401
|
-
observation.httpStatus === 429 &&
|
|
1402
|
-
isV1ResumableProvider(observation.provider)
|
|
1403
|
-
) {
|
|
1404
|
-
return {
|
|
1405
|
-
typedHttp429: { httpStatus: 429, provider: observation.provider },
|
|
1406
|
-
};
|
|
1407
|
-
}
|
|
1408
|
-
return {};
|
|
1409
|
-
}
|
|
1410
|
-
try {
|
|
1411
|
-
const typedHttp429 = await readTypedHttp429Observation(input.runDirectory);
|
|
1412
|
-
return typedHttp429 === undefined ? {} : { typedHttp429 };
|
|
1413
|
-
} catch (error) {
|
|
1414
|
-
const failure = error instanceof Error ? error : new Error(String(error));
|
|
1415
|
-
return {
|
|
1416
|
-
observationReadFailure: {
|
|
1417
|
-
cause: "session",
|
|
1418
|
-
identity: thrownIdentity(failure),
|
|
1419
|
-
diagnostic: failure.message || failure.name,
|
|
1420
|
-
},
|
|
1421
|
-
};
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
/** Spread into presentControlledFailure failureInput from one audited resolution. */
|
|
1426
|
-
export function controlledFailureInputFromResolution(
|
|
1427
|
-
resolution: AuditedRunnerFailureResolution,
|
|
1428
|
-
): {
|
|
1429
|
-
knownFailure?: RoleTurnKnownFailure;
|
|
1430
|
-
typedHttpObservationSettled?: true;
|
|
1431
|
-
typedHttpObservation?: TypedProviderHttpObservation;
|
|
1432
|
-
} {
|
|
1433
|
-
return {
|
|
1434
|
-
...(resolution.knownFailure === undefined ? {} : { knownFailure: resolution.knownFailure }),
|
|
1435
|
-
...(resolution.typedHttpObservationSettled
|
|
1436
|
-
? {
|
|
1437
|
-
typedHttpObservationSettled: true as const,
|
|
1438
|
-
...(resolution.typedHttpObservation === undefined
|
|
1439
|
-
? {}
|
|
1440
|
-
: { typedHttpObservation: resolution.typedHttpObservation }),
|
|
1441
|
-
}
|
|
1442
|
-
: {}),
|
|
1443
|
-
};
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
function safelyRead(object: object, key: string): { readable: true; value: unknown } | { readable: false } {
|
|
1447
|
-
try {
|
|
1448
|
-
return { readable: true, value: (object as Record<string, unknown>)[key] };
|
|
1449
|
-
} catch {
|
|
1450
|
-
return { readable: false };
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
function toolResultText(message: SessionMessage): string {
|
|
1455
|
-
const content = message.content;
|
|
1456
|
-
if (typeof content === "string") return content.trim();
|
|
1457
|
-
if (!Array.isArray(content)) return "";
|
|
1458
|
-
return content
|
|
1459
|
-
.map((part) => {
|
|
1460
|
-
if (
|
|
1461
|
-
isRecord(part) &&
|
|
1462
|
-
typeof (part as { text?: unknown }).text === "string"
|
|
1463
|
-
) {
|
|
1464
|
-
return (part as { text: string }).text;
|
|
1465
|
-
}
|
|
1466
|
-
return "";
|
|
1467
|
-
})
|
|
1468
|
-
.join("")
|
|
1469
|
-
.trim();
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
type BoundErroredToolCandidate = {
|
|
1473
|
-
candidate: unknown;
|
|
1474
|
-
diagnostic: string;
|
|
1475
|
-
callIndex: number;
|
|
1476
|
-
};
|
|
1477
|
-
|
|
1478
|
-
function boundErroredToolCandidate(
|
|
1479
|
-
entries: readonly SessionEntry[],
|
|
1480
|
-
resultIndex: number,
|
|
1481
|
-
message: SessionMessage,
|
|
1482
|
-
toolName: string,
|
|
1483
|
-
): BoundErroredToolCandidate | undefined {
|
|
1484
|
-
if (message.toolName !== toolName || message.isError !== true) return undefined;
|
|
1485
|
-
const bound = boundRoleToolCallForResult(entries, resultIndex, message, toolName);
|
|
1486
|
-
const diagnostic = toolResultText(message);
|
|
1487
|
-
return bound === undefined || diagnostic === ""
|
|
1488
|
-
? undefined
|
|
1489
|
-
: { candidate: bound.candidate, diagnostic, callIndex: bound.callIndex };
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
/** Shared match/cause/identity knobs for session-principal infrastructure failures. */
|
|
1493
|
-
type InfrastructureFailureSpec = Readonly<{
|
|
1494
|
-
matchTool: (toolName: string) => boolean;
|
|
1495
|
-
cause: ControlledFailureCause;
|
|
1496
|
-
identityName: string;
|
|
1497
|
-
/**
|
|
1498
|
-
* Errored results only count as infrastructure when the durable details carry
|
|
1499
|
-
* the typed navigator fact.
|
|
1500
|
-
*/
|
|
1501
|
-
requireInfrastructureFact?: (toolName: string) => boolean;
|
|
1502
|
-
}>;
|
|
1503
|
-
|
|
1504
|
-
const ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC: InfrastructureFailureSpec = {
|
|
1505
|
-
matchTool: (toolName) => toolName === ENGINE_DETOUR_TOOL_NAME,
|
|
1506
|
-
cause: "output",
|
|
1507
|
-
identityName: "EngineDetourInfrastructureError",
|
|
1508
|
-
};
|
|
1509
|
-
|
|
1510
|
-
/**
|
|
1511
|
-
* Prefer a real infrastructure tool failure already on the session principal
|
|
1512
|
-
* over a later secondary provider-stop (failure-honesty).
|
|
1513
|
-
* Tool match + cause + identity are call-site parameters — one extraction body.
|
|
1514
|
-
*/
|
|
1515
|
-
function extractInfrastructureToolFailure(
|
|
1516
|
-
entries: readonly SessionEntry[],
|
|
1517
|
-
spec: InfrastructureFailureSpec,
|
|
1518
|
-
): ControlledFailure | undefined {
|
|
1519
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1520
|
-
const entry = entries[i];
|
|
1521
|
-
if (entry?.type !== "message") continue;
|
|
1522
|
-
const message = entry.message;
|
|
1523
|
-
if (message?.role !== "toolResult") continue;
|
|
1524
|
-
if (message.isError !== true) continue;
|
|
1525
|
-
if (
|
|
1526
|
-
typeof message.toolName !== "string" ||
|
|
1527
|
-
!spec.matchTool(message.toolName)
|
|
1528
|
-
) {
|
|
1529
|
-
continue;
|
|
1530
|
-
}
|
|
1531
|
-
if (spec.requireInfrastructureFact?.(message.toolName) === true) {
|
|
1532
|
-
if (!hasNavigatorInfrastructureFailureBase(message.details)) continue;
|
|
1533
|
-
}
|
|
1534
|
-
const diagnostic = toolResultText(message);
|
|
1535
|
-
if (diagnostic.length === 0) continue;
|
|
1536
|
-
return {
|
|
1537
|
-
cause: spec.cause,
|
|
1538
|
-
diagnostic,
|
|
1539
|
-
identity: { name: spec.identityName },
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1542
|
-
return undefined;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
/**
|
|
1546
|
-
* Read the bound session principal for a parameterized infrastructure tool failure.
|
|
1547
|
-
* `currentAttemptOnly` bounds the reverse scan to the latest top-level user turn
|
|
1548
|
-
* so a prior attempt's residual cannot mask the current failure (#633).
|
|
1549
|
-
*/
|
|
1550
|
-
async function readInfrastructureToolFailure(
|
|
1551
|
-
sessionFile: string,
|
|
1552
|
-
spec: InfrastructureFailureSpec,
|
|
1553
|
-
options: { readonly currentAttemptOnly?: boolean } = {},
|
|
1554
|
-
): Promise<ControlledFailure | undefined> {
|
|
1555
|
-
try {
|
|
1556
|
-
let entries = await readBoundSessionEntries(sessionFile);
|
|
1557
|
-
if (options.currentAttemptOnly === true) {
|
|
1558
|
-
entries = entries.slice(currentAttemptStartIndex(entries));
|
|
1559
|
-
}
|
|
1560
|
-
return extractInfrastructureToolFailure(entries, spec);
|
|
1561
|
-
} catch {
|
|
1562
|
-
return undefined;
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
/**
|
|
1567
|
-
* Prefer a real engine-detour infrastructure tool failure already on the session
|
|
1568
|
-
* principal over a later secondary provider-stop (failure-honesty / #357 T2).
|
|
1569
|
-
* Cause stays `output` — labor leg failed before accepted typed Receipt.
|
|
1570
|
-
*/
|
|
1571
|
-
export function extractEngineDetourInfrastructureFailure(
|
|
1572
|
-
entries: readonly SessionEntry[],
|
|
1573
|
-
): ControlledFailure | undefined {
|
|
1574
|
-
return extractInfrastructureToolFailure(
|
|
1575
|
-
entries,
|
|
1576
|
-
ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC,
|
|
1577
|
-
);
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
/** Read the bound session principal for an engine-detour infrastructure failure. */
|
|
1581
|
-
export async function readEngineDetourInfrastructureFailure(
|
|
1582
|
-
sessionFile: string,
|
|
1583
|
-
): Promise<ControlledFailure | undefined> {
|
|
1584
|
-
return readInfrastructureToolFailure(
|
|
1585
|
-
sessionFile,
|
|
1586
|
-
ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC,
|
|
1587
|
-
);
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
function knownFailureFromControlled(failure: ControlledFailure): RoleTurnKnownFailure {
|
|
1591
|
-
return {
|
|
1592
|
-
...(failure.cause === undefined ? {} : { cause: failure.cause }),
|
|
1593
|
-
diagnostic: failure.diagnostic,
|
|
1594
|
-
...(failure.identity === undefined ? {} : { identity: failure.identity }),
|
|
1595
|
-
};
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
const RUNNER_FAILURE_POLICY = {
|
|
1599
|
-
"engine-detour-known-first": {
|
|
1600
|
-
read: readEngineDetourInfrastructureFailure,
|
|
1601
|
-
rank: "known-first",
|
|
1602
|
-
},
|
|
1603
|
-
"engine-detour-record-first": {
|
|
1604
|
-
read: readEngineDetourInfrastructureFailure,
|
|
1605
|
-
rank: "record-first",
|
|
1606
|
-
},
|
|
1607
|
-
} as const;
|
|
1608
|
-
|
|
1609
|
-
/**
|
|
1610
|
-
* Runner-failure rank for one seat. The composition-root `runnerFailure`
|
|
1611
|
-
* leaf is the only seat difference; absent means the runner knownFailure stands.
|
|
1612
|
-
*/
|
|
1613
|
-
export function seatKnownFailureResolver(
|
|
1614
|
-
role: PackagedRole,
|
|
1615
|
-
): ((input: {
|
|
1616
|
-
result: { knownFailure?: RoleTurnKnownFailure };
|
|
1617
|
-
sessionFile: string;
|
|
1618
|
-
}) => Promise<RoleTurnKnownFailure | undefined>) | undefined {
|
|
1619
|
-
const record = packagedRoleMetadata(role);
|
|
1620
|
-
if (record === undefined || !("runnerFailure" in record)) return undefined;
|
|
1621
|
-
const policy = RUNNER_FAILURE_POLICY[record.runnerFailure];
|
|
1622
|
-
return async ({ result, sessionFile }) => {
|
|
1623
|
-
const infrastructureFailure = await policy.read(sessionFile);
|
|
1624
|
-
if (policy.rank === "record-first") {
|
|
1625
|
-
return infrastructureFailure === undefined
|
|
1626
|
-
? result.knownFailure
|
|
1627
|
-
: knownFailureFromControlled(infrastructureFailure);
|
|
1628
|
-
}
|
|
1629
|
-
return result.knownFailure ?? (
|
|
1630
|
-
infrastructureFailure === undefined ? undefined : knownFailureFromControlled(infrastructureFailure)
|
|
1631
|
-
);
|
|
1632
|
-
};
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
function auditToolNameForRole(
|
|
1636
|
-
role: (typeof AUDITOR_SOUL_ROLES)[number],
|
|
1637
|
-
): string {
|
|
1638
|
-
const name = packagedAuditToolName(role);
|
|
1639
|
-
if (name === undefined) {
|
|
1640
|
-
throw new Error(`audit tool is not declared for ${role}`);
|
|
1641
|
-
}
|
|
1642
|
-
return name;
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
type BoundRoleToolCall = {
|
|
1646
|
-
callIndex: number;
|
|
1647
|
-
candidate: unknown;
|
|
1648
|
-
};
|
|
1649
|
-
|
|
1650
|
-
function boundRoleToolCallForResult(
|
|
1651
|
-
entries: readonly SessionEntry[],
|
|
1652
|
-
resultIndex: number,
|
|
1653
|
-
message: SessionMessage,
|
|
1654
|
-
outputToolName: string,
|
|
1655
|
-
): BoundRoleToolCall | undefined {
|
|
1656
|
-
const callId = message.toolCallId;
|
|
1657
|
-
if (typeof callId !== "string" || callId.trim() === "") return undefined;
|
|
1658
|
-
|
|
1659
|
-
const calls: BoundRoleToolCall[] = [];
|
|
1660
|
-
let resultCount = 0;
|
|
1661
|
-
let matchingResultIndex = -1;
|
|
1662
|
-
for (let index = 0; index < entries.length; index += 1) {
|
|
1663
|
-
const candidateMessage = entries[index]?.message;
|
|
1664
|
-
if (
|
|
1665
|
-
candidateMessage?.role === "assistant" &&
|
|
1666
|
-
Array.isArray(candidateMessage.content)
|
|
1667
|
-
) {
|
|
1668
|
-
for (const part of candidateMessage.content) {
|
|
1669
|
-
if (!isRecord(part) || part.type !== "toolCall" || part.id !== callId) {
|
|
1670
|
-
continue;
|
|
1671
|
-
}
|
|
1672
|
-
if (part.name !== outputToolName) return undefined;
|
|
1673
|
-
calls.push({ callIndex: index, candidate: part.arguments });
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
if (
|
|
1677
|
-
candidateMessage?.role === "toolResult" &&
|
|
1678
|
-
candidateMessage.toolCallId === callId
|
|
1679
|
-
) {
|
|
1680
|
-
resultCount += 1;
|
|
1681
|
-
if (candidateMessage.toolName !== outputToolName) return undefined;
|
|
1682
|
-
matchingResultIndex = index;
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
// A binding is an event-bound one-to-one relation, not a reverse lookup of
|
|
1687
|
-
// whichever result happens to be last in the session.
|
|
1688
|
-
return calls.length === 1 && resultCount === 1 && matchingResultIndex === resultIndex
|
|
1689
|
-
&& calls[0]!.callIndex < resultIndex
|
|
1690
|
-
? calls[0]
|
|
1691
|
-
: undefined;
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
type BoundRetainedAuditResponse = {
|
|
1695
|
-
candidate: unknown;
|
|
1696
|
-
};
|
|
1697
|
-
|
|
1698
|
-
type BoundAuditEscalation = {
|
|
1699
|
-
decision: Extract<ComplianceDecision, { status: "escalate" }>;
|
|
1700
|
-
details: Record<string, unknown>;
|
|
1023
|
+
cwd?: string;
|
|
1024
|
+
/** Parent session principal on durable child session headers. */
|
|
1025
|
+
parentSession?: string;
|
|
1701
1026
|
};
|
|
1702
1027
|
|
|
1703
|
-
function sameAuditValue(left: unknown, right: unknown): boolean {
|
|
1704
|
-
if (Object.is(left, right)) return true;
|
|
1705
|
-
if (Array.isArray(left) && Array.isArray(right)) {
|
|
1706
|
-
return left.length === right.length && left.every((value, index) =>
|
|
1707
|
-
sameAuditValue(value, right[index]),
|
|
1708
|
-
);
|
|
1709
|
-
}
|
|
1710
|
-
if (isRecord(left) && isRecord(right)) {
|
|
1711
|
-
const leftKeys = Object.keys(left);
|
|
1712
|
-
const rightKeys = Object.keys(right);
|
|
1713
|
-
return leftKeys.length === rightKeys.length &&
|
|
1714
|
-
leftKeys.every((key) => Object.hasOwn(right, key) && sameAuditValue(left[key], right[key]));
|
|
1715
|
-
}
|
|
1716
|
-
return false;
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
/** Snapshot the exact enumerable string face that final Terminal projection uses. */
|
|
1720
|
-
function snapshotAuditDetails(details: Record<string, unknown>): Record<string, unknown> {
|
|
1721
|
-
const snapshot: Record<string, unknown> = Object.create(null);
|
|
1722
|
-
for (const key of Object.keys(details)) {
|
|
1723
|
-
Object.defineProperty(snapshot, key, {
|
|
1724
|
-
value: details[key],
|
|
1725
|
-
enumerable: true,
|
|
1726
|
-
configurable: true,
|
|
1727
|
-
writable: true,
|
|
1728
|
-
});
|
|
1729
|
-
}
|
|
1730
|
-
return snapshot;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
1028
|
/**
|
|
1734
|
-
*
|
|
1735
|
-
*
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
1029
|
+
* Read the exact bound Pi session file principal.
|
|
1030
|
+
* Does not scan the session directory for "latest" — resume identity is the file.
|
|
1031
|
+
* A malformed line keeps the parser's own error. Callers that already hold a
|
|
1032
|
+
* terminal note it; they do not relabel it as a host session cause.
|
|
1738
1033
|
*/
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
outputToolName: string,
|
|
1747
|
-
): BoundAuditEscalation | undefined {
|
|
1748
|
-
const roleCall = boundRoleToolCallForResult(
|
|
1749
|
-
entries,
|
|
1750
|
-
resultIndex,
|
|
1751
|
-
message,
|
|
1752
|
-
outputToolName,
|
|
1753
|
-
);
|
|
1754
|
-
if (roleCall === undefined) return undefined;
|
|
1755
|
-
const retained = boundRetainedAuditResponse(
|
|
1756
|
-
entries,
|
|
1757
|
-
roleCall.callIndex,
|
|
1758
|
-
resultIndex,
|
|
1759
|
-
auditToolNameForRole(role),
|
|
1760
|
-
);
|
|
1761
|
-
if (retained === undefined) return undefined;
|
|
1762
|
-
try {
|
|
1763
|
-
const decision = readComplianceCandidate(retained.candidate);
|
|
1764
|
-
if (decision.status !== "escalate") return undefined;
|
|
1765
|
-
const details = message.details;
|
|
1766
|
-
if (!isAuditEscalationResult(details) || !isRecord(details)) return undefined;
|
|
1767
|
-
|
|
1768
|
-
// Read the public face exactly once. Besides making key enumeration and
|
|
1769
|
-
// getters fail closed, this prevents a stateful accessor from authenticating
|
|
1770
|
-
// one value and yielding another during final Terminal projection.
|
|
1771
|
-
const projectedDetails = snapshotAuditDetails(details);
|
|
1772
|
-
const hasDecisionConflicts = Object.hasOwn(decision, "conflicts");
|
|
1773
|
-
const hasDetailsConflicts = Object.hasOwn(projectedDetails, "conflicts");
|
|
1774
|
-
if (hasDecisionConflicts !== hasDetailsConflicts) return undefined;
|
|
1775
|
-
if (hasDecisionConflicts && !sameAuditValue(projectedDetails.conflicts, decision.conflicts)) return undefined;
|
|
1776
|
-
const hasDecisionGate = Object.hasOwn(decision, "decisionGate");
|
|
1777
|
-
const hasDetailsGate = Object.hasOwn(projectedDetails, "auditDecisionGate");
|
|
1778
|
-
if (hasDecisionGate !== hasDetailsGate) return undefined;
|
|
1779
|
-
if (hasDecisionGate && !sameAuditValue(projectedDetails.auditDecisionGate, decision.decisionGate)) return undefined;
|
|
1780
|
-
return { decision, details: projectedDetails };
|
|
1781
|
-
} catch {
|
|
1782
|
-
// Retained/public own-key enumeration, property reads, recursive equality,
|
|
1783
|
-
// and projection are all untrusted session evidence.
|
|
1784
|
-
return undefined;
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
function isUnboundAuditEscalationFace(details: unknown): boolean {
|
|
1789
|
-
try {
|
|
1790
|
-
if (isAuditEscalationResult(details)) return true;
|
|
1791
|
-
} catch {
|
|
1792
|
-
// Hostile access is not authentic escalation evidence.
|
|
1793
|
-
}
|
|
1794
|
-
if (!isRecord(details)) return false;
|
|
1795
|
-
const kind = safelyRead(details, "kind");
|
|
1796
|
-
return kind.readable && kind.value === "audit_escalation";
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
function boundRetainedAuditResponse(
|
|
1800
|
-
entries: readonly SessionEntry[],
|
|
1801
|
-
callIndex: number,
|
|
1802
|
-
resultIndex: number,
|
|
1803
|
-
auditToolName: string,
|
|
1804
|
-
): BoundRetainedAuditResponse | undefined {
|
|
1805
|
-
const matches: BoundRetainedAuditResponse[] = [];
|
|
1806
|
-
for (let index = callIndex + 1; index < resultIndex; index += 1) {
|
|
1807
|
-
const entry = entries[index];
|
|
1808
|
-
if (entry?.type !== "custom" || entry.customType !== COMPLIANCE_RESPONSE_ENTRY_TYPE) {
|
|
1809
|
-
continue;
|
|
1810
|
-
}
|
|
1811
|
-
if (!isRecord(entry.data) || !isRecord(entry.data.response)) continue;
|
|
1812
|
-
const response = entry.data.response;
|
|
1813
|
-
if (!Array.isArray(response.content)) continue;
|
|
1814
|
-
const calls = response.content.filter(
|
|
1815
|
-
(part): part is Record<string, unknown> =>
|
|
1816
|
-
isRecord(part) && part.type === "toolCall",
|
|
1817
|
-
);
|
|
1818
|
-
if (calls.length !== 1 || calls[0]?.name !== auditToolName) continue;
|
|
1819
|
-
matches.push({ candidate: calls[0]?.arguments });
|
|
1034
|
+
export async function readBoundSessionEntries(
|
|
1035
|
+
sessionFile: string,
|
|
1036
|
+
): Promise<SessionEntry[]> {
|
|
1037
|
+
const text = await readFile(sessionFile, "utf8");
|
|
1038
|
+
const entries: SessionEntry[] = [];
|
|
1039
|
+
for (const line of text.trim().split("\n").filter(Boolean)) {
|
|
1040
|
+
entries.push(JSON.parse(line) as SessionEntry);
|
|
1820
1041
|
}
|
|
1821
|
-
|
|
1822
|
-
// intermediate non-decision responses in the same call/result interval.
|
|
1823
|
-
return matches.length === 1 ? matches[0] : undefined;
|
|
1042
|
+
return entries;
|
|
1824
1043
|
}
|
|
1825
1044
|
|
|
1826
1045
|
/**
|
|
@@ -1842,57 +1061,19 @@ type AttemptHistorySource = {
|
|
|
1842
1061
|
readonly sessionFile: string;
|
|
1843
1062
|
};
|
|
1844
1063
|
|
|
1845
|
-
/**
|
|
1846
|
-
* Append one attempt's complete result to the run's session principal.
|
|
1847
|
-
* Append failure throws — callers must not overwrite a pointer artifact when
|
|
1848
|
-
* the history entry backing the overwrite did not land (fail closed).
|
|
1849
|
-
*/
|
|
1064
|
+
/** Append the complete attempt to the package ledger before overwriting pointer artifacts (#419). */
|
|
1850
1065
|
export async function appendRunAttemptHistory(
|
|
1851
1066
|
source: AttemptHistorySource,
|
|
1852
1067
|
outcome: AttemptHistoryOutcome,
|
|
1853
1068
|
): Promise<void> {
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
) {
|
|
1863
|
-
priorEntries += 1;
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
const timestamp = new Date().toISOString();
|
|
1867
|
-
const attemptData = {
|
|
1868
|
-
sequence: priorEntries + 1,
|
|
1869
|
-
role: source.role,
|
|
1870
|
-
runId: source.runId,
|
|
1871
|
-
recordedAt: timestamp,
|
|
1872
|
-
outcome,
|
|
1873
|
-
};
|
|
1874
|
-
const line = `${JSON.stringify({
|
|
1875
|
-
type: "custom",
|
|
1876
|
-
customType: ATTEMPT_HISTORY_ENTRY_TYPE,
|
|
1877
|
-
data: attemptData,
|
|
1878
|
-
id: randomUUID(),
|
|
1879
|
-
parentId,
|
|
1880
|
-
timestamp,
|
|
1881
|
-
})}\n`;
|
|
1882
|
-
await appendFile(source.sessionFile, line, "utf8");
|
|
1883
|
-
try {
|
|
1884
|
-
sitianReport({
|
|
1885
|
-
level: "event",
|
|
1886
|
-
kind: "attempt-history",
|
|
1887
|
-
subject: { runId: source.runId },
|
|
1888
|
-
sessionParent: source.sessionFile,
|
|
1889
|
-
payload: {
|
|
1890
|
-
type: ATTEMPT_HISTORY_ENTRY_TYPE,
|
|
1891
|
-
...attemptData,
|
|
1892
|
-
},
|
|
1893
|
-
source: "settlement",
|
|
1894
|
-
});
|
|
1895
|
-
} catch {}
|
|
1069
|
+
sitianReport({
|
|
1070
|
+
level: "event",
|
|
1071
|
+
kind: "attempt-history",
|
|
1072
|
+
subject: { runId: source.runId },
|
|
1073
|
+
sessionParent: source.sessionFile,
|
|
1074
|
+
payload: { type: ATTEMPT_HISTORY_ENTRY_TYPE, role: source.role, runId: source.runId, outcome },
|
|
1075
|
+
source: "settlement",
|
|
1076
|
+
});
|
|
1896
1077
|
}
|
|
1897
1078
|
|
|
1898
1079
|
function parseNavigatorAttendanceDetails(
|
|
@@ -2033,10 +1214,9 @@ export async function extractGateFactFromSessionDirectory(
|
|
|
2033
1214
|
/**
|
|
2034
1215
|
* Attach optional gate projection onto a settled Terminal base.
|
|
2035
1216
|
* Shared by every settle path so auditor-roles is scanned once here only.
|
|
2036
|
-
* `runId` is
|
|
2037
|
-
* Gate read damage
|
|
2038
|
-
*
|
|
2039
|
-
* JSONL/session cause rather than pretend the gate was absent.
|
|
1217
|
+
* `runId` is optional only for a batch projection that has no parent run.
|
|
1218
|
+
* Gate read damage is a package note beside the host terminal. It does not
|
|
1219
|
+
* replace that terminal and it does not omit the gate without a trace.
|
|
2040
1220
|
*/
|
|
2041
1221
|
/**
|
|
2042
1222
|
* #537: project this-invocation ak_engine_detour usage onto decisiveFacts.
|
|
@@ -2047,7 +1227,7 @@ export async function extractGateFactFromSessionDirectory(
|
|
|
2047
1227
|
* therefore cannot replace an already-formed roleOutcome (no_receipt / failure).
|
|
2048
1228
|
*/
|
|
2049
1229
|
async function attachEngineDetourToolUsage<
|
|
2050
|
-
T extends { roleOutcome: TerminalRoleOutcome
|
|
1230
|
+
T extends { roleOutcome: TerminalRoleOutcome },
|
|
2051
1231
|
>(
|
|
2052
1232
|
base: T,
|
|
2053
1233
|
sessionDirectory: string,
|
|
@@ -2055,13 +1235,26 @@ async function attachEngineDetourToolUsage<
|
|
|
2055
1235
|
readonly runDirectory?: string;
|
|
2056
1236
|
readonly courtAttemptId?: string;
|
|
2057
1237
|
readonly invocationScopeId?: string;
|
|
1238
|
+
readonly notePackageFault?: SettlementCourtScope["notePackageFault"];
|
|
2058
1239
|
} = {},
|
|
2059
1240
|
): Promise<T> {
|
|
2060
1241
|
const runDirectory =
|
|
2061
1242
|
typeof gateContext.runDirectory === "string" && gateContext.runDirectory.length > 0
|
|
2062
1243
|
? gateContext.runDirectory
|
|
2063
1244
|
: runDirectoryFromSessionDirectory(sessionDirectory);
|
|
2064
|
-
|
|
1245
|
+
let engineMounted = false;
|
|
1246
|
+
try {
|
|
1247
|
+
engineMounted = await readInvocationEngineMounted(runDirectory);
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
await noteSettlementFault(
|
|
1250
|
+
runDirectory,
|
|
1251
|
+
gateContext.notePackageFault === undefined
|
|
1252
|
+
? undefined
|
|
1253
|
+
: { notePackageFault: gateContext.notePackageFault },
|
|
1254
|
+
`engine mount read failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
1255
|
+
);
|
|
1256
|
+
return base;
|
|
1257
|
+
}
|
|
2065
1258
|
if (!engineMounted) return base;
|
|
2066
1259
|
|
|
2067
1260
|
// Public-invocation scope from the shared Host envelope (settlement scope), never
|
|
@@ -2073,22 +1266,27 @@ async function attachEngineDetourToolUsage<
|
|
|
2073
1266
|
: undefined;
|
|
2074
1267
|
|
|
2075
1268
|
const sessionFile = sessionFileFromSessionDirectory(sessionDirectory);
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
1269
|
+
let usage: Awaited<ReturnType<typeof readEngineDetourToolUsage>>;
|
|
1270
|
+
try {
|
|
1271
|
+
usage = await readEngineDetourToolUsage({
|
|
1272
|
+
sessionParent: sessionFile,
|
|
1273
|
+
engineMounted: true,
|
|
1274
|
+
...(invocationScopeId === undefined ? {} : { invocationScopeId }),
|
|
1275
|
+
cwd: runDirectory,
|
|
1276
|
+
});
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
await noteSettlementFault(
|
|
1279
|
+
runDirectory,
|
|
1280
|
+
gateContext.notePackageFault === undefined
|
|
1281
|
+
? undefined
|
|
1282
|
+
: { notePackageFault: gateContext.notePackageFault },
|
|
1283
|
+
`engine detour read failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
1284
|
+
);
|
|
1285
|
+
return base;
|
|
1286
|
+
}
|
|
2089
1287
|
return {
|
|
2090
1288
|
...base,
|
|
2091
|
-
roleOutcome: withEngineDetourToolUsageFact(base.roleOutcome,
|
|
1289
|
+
roleOutcome: withEngineDetourToolUsageFact(base.roleOutcome, usage),
|
|
2092
1290
|
};
|
|
2093
1291
|
}
|
|
2094
1292
|
|
|
@@ -2109,6 +1307,7 @@ function detourGateContext(
|
|
|
2109
1307
|
...(scope?.invocationScopeId === undefined || scope.invocationScopeId.length === 0
|
|
2110
1308
|
? {}
|
|
2111
1309
|
: { invocationScopeId: scope.invocationScopeId }),
|
|
1310
|
+
...(scope?.notePackageFault === undefined ? {} : { notePackageFault: scope.notePackageFault }),
|
|
2112
1311
|
};
|
|
2113
1312
|
}
|
|
2114
1313
|
|
|
@@ -2117,7 +1316,6 @@ async function withOptionalGateProjection<
|
|
|
2117
1316
|
roleOutcome: TerminalRoleOutcome;
|
|
2118
1317
|
navigator: TerminalNavigatorFact;
|
|
2119
1318
|
artifacts: readonly TerminalArtifactRef[];
|
|
2120
|
-
resume?: TerminalResume;
|
|
2121
1319
|
},
|
|
2122
1320
|
>(
|
|
2123
1321
|
base: T,
|
|
@@ -2127,6 +1325,7 @@ async function withOptionalGateProjection<
|
|
|
2127
1325
|
readonly parentSessionFile?: string;
|
|
2128
1326
|
readonly courtAttemptId?: string;
|
|
2129
1327
|
readonly invocationScopeId?: string;
|
|
1328
|
+
readonly notePackageFault?: SettlementCourtScope["notePackageFault"];
|
|
2130
1329
|
} = {},
|
|
2131
1330
|
): Promise<T & { gate?: TerminalGateFact }> {
|
|
2132
1331
|
// A gate transport failure is already represented by typed evidence and has no
|
|
@@ -2142,9 +1341,19 @@ async function withOptionalGateProjection<
|
|
|
2142
1341
|
|
|
2143
1342
|
let next: T & { gate?: TerminalGateFact } = base;
|
|
2144
1343
|
if (!skipGate) {
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
1344
|
+
try {
|
|
1345
|
+
const gate = await extractGateFactFromSessionDirectory(sessionDirectory, gateContext);
|
|
1346
|
+
if (gate !== undefined) next = { ...base, gate };
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
const runDirectory = gateContext.runDirectory ?? runDirectoryFromSessionDirectory(sessionDirectory);
|
|
1349
|
+
await noteSettlementFault(
|
|
1350
|
+
runDirectory,
|
|
1351
|
+
gateContext.notePackageFault === undefined
|
|
1352
|
+
? undefined
|
|
1353
|
+
: { notePackageFault: gateContext.notePackageFault },
|
|
1354
|
+
`gate read failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
2148
1357
|
}
|
|
2149
1358
|
|
|
2150
1359
|
return attachEngineDetourToolUsage(next, sessionDirectory, gateContext);
|
|
@@ -2207,6 +1416,8 @@ function extractNavigatorAttendanceFact(
|
|
|
2207
1416
|
*/
|
|
2208
1417
|
async function extractNavigatorFactFromAdmittedSession(
|
|
2209
1418
|
sessionFile: string,
|
|
1419
|
+
runDirectory: string,
|
|
1420
|
+
scope: SettlementCourtScope | undefined,
|
|
2210
1421
|
): Promise<TerminalNavigatorFact> {
|
|
2211
1422
|
try {
|
|
2212
1423
|
const entries = await readBoundSessionEntries(sessionFile);
|
|
@@ -2219,6 +1430,11 @@ async function extractNavigatorFactFromAdmittedSession(
|
|
|
2219
1430
|
reason: "Navigator attendance is missing from the session",
|
|
2220
1431
|
};
|
|
2221
1432
|
}
|
|
1433
|
+
await noteSettlementFault(
|
|
1434
|
+
runDirectory,
|
|
1435
|
+
scope,
|
|
1436
|
+
`navigator session read failed beside host terminal: ${sessionFile}: ${describeErrorIdentity(error)}`,
|
|
1437
|
+
);
|
|
2222
1438
|
return {
|
|
2223
1439
|
disposition: "unavailable",
|
|
2224
1440
|
source: "unknown",
|
|
@@ -2308,31 +1524,26 @@ function acceptedArtifactAttachmentRefs(
|
|
|
2308
1524
|
* Seat-specific structured fields stay in callers; do not fork this flow.
|
|
2309
1525
|
*/
|
|
2310
1526
|
async function publishAcceptedTerminalArtifacts(
|
|
2311
|
-
admitted:
|
|
2312
|
-
readonly role: TerminalRoleName;
|
|
2313
|
-
readonly runId: string;
|
|
2314
|
-
readonly runDirectory: string;
|
|
2315
|
-
},
|
|
1527
|
+
admitted: AdmittedRoleInvocation,
|
|
2316
1528
|
roleOutcome: TerminalRoleOutcome,
|
|
2317
1529
|
coordinates: DurablePrincipalCoordinates,
|
|
1530
|
+
recordAttemptHistory: boolean,
|
|
2318
1531
|
bodies: {
|
|
2319
1532
|
readonly report: Record<string, unknown>;
|
|
2320
1533
|
readonly evidence: Record<string, unknown>;
|
|
2321
1534
|
},
|
|
2322
1535
|
): Promise<TerminalArtifactRef[]> {
|
|
2323
|
-
// #419:
|
|
2324
|
-
//
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
role: admitted.role,
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
roleOutcome,
|
|
2332
|
-
);
|
|
1536
|
+
// #419: a dispatched turn appends before rewriting last-write-wins views;
|
|
1537
|
+
// a later projection may refresh views but must not invent another attempt.
|
|
1538
|
+
if (recordAttemptHistory) {
|
|
1539
|
+
await appendRunAttemptHistory(
|
|
1540
|
+
{ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile },
|
|
1541
|
+
roleOutcome,
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
2333
1544
|
const artifactsDir = await ensureTerminalArtifactFace(admitted.runDirectory);
|
|
2334
|
-
const reportPath = join(artifactsDir,
|
|
2335
|
-
const evidencePath = join(artifactsDir,
|
|
1545
|
+
const reportPath = join(artifactsDir, RUN_TERMINAL_REPORT_FILE);
|
|
1546
|
+
const evidencePath = join(artifactsDir, RUN_TERMINAL_EVIDENCE_FILE);
|
|
2336
1547
|
await writeFile(
|
|
2337
1548
|
reportPath,
|
|
2338
1549
|
`${JSON.stringify(bodies.report, null, 2)}\n`,
|
|
@@ -2349,140 +1560,78 @@ async function publishAcceptedTerminalArtifacts(
|
|
|
2349
1560
|
];
|
|
2350
1561
|
}
|
|
2351
1562
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
coordinates: DurablePrincipalCoordinates,
|
|
2357
|
-
entries: readonly SessionEntry[] = [],
|
|
2358
|
-
): Promise<TerminalArtifactRef[]> {
|
|
2359
|
-
return publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries);
|
|
2360
|
-
}
|
|
1563
|
+
type LawfulSessionRead =
|
|
1564
|
+
| { readonly kind: "entries"; readonly entries: SessionEntry[] }
|
|
1565
|
+
| { readonly kind: "absent" }
|
|
1566
|
+
| { readonly kind: "fault"; readonly error: unknown };
|
|
2361
1567
|
|
|
2362
1568
|
/**
|
|
2363
|
-
* Read session entries for lawful settlement.
|
|
2364
|
-
*
|
|
1569
|
+
* Read session entries for lawful settlement.
|
|
1570
|
+
* Missing path is absence. A present file that cannot be read is a fault,
|
|
1571
|
+
* not a host cause and not an empty transcript.
|
|
2365
1572
|
*/
|
|
2366
1573
|
async function readLawfulSettlementEntries(
|
|
2367
1574
|
sessionFile: string,
|
|
2368
|
-
): Promise<
|
|
1575
|
+
): Promise<LawfulSessionRead> {
|
|
2369
1576
|
try {
|
|
2370
|
-
return await readBoundSessionEntries(sessionFile);
|
|
1577
|
+
return { kind: "entries", entries: await readBoundSessionEntries(sessionFile) };
|
|
2371
1578
|
} catch (error) {
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
// Malformed JSONL and other read failures keep typed session identity.
|
|
2375
|
-
throw error instanceof Error &&
|
|
2376
|
-
(error as { knownCause?: unknown }).knownCause === "session"
|
|
2377
|
-
? error
|
|
2378
|
-
: sessionReadFailure(error, "session unreadable");
|
|
1579
|
+
if (isEnoent(error)) return { kind: "absent" };
|
|
1580
|
+
return { kind: "fault", error };
|
|
2379
1581
|
}
|
|
2380
1582
|
}
|
|
2381
1583
|
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
* residual tool or attach run-scoped submission history.
|
|
2385
|
-
* Absence stays undefined. Session-read and publication errors keep their identity.
|
|
2386
|
-
* Outcome is read before publication so a later write error cannot erase it.
|
|
2387
|
-
*/
|
|
2388
|
-
async function settleSealedLedgerTerminal(
|
|
2389
|
-
admitted: AdmittedRoleInvocation,
|
|
2390
|
-
authority: DurablePrincipalAuthority,
|
|
2391
|
-
scope: SettlementCourtScope | undefined,
|
|
2392
|
-
publish: (
|
|
2393
|
-
roleOutcome: Extract<TerminalRoleOutcome, { kind: "accepted" | "audit_escalation" }>,
|
|
2394
|
-
coordinates: DurablePrincipalCoordinates,
|
|
2395
|
-
entries: readonly SessionEntry[],
|
|
2396
|
-
) => Promise<TerminalArtifactRef[]>,
|
|
2397
|
-
accept?: (
|
|
2398
|
-
roleOutcome: Extract<TerminalRoleOutcome, { kind: "accepted" | "audit_escalation" }>,
|
|
2399
|
-
) => boolean,
|
|
2400
|
-
): Promise<TerminalResult | undefined> {
|
|
2401
|
-
const roleOutcome = await sealedLedgerOutcome(admitted, admitted.role, scope);
|
|
2402
|
-
if (roleOutcome === undefined) return undefined;
|
|
2403
|
-
if (accept !== undefined && !accept(roleOutcome)) return undefined;
|
|
2404
|
-
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
2405
|
-
const entries = await readLawfulSettlementEntries(coordinates.sessionFile) ?? [];
|
|
2406
|
-
const artifacts = await publish(roleOutcome, coordinates, entries);
|
|
2407
|
-
return withOptionalGateProjection(
|
|
2408
|
-
{
|
|
2409
|
-
roleOutcome,
|
|
2410
|
-
navigator: extractNavigatorFact(entries),
|
|
2411
|
-
artifacts,
|
|
2412
|
-
runId: admitted.runId,
|
|
2413
|
-
},
|
|
2414
|
-
coordinates.sessionDirectory,
|
|
2415
|
-
detourGateContext(admitted, scope),
|
|
2416
|
-
);
|
|
1584
|
+
function entriesOf(read: LawfulSessionRead): SessionEntry[] {
|
|
1585
|
+
return read.kind === "entries" ? read.entries : [];
|
|
2417
1586
|
}
|
|
2418
1587
|
|
|
2419
|
-
/**
|
|
2420
|
-
|
|
2421
|
-
* the latest bound errored tool result in the scan window, which settles as
|
|
2422
|
-
* output failure. Both faces attach run-scoped submissions. Absence of both
|
|
2423
|
-
* stays undefined. Session is read first so a read failure keeps its identity
|
|
2424
|
-
* even when the ledger has no accepted row.
|
|
2425
|
-
*/
|
|
2426
|
-
async function settleSealedAcceptedOrToolResidual(
|
|
1588
|
+
/** Sealed seats present the ledger. A session tool error does not invent a host failure. */
|
|
1589
|
+
async function settleSealedSeat(
|
|
2427
1590
|
admitted: AdmittedRoleInvocation,
|
|
2428
1591
|
authority: DurablePrincipalAuthority,
|
|
2429
1592
|
scope: SettlementCourtScope | undefined,
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
publish: (
|
|
2434
|
-
roleOutcome: Extract<TerminalRoleOutcome, { kind: "accepted" }>,
|
|
2435
|
-
coordinates: DurablePrincipalCoordinates,
|
|
2436
|
-
entries: readonly SessionEntry[],
|
|
2437
|
-
) => Promise<TerminalArtifactRef[]>,
|
|
1593
|
+
options: {
|
|
1594
|
+
acceptedOnly: boolean;
|
|
1595
|
+
},
|
|
2438
1596
|
): Promise<TerminalResult | undefined> {
|
|
2439
1597
|
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
2440
|
-
const
|
|
2441
|
-
|
|
2442
|
-
const roleOutcome = await sealedLedgerOutcome(admitted, role, scope);
|
|
2443
|
-
if (roleOutcome?.role !== role || roleOutcome.kind !== "accepted") {
|
|
2444
|
-
const scanStart = scan === "current-attempt" ? currentAttemptStartIndex(entries) : 0;
|
|
2445
|
-
for (let index = entries.length - 1; index >= scanStart; index -= 1) {
|
|
2446
|
-
const message = entries[index]?.message;
|
|
2447
|
-
if (message?.role !== "toolResult") continue;
|
|
2448
|
-
const residual = boundErroredToolCandidate(entries, index, message, residualToolName);
|
|
2449
|
-
if (residual === undefined) continue;
|
|
2450
|
-
const candidate = residual.candidate;
|
|
2451
|
-
const details = isRecord(candidate) ? candidate : { candidate };
|
|
2452
|
-
const failed = await settleFailureTerminalResult(
|
|
2453
|
-
admitted,
|
|
2454
|
-
{
|
|
2455
|
-
cause: "output",
|
|
2456
|
-
diagnostic: residual.diagnostic,
|
|
2457
|
-
details,
|
|
2458
|
-
},
|
|
2459
|
-
authority,
|
|
2460
|
-
scope ?? {},
|
|
2461
|
-
);
|
|
2462
|
-
return attachRecordedSubmissions(admitted, failed, scope);
|
|
2463
|
-
}
|
|
1598
|
+
const roleOutcome = await sealedLedgerOutcome(admitted, admitted.role, scope);
|
|
1599
|
+
if (roleOutcome === undefined || (options.acceptedOnly && roleOutcome.kind !== "accepted")) {
|
|
2464
1600
|
return undefined;
|
|
2465
1601
|
}
|
|
2466
|
-
const
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
1602
|
+
const read = await readLawfulSettlementEntries(coordinates.sessionFile);
|
|
1603
|
+
if (read.kind === "fault") {
|
|
1604
|
+
await noteSettlementFault(
|
|
1605
|
+
admitted.runDirectory,
|
|
1606
|
+
scope,
|
|
1607
|
+
`session read failed beside lawful terminal: ${describeErrorIdentity(read.error)}`,
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
return finishLawfulSeat(admitted, coordinates, entriesOf(read), roleOutcome, scope);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
async function finishLawfulSeat(
|
|
1614
|
+
admitted: AdmittedRoleInvocation,
|
|
1615
|
+
coordinates: DurablePrincipalCoordinates,
|
|
1616
|
+
entries: readonly SessionEntry[],
|
|
1617
|
+
roleOutcome: TerminalRoleOutcome,
|
|
1618
|
+
scope: SettlementCourtScope | undefined,
|
|
1619
|
+
): Promise<TerminalResult> {
|
|
1620
|
+
const artifacts = scope?.previewOnly === true
|
|
1621
|
+
? []
|
|
1622
|
+
: await publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries, scope?.recordAttemptHistory === true);
|
|
1623
|
+
const terminal = await withOptionalGateProjection({
|
|
1624
|
+
roleOutcome,
|
|
1625
|
+
navigator: extractNavigatorFact(entries),
|
|
1626
|
+
artifacts,
|
|
1627
|
+
runId: admitted.runId,
|
|
1628
|
+
}, coordinates.sessionDirectory, detourGateContext(admitted, scope));
|
|
1629
|
+
return attachRecordedSubmissions(admitted, terminal, scope);
|
|
2481
1630
|
}
|
|
2482
1631
|
|
|
2483
1632
|
/**
|
|
2484
1633
|
* One settlement for every registered seat. The registry `settlement` leaf
|
|
2485
|
-
* picks sealed ledger
|
|
1634
|
+
* picks the sealed ledger or the accepted-tool ledger.
|
|
2486
1635
|
* Seat evidence stays on the artifact face.
|
|
2487
1636
|
*/
|
|
2488
1637
|
async function settleSeat(
|
|
@@ -2497,59 +1646,9 @@ async function settleSeat(
|
|
|
2497
1646
|
if (record.settlement === "accepted") {
|
|
2498
1647
|
return trySettleAcceptedSeatTerminalResult(admitted, authority, scope);
|
|
2499
1648
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
entries: readonly SessionEntry[],
|
|
2504
|
-
) => publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries);
|
|
2505
|
-
if (record.settlement === "residual") {
|
|
2506
|
-
return settleSealedAcceptedOrToolResidual(
|
|
2507
|
-
admitted,
|
|
2508
|
-
authority,
|
|
2509
|
-
scope,
|
|
2510
|
-
admitted.role,
|
|
2511
|
-
record.residualTool,
|
|
2512
|
-
record.residualScan,
|
|
2513
|
-
publish,
|
|
2514
|
-
);
|
|
2515
|
-
}
|
|
2516
|
-
const accept = "sealedAcceptedOnly" in record && record.sealedAcceptedOnly === true
|
|
2517
|
-
? (
|
|
2518
|
-
roleOutcome: Extract<TerminalRoleOutcome, { kind: "accepted" | "audit_escalation" }>,
|
|
2519
|
-
) => roleOutcome.role === admitted.role && roleOutcome.kind === "accepted"
|
|
2520
|
-
: undefined;
|
|
2521
|
-
return settleSealedLedgerTerminal(admitted, authority, scope, publish, accept);
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
|
-
/**
|
|
2525
|
-
* Try to settle any registered seat. Undefined only for genuine absence.
|
|
2526
|
-
* Session malformation and publication exceptions keep their typed identity.
|
|
2527
|
-
*/
|
|
2528
|
-
export async function trySettleSeatTerminalResult(
|
|
2529
|
-
admitted: AdmittedRoleInvocation,
|
|
2530
|
-
authority: DurablePrincipalAuthority,
|
|
2531
|
-
scope?: SettlementCourtScope,
|
|
2532
|
-
): Promise<TerminalResult | undefined> {
|
|
2533
|
-
return settleSeat(admitted, authority, scope);
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
/**
|
|
2537
|
-
* Settle any registered seat. Throws when no lawful outcome is present.
|
|
2538
|
-
* Session-read and publication failures retain their typed identity.
|
|
2539
|
-
*/
|
|
2540
|
-
export async function settleSeatTerminalResult(
|
|
2541
|
-
admitted: AdmittedRoleInvocation,
|
|
2542
|
-
authority: DurablePrincipalAuthority,
|
|
2543
|
-
scope?: SettlementCourtScope,
|
|
2544
|
-
): Promise<TerminalResult> {
|
|
2545
|
-
const settled = await settleSeat(admitted, authority, scope);
|
|
2546
|
-
if (settled === undefined) {
|
|
2547
|
-
const label = admitted.role.charAt(0).toUpperCase() + admitted.role.slice(1);
|
|
2548
|
-
throw new Error(
|
|
2549
|
-
`${label} Role run completed without a lawful typed terminal result`,
|
|
2550
|
-
);
|
|
2551
|
-
}
|
|
2552
|
-
return settled;
|
|
1649
|
+
return settleSealedSeat(admitted, authority, scope, {
|
|
1650
|
+
acceptedOnly: "sealedAcceptedOnly" in record && record.sealedAcceptedOnly === true,
|
|
1651
|
+
});
|
|
2553
1652
|
}
|
|
2554
1653
|
|
|
2555
1654
|
/**
|
|
@@ -2631,12 +1730,13 @@ async function publishDeclaredSeatArtifacts(
|
|
|
2631
1730
|
roleOutcome: TerminalRoleOutcome,
|
|
2632
1731
|
coordinates: DurablePrincipalCoordinates,
|
|
2633
1732
|
entries: readonly SessionEntry[],
|
|
1733
|
+
recordAttemptHistory: boolean,
|
|
2634
1734
|
): Promise<TerminalArtifactRef[]> {
|
|
2635
1735
|
const face = seatArtifactFace(admitted.role);
|
|
2636
1736
|
const phase = face.reportPhase === true
|
|
2637
1737
|
? { phase: readAdmittedPath(admitted, "phase") }
|
|
2638
1738
|
: {};
|
|
2639
|
-
return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
|
|
1739
|
+
return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, recordAttemptHistory, {
|
|
2640
1740
|
report: {
|
|
2641
1741
|
role: admitted.role,
|
|
2642
1742
|
runId: admitted.runId,
|
|
@@ -2656,16 +1756,6 @@ async function publishDeclaredSeatArtifacts(
|
|
|
2656
1756
|
});
|
|
2657
1757
|
}
|
|
2658
1758
|
|
|
2659
|
-
/**
|
|
2660
|
-
* Shared accepted-settlement skeleton for seats that scan residual tool
|
|
2661
|
-
* candidates then project sealed ledger outcome (#502 DRY).
|
|
2662
|
-
* #757: sealed receipts pass through full decisiveFacts — one path, no per-seat projector.
|
|
2663
|
-
*/
|
|
2664
|
-
type SeatAcceptedSettlementSpec = {
|
|
2665
|
-
readonly role: TerminalRoleName;
|
|
2666
|
-
readonly toolName: string;
|
|
2667
|
-
};
|
|
2668
|
-
|
|
2669
1759
|
/** Latest top-level user message index; 0 when the session has none (initial attempt). */
|
|
2670
1760
|
function currentAttemptStartIndex(entries: readonly SessionEntry[]): number {
|
|
2671
1761
|
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
@@ -2677,93 +1767,8 @@ function currentAttemptStartIndex(entries: readonly SessionEntry[]): number {
|
|
|
2677
1767
|
return 0;
|
|
2678
1768
|
}
|
|
2679
1769
|
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
authority: DurablePrincipalAuthority,
|
|
2683
|
-
spec: SeatAcceptedSettlementSpec,
|
|
2684
|
-
scope?: SettlementCourtScope,
|
|
2685
|
-
): Promise<TerminalResult | undefined> {
|
|
2686
|
-
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
2687
|
-
const { sessionDirectory, sessionFile } = coordinates;
|
|
2688
|
-
const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
|
|
2689
|
-
const submissions = await recordedSubmissionPayloads(admitted, scope);
|
|
2690
|
-
// #843: collector shape (ledger closed first) plus current user-turn freshness.
|
|
2691
|
-
// Only the ledger-owned closure establishes this turn's success. A non-error
|
|
2692
|
-
// toolResult may be a candidate whose nested gate continued the conversation.
|
|
2693
|
-
// A current-attempt
|
|
2694
|
-
// residual without that success is this turn's own failure and must not be
|
|
2695
|
-
// masked by run-scoped stale acceptance (bare resume without courtAttemptId).
|
|
2696
|
-
// Same-turn accept-then-bounce keeps the success marker: terminal stays
|
|
2697
|
-
// accepted; rejection facts remain on payloads/gate (not latest-wins flip).
|
|
2698
|
-
const scanStart = currentAttemptStartIndex(entries);
|
|
2699
|
-
let thisAttemptHasSeatSuccess = false;
|
|
2700
|
-
let residual: BoundErroredToolCandidate | undefined;
|
|
2701
|
-
for (let index = entries.length - 1; index >= scanStart; index -= 1) {
|
|
2702
|
-
const entry = entries[index];
|
|
2703
|
-
if (entry?.type === "custom" && entry.customType === "ak-role-submission-closure"
|
|
2704
|
-
&& isRecord(entry.data) && entry.data.toolName === spec.toolName
|
|
2705
|
-
&& classifyPackagedRoleTerminalResult(entry.data).kind === "accepted") {
|
|
2706
|
-
thisAttemptHasSeatSuccess = true;
|
|
2707
|
-
}
|
|
2708
|
-
const message = entries[index]?.message;
|
|
2709
|
-
if (message?.role !== "toolResult") continue;
|
|
2710
|
-
if (message.toolName !== spec.toolName) continue;
|
|
2711
|
-
if (message.isError === false) continue;
|
|
2712
|
-
if (residual === undefined) {
|
|
2713
|
-
residual = boundErroredToolCandidate(
|
|
2714
|
-
entries,
|
|
2715
|
-
index,
|
|
2716
|
-
message,
|
|
2717
|
-
spec.toolName,
|
|
2718
|
-
);
|
|
2719
|
-
}
|
|
2720
|
-
}
|
|
2721
|
-
const roleOutcome = await sealedLedgerOutcome(admitted, spec.role as TerminalRoleName, scope);
|
|
2722
|
-
if (roleOutcome !== undefined && (
|
|
2723
|
-
thisAttemptHasSeatSuccess || residual === undefined || (scope?.courtAttemptId !== undefined && scope.courtAttemptId.length > 0)
|
|
2724
|
-
)) {
|
|
2725
|
-
const navigator = extractNavigatorFact(entries);
|
|
2726
|
-
const artifacts = await publishDeclaredSeatArtifacts(
|
|
2727
|
-
admitted,
|
|
2728
|
-
roleOutcome,
|
|
2729
|
-
coordinates,
|
|
2730
|
-
entries,
|
|
2731
|
-
);
|
|
2732
|
-
return withSubmissions(
|
|
2733
|
-
await withOptionalGateProjection(
|
|
2734
|
-
{
|
|
2735
|
-
roleOutcome,
|
|
2736
|
-
navigator,
|
|
2737
|
-
artifacts,
|
|
2738
|
-
runId: admitted.runId,
|
|
2739
|
-
},
|
|
2740
|
-
sessionDirectory,
|
|
2741
|
-
detourGateContext(admitted, scope),
|
|
2742
|
-
),
|
|
2743
|
-
submissions,
|
|
2744
|
-
);
|
|
2745
|
-
}
|
|
2746
|
-
if (residual !== undefined) {
|
|
2747
|
-
const details = isRecord(residual.candidate)
|
|
2748
|
-
? residual.candidate
|
|
2749
|
-
: { candidate: residual.candidate };
|
|
2750
|
-
const failed = await settleFailureTerminalResult(
|
|
2751
|
-
admitted,
|
|
2752
|
-
{
|
|
2753
|
-
cause: "output",
|
|
2754
|
-
diagnostic: residual.diagnostic,
|
|
2755
|
-
details,
|
|
2756
|
-
},
|
|
2757
|
-
authority,
|
|
2758
|
-
scope ?? {},
|
|
2759
|
-
);
|
|
2760
|
-
return withSubmissions(failed, submissions);
|
|
2761
|
-
}
|
|
2762
|
-
return undefined;
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
/** Accepted-tool seats: one scan, tool name from the composition-root record. */
|
|
2766
|
-
export async function trySettleAcceptedSeatTerminalResult(
|
|
1770
|
+
/** Accepted-tool seats retain declaration checks and optional officer projection. */
|
|
1771
|
+
async function trySettleAcceptedSeatTerminalResult(
|
|
2767
1772
|
admitted: AdmittedRoleInvocation,
|
|
2768
1773
|
authority: DurablePrincipalAuthority,
|
|
2769
1774
|
scope?: SettlementCourtScope,
|
|
@@ -2772,10 +1777,7 @@ export async function trySettleAcceptedSeatTerminalResult(
|
|
|
2772
1777
|
if (toolName === undefined) {
|
|
2773
1778
|
throw new Error(`accepted-seat settlement is not declared for ${admitted.role}`);
|
|
2774
1779
|
}
|
|
2775
|
-
const settled = await
|
|
2776
|
-
role: admitted.role,
|
|
2777
|
-
toolName,
|
|
2778
|
-
}, scope);
|
|
1780
|
+
const settled = await settleSealedSeat(admitted, authority, scope, { acceptedOnly: false });
|
|
2779
1781
|
const record = packagedRoleMetadata(admitted.role);
|
|
2780
1782
|
if (
|
|
2781
1783
|
settled === undefined
|
|
@@ -2785,14 +1787,14 @@ export async function trySettleAcceptedSeatTerminalResult(
|
|
|
2785
1787
|
) {
|
|
2786
1788
|
return settled;
|
|
2787
1789
|
}
|
|
2788
|
-
return applySecretariatCountersignTerminal(admitted, authority, settled);
|
|
1790
|
+
return applySecretariatCountersignTerminal(admitted, authority, settled, scope);
|
|
2789
1791
|
}
|
|
2790
1792
|
|
|
2791
1793
|
/** One settlement dispatch. The registry `settlement` leaf selects the path. */
|
|
2792
1794
|
export async function trySettlePublicSeat(
|
|
2793
1795
|
admitted: AdmittedRoleInvocation,
|
|
2794
1796
|
authority: DurablePrincipalAuthority,
|
|
2795
|
-
scope:
|
|
1797
|
+
scope: SettlementCourtScope | undefined,
|
|
2796
1798
|
): Promise<TerminalResult | undefined> {
|
|
2797
1799
|
return settleSeat(admitted, authority, scope);
|
|
2798
1800
|
}
|
|
@@ -2881,9 +1883,22 @@ async function applySecretariatCountersignTerminal(
|
|
|
2881
1883
|
admitted: AdmittedRoleInvocation,
|
|
2882
1884
|
authority: DurablePrincipalAuthority,
|
|
2883
1885
|
settled: TerminalResult,
|
|
1886
|
+
scope?: SettlementCourtScope,
|
|
2884
1887
|
): Promise<TerminalResult> {
|
|
2885
1888
|
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
2886
|
-
|
|
1889
|
+
let entries: SessionEntry[] = [];
|
|
1890
|
+
try {
|
|
1891
|
+
entries = await readBoundSessionEntries(coordinates.sessionFile);
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
if (!isEnoent(error)) {
|
|
1894
|
+
await noteSettlementFault(
|
|
1895
|
+
admitted.runDirectory,
|
|
1896
|
+
scope,
|
|
1897
|
+
`secretariat officer read failed beside lawful terminal: ${describeErrorIdentity(error)}`,
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
return settled;
|
|
1901
|
+
}
|
|
2887
1902
|
const officer = countersignTerminalFromEntries(entries);
|
|
2888
1903
|
if (officer === undefined) return settled;
|
|
2889
1904
|
|
|
@@ -2926,15 +1941,25 @@ export async function attachPostAuditProjection(
|
|
|
2926
1941
|
admitted: AdmittedRoleInvocation,
|
|
2927
1942
|
authority: DurablePrincipalAuthority,
|
|
2928
1943
|
terminal: TerminalResult,
|
|
1944
|
+
scope?: SettlementCourtScope,
|
|
2929
1945
|
): Promise<TerminalResult> {
|
|
2930
1946
|
const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
1947
|
+
let gate: TerminalGateFact | undefined;
|
|
1948
|
+
try {
|
|
1949
|
+
gate = await extractGateFactFromSessionDirectory(sessionDirectory, {
|
|
1950
|
+
runDirectory: admitted.runDirectory,
|
|
1951
|
+
parentSessionFile: sessionFile,
|
|
1952
|
+
});
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
await noteSettlementFault(
|
|
1955
|
+
admitted.runDirectory,
|
|
1956
|
+
scope,
|
|
1957
|
+
`gate read failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
2935
1960
|
const withGate = gate === undefined ? terminal : { ...terminal, gate };
|
|
2936
1961
|
if (admitted.role !== "secretariat") return withGate;
|
|
2937
|
-
return applySecretariatCountersignTerminal(admitted, authority, withGate);
|
|
1962
|
+
return applySecretariatCountersignTerminal(admitted, authority, withGate, scope);
|
|
2938
1963
|
}
|
|
2939
1964
|
|
|
2940
1965
|
/** One failed attempt to place a durable failure artifact (path is private layout). */
|
|
@@ -2952,17 +1977,10 @@ function publicationAttemptFromError(
|
|
|
2952
1977
|
error: unknown,
|
|
2953
1978
|
): PublicationAttempt {
|
|
2954
1979
|
if (error instanceof Error) {
|
|
2955
|
-
const identity: { name?: string; code?: string | number } = {
|
|
2956
|
-
name: error.name,
|
|
2957
|
-
};
|
|
2958
|
-
const code = (error as { code?: unknown }).code;
|
|
2959
|
-
if (typeof code === "string" || typeof code === "number") {
|
|
2960
|
-
identity.code = code;
|
|
2961
|
-
}
|
|
2962
1980
|
return {
|
|
2963
1981
|
path,
|
|
2964
1982
|
diagnostic: error.message || error.name || "write failed",
|
|
2965
|
-
identity,
|
|
1983
|
+
identity: thrownIdentity(error),
|
|
2966
1984
|
};
|
|
2967
1985
|
}
|
|
2968
1986
|
return { path, diagnostic: String(error) };
|
|
@@ -3019,7 +2037,7 @@ async function writeFailureJsonRetainingCause(
|
|
|
3019
2037
|
const candidates: string[] = [
|
|
3020
2038
|
...preferredCandidates,
|
|
3021
2039
|
// One unique name per fallback dir — collisions on fixed names cannot exhaust this.
|
|
3022
|
-
...uniqueFallbackDirs.map((dir) => join(dir, `${stem}.${randomUUID()}.json`)),
|
|
2040
|
+
...uniqueFallbackDirs.map((dir) => join(dir, stem === UNIQUE_ERROR_FALLBACK_STEM ? uniqueErrorFallbackName() : `${stem}.${randomUUID()}.json`)),
|
|
3023
2041
|
];
|
|
3024
2042
|
for (let i = 0; i < candidates.length; i += 1) {
|
|
3025
2043
|
const path = candidates[i]!;
|
|
@@ -3060,6 +2078,7 @@ export async function publishFailureArtifacts(
|
|
|
3060
2078
|
failure: ControlledFailure,
|
|
3061
2079
|
authority: DurablePrincipalAuthority,
|
|
3062
2080
|
onErrorPublished?: (path: string) => void,
|
|
2081
|
+
recordAttemptHistory = false,
|
|
3063
2082
|
): Promise<TerminalArtifactRef[]> {
|
|
3064
2083
|
const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
|
|
3065
2084
|
const { baseDir, attempt: baseAttempt } = await resolveFailureArtifactsBase(
|
|
@@ -3067,33 +2086,26 @@ export async function publishFailureArtifacts(
|
|
|
3067
2086
|
);
|
|
3068
2087
|
const priorIssues: PublicationAttempt[] =
|
|
3069
2088
|
baseAttempt === undefined ? [] : [baseAttempt];
|
|
3070
|
-
// #
|
|
3071
|
-
//
|
|
3072
|
-
//
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
publicationAttemptFromError(
|
|
3080
|
-
|
|
3081
|
-
error,
|
|
3082
|
-
),
|
|
3083
|
-
);
|
|
2089
|
+
// #419: a dispatched failure joins history before fixed-name views change.
|
|
2090
|
+
// Re-projecting an existing failure does not append. History write failure
|
|
2091
|
+
// rides publicationIssues rather than stranding the controlled failure.
|
|
2092
|
+
if (recordAttemptHistory) {
|
|
2093
|
+
try {
|
|
2094
|
+
await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile }, {
|
|
2095
|
+
kind: "failure", role: admitted.role, ...failure,
|
|
2096
|
+
});
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
priorIssues.push(publicationAttemptFromError(sessionFile, error));
|
|
2099
|
+
}
|
|
3084
2100
|
}
|
|
3085
|
-
// #
|
|
3086
|
-
//
|
|
3087
|
-
// strand the original controlled failure outside settlement — it rides
|
|
3088
|
-
// publicationIssues instead of aborting durability.
|
|
2101
|
+
// #953: clear the prior success face, but a failure here must not strand
|
|
2102
|
+
// the original controlled cause; preserve the issue beside the fallback.
|
|
3089
2103
|
try {
|
|
3090
|
-
await
|
|
3091
|
-
kind: "failure",
|
|
3092
|
-
role: admitted.role,
|
|
3093
|
-
...failure,
|
|
3094
|
-
});
|
|
2104
|
+
await clearOppositeTerminalArtifactFace(admitted.runDirectory);
|
|
3095
2105
|
} catch (error) {
|
|
3096
|
-
priorIssues.push(publicationAttemptFromError(
|
|
2106
|
+
priorIssues.push(publicationAttemptFromError(
|
|
2107
|
+
roleRunArtifactsDirectory(admitted.runDirectory), error,
|
|
2108
|
+
));
|
|
3097
2109
|
}
|
|
3098
2110
|
|
|
3099
2111
|
// Prefer conventional names; unique fallback dirs keep colliding fixed paths
|
|
@@ -3106,23 +2118,23 @@ export async function publishFailureArtifacts(
|
|
|
3106
2118
|
);
|
|
3107
2119
|
const errorCandidates = underArtifacts
|
|
3108
2120
|
? [
|
|
3109
|
-
join(baseDir,
|
|
3110
|
-
join(baseDir,
|
|
3111
|
-
join(admitted.runDirectory,
|
|
2121
|
+
join(baseDir, RUN_TERMINAL_ERROR_FILE),
|
|
2122
|
+
join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
|
|
2123
|
+
join(admitted.runDirectory, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
|
|
3112
2124
|
]
|
|
3113
2125
|
: [
|
|
3114
|
-
join(baseDir,
|
|
3115
|
-
join(baseDir,
|
|
2126
|
+
join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
|
|
2127
|
+
join(baseDir, RUN_TERMINAL_ERROR_FILE),
|
|
3116
2128
|
];
|
|
3117
2129
|
const evidenceCandidates = underArtifacts
|
|
3118
2130
|
? [
|
|
3119
|
-
join(baseDir,
|
|
2131
|
+
join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
|
|
3120
2132
|
join(baseDir, "evidence.settlement.json"),
|
|
3121
2133
|
join(admitted.runDirectory, "evidence.settlement.json"),
|
|
3122
2134
|
]
|
|
3123
2135
|
: [
|
|
3124
2136
|
join(baseDir, "evidence.settlement.json"),
|
|
3125
|
-
join(baseDir,
|
|
2137
|
+
join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
|
|
3126
2138
|
];
|
|
3127
2139
|
|
|
3128
2140
|
const errorPayloadBase: Record<string, unknown> = {
|
|
@@ -3133,12 +2145,15 @@ export async function publishFailureArtifacts(
|
|
|
3133
2145
|
diagnostic: failure.diagnostic,
|
|
3134
2146
|
...(failure.identity === undefined ? {} : { identity: failure.identity }),
|
|
3135
2147
|
...(failure.details === undefined ? {} : { details: failure.details }),
|
|
2148
|
+
// This package's own facts, kept beside the host's report.
|
|
2149
|
+
...(failure.packageFact === undefined ? {} : { packageFact: failure.packageFact }),
|
|
2150
|
+
...(failure.stderr === undefined ? {} : { stderr: failure.stderr }),
|
|
3136
2151
|
};
|
|
3137
2152
|
|
|
3138
2153
|
const errorWrite = await writeFailureJsonRetainingCause(
|
|
3139
2154
|
errorCandidates,
|
|
3140
2155
|
uniqueFallbackDirs,
|
|
3141
|
-
|
|
2156
|
+
UNIQUE_ERROR_FALLBACK_STEM,
|
|
3142
2157
|
errorPayloadBase,
|
|
3143
2158
|
priorIssues,
|
|
3144
2159
|
);
|
|
@@ -3149,12 +2164,7 @@ export async function publishFailureArtifacts(
|
|
|
3149
2164
|
sessionDirectory: sessionDirectory,
|
|
3150
2165
|
sessionFile: sessionFile,
|
|
3151
2166
|
admittedRequestPath: admitted.admittedRequestPath,
|
|
3152
|
-
attachments: admitted.attachments
|
|
3153
|
-
provenancePath: a.provenancePath,
|
|
3154
|
-
frozenPath: a.frozenPath,
|
|
3155
|
-
sha256: a.sha256,
|
|
3156
|
-
byteLength: a.byteLength,
|
|
3157
|
-
})),
|
|
2167
|
+
attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
|
|
3158
2168
|
...(failure.cause === undefined ? {} : { failureCause: failure.cause }),
|
|
3159
2169
|
};
|
|
3160
2170
|
const evidenceWrite = await writeFailureJsonRetainingCause(
|
|
@@ -3181,68 +2191,35 @@ export async function settleFailureTerminalResult(
|
|
|
3181
2191
|
failure: ControlledFailure,
|
|
3182
2192
|
authority: DurablePrincipalAuthority,
|
|
3183
2193
|
options: SettlementCourtScope & {
|
|
3184
|
-
readonly resume?: TerminalResume;
|
|
3185
2194
|
readonly onErrorPublished?: (path: string) => void;
|
|
3186
2195
|
} = {},
|
|
3187
2196
|
): Promise<TerminalResult> {
|
|
3188
2197
|
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
3189
2198
|
const { sessionDirectory, sessionFile } = coordinates;
|
|
3190
|
-
//
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
if (
|
|
3213
|
-
facts !== undefined &&
|
|
3214
|
-
facts.runPointer === admitted.runDirectory &&
|
|
3215
|
-
facts.attemptPointer === `current:${admitted.runDirectory}`
|
|
3216
|
-
) {
|
|
3217
|
-
let decisiveFacts: NoReceiptLifecycleFacts & Record<string, unknown> = facts;
|
|
3218
|
-
// #478: no_receipt is still a public Terminal — project accepted gate facts.
|
|
3219
|
-
// #953: empty public terminal face — clear every reader-adoptable prior face.
|
|
3220
|
-
await clearOppositeTerminalArtifactFace(admitted.runDirectory);
|
|
3221
|
-
return withOptionalGateProjection(
|
|
3222
|
-
{
|
|
3223
|
-
roleOutcome: {
|
|
3224
|
-
kind: "no_receipt",
|
|
3225
|
-
role: admitted.role,
|
|
3226
|
-
status: "no-accepted-receipt",
|
|
3227
|
-
...facts,
|
|
3228
|
-
decisiveFacts,
|
|
3229
|
-
},
|
|
3230
|
-
navigator: await extractNavigatorFactFromAdmittedSession(sessionFile),
|
|
3231
|
-
artifacts: [],
|
|
3232
|
-
runId: admitted.runId,
|
|
3233
|
-
},
|
|
3234
|
-
sessionDirectory,
|
|
3235
|
-
detourGateContext(admitted, options),
|
|
3236
|
-
);
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
2199
|
+
// Exact-session attendance only — never infer no-advice from caller omission.
|
|
2200
|
+
const navigator = await extractNavigatorFactFromAdmittedSession(
|
|
2201
|
+
sessionFile,
|
|
2202
|
+
admitted.runDirectory,
|
|
2203
|
+
options,
|
|
2204
|
+
);
|
|
2205
|
+
let artifacts: TerminalArtifactRef[] = [];
|
|
2206
|
+
if (options.previewOnly !== true) {
|
|
2207
|
+
try {
|
|
2208
|
+
artifacts = await publishFailureArtifacts(
|
|
2209
|
+
admitted,
|
|
2210
|
+
failure,
|
|
2211
|
+
authority,
|
|
2212
|
+
options.onErrorPublished,
|
|
2213
|
+
options.recordAttemptHistory === true,
|
|
2214
|
+
);
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
await noteSettlementFault(
|
|
2217
|
+
admitted.runDirectory,
|
|
2218
|
+
options,
|
|
2219
|
+
`failure artifact publication failed beside host terminal: ${describeErrorIdentity(error)}`,
|
|
2220
|
+
);
|
|
3239
2221
|
}
|
|
3240
2222
|
}
|
|
3241
|
-
// Exact-session attendance only — never infer no-advice from caller omission.
|
|
3242
|
-
const navigator = await extractNavigatorFactFromAdmittedSession(sessionFile);
|
|
3243
|
-
// Private durable artifacts retain the original diagnostic identity (including run ID).
|
|
3244
|
-
const artifacts = await publishFailureArtifacts(admitted, failure, authority, options.onErrorPublished);
|
|
3245
|
-
const errorPath = artifacts.find((artifact) => artifact.kind === "error")?.path;
|
|
3246
2223
|
const decisiveFacts: Record<string, unknown> = {
|
|
3247
2224
|
...(failure.cause === undefined ? {} : { cause: failure.cause }),
|
|
3248
2225
|
diagnostic: failure.diagnostic,
|
|
@@ -3256,30 +2233,11 @@ export async function settleFailureTerminalResult(
|
|
|
3256
2233
|
if (failure.details !== undefined) {
|
|
3257
2234
|
decisiveFacts.secondaryEvidence = failure.details;
|
|
3258
2235
|
}
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
const roleOutcome: TerminalRoleOutcome = {
|
|
3265
|
-
kind: "failure",
|
|
3266
|
-
role: admitted.role,
|
|
3267
|
-
...(failure.cause === undefined ? {} : { cause: failure.cause }),
|
|
3268
|
-
diagnostic: failure.diagnostic,
|
|
3269
|
-
decisiveFacts,
|
|
3270
|
-
};
|
|
3271
|
-
const terminal = await withOptionalGateProjection(
|
|
3272
|
-
{
|
|
3273
|
-
roleOutcome,
|
|
3274
|
-
navigator,
|
|
3275
|
-
artifacts: [],
|
|
3276
|
-
resume: options.resume,
|
|
3277
|
-
},
|
|
3278
|
-
sessionDirectory,
|
|
3279
|
-
detourGateContext(admitted, options),
|
|
3280
|
-
);
|
|
3281
|
-
if (errorPath !== undefined) privateFailureErrorPaths.set(terminal, errorPath);
|
|
3282
|
-
return terminal;
|
|
2236
|
+
if (failure.packageFact !== undefined) {
|
|
2237
|
+
decisiveFacts.packageFact = failure.packageFact;
|
|
2238
|
+
}
|
|
2239
|
+
if (failure.stderr !== undefined) {
|
|
2240
|
+
decisiveFacts.stderr = failure.stderr;
|
|
3283
2241
|
}
|
|
3284
2242
|
const roleOutcome: TerminalRoleOutcome = {
|
|
3285
2243
|
kind: "failure",
|
|
@@ -3288,31 +2246,13 @@ export async function settleFailureTerminalResult(
|
|
|
3288
2246
|
diagnostic: failure.diagnostic,
|
|
3289
2247
|
decisiveFacts,
|
|
3290
2248
|
};
|
|
3291
|
-
// #478: ordinary controlled failure still surfaces accepted gate facts.
|
|
3292
2249
|
return withOptionalGateProjection(
|
|
3293
|
-
{
|
|
3294
|
-
roleOutcome,
|
|
3295
|
-
navigator,
|
|
3296
|
-
artifacts,
|
|
3297
|
-
runId: admitted.runId,
|
|
3298
|
-
},
|
|
2250
|
+
{ roleOutcome, navigator, artifacts, runId: admitted.runId },
|
|
3299
2251
|
sessionDirectory,
|
|
3300
2252
|
detourGateContext(admitted, options),
|
|
3301
2253
|
);
|
|
3302
2254
|
}
|
|
3303
2255
|
|
|
3304
|
-
/** Judge-named alias retained for #107 call sites. */
|
|
3305
|
-
export async function settleJudgeFailureTerminalResult(
|
|
3306
|
-
admitted: AdmittedJudgeInvocation,
|
|
3307
|
-
failure: ControlledFailure,
|
|
3308
|
-
authority: DurablePrincipalAuthority,
|
|
3309
|
-
options: SettlementCourtScope & {
|
|
3310
|
-
readonly resume?: TerminalResume;
|
|
3311
|
-
} = {},
|
|
3312
|
-
): Promise<TerminalResult> {
|
|
3313
|
-
return settleFailureTerminalResult(admitted, failure, authority, options);
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
2256
|
/**
|
|
3317
2257
|
* Emit one complete failure Terminal on stdout and one concise stderr diagnostic.
|
|
3318
2258
|
* Artifacts are already durable on the TerminalResult.
|
|
@@ -3327,9 +2267,11 @@ export function presentFailureTerminal(
|
|
|
3327
2267
|
io.stdout(formatTerminalResult(terminal));
|
|
3328
2268
|
if (terminal.roleOutcome.kind === "failure") {
|
|
3329
2269
|
if (io.omitFailureStderrDiagnostic) return;
|
|
2270
|
+
const hostStderr = terminal.roleOutcome.decisiveFacts.stderr;
|
|
3330
2271
|
io.stderr(formatFailureStderrDiagnostic({
|
|
3331
2272
|
...(terminal.roleOutcome.cause === undefined ? {} : { cause: terminal.roleOutcome.cause }),
|
|
3332
2273
|
diagnostic: terminal.roleOutcome.diagnostic,
|
|
2274
|
+
...(typeof hostStderr === "string" && hostStderr.length > 0 ? { stderr: hostStderr } : {}),
|
|
3333
2275
|
}));
|
|
3334
2276
|
return;
|
|
3335
2277
|
}
|