@akagilnc/pi-workflow-roles 0.1.5406 → 0.1.5475
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/README.md +42 -55
- package/README.zh-CN.md +59 -73
- package/THIRD_PARTY_NOTICES.md +4 -7
- package/dist/acp-host/production-host.js +8261 -9073
- package/dist/collector-tool-schemas.js +1 -1
- package/dist/compliance-transport.js +2 -5
- package/dist/engine-detour-usage.js +3 -30
- package/dist/gatekeeper-role.js +4 -18
- package/dist/headless-host/description.js +0 -209
- package/dist/headless-host/production-host.js +6973 -8035
- package/dist/host-contracts.js +8 -0
- package/dist/migrate-book-topology.js +30 -27
- package/dist/packaged-role-registry.js +1 -9
- package/dist/pi/role-turn-host.js +37 -19
- package/dist/public-cli/auto-resume.js +29 -66
- package/dist/public-cli/countersign-run.js +4 -18
- package/dist/public-cli/instruction-seat-run.js +59 -76
- package/dist/public-cli/main.js +9844 -10720
- package/dist/public-cli/option-definitions.js +1 -1
- package/dist/public-cli/post-admission.js +204 -285
- package/dist/public-cli/process-cancel.js +8 -0
- package/dist/public-cli/run-lifecycle.js +24 -94
- package/dist/public-cli/settlement.js +614 -1524
- package/dist/public-cli/terminal.js +23 -53
- package/dist/public-role-summons.js +3 -8
- package/dist/receipt-delivery-policy.js +11 -12
- package/dist/run-terminal-artifacts.js +19 -14
- package/dist/serialize-thrown-value.js +7 -1
- package/dist/submission-ledger.js +21 -89
- package/package.json +1 -1
- package/packets/fixer-repair.md +3 -1
- package/packets/judge-apply.md +7 -16
- package/packets/judge-authority.md +7 -17
- package/packets/judge-plan.md +7 -18
- package/packets/judge-review.md +8 -22
- package/packets/judge-submission.md +4 -7
- package/resources/collector-bot-handbook.md +2 -2
- package/resources/diarist-collect.md +2 -2
- package/resources/engine-dispatch.md +2 -2
- package/resources/navigator-route-playbook.md +1 -1
- package/resources/secretariat-ticket-identity.md +1 -1
- package/scripts/build-package.mjs +0 -2
- package/souls/coder-output-guide.md +3 -3
- package/souls/coder.md +2 -3
- package/souls/doctor-auditor.md +0 -3
- package/souls/fixer-auditor.md +0 -2
- package/souls/fixer-output-guide.md +3 -3
- package/souls/fixer.md +2 -2
- package/souls/gate-output-guide.md +1 -1
- package/souls/quality-law.md +2 -0
- package/src/acp-host/role-turn-host.ts +168 -75
- package/src/collector-tool-schemas.ts +1 -1
- package/src/compliance-transport.ts +3 -6
- package/src/engine-detour-usage.ts +3 -33
- package/src/external-host-turn-loop.ts +81 -36
- package/src/gatekeeper-role.ts +5 -20
- 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 +13 -10
- package/src/navigator-public-session.ts +1 -1
- package/src/navigator-session-contracts.ts +1 -1
- package/src/package-contracts/gatekeeper-output.ts +1 -1
- package/src/package-contracts/navigator-output.ts +1 -1
- package/src/package-contracts/terminating-infrastructure.ts +1 -1
- package/src/packaged-role-registry.ts +2 -10
- package/src/pi/adapter.ts +0 -3
- package/src/pi/role-turn-host.ts +42 -28
- package/src/prepared-role-turn.ts +2 -0
- package/src/public-cli/auto-resume.ts +27 -66
- package/src/public-cli/countersign-run.ts +4 -22
- package/src/public-cli/instruction-seat-run.ts +79 -80
- package/src/public-cli/option-definitions.ts +1 -1
- package/src/public-cli/post-admission.ts +264 -340
- package/src/public-cli/process-cancel.ts +9 -0
- package/src/public-cli/run-lifecycle.ts +25 -126
- package/src/public-cli/settlement.ts +792 -1912
- package/src/public-cli/terminal.ts +29 -69
- package/src/public-role-summons.ts +3 -8
- package/src/receipt-delivery-policy.ts +11 -12
- package/src/role-envelope.ts +16 -9
- package/src/role-runtime.ts +14 -81
- package/src/run-terminal-artifacts.ts +21 -17
- package/src/serialize-thrown-value.ts +7 -1
- package/src/submission-ledger.ts +30 -116
- 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,92 +4,61 @@
|
|
|
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
64
|
noReceiptLifecycleFacts,
|
|
@@ -104,18 +73,17 @@ import type {
|
|
|
104
73
|
DurablePrincipalCoordinates,
|
|
105
74
|
} from "../host-contracts.ts";
|
|
106
75
|
import { roleRunArtifactsDirectory } from "../role-run-placement.ts";
|
|
107
|
-
import { rewriteRunDirectoryPathValue } from "../role-run-relocation.ts";
|
|
108
76
|
import {
|
|
109
77
|
listSeamOwnedUniqueErrorFacePaths,
|
|
78
|
+
uniqueErrorFallbackName,
|
|
79
|
+
UNIQUE_ERROR_FALLBACK_STEM,
|
|
110
80
|
RUN_TERMINAL_ARTIFACT_FILES,
|
|
111
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,
|
|
112
86
|
} from "../run-terminal-artifacts.ts";
|
|
113
|
-
import {
|
|
114
|
-
ensureRunArtifactsDir,
|
|
115
|
-
homeFromRunDirectory,
|
|
116
|
-
type AdmittedJudgeInvocation,
|
|
117
|
-
type AdmittedRoleInvocation,
|
|
118
|
-
} from "./invocation.ts";
|
|
119
87
|
|
|
120
88
|
/** Ledger reads use the run's machine home — not ambient process HOME (child write vs parent settle). */
|
|
121
89
|
function sealedLedgerHome(admitted: Pick<AdmittedRoleInvocation, "runDirectory">): string {
|
|
@@ -127,12 +95,123 @@ function sealedLedgerHome(admitted: Pick<AdmittedRoleInvocation, "runDirectory">
|
|
|
127
95
|
* courtAttemptId tags the new court; recorded payloads stay run-scoped (#836).
|
|
128
96
|
*/
|
|
129
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;
|
|
130
102
|
readonly courtAttemptId?: string;
|
|
131
103
|
/** Public-invocation scope from the shared Host envelope (#537). */
|
|
132
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>;
|
|
133
110
|
};
|
|
134
111
|
|
|
135
|
-
|
|
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(
|
|
136
215
|
admitted: Pick<AdmittedRoleInvocation, "runDirectory">,
|
|
137
216
|
scope?: SettlementCourtScope,
|
|
138
217
|
): { home: string; sessionParent: string; attemptId?: string } {
|
|
@@ -208,29 +287,6 @@ async function sealedLedgerOutcome(
|
|
|
208
287
|
return roleOutcomeFromRows(role, rows);
|
|
209
288
|
}
|
|
210
289
|
|
|
211
|
-
/**
|
|
212
|
-
* True when this court/attempt itself already produced a sealed or
|
|
213
|
-
* audit-escalation submission (#836 r12 class 2). Presentation
|
|
214
|
-
* (submissions/payloads) always stays run-scoped and unfiltered — this is a
|
|
215
|
-
* control-flow signal only, consumed to keep a stale prior-attempt
|
|
216
|
-
* acceptance from outranking this attempt's own real host-turn failure.
|
|
217
|
-
* Absent a courtAttemptId scope there is no distinct prior attempt to stale
|
|
218
|
-
* against, so this defaults true (unchanged behavior for ordinary,
|
|
219
|
-
* non-court dispatches).
|
|
220
|
-
*/
|
|
221
|
-
export async function attemptProducedFreshSubmission(
|
|
222
|
-
admitted: AdmittedRoleInvocation,
|
|
223
|
-
scope?: SettlementCourtScope,
|
|
224
|
-
): Promise<boolean> {
|
|
225
|
-
if (scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0) return true;
|
|
226
|
-
return hasFreshAttemptSubmission(
|
|
227
|
-
admitted.projectRoot,
|
|
228
|
-
admitted.runId,
|
|
229
|
-
scope.courtAttemptId,
|
|
230
|
-
ledgerReadScope(admitted, scope),
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
290
|
/** #836: every raw role payload in settle scope (调几次记几次). */
|
|
235
291
|
export async function recordedSubmissionPayloads(
|
|
236
292
|
admitted: Pick<AdmittedRoleInvocation, "projectRoot" | "runId" | "runDirectory">,
|
|
@@ -248,23 +304,8 @@ export function withSubmissions<T extends TerminalResult>(
|
|
|
248
304
|
submissions: readonly unknown[],
|
|
249
305
|
): T {
|
|
250
306
|
if (submissions.length === 0) return terminal;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// #879: accepted/audit_escalation may still fill a missing this-court
|
|
254
|
-
// payloads face from that history. #953: failure must not — prior receipts
|
|
255
|
-
// stay on the historical carrier only, never as the current failure's
|
|
256
|
-
// ordinary payloads/receipt face.
|
|
257
|
-
if (roleOutcome.kind === "failure") {
|
|
258
|
-
return { ...terminal, submissions };
|
|
259
|
-
}
|
|
260
|
-
const withPayloads =
|
|
261
|
-
roleOutcome.kind === "accepted" || roleOutcome.kind === "audit_escalation"
|
|
262
|
-
? {
|
|
263
|
-
...roleOutcome,
|
|
264
|
-
payloads: coalesceSubmissionRows(roleOutcome.payloads, submissions),
|
|
265
|
-
}
|
|
266
|
-
: roleOutcome;
|
|
267
|
-
return { ...terminal, roleOutcome: withPayloads, submissions };
|
|
307
|
+
// Run history is presentation, never a substitute for this court's reply (#879).
|
|
308
|
+
return { ...terminal, submissions };
|
|
268
309
|
}
|
|
269
310
|
|
|
270
311
|
/** Attach full ledger submissions onto any settled terminal (#836). */
|
|
@@ -277,13 +318,19 @@ export async function attachRecordedSubmissions<T extends TerminalResult>(
|
|
|
277
318
|
// pass courtAttemptId here — roleOutcome already carries this-court payloads
|
|
278
319
|
// from sealedLedgerOutcome when scoped; withSubmissions keeps them.
|
|
279
320
|
void scope;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
}
|
|
287
334
|
}
|
|
288
335
|
|
|
289
336
|
/**
|
|
@@ -291,66 +338,6 @@ export async function attachRecordedSubmissions<T extends TerminalResult>(
|
|
|
291
338
|
* Does not invent an output failure for an empty ledger.
|
|
292
339
|
* #953: empty public terminal face — clear every reader-adoptable prior face.
|
|
293
340
|
*/
|
|
294
|
-
/**
|
|
295
|
-
* Lifecycle fact the in-process runtime already wrote for this run and attempt.
|
|
296
|
-
* Absent, malformed, or a previous attempt's bytes are not a count.
|
|
297
|
-
*/
|
|
298
|
-
function lifecycleFactsFromEntry(entry: SessionEntry): unknown {
|
|
299
|
-
const customType = entry.customType ?? entry.message?.customType;
|
|
300
|
-
if (customType !== NO_RECEIPT_LIFECYCLE_ENTRY_TYPE) return undefined;
|
|
301
|
-
return entry.data ?? entry.message?.details;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async function readCurrentAttemptNoReceiptFacts(
|
|
305
|
-
admitted: AdmittedRoleInvocation,
|
|
306
|
-
authority: DurablePrincipalAuthority,
|
|
307
|
-
scope?: SettlementCourtScope,
|
|
308
|
-
): Promise<NoReceiptLifecycleFacts | undefined> {
|
|
309
|
-
const { sessionFile } = coordinatesFromAdmitted(authority, admitted);
|
|
310
|
-
const entries = await readBoundSessionEntries(sessionFile).catch(() => undefined);
|
|
311
|
-
if (entries === undefined) return undefined;
|
|
312
|
-
const scopeId = scope?.invocationScopeId?.trim() ?? "";
|
|
313
|
-
if (scopeId.length > 0) {
|
|
314
|
-
const pointer = receiptAttemptPointer(admitted.runDirectory, scopeId);
|
|
315
|
-
let selected: NoReceiptLifecycleFacts | undefined;
|
|
316
|
-
for (const entry of entries) {
|
|
317
|
-
const raw = lifecycleFactsFromEntry(entry);
|
|
318
|
-
if (raw === undefined) continue;
|
|
319
|
-
try {
|
|
320
|
-
const facts = parseNoReceiptLifecycleFacts(raw);
|
|
321
|
-
if (facts.runPointer !== admitted.runDirectory) continue;
|
|
322
|
-
if (facts.attemptPointer !== pointer) continue;
|
|
323
|
-
if (selected === undefined || facts.deliveryTurns >= selected.deliveryTurns) {
|
|
324
|
-
selected = facts;
|
|
325
|
-
}
|
|
326
|
-
} catch {
|
|
327
|
-
// A malformed historical entry is not this call's count.
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return selected;
|
|
331
|
-
}
|
|
332
|
-
let attemptStart = 0;
|
|
333
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
334
|
-
if (entries[index]?.type === "message" && entries[index]?.message?.role === "user") {
|
|
335
|
-
attemptStart = index;
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
const lifecycleEntry = entries.slice(attemptStart).reverse().find((entry: SessionEntry) =>
|
|
340
|
-
lifecycleFactsFromEntry(entry) !== undefined);
|
|
341
|
-
const raw = lifecycleEntry === undefined ? undefined : lifecycleFactsFromEntry(lifecycleEntry);
|
|
342
|
-
if (raw === undefined) return undefined;
|
|
343
|
-
let facts: NoReceiptLifecycleFacts;
|
|
344
|
-
try {
|
|
345
|
-
facts = parseNoReceiptLifecycleFacts(raw);
|
|
346
|
-
} catch {
|
|
347
|
-
return undefined;
|
|
348
|
-
}
|
|
349
|
-
if (facts.runPointer !== admitted.runDirectory) return undefined;
|
|
350
|
-
if (facts.attemptPointer !== receiptAttemptPointer(admitted.runDirectory)) return undefined;
|
|
351
|
-
return facts;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
341
|
export async function settleHostEndedNoReceipt(
|
|
355
342
|
admitted: AdmittedRoleInvocation,
|
|
356
343
|
authority: DurablePrincipalAuthority,
|
|
@@ -363,48 +350,135 @@ export async function settleHostEndedNoReceipt(
|
|
|
363
350
|
*/
|
|
364
351
|
issuedDeliveryRequests = 0,
|
|
365
352
|
): Promise<TerminalResult> {
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
? undefined
|
|
369
|
-
: await readCurrentAttemptNoReceiptFacts(admitted, authority, scope);
|
|
370
|
-
let terminalToolCalled = persisted?.terminalToolCalled ?? false;
|
|
371
|
-
let rejectedReceipts: readonly { reason: string }[] = persisted?.rejectedReceipts ?? [];
|
|
372
|
-
let recordedTurns = persisted?.deliveryTurns ?? 0;
|
|
373
|
-
if (scopeId.length > 0) {
|
|
374
|
-
const { sessionFile } = coordinatesFromAdmitted(authority, admitted);
|
|
375
|
-
const entries = await readBoundSessionEntries(sessionFile).catch(() => undefined);
|
|
376
|
-
if (entries !== undefined) {
|
|
377
|
-
const prior = priorReceiptContinuation(entries, scopeId);
|
|
378
|
-
terminalToolCalled = prior.terminalToolCalled;
|
|
379
|
-
rejectedReceipts = prior.rejectedReceipts;
|
|
380
|
-
recordedTurns = prior.deliveryTurns;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
const facts = noReceiptLifecycleFacts({
|
|
384
|
-
terminalToolCalled,
|
|
385
|
-
rejectedReceipts,
|
|
386
|
-
deliveryTurns: Math.max(issuedDeliveryRequests, recordedTurns),
|
|
353
|
+
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
354
|
+
const binding = {
|
|
387
355
|
runPointer: admitted.runDirectory,
|
|
388
356
|
attemptPointer: receiptAttemptPointer(admitted.runDirectory, scope?.invocationScopeId),
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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;
|
|
404
427
|
},
|
|
405
|
-
coordinates.sessionDirectory,
|
|
406
|
-
detourGateContext(admitted, scope),
|
|
407
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));
|
|
408
482
|
}
|
|
409
483
|
|
|
410
484
|
/** Transitional host-session reads remain only for non-sealed failure and audit evidence. */
|
|
@@ -415,7 +489,6 @@ function coordinatesFromAdmitted(
|
|
|
415
489
|
return authority.decode(admitted.principal);
|
|
416
490
|
}
|
|
417
491
|
import {
|
|
418
|
-
coalesceSubmissionRows,
|
|
419
492
|
exitCodeForTerminalOutcome,
|
|
420
493
|
formatTerminalResult,
|
|
421
494
|
isLawfulTypedTerminalOutcome,
|
|
@@ -426,12 +499,11 @@ import {
|
|
|
426
499
|
type TerminalGateSeat,
|
|
427
500
|
type TerminalNavigatorFact,
|
|
428
501
|
type TerminalResult,
|
|
429
|
-
type TerminalResume,
|
|
430
502
|
type TerminalRoleName,
|
|
431
503
|
type TerminalRoleOutcome,
|
|
432
504
|
} from "./terminal.ts";
|
|
433
505
|
|
|
434
|
-
import { isEnoent, isMissingPathError, isRecord } from "../unknown-value.ts";
|
|
506
|
+
import { isEnoent, isMissingPathError, isRecord, errorText } from "../unknown-value.ts";
|
|
435
507
|
|
|
436
508
|
export type { ControlledFailureCause };
|
|
437
509
|
|
|
@@ -441,39 +513,55 @@ export {
|
|
|
441
513
|
isLawfulTypedTerminalOutcome,
|
|
442
514
|
};
|
|
443
515
|
|
|
444
|
-
/**
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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 & {
|
|
448
523
|
readonly diagnostic: string;
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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;
|
|
454
537
|
};
|
|
455
538
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
export
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
+
};
|
|
477
565
|
|
|
478
566
|
/**
|
|
479
567
|
* Host stderr as recorded — full bytes, no flood filter, no char clip (#836).
|
|
@@ -490,14 +578,13 @@ export function formatCliDiagnostic(message: string): string {
|
|
|
490
578
|
return `ak-role: ${message}\n`;
|
|
491
579
|
}
|
|
492
580
|
|
|
493
|
-
/**
|
|
494
|
-
* One concise stderr line for humans. Durable Error Artifact / Terminal keep the
|
|
495
|
-
* full original diagnostic — presentation collapses newlines and flood frames.
|
|
496
|
-
*/
|
|
497
581
|
/** #836: full diagnostic on stderr — no first-line clip / flood filter. */
|
|
498
582
|
export function formatFailureStderrDiagnostic(failure: ControlledFailure): string {
|
|
499
583
|
const text = failure.diagnostic.trim().length > 0 ? failure.diagnostic : "failure";
|
|
500
|
-
|
|
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`;
|
|
501
588
|
}
|
|
502
589
|
|
|
503
590
|
/**
|
|
@@ -541,42 +628,27 @@ export function presentControlledFailure(
|
|
|
541
628
|
io.stderr(formatFailureStderrDiagnostic(failure));
|
|
542
629
|
}
|
|
543
630
|
|
|
544
|
-
/**
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
export
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
await readFile(sessionFile, "utf8");
|
|
555
|
-
return { state: "present" };
|
|
556
|
-
} catch (error) {
|
|
557
|
-
if (isEnoent(error)) return { state: "missing" };
|
|
558
|
-
return {
|
|
559
|
-
state: "unreadable",
|
|
560
|
-
diagnostic:
|
|
561
|
-
error instanceof Error
|
|
562
|
-
? error.message || error.name
|
|
563
|
-
: String(error),
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
}
|
|
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
|
+
};
|
|
567
641
|
|
|
568
|
-
function thrownIdentity(error: Error): {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
} {
|
|
572
|
-
const identity: { name?: string; code?: string | number } = {
|
|
642
|
+
function thrownIdentity(error: Error): ThrownErrorIdentity {
|
|
643
|
+
const code = (error as { code?: unknown }).code;
|
|
644
|
+
return {
|
|
573
645
|
name: error.name,
|
|
646
|
+
...(typeof code === "string" || typeof code === "number" ? { code } : {}),
|
|
574
647
|
};
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
return identity;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function isControlledFailureCause(cause: unknown): cause is ControlledFailureCause {
|
|
651
|
+
return CONTROLLED_FAILURE_CAUSES.some((known) => known === cause);
|
|
580
652
|
}
|
|
581
653
|
|
|
582
654
|
/** Production-owned typed thrown failure (explicit-internal channel). */
|
|
@@ -589,48 +661,43 @@ function isTypedActivationError(
|
|
|
589
661
|
} {
|
|
590
662
|
if (!(error instanceof Error)) return false;
|
|
591
663
|
const cause = (error as { knownCause?: unknown }).knownCause;
|
|
592
|
-
return (
|
|
593
|
-
cause === "provider" ||
|
|
594
|
-
cause === "activation" ||
|
|
595
|
-
cause === "session" ||
|
|
596
|
-
cause === "output" ||
|
|
597
|
-
cause === "timeout"
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/** Flatten nested AggregateError leaves; non-aggregate values stay as one fact. */
|
|
602
|
-
function flattenThrownFailureLeaves(error: unknown): unknown[] {
|
|
603
|
-
if (!(error instanceof AggregateError)) {
|
|
604
|
-
return [error];
|
|
605
|
-
}
|
|
606
|
-
const leaves: unknown[] = [];
|
|
607
|
-
for (const item of error.errors) {
|
|
608
|
-
leaves.push(...flattenThrownFailureLeaves(item));
|
|
609
|
-
}
|
|
610
|
-
return leaves;
|
|
664
|
+
return isControlledFailureCause(cause);
|
|
611
665
|
}
|
|
612
666
|
|
|
613
667
|
/**
|
|
614
668
|
* Project one thrown value into a ControlledFailure leaf.
|
|
615
669
|
* Sole owner for thrown-leaf identity/diagnostic mapping.
|
|
616
|
-
*
|
|
670
|
+
* Aggregate shells and their direct children remain distinct facts.
|
|
617
671
|
*/
|
|
618
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
|
+
};
|
|
619
691
|
if (isTypedActivationError(error)) {
|
|
620
692
|
const identity = thrownIdentity(error);
|
|
621
|
-
if (error.failureCode !== undefined && identity.code === undefined) {
|
|
622
|
-
identity.code = error.failureCode;
|
|
623
|
-
}
|
|
624
693
|
return {
|
|
625
694
|
cause: error.knownCause,
|
|
626
695
|
diagnostic: error.message || error.name || "exception",
|
|
627
|
-
identity
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
error: serializeThrownValue(error),
|
|
633
|
-
},
|
|
696
|
+
identity: error.failureCode !== undefined && identity.code === undefined
|
|
697
|
+
? { ...identity, code: error.failureCode }
|
|
698
|
+
: identity,
|
|
699
|
+
details,
|
|
700
|
+
...packageFact,
|
|
634
701
|
};
|
|
635
702
|
}
|
|
636
703
|
if (error instanceof Error) {
|
|
@@ -639,72 +706,39 @@ export function projectThrownFailureLeaf(error: unknown): ControlledFailure {
|
|
|
639
706
|
return {
|
|
640
707
|
diagnostic: error.message || error.name || "exception",
|
|
641
708
|
identity,
|
|
642
|
-
details
|
|
709
|
+
details,
|
|
710
|
+
...packageFact,
|
|
643
711
|
};
|
|
644
712
|
}
|
|
645
713
|
return {
|
|
646
714
|
diagnostic: error !== null && typeof error === "object" ? "non-Error throw" : String(error),
|
|
647
|
-
details
|
|
715
|
+
details,
|
|
716
|
+
...packageFact,
|
|
648
717
|
};
|
|
649
718
|
}
|
|
650
719
|
|
|
651
720
|
/**
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
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.
|
|
655
724
|
*/
|
|
656
|
-
function classifyThrownFailure(error: unknown): ControlledFailure {
|
|
657
|
-
if (!(error instanceof AggregateError)) {
|
|
658
|
-
return projectThrownFailureLeaf(error);
|
|
659
|
-
}
|
|
660
|
-
const leaves = flattenThrownFailureLeaves(error);
|
|
661
|
-
if (leaves.length === 0) {
|
|
662
|
-
// Empty aggregate — retain the aggregate shell rather than invent a cause.
|
|
663
|
-
return projectThrownFailureLeaf(error);
|
|
664
|
-
}
|
|
665
|
-
const primary = projectThrownFailureLeaf(leaves[0]);
|
|
666
|
-
if (leaves.length === 1) {
|
|
667
|
-
return primary;
|
|
668
|
-
}
|
|
669
|
-
const priorConcurrent = Array.isArray(primary.details?.concurrentFailures)
|
|
670
|
-
? primary.details.concurrentFailures
|
|
671
|
-
: [];
|
|
672
|
-
const concurrentFailures = [
|
|
673
|
-
...priorConcurrent,
|
|
674
|
-
...leaves.slice(1).map((leaf) => {
|
|
675
|
-
const secondary = projectThrownFailureLeaf(leaf);
|
|
676
|
-
return {
|
|
677
|
-
...(secondary.cause === undefined ? {} : { cause: secondary.cause }),
|
|
678
|
-
diagnostic: secondary.diagnostic,
|
|
679
|
-
...(secondary.identity === undefined ? {} : { identity: secondary.identity }),
|
|
680
|
-
...(secondary.details === undefined ? {} : { details: secondary.details }),
|
|
681
|
-
};
|
|
682
|
-
}),
|
|
683
|
-
];
|
|
684
|
-
return {
|
|
685
|
-
...(primary.cause === undefined ? {} : { cause: primary.cause }),
|
|
686
|
-
diagnostic: primary.diagnostic,
|
|
687
|
-
...(primary.identity === undefined ? {} : { identity: primary.identity }),
|
|
688
|
-
details: {
|
|
689
|
-
...(primary.details ?? {}),
|
|
690
|
-
concurrentFailures,
|
|
691
|
-
},
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/** Merge caller-owned secondary evidence into a classified failure without washing path facts. */
|
|
696
725
|
function withKnownDetails(
|
|
697
726
|
failure: ControlledFailure,
|
|
698
727
|
knownDetails: Readonly<Record<string, unknown>> | undefined,
|
|
728
|
+
packageFact?: PackageSideFact,
|
|
699
729
|
): ControlledFailure {
|
|
700
|
-
|
|
701
|
-
|
|
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.
|
|
702
736
|
return {
|
|
703
737
|
...failure,
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
},
|
|
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 }),
|
|
708
742
|
};
|
|
709
743
|
}
|
|
710
744
|
|
|
@@ -712,11 +746,49 @@ function withKnownDetails(
|
|
|
712
746
|
* Classify a controlled post-admission failure without washing original identities.
|
|
713
747
|
* Cause classes are closed; diagnostic text retains the original identity when known.
|
|
714
748
|
*
|
|
715
|
-
* Order: thrown → knownCause → timeout → activation (nonzero) →
|
|
749
|
+
* Order: thrown → knownCause → timeout → activation (nonzero) → clean exit.
|
|
716
750
|
* knownCause precedes timeout so a co-present typed provider/session identity is not
|
|
717
751
|
* washed when the child also timed out. Cause is never inferred from stderr wording.
|
|
718
|
-
* AggregateError
|
|
752
|
+
* AggregateError shells, direct children and raw testimony stay distinct.
|
|
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的失败原因?).
|
|
719
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
|
+
|
|
720
792
|
export function classifyPostAdmissionFailure(input: {
|
|
721
793
|
timedOut: boolean;
|
|
722
794
|
code: number | null;
|
|
@@ -727,7 +799,6 @@ export function classifyPostAdmissionFailure(input: {
|
|
|
727
799
|
* being washed into activation/null-exit paths that treat missing thrown as absence.
|
|
728
800
|
*/
|
|
729
801
|
thrown?: unknown;
|
|
730
|
-
session?: SessionReadiness;
|
|
731
802
|
/** Upstream-typed cause when the failure origin is already known. */
|
|
732
803
|
knownCause?: ControlledFailureCause;
|
|
733
804
|
/** Optional identity paired with knownCause (production channel). */
|
|
@@ -742,71 +813,112 @@ export function classifyPostAdmissionFailure(input: {
|
|
|
742
813
|
knownDiagnostic?: string;
|
|
743
814
|
/** Secondary evidence already carried by the typed production failure. */
|
|
744
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;
|
|
745
825
|
}): ControlledFailure {
|
|
746
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 (失败诚实宪法:接住可以,洗白不行).
|
|
747
830
|
if (Object.hasOwn(input, "thrown")) {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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 (失败诚实宪法:接住可以,洗白不行).
|
|
766
877
|
return {
|
|
767
|
-
cause: input.knownCause,
|
|
768
|
-
diagnostic
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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)),
|
|
775
886
|
...(input.knownIdentity === undefined
|
|
776
887
|
? {}
|
|
777
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 },
|
|
778
894
|
};
|
|
779
895
|
}
|
|
780
|
-
// #881: original
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
// not a stand-in primary failure record.
|
|
784
|
-
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 ||
|
|
785
899
|
(input.knownDiagnostic !== undefined && input.knownDiagnostic.trim() !== "") ||
|
|
786
|
-
input.knownIdentity !== undefined
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
input.
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
undefined,
|
|
809
|
-
);
|
|
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
|
+
};
|
|
810
922
|
}
|
|
811
923
|
if (input.timedOut) {
|
|
812
924
|
return withKnownDetails(
|
|
@@ -816,46 +928,38 @@ export function classifyPostAdmissionFailure(input: {
|
|
|
816
928
|
details: { timedOut: true, exitCode: input.code },
|
|
817
929
|
},
|
|
818
930
|
input.knownDetails,
|
|
931
|
+
packageFactsOf(input),
|
|
819
932
|
);
|
|
820
933
|
}
|
|
821
934
|
if (input.code !== 0) {
|
|
822
|
-
|
|
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);
|
|
823
940
|
return withKnownDetails(
|
|
824
941
|
{
|
|
825
|
-
cause: "activation",
|
|
826
942
|
diagnostic: conciseChildDiagnostic(input.stderr, fallback),
|
|
827
943
|
details: { exitCode: input.code },
|
|
828
944
|
},
|
|
829
945
|
input.knownDetails,
|
|
946
|
+
packageFactsOf(input),
|
|
830
947
|
);
|
|
831
948
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
details: { exitCode: input.code, session: "missing" },
|
|
838
|
-
},
|
|
839
|
-
input.knownDetails,
|
|
840
|
-
);
|
|
841
|
-
}
|
|
842
|
-
if (input.session?.state === "unreadable") {
|
|
843
|
-
return withKnownDetails(
|
|
844
|
-
{
|
|
845
|
-
cause: "session",
|
|
846
|
-
diagnostic: input.session.diagnostic,
|
|
847
|
-
details: { exitCode: input.code, session: "unreadable" },
|
|
848
|
-
},
|
|
849
|
-
input.knownDetails,
|
|
850
|
-
);
|
|
851
|
-
}
|
|
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.
|
|
852
954
|
return withKnownDetails(
|
|
853
955
|
{
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
956
|
+
diagnostic: conciseChildDiagnostic(
|
|
957
|
+
input.stderr,
|
|
958
|
+
"role run completed without a lawful typed terminal result",
|
|
959
|
+
),
|
|
857
960
|
},
|
|
858
961
|
input.knownDetails,
|
|
962
|
+
packageFactsOf(input),
|
|
859
963
|
);
|
|
860
964
|
}
|
|
861
965
|
|
|
@@ -914,998 +1018,28 @@ type SessionEntry = {
|
|
|
914
1018
|
data?: unknown;
|
|
915
1019
|
timestamp?: string;
|
|
916
1020
|
/** Session principal id from the durable header entry. */
|
|
917
|
-
id?: string;
|
|
918
|
-
/** Session cwd from the durable header entry. */
|
|
919
|
-
cwd?: string;
|
|
920
|
-
/** Parent session principal on durable child session headers. */
|
|
921
|
-
parentSession?: string;
|
|
922
|
-
};
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Preserve session-read failure identity as a typed session cause.
|
|
926
|
-
* SyntaxError keeps its name so durable settlement does not wash malformed JSONL
|
|
927
|
-
* into generic output absence.
|
|
928
|
-
*/
|
|
929
|
-
function sessionReadFailure(
|
|
930
|
-
error: unknown,
|
|
931
|
-
fallbackMessage: string,
|
|
932
|
-
): Error & {
|
|
933
|
-
knownCause: ControlledFailureCause;
|
|
934
|
-
failureCode?: string | number;
|
|
935
|
-
} {
|
|
936
|
-
if (error instanceof SyntaxError) {
|
|
937
|
-
const failed = new SyntaxError(
|
|
938
|
-
error.message || fallbackMessage,
|
|
939
|
-
) as SyntaxError & {
|
|
940
|
-
knownCause: ControlledFailureCause;
|
|
941
|
-
failureCode?: string | number;
|
|
942
|
-
};
|
|
943
|
-
failed.knownCause = "session";
|
|
944
|
-
return failed;
|
|
945
|
-
}
|
|
946
|
-
if (error instanceof Error) {
|
|
947
|
-
const failed = new Error(
|
|
948
|
-
error.message || error.name || fallbackMessage,
|
|
949
|
-
) as Error & {
|
|
950
|
-
knownCause: ControlledFailureCause;
|
|
951
|
-
failureCode?: string | number;
|
|
952
|
-
code?: string | number;
|
|
953
|
-
};
|
|
954
|
-
failed.name = error.name || "Error";
|
|
955
|
-
failed.knownCause = "session";
|
|
956
|
-
const code = (error as { code?: unknown }).code;
|
|
957
|
-
if (typeof code === "string" || typeof code === "number") {
|
|
958
|
-
failed.failureCode = code;
|
|
959
|
-
failed.code = code;
|
|
960
|
-
}
|
|
961
|
-
return failed;
|
|
962
|
-
}
|
|
963
|
-
const failed = new Error(String(error)) as Error & {
|
|
964
|
-
knownCause: ControlledFailureCause;
|
|
965
|
-
failureCode?: string | number;
|
|
966
|
-
};
|
|
967
|
-
failed.knownCause = "session";
|
|
968
|
-
return failed;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Read the exact bound Pi session file principal.
|
|
973
|
-
* Does not scan the session directory for "latest" — resume identity is the file.
|
|
974
|
-
*/
|
|
975
|
-
export async function readBoundSessionEntries(
|
|
976
|
-
sessionFile: string,
|
|
977
|
-
): Promise<SessionEntry[]> {
|
|
978
|
-
const text = await readFile(sessionFile, "utf8");
|
|
979
|
-
const entries: SessionEntry[] = [];
|
|
980
|
-
for (const line of text.trim().split("\n").filter(Boolean)) {
|
|
981
|
-
try {
|
|
982
|
-
entries.push(JSON.parse(line) as SessionEntry);
|
|
983
|
-
} catch (error) {
|
|
984
|
-
throw sessionReadFailure(error, "malformed session JSONL");
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
return entries;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
/**
|
|
991
|
-
* Latest native assistant provider-stop in a session (stopReason error|aborted).
|
|
992
|
-
* Only the final assistant turn decides terminality — an older error followed by a
|
|
993
|
-
* later non-error stop is not a provider failure (would wash a no-lawful-output path).
|
|
994
|
-
* Typed production source for provider cause — not child stderr prose.
|
|
995
|
-
*/
|
|
996
|
-
type SessionProviderStop = {
|
|
997
|
-
stopReason: "error" | "aborted";
|
|
998
|
-
errorMessage?: string;
|
|
999
|
-
provider?: string;
|
|
1000
|
-
model?: string;
|
|
1001
|
-
api?: string;
|
|
1002
|
-
rawStopReason?: string;
|
|
1003
|
-
diagnostics?: unknown;
|
|
1004
|
-
httpStatus?: number;
|
|
1005
|
-
body?: unknown;
|
|
1006
|
-
code?: unknown;
|
|
1007
|
-
errno?: unknown;
|
|
1008
|
-
};
|
|
1009
|
-
|
|
1010
|
-
function typedHttpStatusFromMessage(message: SessionMessage): number | undefined {
|
|
1011
|
-
for (const candidate of [message.httpStatus, message.statusCode, message.status]) {
|
|
1012
|
-
if (typeof candidate === "number" && (candidate < 200 || candidate >= 300)) return candidate;
|
|
1013
|
-
}
|
|
1014
|
-
return undefined;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
function sessionProviderStopFromAssistant(message: SessionMessage | undefined): SessionProviderStop | undefined {
|
|
1018
|
-
if (message?.role !== "assistant") return undefined;
|
|
1019
|
-
if (message.stopReason !== "error" && message.stopReason !== "aborted") return undefined;
|
|
1020
|
-
const httpStatus = typedHttpStatusFromMessage(message);
|
|
1021
|
-
return {
|
|
1022
|
-
stopReason: message.stopReason,
|
|
1023
|
-
// Preserve held errorMessage bytes — emptiness check must not rewrite.
|
|
1024
|
-
...(typeof message.errorMessage === "string" && message.errorMessage.trim() !== ""
|
|
1025
|
-
? { errorMessage: message.errorMessage }
|
|
1026
|
-
: {}),
|
|
1027
|
-
...(typeof message.provider === "string" && message.provider.trim() !== ""
|
|
1028
|
-
? { provider: message.provider }
|
|
1029
|
-
: {}),
|
|
1030
|
-
...(typeof message.model === "string" && message.model.trim() !== ""
|
|
1031
|
-
? { model: message.model }
|
|
1032
|
-
: {}),
|
|
1033
|
-
...(typeof message.api === "string" && message.api.trim() !== ""
|
|
1034
|
-
? { api: message.api }
|
|
1035
|
-
: {}),
|
|
1036
|
-
...(typeof message.rawStopReason === "string" && message.rawStopReason.trim() !== ""
|
|
1037
|
-
? { rawStopReason: message.rawStopReason }
|
|
1038
|
-
: {}),
|
|
1039
|
-
...(message.diagnostics === undefined ? {} : { diagnostics: message.diagnostics }),
|
|
1040
|
-
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
1041
|
-
...(message.body === undefined ? {} : { body: message.body }),
|
|
1042
|
-
...(message.code === undefined ? {} : { code: message.code }),
|
|
1043
|
-
...(message.errno === undefined ? {} : { errno: message.errno }),
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
export function extractSessionProviderStop(
|
|
1048
|
-
entries: readonly SessionEntry[],
|
|
1049
|
-
): SessionProviderStop | undefined {
|
|
1050
|
-
// A resumed dispatch appends a typed top-level user turn to the same session.
|
|
1051
|
-
// Older attempt native stops must not replace the newer attempt's stop.
|
|
1052
|
-
// Sessions without a user turn are the initial attempt.
|
|
1053
|
-
// Auditor retained responses live in Sitian (kind=auditor); see readSessionProviderStop.
|
|
1054
|
-
let attemptStart = 0;
|
|
1055
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1056
|
-
const entry = entries[i];
|
|
1057
|
-
if (entry?.type === "message" && entry.message?.role === "user") {
|
|
1058
|
-
attemptStart = i;
|
|
1059
|
-
break;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
for (let i = entries.length - 1; i >= attemptStart; i -= 1) {
|
|
1064
|
-
const entry = entries[i];
|
|
1065
|
-
if (entry?.type !== "message") continue;
|
|
1066
|
-
const message = entry.message;
|
|
1067
|
-
if (message?.role !== "assistant") continue;
|
|
1068
|
-
// Latest assistant in the current attempt only.
|
|
1069
|
-
return sessionProviderStopFromAssistant(message);
|
|
1070
|
-
}
|
|
1071
|
-
return undefined;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
/**
|
|
1075
|
-
* Latest Sitian-retained auditor response stop for this parent session principal.
|
|
1076
|
-
* Writer: retainComplianceResponse → sitianReport(kind=auditor, payload={version,response}).
|
|
1077
|
-
* Payload stopReason is preserved as retained — aborted stays aborted; no 500/error wash here.
|
|
1078
|
-
*/
|
|
1079
|
-
async function readSitianRetainedAuditorProviderStop(
|
|
1080
|
-
sessionFile: string,
|
|
1081
|
-
): Promise<SessionProviderStop | undefined> {
|
|
1082
|
-
try {
|
|
1083
|
-
const { recordFile } = resolveSitianRecordPath({
|
|
1084
|
-
level: "event",
|
|
1085
|
-
kind: "auditor",
|
|
1086
|
-
sessionParent: sessionFile,
|
|
1087
|
-
// Path is driven by sessionParent when under ledger home; cwd is a fallback only.
|
|
1088
|
-
cwd: dirname(sessionFile),
|
|
1089
|
-
});
|
|
1090
|
-
const { records } = await readSitianRecords(recordFile);
|
|
1091
|
-
for (let i = records.length - 1; i >= 0; i -= 1) {
|
|
1092
|
-
const payload = records[i]?.payload;
|
|
1093
|
-
if (!isRecord(payload) || !isRecord(payload.response)) continue;
|
|
1094
|
-
// Lifecycle events carry `type` (binding / compliance_failure); retain does not.
|
|
1095
|
-
if (typeof payload.type === "string") continue;
|
|
1096
|
-
const stop = sessionProviderStopFromAssistant(payload.response as SessionMessage);
|
|
1097
|
-
if (stop !== undefined) return stop;
|
|
1098
|
-
// Latest retain exists but is not a provider-stop — do not scan older retains
|
|
1099
|
-
// (mirrors former session COMPLIANCE_RESPONSE preference break).
|
|
1100
|
-
break;
|
|
1101
|
-
}
|
|
1102
|
-
} catch {
|
|
1103
|
-
// Missing volume or unreadable path is absence, not a settlement failure.
|
|
1104
|
-
}
|
|
1105
|
-
return undefined;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
/** Read retained auditor stop (Sitian) then native session assistant stop, if any. */
|
|
1109
|
-
export async function readSessionProviderStop(
|
|
1110
|
-
sessionFile: string,
|
|
1111
|
-
): Promise<SessionProviderStop | undefined> {
|
|
1112
|
-
const retained = await readSitianRetainedAuditorProviderStop(sessionFile);
|
|
1113
|
-
if (retained !== undefined) return retained;
|
|
1114
|
-
try {
|
|
1115
|
-
const entries = await readBoundSessionEntries(sessionFile);
|
|
1116
|
-
return extractSessionProviderStop(entries);
|
|
1117
|
-
} catch {
|
|
1118
|
-
return undefined;
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
type BoundAuditorVolume = {
|
|
1123
|
-
readonly entries: SessionEntry[];
|
|
1124
|
-
readonly attemptEntryId?: string;
|
|
1125
|
-
readonly parentId: string;
|
|
1126
|
-
readonly sessionFile: string;
|
|
1127
|
-
};
|
|
1128
|
-
|
|
1129
|
-
async function loadBoundAuditorVolumes(
|
|
1130
|
-
sessionFile: string,
|
|
1131
|
-
): Promise<readonly BoundAuditorVolume[] | undefined> {
|
|
1132
|
-
let parentEntries: SessionEntry[];
|
|
1133
|
-
try {
|
|
1134
|
-
parentEntries = await readBoundSessionEntries(sessionFile);
|
|
1135
|
-
} catch (error) {
|
|
1136
|
-
if (isEnoent(error)) return undefined;
|
|
1137
|
-
throw sessionReadFailure(error, "failed to read parent session for auditor binding");
|
|
1138
|
-
}
|
|
1139
|
-
const parentId = parentEntries.find((entry) => entry.type === "session")?.id;
|
|
1140
|
-
if (parentId === undefined) return undefined;
|
|
1141
|
-
const childDirectories = [join(dirname(sessionFile), "auditor-roles")];
|
|
1142
|
-
const valid: BoundAuditorVolume[] = [];
|
|
1143
|
-
let sawAnyDirectory = false;
|
|
1144
|
-
for (const childDirectory of childDirectories) {
|
|
1145
|
-
let names: string[];
|
|
1146
|
-
try {
|
|
1147
|
-
names = await readdir(childDirectory);
|
|
1148
|
-
sawAnyDirectory = true;
|
|
1149
|
-
} catch (error) {
|
|
1150
|
-
if (isEnoent(error)) continue;
|
|
1151
|
-
throw sessionReadFailure(error, "failed to read bound auditor session directory");
|
|
1152
|
-
}
|
|
1153
|
-
for (const file of names.filter((name) => name.endsWith(".jsonl")).sort().reverse()) {
|
|
1154
|
-
let entries: SessionEntry[];
|
|
1155
|
-
try {
|
|
1156
|
-
entries = await readBoundSessionEntries(join(childDirectory, file));
|
|
1157
|
-
} catch (error) {
|
|
1158
|
-
throw sessionReadFailure(error, "failed to read discovered auditor session");
|
|
1159
|
-
}
|
|
1160
|
-
const header = entries.find((entry) => entry.type === "session");
|
|
1161
|
-
if (!isRecord(header)) continue;
|
|
1162
|
-
// Parent-attempt binding owns its interval on multi-attempt volumes
|
|
1163
|
-
// (never whole-volume provider/compliance).
|
|
1164
|
-
const bindingIndexes: number[] = [];
|
|
1165
|
-
for (let i = 0; i < entries.length; i += 1) {
|
|
1166
|
-
const entry = entries[i];
|
|
1167
|
-
if (entry?.type === "custom" && entry.customType === AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE) {
|
|
1168
|
-
bindingIndexes.push(i);
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
const bindingPasses: Array<{ entry: SessionEntry | undefined; start: number; end: number }> =
|
|
1172
|
-
bindingIndexes.length > 0
|
|
1173
|
-
? bindingIndexes.map((start, idx) => ({
|
|
1174
|
-
entry: entries[start],
|
|
1175
|
-
start,
|
|
1176
|
-
end: idx + 1 < bindingIndexes.length ? bindingIndexes[idx + 1]! : entries.length,
|
|
1177
|
-
}))
|
|
1178
|
-
: [{ entry: undefined, start: 0, end: entries.length }];
|
|
1179
|
-
for (const { entry: bindingEntry, start, end } of bindingPasses) {
|
|
1180
|
-
const bindingParent =
|
|
1181
|
-
bindingEntry !== undefined &&
|
|
1182
|
-
isRecord(bindingEntry.data) &&
|
|
1183
|
-
isRecord(bindingEntry.data.parent)
|
|
1184
|
-
? bindingEntry.data.parent
|
|
1185
|
-
: undefined;
|
|
1186
|
-
const attemptEntryId =
|
|
1187
|
-
typeof bindingParent?.attemptEntryId === "string"
|
|
1188
|
-
? bindingParent.attemptEntryId
|
|
1189
|
-
: undefined;
|
|
1190
|
-
const boundSessionFile =
|
|
1191
|
-
typeof bindingParent?.sessionFile === "string"
|
|
1192
|
-
? bindingParent.sessionFile
|
|
1193
|
-
: typeof header.parentSession === "string"
|
|
1194
|
-
? header.parentSession
|
|
1195
|
-
: undefined;
|
|
1196
|
-
if (boundSessionFile !== sessionFile) continue;
|
|
1197
|
-
if (bindingParent !== undefined && bindingParent.sessionId !== parentId) continue;
|
|
1198
|
-
if (bindingParent === undefined && header.parentSession !== sessionFile) continue;
|
|
1199
|
-
valid.push({
|
|
1200
|
-
entries: entries.slice(start, end),
|
|
1201
|
-
parentId,
|
|
1202
|
-
sessionFile,
|
|
1203
|
-
...(attemptEntryId === undefined ? {} : { attemptEntryId }),
|
|
1204
|
-
});
|
|
1205
|
-
// Keep every interval bound to this parent. Auditor payload is relayed as
|
|
1206
|
-
// recorded; code does not expire it from later user-message shape (#858).
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
if (!sawAnyDirectory && valid.length === 0) return undefined;
|
|
1211
|
-
return valid;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
function complianceFailureFromAuditorVolumes(
|
|
1215
|
-
volumes: readonly BoundAuditorVolume[],
|
|
1216
|
-
): RoleTurnKnownFailure | undefined {
|
|
1217
|
-
for (const { entries, attemptEntryId, parentId, sessionFile } of volumes) {
|
|
1218
|
-
const stop = extractSessionProviderStop(entries);
|
|
1219
|
-
if (stop === undefined) continue;
|
|
1220
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1221
|
-
const entry = entries[i];
|
|
1222
|
-
if (entry?.type !== "custom" || entry.customType !== AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE || !isRecord(entry.data)) continue;
|
|
1223
|
-
const parent = isRecord(entry.data.parent) ? entry.data.parent : undefined;
|
|
1224
|
-
const failure = isRecord(entry.data.failure) ? entry.data.failure : undefined;
|
|
1225
|
-
if (parent?.sessionId !== parentId || parent.sessionFile !== sessionFile || parent.attemptEntryId !== attemptEntryId) continue;
|
|
1226
|
-
// #881: keep the recorded failure as written — typed cause when present, else raw diagnostic only.
|
|
1227
|
-
if (failure === undefined) continue;
|
|
1228
|
-
const identity = isRecord(failure.identity) ? failure.identity : undefined;
|
|
1229
|
-
const typedCause =
|
|
1230
|
-
failure.cause === "provider" ||
|
|
1231
|
-
failure.cause === "activation" ||
|
|
1232
|
-
failure.cause === "session" ||
|
|
1233
|
-
failure.cause === "output" ||
|
|
1234
|
-
failure.cause === "timeout"
|
|
1235
|
-
? (failure.cause as ControlledFailureCause)
|
|
1236
|
-
: undefined;
|
|
1237
|
-
return {
|
|
1238
|
-
...(typedCause === undefined ? {} : { cause: typedCause }),
|
|
1239
|
-
...(identity === undefined ? {} : { identity: {
|
|
1240
|
-
...(typeof identity.name === "string" ? { name: identity.name } : {}),
|
|
1241
|
-
...(typeof identity.code === "string" || typeof identity.code === "number" ? { code: identity.code } : {}),
|
|
1242
|
-
} }),
|
|
1243
|
-
...(typeof failure.diagnostic === "string" ? { diagnostic: failure.diagnostic } : {}),
|
|
1244
|
-
...(isRecord(failure.details) ? { details: failure.details } : {}),
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
return undefined;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
function providerStopFallbackFromAuditorVolumes(
|
|
1252
|
-
volumes: readonly BoundAuditorVolume[],
|
|
1253
|
-
): RoleTurnKnownFailure | undefined {
|
|
1254
|
-
for (const { entries } of volumes) {
|
|
1255
|
-
const stop = extractSessionProviderStop(entries);
|
|
1256
|
-
if (stop === undefined) continue;
|
|
1257
|
-
const primary = knownFailureFromProviderStop(stop)!;
|
|
1258
|
-
return {
|
|
1259
|
-
...primary,
|
|
1260
|
-
details: {
|
|
1261
|
-
...(primary.details ?? {}),
|
|
1262
|
-
secondaryEvidence: "unavailable",
|
|
1263
|
-
},
|
|
1264
|
-
};
|
|
1265
|
-
}
|
|
1266
|
-
return undefined;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
/** Strong auditor tier only — retained compliance-failure entries, no provider-stop fallback. */
|
|
1270
|
-
async function readBoundAuditorComplianceFailure(
|
|
1271
|
-
sessionFile: string,
|
|
1272
|
-
): Promise<RoleTurnKnownFailure | undefined> {
|
|
1273
|
-
const volumes = await loadBoundAuditorVolumes(sessionFile);
|
|
1274
|
-
if (volumes === undefined) return undefined;
|
|
1275
|
-
return complianceFailureFromAuditorVolumes(volumes);
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
/** Weaker auditor tier: provider stop without a retained compliance-failure entry. */
|
|
1279
|
-
async function readBoundAuditorProviderStopFallback(
|
|
1280
|
-
sessionFile: string,
|
|
1281
|
-
): Promise<RoleTurnKnownFailure | undefined> {
|
|
1282
|
-
const volumes = await loadBoundAuditorVolumes(sessionFile);
|
|
1283
|
-
if (volumes === undefined) return undefined;
|
|
1284
|
-
return providerStopFallbackFromAuditorVolumes(volumes);
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
function typedFailedTerminatingToolKnownFailure(
|
|
1288
|
-
entries: readonly SessionEntry[],
|
|
1289
|
-
): RoleTurnKnownFailure | undefined {
|
|
1290
|
-
let attemptStart = 0;
|
|
1291
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1292
|
-
if (entries[i]?.type === "message" && entries[i]?.message?.role === "user") {
|
|
1293
|
-
attemptStart = i;
|
|
1294
|
-
break;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
const attemptEntries = entries.slice(attemptStart);
|
|
1298
|
-
for (let i = attemptEntries.length - 1; i >= 0; i -= 1) {
|
|
1299
|
-
const message = attemptEntries[i]?.message;
|
|
1300
|
-
if (attemptEntries[i]?.type !== "message" || message?.role !== "toolResult") continue;
|
|
1301
|
-
const classification = classifyPackagedRoleTerminalResult(message);
|
|
1302
|
-
if (classification.kind !== "infrastructure") continue;
|
|
1303
|
-
if (typeof message.toolCallId !== "string" || typeof message.toolName !== "string") continue;
|
|
1304
|
-
if (boundRoleToolCallForResult(attemptEntries, i, message, message.toolName) === undefined) continue;
|
|
1305
|
-
const textPart = Array.isArray(message.content)
|
|
1306
|
-
? message.content.find((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
|
|
1307
|
-
: undefined;
|
|
1308
|
-
const diagnostic = isRecord(textPart) ? textPart.text : undefined;
|
|
1309
|
-
// Durable details already carry fact + typed evidence from envelope one-shot projection (#475).
|
|
1310
|
-
// Do not re-parse retained compliance responses here.
|
|
1311
|
-
// Host infrastructure must NOT map to cause=output — that cause is reserved for
|
|
1312
|
-
// role-output failures (isError residual / no sealed receipt), not host infra.
|
|
1313
|
-
const details = isRecord(message.details) ? message.details : classification.fact;
|
|
1314
|
-
return {
|
|
1315
|
-
cause: "activation",
|
|
1316
|
-
identity: { name: message.toolName, code: message.toolCallId },
|
|
1317
|
-
...(typeof diagnostic === "string" && diagnostic.trim() !== "" ? { diagnostic } : {}),
|
|
1318
|
-
details,
|
|
1319
|
-
};
|
|
1320
|
-
}
|
|
1321
|
-
return undefined;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
/**
|
|
1325
|
-
* One audited-runner resolution: knownFailure plus the typed-HTTP sidecar outcome
|
|
1326
|
-
* from the same read. Callers that also decide v1 resume must consume this once —
|
|
1327
|
-
* never re-read the sidecar in presentControlledFailure.
|
|
1328
|
-
*/
|
|
1329
|
-
export type AuditedRunnerFailureResolution = {
|
|
1330
|
-
readonly knownFailure?: RoleTurnKnownFailure;
|
|
1331
|
-
/** Successful sidecar read (not absence). */
|
|
1332
|
-
readonly typedHttpObservation?: TypedProviderHttpObservation;
|
|
1333
|
-
/**
|
|
1334
|
-
* True when this resolution already performed the typed-HTTP sidecar read
|
|
1335
|
-
* (success, absence, or non-absence failure folded into knownFailure).
|
|
1336
|
-
* False when an earlier evidence tier short-circuited before the sidecar.
|
|
1337
|
-
*/
|
|
1338
|
-
readonly typedHttpObservationSettled: boolean;
|
|
1339
|
-
};
|
|
1340
|
-
|
|
1341
|
-
function resolutionOf(
|
|
1342
|
-
knownFailure: RoleTurnKnownFailure | undefined,
|
|
1343
|
-
typedHttp: {
|
|
1344
|
-
readonly settled: boolean;
|
|
1345
|
-
readonly observation?: TypedProviderHttpObservation;
|
|
1346
|
-
} = { settled: false },
|
|
1347
|
-
): AuditedRunnerFailureResolution {
|
|
1348
|
-
return {
|
|
1349
|
-
...(knownFailure === undefined ? {} : { knownFailure }),
|
|
1350
|
-
...(typedHttp.observation === undefined ? {} : { typedHttpObservation: typedHttp.observation }),
|
|
1351
|
-
typedHttpObservationSettled: typedHttp.settled,
|
|
1352
|
-
};
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
/** Sole evidence-priority owner for public runners with Soul auditors. */
|
|
1356
|
-
export async function resolveAuditedRunnerFailureResolution(input: {
|
|
1357
|
-
runner: RoleTurnKnownFailure | undefined;
|
|
1358
|
-
sessionFile: string;
|
|
1359
|
-
credential: RoleTurnKnownFailure | undefined;
|
|
1360
|
-
/** Optional run directory for typed provider HTTP observation (resume/429). */
|
|
1361
|
-
runDirectory?: string;
|
|
1362
|
-
}): Promise<AuditedRunnerFailureResolution> {
|
|
1363
|
-
if (input.runner !== undefined) return resolutionOf(input.runner);
|
|
1364
|
-
// Bound auditor compliance-failure retention outranks a parent failure that the
|
|
1365
|
-
// auditor path itself caused (retention EISDIR race). A typed terminating-tool
|
|
1366
|
-
// host failure is next — it outranks weaker auditor provider-stop fallback so
|
|
1367
|
-
// parent failInfrastructure abort pollution cannot wash a real diagnostic (#475).
|
|
1368
|
-
try {
|
|
1369
|
-
const auditorCompliance = await readBoundAuditorComplianceFailure(input.sessionFile);
|
|
1370
|
-
if (auditorCompliance !== undefined) return resolutionOf(auditorCompliance);
|
|
1371
|
-
} catch (error) {
|
|
1372
|
-
const failure = sessionReadFailure(error, "failed to recover bound auditor failure");
|
|
1373
|
-
return resolutionOf({
|
|
1374
|
-
cause: "session",
|
|
1375
|
-
identity: thrownIdentity(failure),
|
|
1376
|
-
diagnostic: failure.message || failure.name,
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
try {
|
|
1380
|
-
const terminatingFailure = typedFailedTerminatingToolKnownFailure(
|
|
1381
|
-
await readBoundSessionEntries(input.sessionFile),
|
|
1382
|
-
);
|
|
1383
|
-
if (terminatingFailure !== undefined) return resolutionOf(terminatingFailure);
|
|
1384
|
-
} catch (error) {
|
|
1385
|
-
if (!isEnoent(error)) {
|
|
1386
|
-
const failure = sessionReadFailure(error, "failed to recover typed terminating-tool failure");
|
|
1387
|
-
return resolutionOf({
|
|
1388
|
-
cause: "session",
|
|
1389
|
-
identity: thrownIdentity(failure),
|
|
1390
|
-
diagnostic: failure.message || failure.name,
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
try {
|
|
1395
|
-
const auditorStop = await readBoundAuditorProviderStopFallback(input.sessionFile);
|
|
1396
|
-
if (auditorStop !== undefined) return resolutionOf(auditorStop);
|
|
1397
|
-
} catch (error) {
|
|
1398
|
-
const failure = sessionReadFailure(error, "failed to recover bound auditor provider stop");
|
|
1399
|
-
return resolutionOf({
|
|
1400
|
-
cause: "session",
|
|
1401
|
-
identity: thrownIdentity(failure),
|
|
1402
|
-
diagnostic: failure.message || failure.name,
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
// Parent session provider-stop is next; credential is last.
|
|
1406
|
-
const parentStop = await readSessionProviderStop(input.sessionFile);
|
|
1407
|
-
// Typed HTTP observation: ENOENT=absence; other read/parse/shape failures keep real cause.
|
|
1408
|
-
// This is the single sidecar read for both knownFailure projection and v1 resume.
|
|
1409
|
-
let httpObservation: TypedProviderHttpObservation | undefined;
|
|
1410
|
-
if (input.runDirectory !== undefined) {
|
|
1411
|
-
try {
|
|
1412
|
-
httpObservation = await readLatestTypedProviderHttpObservation(input.runDirectory);
|
|
1413
|
-
} catch (error) {
|
|
1414
|
-
const failure = error instanceof Error ? error : new Error(String(error));
|
|
1415
|
-
return resolutionOf(
|
|
1416
|
-
{
|
|
1417
|
-
cause: "session",
|
|
1418
|
-
identity: thrownIdentity(failure),
|
|
1419
|
-
diagnostic: failure.message || failure.name,
|
|
1420
|
-
},
|
|
1421
|
-
{ settled: true },
|
|
1422
|
-
);
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
const typedHttp = {
|
|
1426
|
-
settled: input.runDirectory !== undefined,
|
|
1427
|
-
...(httpObservation === undefined ? {} : { observation: httpObservation }),
|
|
1428
|
-
};
|
|
1429
|
-
if (parentStop === undefined) {
|
|
1430
|
-
if (input.credential !== undefined) return resolutionOf(input.credential, typedHttp);
|
|
1431
|
-
if (httpObservation === undefined) return resolutionOf(undefined, typedHttp);
|
|
1432
|
-
// Project the HTTP observation's status + provider/source association.
|
|
1433
|
-
return resolutionOf(
|
|
1434
|
-
knownFailureFromProviderStop({
|
|
1435
|
-
stopReason: "error",
|
|
1436
|
-
httpStatus: httpObservation.httpStatus,
|
|
1437
|
-
provider: httpObservation.provider,
|
|
1438
|
-
}),
|
|
1439
|
-
typedHttp,
|
|
1440
|
-
);
|
|
1441
|
-
}
|
|
1442
|
-
return resolutionOf(
|
|
1443
|
-
knownFailureFromProviderStop({
|
|
1444
|
-
...parentStop,
|
|
1445
|
-
...(httpObservation === undefined
|
|
1446
|
-
? {}
|
|
1447
|
-
: {
|
|
1448
|
-
httpStatus: httpObservation.httpStatus,
|
|
1449
|
-
// Observation association outranks session-configured provider name alone.
|
|
1450
|
-
provider: httpObservation.provider,
|
|
1451
|
-
}),
|
|
1452
|
-
}),
|
|
1453
|
-
typedHttp,
|
|
1454
|
-
);
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
/** Sole evidence-priority owner for public runners with Soul auditors. */
|
|
1458
|
-
export async function resolveAuditedRunnerKnownFailure(input: {
|
|
1459
|
-
runner: RoleTurnKnownFailure | undefined;
|
|
1460
|
-
sessionFile: string;
|
|
1461
|
-
credential: RoleTurnKnownFailure | undefined;
|
|
1462
|
-
/** Optional run directory for typed provider HTTP observation (resume/429). */
|
|
1463
|
-
runDirectory?: string;
|
|
1464
|
-
}): Promise<RoleTurnKnownFailure | undefined> {
|
|
1465
|
-
return (await resolveAuditedRunnerFailureResolution(input)).knownFailure;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
/**
|
|
1469
|
-
* v1 resume observation for controlled-failure settlement — at most one sidecar read.
|
|
1470
|
-
* Prefer the pre-resolved outcome from resolveAuditedRunnerFailureResolution.
|
|
1471
|
-
* Non-absence failures never throw: they return observationReadFailure for the
|
|
1472
|
-
* existing controlled-failure → error.json chain.
|
|
1473
|
-
*/
|
|
1474
|
-
export async function resolveControlledFailureResumeObservation(input: {
|
|
1475
|
-
readonly runDirectory: string;
|
|
1476
|
-
readonly typedHttpObservationSettled?: boolean;
|
|
1477
|
-
readonly typedHttpObservation?: TypedProviderHttpObservation;
|
|
1478
|
-
}): Promise<{
|
|
1479
|
-
readonly typedHttp429?: TypedHttp429Observation;
|
|
1480
|
-
readonly observationReadFailure?: RoleTurnKnownFailure;
|
|
1481
|
-
}> {
|
|
1482
|
-
if (input.typedHttpObservationSettled === true) {
|
|
1483
|
-
const observation = input.typedHttpObservation;
|
|
1484
|
-
if (
|
|
1485
|
-
observation !== undefined &&
|
|
1486
|
-
observation.httpStatus === 429 &&
|
|
1487
|
-
isV1ResumableProvider(observation.provider)
|
|
1488
|
-
) {
|
|
1489
|
-
return {
|
|
1490
|
-
typedHttp429: { httpStatus: 429, provider: observation.provider },
|
|
1491
|
-
};
|
|
1492
|
-
}
|
|
1493
|
-
return {};
|
|
1494
|
-
}
|
|
1495
|
-
try {
|
|
1496
|
-
const typedHttp429 = await readTypedHttp429Observation(input.runDirectory);
|
|
1497
|
-
return typedHttp429 === undefined ? {} : { typedHttp429 };
|
|
1498
|
-
} catch (error) {
|
|
1499
|
-
const failure = error instanceof Error ? error : new Error(String(error));
|
|
1500
|
-
return {
|
|
1501
|
-
observationReadFailure: {
|
|
1502
|
-
cause: "session",
|
|
1503
|
-
identity: thrownIdentity(failure),
|
|
1504
|
-
diagnostic: failure.message || failure.name,
|
|
1505
|
-
},
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
/** Spread into presentControlledFailure failureInput from one audited resolution. */
|
|
1511
|
-
export function controlledFailureInputFromResolution(
|
|
1512
|
-
resolution: AuditedRunnerFailureResolution,
|
|
1513
|
-
): {
|
|
1514
|
-
knownFailure?: RoleTurnKnownFailure;
|
|
1515
|
-
typedHttpObservationSettled?: true;
|
|
1516
|
-
typedHttpObservation?: TypedProviderHttpObservation;
|
|
1517
|
-
} {
|
|
1518
|
-
return {
|
|
1519
|
-
...(resolution.knownFailure === undefined ? {} : { knownFailure: resolution.knownFailure }),
|
|
1520
|
-
...(resolution.typedHttpObservationSettled
|
|
1521
|
-
? {
|
|
1522
|
-
typedHttpObservationSettled: true as const,
|
|
1523
|
-
...(resolution.typedHttpObservation === undefined
|
|
1524
|
-
? {}
|
|
1525
|
-
: { typedHttpObservation: resolution.typedHttpObservation }),
|
|
1526
|
-
}
|
|
1527
|
-
: {}),
|
|
1528
|
-
};
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
function safelyRead(object: object, key: string): { readable: true; value: unknown } | { readable: false } {
|
|
1532
|
-
try {
|
|
1533
|
-
return { readable: true, value: (object as Record<string, unknown>)[key] };
|
|
1534
|
-
} catch {
|
|
1535
|
-
return { readable: false };
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
function toolResultText(message: SessionMessage): string {
|
|
1540
|
-
const content = message.content;
|
|
1541
|
-
if (typeof content === "string") return content.trim();
|
|
1542
|
-
if (!Array.isArray(content)) return "";
|
|
1543
|
-
return content
|
|
1544
|
-
.map((part) => {
|
|
1545
|
-
if (
|
|
1546
|
-
isRecord(part) &&
|
|
1547
|
-
typeof (part as { text?: unknown }).text === "string"
|
|
1548
|
-
) {
|
|
1549
|
-
return (part as { text: string }).text;
|
|
1550
|
-
}
|
|
1551
|
-
return "";
|
|
1552
|
-
})
|
|
1553
|
-
.join("")
|
|
1554
|
-
.trim();
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
type BoundErroredToolCandidate = {
|
|
1558
|
-
candidate: unknown;
|
|
1559
|
-
diagnostic: string;
|
|
1560
|
-
callIndex: number;
|
|
1561
|
-
};
|
|
1562
|
-
|
|
1563
|
-
function boundErroredToolCandidate(
|
|
1564
|
-
entries: readonly SessionEntry[],
|
|
1565
|
-
resultIndex: number,
|
|
1566
|
-
message: SessionMessage,
|
|
1567
|
-
toolName: string,
|
|
1568
|
-
): BoundErroredToolCandidate | undefined {
|
|
1569
|
-
if (message.toolName !== toolName || message.isError !== true) return undefined;
|
|
1570
|
-
const bound = boundRoleToolCallForResult(entries, resultIndex, message, toolName);
|
|
1571
|
-
const diagnostic = toolResultText(message);
|
|
1572
|
-
return bound === undefined || diagnostic === ""
|
|
1573
|
-
? undefined
|
|
1574
|
-
: { candidate: bound.candidate, diagnostic, callIndex: bound.callIndex };
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
/** Shared match/cause/identity knobs for session-principal infrastructure failures. */
|
|
1578
|
-
type InfrastructureFailureSpec = Readonly<{
|
|
1579
|
-
matchTool: (toolName: string) => boolean;
|
|
1580
|
-
cause: ControlledFailureCause;
|
|
1581
|
-
identityName: string;
|
|
1582
|
-
/**
|
|
1583
|
-
* Errored results only count as infrastructure when the durable details carry
|
|
1584
|
-
* the typed navigator fact.
|
|
1585
|
-
*/
|
|
1586
|
-
requireInfrastructureFact?: (toolName: string) => boolean;
|
|
1587
|
-
}>;
|
|
1588
|
-
|
|
1589
|
-
const ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC: InfrastructureFailureSpec = {
|
|
1590
|
-
matchTool: (toolName) => toolName === ENGINE_DETOUR_TOOL_NAME,
|
|
1591
|
-
cause: "output",
|
|
1592
|
-
identityName: "EngineDetourInfrastructureError",
|
|
1593
|
-
};
|
|
1594
|
-
|
|
1595
|
-
/**
|
|
1596
|
-
* Prefer a real infrastructure tool failure already on the session principal
|
|
1597
|
-
* over a later secondary provider-stop (failure-honesty).
|
|
1598
|
-
* Tool match + cause + identity are call-site parameters — one extraction body.
|
|
1599
|
-
*/
|
|
1600
|
-
function extractInfrastructureToolFailure(
|
|
1601
|
-
entries: readonly SessionEntry[],
|
|
1602
|
-
spec: InfrastructureFailureSpec,
|
|
1603
|
-
): ControlledFailure | undefined {
|
|
1604
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1605
|
-
const entry = entries[i];
|
|
1606
|
-
if (entry?.type !== "message") continue;
|
|
1607
|
-
const message = entry.message;
|
|
1608
|
-
if (message?.role !== "toolResult") continue;
|
|
1609
|
-
if (message.isError !== true) continue;
|
|
1610
|
-
if (
|
|
1611
|
-
typeof message.toolName !== "string" ||
|
|
1612
|
-
!spec.matchTool(message.toolName)
|
|
1613
|
-
) {
|
|
1614
|
-
continue;
|
|
1615
|
-
}
|
|
1616
|
-
if (spec.requireInfrastructureFact?.(message.toolName) === true) {
|
|
1617
|
-
if (!hasNavigatorInfrastructureFailureBase(message.details)) continue;
|
|
1618
|
-
}
|
|
1619
|
-
const diagnostic = toolResultText(message);
|
|
1620
|
-
if (diagnostic.length === 0) continue;
|
|
1621
|
-
return {
|
|
1622
|
-
cause: spec.cause,
|
|
1623
|
-
diagnostic,
|
|
1624
|
-
identity: { name: spec.identityName },
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
return undefined;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
/**
|
|
1631
|
-
* Read the bound session principal for a parameterized infrastructure tool failure.
|
|
1632
|
-
* `currentAttemptOnly` bounds the reverse scan to the latest top-level user turn
|
|
1633
|
-
* so a prior attempt's residual cannot mask the current failure (#633).
|
|
1634
|
-
*/
|
|
1635
|
-
async function readInfrastructureToolFailure(
|
|
1636
|
-
sessionFile: string,
|
|
1637
|
-
spec: InfrastructureFailureSpec,
|
|
1638
|
-
options: { readonly currentAttemptOnly?: boolean } = {},
|
|
1639
|
-
): Promise<ControlledFailure | undefined> {
|
|
1640
|
-
try {
|
|
1641
|
-
let entries = await readBoundSessionEntries(sessionFile);
|
|
1642
|
-
if (options.currentAttemptOnly === true) {
|
|
1643
|
-
entries = entries.slice(currentAttemptStartIndex(entries));
|
|
1644
|
-
}
|
|
1645
|
-
return extractInfrastructureToolFailure(entries, spec);
|
|
1646
|
-
} catch {
|
|
1647
|
-
return undefined;
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
/**
|
|
1652
|
-
* Prefer a real engine-detour infrastructure tool failure already on the session
|
|
1653
|
-
* principal over a later secondary provider-stop (failure-honesty / #357 T2).
|
|
1654
|
-
* Cause stays `output` — labor leg failed before accepted typed Receipt.
|
|
1655
|
-
*/
|
|
1656
|
-
export function extractEngineDetourInfrastructureFailure(
|
|
1657
|
-
entries: readonly SessionEntry[],
|
|
1658
|
-
): ControlledFailure | undefined {
|
|
1659
|
-
return extractInfrastructureToolFailure(
|
|
1660
|
-
entries,
|
|
1661
|
-
ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC,
|
|
1662
|
-
);
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
/** Read the bound session principal for an engine-detour infrastructure failure. */
|
|
1666
|
-
export async function readEngineDetourInfrastructureFailure(
|
|
1667
|
-
sessionFile: string,
|
|
1668
|
-
): Promise<ControlledFailure | undefined> {
|
|
1669
|
-
return readInfrastructureToolFailure(
|
|
1670
|
-
sessionFile,
|
|
1671
|
-
ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC,
|
|
1672
|
-
);
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
function knownFailureFromControlled(failure: ControlledFailure): RoleTurnKnownFailure {
|
|
1676
|
-
return {
|
|
1677
|
-
...(failure.cause === undefined ? {} : { cause: failure.cause }),
|
|
1678
|
-
diagnostic: failure.diagnostic,
|
|
1679
|
-
...(failure.identity === undefined ? {} : { identity: failure.identity }),
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
const RUNNER_FAILURE_POLICY = {
|
|
1684
|
-
"engine-detour-known-first": {
|
|
1685
|
-
read: readEngineDetourInfrastructureFailure,
|
|
1686
|
-
rank: "known-first",
|
|
1687
|
-
},
|
|
1688
|
-
"engine-detour-record-first": {
|
|
1689
|
-
read: readEngineDetourInfrastructureFailure,
|
|
1690
|
-
rank: "record-first",
|
|
1691
|
-
},
|
|
1692
|
-
} as const;
|
|
1693
|
-
|
|
1694
|
-
/**
|
|
1695
|
-
* Runner-failure rank for one seat. The composition-root `runnerFailure`
|
|
1696
|
-
* leaf is the only seat difference; absent means the runner knownFailure stands.
|
|
1697
|
-
*/
|
|
1698
|
-
export function seatKnownFailureResolver(
|
|
1699
|
-
role: PackagedRole,
|
|
1700
|
-
): ((input: {
|
|
1701
|
-
result: { knownFailure?: RoleTurnKnownFailure };
|
|
1702
|
-
sessionFile: string;
|
|
1703
|
-
}) => Promise<RoleTurnKnownFailure | undefined>) | undefined {
|
|
1704
|
-
const record = packagedRoleMetadata(role);
|
|
1705
|
-
if (record === undefined || !("runnerFailure" in record)) return undefined;
|
|
1706
|
-
const policy = RUNNER_FAILURE_POLICY[record.runnerFailure];
|
|
1707
|
-
return async ({ result, sessionFile }) => {
|
|
1708
|
-
const infrastructureFailure = await policy.read(sessionFile);
|
|
1709
|
-
if (policy.rank === "record-first") {
|
|
1710
|
-
return infrastructureFailure === undefined
|
|
1711
|
-
? result.knownFailure
|
|
1712
|
-
: knownFailureFromControlled(infrastructureFailure);
|
|
1713
|
-
}
|
|
1714
|
-
return result.knownFailure ?? (
|
|
1715
|
-
infrastructureFailure === undefined ? undefined : knownFailureFromControlled(infrastructureFailure)
|
|
1716
|
-
);
|
|
1717
|
-
};
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
function auditToolNameForRole(
|
|
1721
|
-
role: (typeof AUDITOR_SOUL_ROLES)[number],
|
|
1722
|
-
): string {
|
|
1723
|
-
const name = packagedAuditToolName(role);
|
|
1724
|
-
if (name === undefined) {
|
|
1725
|
-
throw new Error(`audit tool is not declared for ${role}`);
|
|
1726
|
-
}
|
|
1727
|
-
return name;
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
type BoundRoleToolCall = {
|
|
1731
|
-
callIndex: number;
|
|
1732
|
-
candidate: unknown;
|
|
1733
|
-
};
|
|
1734
|
-
|
|
1735
|
-
function boundRoleToolCallForResult(
|
|
1736
|
-
entries: readonly SessionEntry[],
|
|
1737
|
-
resultIndex: number,
|
|
1738
|
-
message: SessionMessage,
|
|
1739
|
-
outputToolName: string,
|
|
1740
|
-
): BoundRoleToolCall | undefined {
|
|
1741
|
-
const callId = message.toolCallId;
|
|
1742
|
-
if (typeof callId !== "string" || callId.trim() === "") return undefined;
|
|
1743
|
-
|
|
1744
|
-
const calls: BoundRoleToolCall[] = [];
|
|
1745
|
-
let resultCount = 0;
|
|
1746
|
-
let matchingResultIndex = -1;
|
|
1747
|
-
for (let index = 0; index < entries.length; index += 1) {
|
|
1748
|
-
const candidateMessage = entries[index]?.message;
|
|
1749
|
-
if (
|
|
1750
|
-
candidateMessage?.role === "assistant" &&
|
|
1751
|
-
Array.isArray(candidateMessage.content)
|
|
1752
|
-
) {
|
|
1753
|
-
for (const part of candidateMessage.content) {
|
|
1754
|
-
if (!isRecord(part) || part.type !== "toolCall" || part.id !== callId) {
|
|
1755
|
-
continue;
|
|
1756
|
-
}
|
|
1757
|
-
if (part.name !== outputToolName) return undefined;
|
|
1758
|
-
calls.push({ callIndex: index, candidate: part.arguments });
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
if (
|
|
1762
|
-
candidateMessage?.role === "toolResult" &&
|
|
1763
|
-
candidateMessage.toolCallId === callId
|
|
1764
|
-
) {
|
|
1765
|
-
resultCount += 1;
|
|
1766
|
-
if (candidateMessage.toolName !== outputToolName) return undefined;
|
|
1767
|
-
matchingResultIndex = index;
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
// A binding is an event-bound one-to-one relation, not a reverse lookup of
|
|
1772
|
-
// whichever result happens to be last in the session.
|
|
1773
|
-
return calls.length === 1 && resultCount === 1 && matchingResultIndex === resultIndex
|
|
1774
|
-
&& calls[0]!.callIndex < resultIndex
|
|
1775
|
-
? calls[0]
|
|
1776
|
-
: undefined;
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
type BoundRetainedAuditResponse = {
|
|
1780
|
-
candidate: unknown;
|
|
1781
|
-
};
|
|
1782
|
-
|
|
1783
|
-
type BoundAuditEscalation = {
|
|
1784
|
-
decision: Extract<ComplianceDecision, { status: "escalate" }>;
|
|
1785
|
-
details: Record<string, unknown>;
|
|
1021
|
+
id?: string;
|
|
1022
|
+
/** Session cwd from the durable header entry. */
|
|
1023
|
+
cwd?: string;
|
|
1024
|
+
/** Parent session principal on durable child session headers. */
|
|
1025
|
+
parentSession?: string;
|
|
1786
1026
|
};
|
|
1787
1027
|
|
|
1788
|
-
function sameAuditValue(left: unknown, right: unknown): boolean {
|
|
1789
|
-
if (Object.is(left, right)) return true;
|
|
1790
|
-
if (Array.isArray(left) && Array.isArray(right)) {
|
|
1791
|
-
return left.length === right.length && left.every((value, index) =>
|
|
1792
|
-
sameAuditValue(value, right[index]),
|
|
1793
|
-
);
|
|
1794
|
-
}
|
|
1795
|
-
if (isRecord(left) && isRecord(right)) {
|
|
1796
|
-
const leftKeys = Object.keys(left);
|
|
1797
|
-
const rightKeys = Object.keys(right);
|
|
1798
|
-
return leftKeys.length === rightKeys.length &&
|
|
1799
|
-
leftKeys.every((key) => Object.hasOwn(right, key) && sameAuditValue(left[key], right[key]));
|
|
1800
|
-
}
|
|
1801
|
-
return false;
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
/** Snapshot the exact enumerable string face that final Terminal projection uses. */
|
|
1805
|
-
function snapshotAuditDetails(details: Record<string, unknown>): Record<string, unknown> {
|
|
1806
|
-
const snapshot: Record<string, unknown> = Object.create(null);
|
|
1807
|
-
for (const key of Object.keys(details)) {
|
|
1808
|
-
Object.defineProperty(snapshot, key, {
|
|
1809
|
-
value: details[key],
|
|
1810
|
-
enumerable: true,
|
|
1811
|
-
configurable: true,
|
|
1812
|
-
writable: true,
|
|
1813
|
-
});
|
|
1814
|
-
}
|
|
1815
|
-
return snapshot;
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
1028
|
/**
|
|
1819
|
-
*
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
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.
|
|
1823
1033
|
*/
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
outputToolName: string,
|
|
1832
|
-
): BoundAuditEscalation | undefined {
|
|
1833
|
-
const roleCall = boundRoleToolCallForResult(
|
|
1834
|
-
entries,
|
|
1835
|
-
resultIndex,
|
|
1836
|
-
message,
|
|
1837
|
-
outputToolName,
|
|
1838
|
-
);
|
|
1839
|
-
if (roleCall === undefined) return undefined;
|
|
1840
|
-
const retained = boundRetainedAuditResponse(
|
|
1841
|
-
entries,
|
|
1842
|
-
roleCall.callIndex,
|
|
1843
|
-
resultIndex,
|
|
1844
|
-
auditToolNameForRole(role),
|
|
1845
|
-
);
|
|
1846
|
-
if (retained === undefined) return undefined;
|
|
1847
|
-
try {
|
|
1848
|
-
const decision = readComplianceCandidate(retained.candidate);
|
|
1849
|
-
if (decision.status !== "escalate") return undefined;
|
|
1850
|
-
const details = message.details;
|
|
1851
|
-
if (!isAuditEscalationResult(details) || !isRecord(details)) return undefined;
|
|
1852
|
-
|
|
1853
|
-
// Read the public face exactly once. Besides making key enumeration and
|
|
1854
|
-
// getters fail closed, this prevents a stateful accessor from authenticating
|
|
1855
|
-
// one value and yielding another during final Terminal projection.
|
|
1856
|
-
const projectedDetails = snapshotAuditDetails(details);
|
|
1857
|
-
const hasDecisionConflicts = Object.hasOwn(decision, "conflicts");
|
|
1858
|
-
const hasDetailsConflicts = Object.hasOwn(projectedDetails, "conflicts");
|
|
1859
|
-
if (hasDecisionConflicts !== hasDetailsConflicts) return undefined;
|
|
1860
|
-
if (hasDecisionConflicts && !sameAuditValue(projectedDetails.conflicts, decision.conflicts)) return undefined;
|
|
1861
|
-
const hasDecisionGate = Object.hasOwn(decision, "decisionGate");
|
|
1862
|
-
const hasDetailsGate = Object.hasOwn(projectedDetails, "auditDecisionGate");
|
|
1863
|
-
if (hasDecisionGate !== hasDetailsGate) return undefined;
|
|
1864
|
-
if (hasDecisionGate && !sameAuditValue(projectedDetails.auditDecisionGate, decision.decisionGate)) return undefined;
|
|
1865
|
-
return { decision, details: projectedDetails };
|
|
1866
|
-
} catch {
|
|
1867
|
-
// Retained/public own-key enumeration, property reads, recursive equality,
|
|
1868
|
-
// and projection are all untrusted session evidence.
|
|
1869
|
-
return undefined;
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
function isUnboundAuditEscalationFace(details: unknown): boolean {
|
|
1874
|
-
try {
|
|
1875
|
-
if (isAuditEscalationResult(details)) return true;
|
|
1876
|
-
} catch {
|
|
1877
|
-
// Hostile access is not authentic escalation evidence.
|
|
1878
|
-
}
|
|
1879
|
-
if (!isRecord(details)) return false;
|
|
1880
|
-
const kind = safelyRead(details, "kind");
|
|
1881
|
-
return kind.readable && kind.value === "audit_escalation";
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
function boundRetainedAuditResponse(
|
|
1885
|
-
entries: readonly SessionEntry[],
|
|
1886
|
-
callIndex: number,
|
|
1887
|
-
resultIndex: number,
|
|
1888
|
-
auditToolName: string,
|
|
1889
|
-
): BoundRetainedAuditResponse | undefined {
|
|
1890
|
-
const matches: BoundRetainedAuditResponse[] = [];
|
|
1891
|
-
for (let index = callIndex + 1; index < resultIndex; index += 1) {
|
|
1892
|
-
const entry = entries[index];
|
|
1893
|
-
if (entry?.type !== "custom" || entry.customType !== COMPLIANCE_RESPONSE_ENTRY_TYPE) {
|
|
1894
|
-
continue;
|
|
1895
|
-
}
|
|
1896
|
-
if (!isRecord(entry.data) || !isRecord(entry.data.response)) continue;
|
|
1897
|
-
const response = entry.data.response;
|
|
1898
|
-
if (!Array.isArray(response.content)) continue;
|
|
1899
|
-
const calls = response.content.filter(
|
|
1900
|
-
(part): part is Record<string, unknown> =>
|
|
1901
|
-
isRecord(part) && part.type === "toolCall",
|
|
1902
|
-
);
|
|
1903
|
-
if (calls.length !== 1 || calls[0]?.name !== auditToolName) continue;
|
|
1904
|
-
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);
|
|
1905
1041
|
}
|
|
1906
|
-
|
|
1907
|
-
// intermediate non-decision responses in the same call/result interval.
|
|
1908
|
-
return matches.length === 1 ? matches[0] : undefined;
|
|
1042
|
+
return entries;
|
|
1909
1043
|
}
|
|
1910
1044
|
|
|
1911
1045
|
/**
|
|
@@ -1927,57 +1061,19 @@ type AttemptHistorySource = {
|
|
|
1927
1061
|
readonly sessionFile: string;
|
|
1928
1062
|
};
|
|
1929
1063
|
|
|
1930
|
-
/**
|
|
1931
|
-
* Append one attempt's complete result to the run's session principal.
|
|
1932
|
-
* Append failure throws — callers must not overwrite a pointer artifact when
|
|
1933
|
-
* the history entry backing the overwrite did not land (fail closed).
|
|
1934
|
-
*/
|
|
1064
|
+
/** Append the complete attempt to the package ledger before overwriting pointer artifacts (#419). */
|
|
1935
1065
|
export async function appendRunAttemptHistory(
|
|
1936
1066
|
source: AttemptHistorySource,
|
|
1937
1067
|
outcome: AttemptHistoryOutcome,
|
|
1938
1068
|
): Promise<void> {
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
) {
|
|
1948
|
-
priorEntries += 1;
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
const timestamp = new Date().toISOString();
|
|
1952
|
-
const attemptData = {
|
|
1953
|
-
sequence: priorEntries + 1,
|
|
1954
|
-
role: source.role,
|
|
1955
|
-
runId: source.runId,
|
|
1956
|
-
recordedAt: timestamp,
|
|
1957
|
-
outcome,
|
|
1958
|
-
};
|
|
1959
|
-
const line = `${JSON.stringify({
|
|
1960
|
-
type: "custom",
|
|
1961
|
-
customType: ATTEMPT_HISTORY_ENTRY_TYPE,
|
|
1962
|
-
data: attemptData,
|
|
1963
|
-
id: randomUUID(),
|
|
1964
|
-
parentId,
|
|
1965
|
-
timestamp,
|
|
1966
|
-
})}\n`;
|
|
1967
|
-
await appendFile(source.sessionFile, line, "utf8");
|
|
1968
|
-
try {
|
|
1969
|
-
sitianReport({
|
|
1970
|
-
level: "event",
|
|
1971
|
-
kind: "attempt-history",
|
|
1972
|
-
subject: { runId: source.runId },
|
|
1973
|
-
sessionParent: source.sessionFile,
|
|
1974
|
-
payload: {
|
|
1975
|
-
type: ATTEMPT_HISTORY_ENTRY_TYPE,
|
|
1976
|
-
...attemptData,
|
|
1977
|
-
},
|
|
1978
|
-
source: "settlement",
|
|
1979
|
-
});
|
|
1980
|
-
} 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
|
+
});
|
|
1981
1077
|
}
|
|
1982
1078
|
|
|
1983
1079
|
function parseNavigatorAttendanceDetails(
|
|
@@ -2118,10 +1214,9 @@ export async function extractGateFactFromSessionDirectory(
|
|
|
2118
1214
|
/**
|
|
2119
1215
|
* Attach optional gate projection onto a settled Terminal base.
|
|
2120
1216
|
* Shared by every settle path so auditor-roles is scanned once here only.
|
|
2121
|
-
* `runId` is
|
|
2122
|
-
* Gate read damage
|
|
2123
|
-
*
|
|
2124
|
-
* 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.
|
|
2125
1220
|
*/
|
|
2126
1221
|
/**
|
|
2127
1222
|
* #537: project this-invocation ak_engine_detour usage onto decisiveFacts.
|
|
@@ -2132,7 +1227,7 @@ export async function extractGateFactFromSessionDirectory(
|
|
|
2132
1227
|
* therefore cannot replace an already-formed roleOutcome (no_receipt / failure).
|
|
2133
1228
|
*/
|
|
2134
1229
|
async function attachEngineDetourToolUsage<
|
|
2135
|
-
T extends { roleOutcome: TerminalRoleOutcome
|
|
1230
|
+
T extends { roleOutcome: TerminalRoleOutcome },
|
|
2136
1231
|
>(
|
|
2137
1232
|
base: T,
|
|
2138
1233
|
sessionDirectory: string,
|
|
@@ -2140,13 +1235,26 @@ async function attachEngineDetourToolUsage<
|
|
|
2140
1235
|
readonly runDirectory?: string;
|
|
2141
1236
|
readonly courtAttemptId?: string;
|
|
2142
1237
|
readonly invocationScopeId?: string;
|
|
1238
|
+
readonly notePackageFault?: SettlementCourtScope["notePackageFault"];
|
|
2143
1239
|
} = {},
|
|
2144
1240
|
): Promise<T> {
|
|
2145
1241
|
const runDirectory =
|
|
2146
1242
|
typeof gateContext.runDirectory === "string" && gateContext.runDirectory.length > 0
|
|
2147
1243
|
? gateContext.runDirectory
|
|
2148
1244
|
: runDirectoryFromSessionDirectory(sessionDirectory);
|
|
2149
|
-
|
|
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
|
+
}
|
|
2150
1258
|
if (!engineMounted) return base;
|
|
2151
1259
|
|
|
2152
1260
|
// Public-invocation scope from the shared Host envelope (settlement scope), never
|
|
@@ -2158,22 +1266,27 @@ async function attachEngineDetourToolUsage<
|
|
|
2158
1266
|
: undefined;
|
|
2159
1267
|
|
|
2160
1268
|
const sessionFile = sessionFileFromSessionDirectory(sessionDirectory);
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
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
|
+
}
|
|
2174
1287
|
return {
|
|
2175
1288
|
...base,
|
|
2176
|
-
roleOutcome: withEngineDetourToolUsageFact(base.roleOutcome,
|
|
1289
|
+
roleOutcome: withEngineDetourToolUsageFact(base.roleOutcome, usage),
|
|
2177
1290
|
};
|
|
2178
1291
|
}
|
|
2179
1292
|
|
|
@@ -2194,6 +1307,7 @@ function detourGateContext(
|
|
|
2194
1307
|
...(scope?.invocationScopeId === undefined || scope.invocationScopeId.length === 0
|
|
2195
1308
|
? {}
|
|
2196
1309
|
: { invocationScopeId: scope.invocationScopeId }),
|
|
1310
|
+
...(scope?.notePackageFault === undefined ? {} : { notePackageFault: scope.notePackageFault }),
|
|
2197
1311
|
};
|
|
2198
1312
|
}
|
|
2199
1313
|
|
|
@@ -2202,7 +1316,6 @@ async function withOptionalGateProjection<
|
|
|
2202
1316
|
roleOutcome: TerminalRoleOutcome;
|
|
2203
1317
|
navigator: TerminalNavigatorFact;
|
|
2204
1318
|
artifacts: readonly TerminalArtifactRef[];
|
|
2205
|
-
resume?: TerminalResume;
|
|
2206
1319
|
},
|
|
2207
1320
|
>(
|
|
2208
1321
|
base: T,
|
|
@@ -2212,6 +1325,7 @@ async function withOptionalGateProjection<
|
|
|
2212
1325
|
readonly parentSessionFile?: string;
|
|
2213
1326
|
readonly courtAttemptId?: string;
|
|
2214
1327
|
readonly invocationScopeId?: string;
|
|
1328
|
+
readonly notePackageFault?: SettlementCourtScope["notePackageFault"];
|
|
2215
1329
|
} = {},
|
|
2216
1330
|
): Promise<T & { gate?: TerminalGateFact }> {
|
|
2217
1331
|
// A gate transport failure is already represented by typed evidence and has no
|
|
@@ -2227,9 +1341,19 @@ async function withOptionalGateProjection<
|
|
|
2227
1341
|
|
|
2228
1342
|
let next: T & { gate?: TerminalGateFact } = base;
|
|
2229
1343
|
if (!skipGate) {
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
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
|
+
}
|
|
2233
1357
|
}
|
|
2234
1358
|
|
|
2235
1359
|
return attachEngineDetourToolUsage(next, sessionDirectory, gateContext);
|
|
@@ -2292,6 +1416,8 @@ function extractNavigatorAttendanceFact(
|
|
|
2292
1416
|
*/
|
|
2293
1417
|
async function extractNavigatorFactFromAdmittedSession(
|
|
2294
1418
|
sessionFile: string,
|
|
1419
|
+
runDirectory: string,
|
|
1420
|
+
scope: SettlementCourtScope | undefined,
|
|
2295
1421
|
): Promise<TerminalNavigatorFact> {
|
|
2296
1422
|
try {
|
|
2297
1423
|
const entries = await readBoundSessionEntries(sessionFile);
|
|
@@ -2304,6 +1430,11 @@ async function extractNavigatorFactFromAdmittedSession(
|
|
|
2304
1430
|
reason: "Navigator attendance is missing from the session",
|
|
2305
1431
|
};
|
|
2306
1432
|
}
|
|
1433
|
+
await noteSettlementFault(
|
|
1434
|
+
runDirectory,
|
|
1435
|
+
scope,
|
|
1436
|
+
`navigator session read failed beside host terminal: ${sessionFile}: ${describeErrorIdentity(error)}`,
|
|
1437
|
+
);
|
|
2307
1438
|
return {
|
|
2308
1439
|
disposition: "unavailable",
|
|
2309
1440
|
source: "unknown",
|
|
@@ -2393,31 +1524,26 @@ function acceptedArtifactAttachmentRefs(
|
|
|
2393
1524
|
* Seat-specific structured fields stay in callers; do not fork this flow.
|
|
2394
1525
|
*/
|
|
2395
1526
|
async function publishAcceptedTerminalArtifacts(
|
|
2396
|
-
admitted:
|
|
2397
|
-
readonly role: TerminalRoleName;
|
|
2398
|
-
readonly runId: string;
|
|
2399
|
-
readonly runDirectory: string;
|
|
2400
|
-
},
|
|
1527
|
+
admitted: AdmittedRoleInvocation,
|
|
2401
1528
|
roleOutcome: TerminalRoleOutcome,
|
|
2402
1529
|
coordinates: DurablePrincipalCoordinates,
|
|
1530
|
+
recordAttemptHistory: boolean,
|
|
2403
1531
|
bodies: {
|
|
2404
1532
|
readonly report: Record<string, unknown>;
|
|
2405
1533
|
readonly evidence: Record<string, unknown>;
|
|
2406
1534
|
},
|
|
2407
1535
|
): Promise<TerminalArtifactRef[]> {
|
|
2408
|
-
// #419:
|
|
2409
|
-
//
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
role: admitted.role,
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
roleOutcome,
|
|
2417
|
-
);
|
|
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
|
+
}
|
|
2418
1544
|
const artifactsDir = await ensureTerminalArtifactFace(admitted.runDirectory);
|
|
2419
|
-
const reportPath = join(artifactsDir,
|
|
2420
|
-
const evidencePath = join(artifactsDir,
|
|
1545
|
+
const reportPath = join(artifactsDir, RUN_TERMINAL_REPORT_FILE);
|
|
1546
|
+
const evidencePath = join(artifactsDir, RUN_TERMINAL_EVIDENCE_FILE);
|
|
2421
1547
|
await writeFile(
|
|
2422
1548
|
reportPath,
|
|
2423
1549
|
`${JSON.stringify(bodies.report, null, 2)}\n`,
|
|
@@ -2434,140 +1560,78 @@ async function publishAcceptedTerminalArtifacts(
|
|
|
2434
1560
|
];
|
|
2435
1561
|
}
|
|
2436
1562
|
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
coordinates: DurablePrincipalCoordinates,
|
|
2442
|
-
entries: readonly SessionEntry[] = [],
|
|
2443
|
-
): Promise<TerminalArtifactRef[]> {
|
|
2444
|
-
return publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries);
|
|
2445
|
-
}
|
|
1563
|
+
type LawfulSessionRead =
|
|
1564
|
+
| { readonly kind: "entries"; readonly entries: SessionEntry[] }
|
|
1565
|
+
| { readonly kind: "absent" }
|
|
1566
|
+
| { readonly kind: "fault"; readonly error: unknown };
|
|
2446
1567
|
|
|
2447
1568
|
/**
|
|
2448
|
-
* Read session entries for lawful settlement.
|
|
2449
|
-
*
|
|
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.
|
|
2450
1572
|
*/
|
|
2451
1573
|
async function readLawfulSettlementEntries(
|
|
2452
1574
|
sessionFile: string,
|
|
2453
|
-
): Promise<
|
|
1575
|
+
): Promise<LawfulSessionRead> {
|
|
2454
1576
|
try {
|
|
2455
|
-
return await readBoundSessionEntries(sessionFile);
|
|
1577
|
+
return { kind: "entries", entries: await readBoundSessionEntries(sessionFile) };
|
|
2456
1578
|
} catch (error) {
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
// Malformed JSONL and other read failures keep typed session identity.
|
|
2460
|
-
throw error instanceof Error &&
|
|
2461
|
-
(error as { knownCause?: unknown }).knownCause === "session"
|
|
2462
|
-
? error
|
|
2463
|
-
: sessionReadFailure(error, "session unreadable");
|
|
1579
|
+
if (isEnoent(error)) return { kind: "absent" };
|
|
1580
|
+
return { kind: "fault", error };
|
|
2464
1581
|
}
|
|
2465
1582
|
}
|
|
2466
1583
|
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
* residual tool or attach run-scoped submission history.
|
|
2470
|
-
* Absence stays undefined. Session-read and publication errors keep their identity.
|
|
2471
|
-
* Outcome is read before publication so a later write error cannot erase it.
|
|
2472
|
-
*/
|
|
2473
|
-
async function settleSealedLedgerTerminal(
|
|
2474
|
-
admitted: AdmittedRoleInvocation,
|
|
2475
|
-
authority: DurablePrincipalAuthority,
|
|
2476
|
-
scope: SettlementCourtScope | undefined,
|
|
2477
|
-
publish: (
|
|
2478
|
-
roleOutcome: Extract<TerminalRoleOutcome, { kind: "accepted" | "audit_escalation" }>,
|
|
2479
|
-
coordinates: DurablePrincipalCoordinates,
|
|
2480
|
-
entries: readonly SessionEntry[],
|
|
2481
|
-
) => Promise<TerminalArtifactRef[]>,
|
|
2482
|
-
accept?: (
|
|
2483
|
-
roleOutcome: Extract<TerminalRoleOutcome, { kind: "accepted" | "audit_escalation" }>,
|
|
2484
|
-
) => boolean,
|
|
2485
|
-
): Promise<TerminalResult | undefined> {
|
|
2486
|
-
const roleOutcome = await sealedLedgerOutcome(admitted, admitted.role, scope);
|
|
2487
|
-
if (roleOutcome === undefined) return undefined;
|
|
2488
|
-
if (accept !== undefined && !accept(roleOutcome)) return undefined;
|
|
2489
|
-
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
2490
|
-
const entries = await readLawfulSettlementEntries(coordinates.sessionFile) ?? [];
|
|
2491
|
-
const artifacts = await publish(roleOutcome, coordinates, entries);
|
|
2492
|
-
return withOptionalGateProjection(
|
|
2493
|
-
{
|
|
2494
|
-
roleOutcome,
|
|
2495
|
-
navigator: extractNavigatorFact(entries),
|
|
2496
|
-
artifacts,
|
|
2497
|
-
runId: admitted.runId,
|
|
2498
|
-
},
|
|
2499
|
-
coordinates.sessionDirectory,
|
|
2500
|
-
detourGateContext(admitted, scope),
|
|
2501
|
-
);
|
|
1584
|
+
function entriesOf(read: LawfulSessionRead): SessionEntry[] {
|
|
1585
|
+
return read.kind === "entries" ? read.entries : [];
|
|
2502
1586
|
}
|
|
2503
1587
|
|
|
2504
|
-
/**
|
|
2505
|
-
|
|
2506
|
-
* the latest bound errored tool result in the scan window, which settles as
|
|
2507
|
-
* output failure. Both faces attach run-scoped submissions. Absence of both
|
|
2508
|
-
* stays undefined. Session is read first so a read failure keeps its identity
|
|
2509
|
-
* even when the ledger has no accepted row.
|
|
2510
|
-
*/
|
|
2511
|
-
async function settleSealedAcceptedOrToolResidual(
|
|
1588
|
+
/** Sealed seats present the ledger. A session tool error does not invent a host failure. */
|
|
1589
|
+
async function settleSealedSeat(
|
|
2512
1590
|
admitted: AdmittedRoleInvocation,
|
|
2513
1591
|
authority: DurablePrincipalAuthority,
|
|
2514
1592
|
scope: SettlementCourtScope | undefined,
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
publish: (
|
|
2519
|
-
roleOutcome: Extract<TerminalRoleOutcome, { kind: "accepted" }>,
|
|
2520
|
-
coordinates: DurablePrincipalCoordinates,
|
|
2521
|
-
entries: readonly SessionEntry[],
|
|
2522
|
-
) => Promise<TerminalArtifactRef[]>,
|
|
1593
|
+
options: {
|
|
1594
|
+
acceptedOnly: boolean;
|
|
1595
|
+
},
|
|
2523
1596
|
): Promise<TerminalResult | undefined> {
|
|
2524
1597
|
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
2525
|
-
const
|
|
2526
|
-
|
|
2527
|
-
const roleOutcome = await sealedLedgerOutcome(admitted, role, scope);
|
|
2528
|
-
if (roleOutcome?.role !== role || roleOutcome.kind !== "accepted") {
|
|
2529
|
-
const scanStart = scan === "current-attempt" ? currentAttemptStartIndex(entries) : 0;
|
|
2530
|
-
for (let index = entries.length - 1; index >= scanStart; index -= 1) {
|
|
2531
|
-
const message = entries[index]?.message;
|
|
2532
|
-
if (message?.role !== "toolResult") continue;
|
|
2533
|
-
const residual = boundErroredToolCandidate(entries, index, message, residualToolName);
|
|
2534
|
-
if (residual === undefined) continue;
|
|
2535
|
-
const candidate = residual.candidate;
|
|
2536
|
-
const details = isRecord(candidate) ? candidate : { candidate };
|
|
2537
|
-
const failed = await settleFailureTerminalResult(
|
|
2538
|
-
admitted,
|
|
2539
|
-
{
|
|
2540
|
-
cause: "output",
|
|
2541
|
-
diagnostic: residual.diagnostic,
|
|
2542
|
-
details,
|
|
2543
|
-
},
|
|
2544
|
-
authority,
|
|
2545
|
-
scope ?? {},
|
|
2546
|
-
);
|
|
2547
|
-
return attachRecordedSubmissions(admitted, failed, scope);
|
|
2548
|
-
}
|
|
1598
|
+
const roleOutcome = await sealedLedgerOutcome(admitted, admitted.role, scope);
|
|
1599
|
+
if (roleOutcome === undefined || (options.acceptedOnly && roleOutcome.kind !== "accepted")) {
|
|
2549
1600
|
return undefined;
|
|
2550
1601
|
}
|
|
2551
|
-
const
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
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);
|
|
2566
1630
|
}
|
|
2567
1631
|
|
|
2568
1632
|
/**
|
|
2569
1633
|
* One settlement for every registered seat. The registry `settlement` leaf
|
|
2570
|
-
* picks sealed ledger
|
|
1634
|
+
* picks the sealed ledger or the accepted-tool ledger.
|
|
2571
1635
|
* Seat evidence stays on the artifact face.
|
|
2572
1636
|
*/
|
|
2573
1637
|
async function settleSeat(
|
|
@@ -2582,59 +1646,9 @@ async function settleSeat(
|
|
|
2582
1646
|
if (record.settlement === "accepted") {
|
|
2583
1647
|
return trySettleAcceptedSeatTerminalResult(admitted, authority, scope);
|
|
2584
1648
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
entries: readonly SessionEntry[],
|
|
2589
|
-
) => publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries);
|
|
2590
|
-
if (record.settlement === "residual") {
|
|
2591
|
-
return settleSealedAcceptedOrToolResidual(
|
|
2592
|
-
admitted,
|
|
2593
|
-
authority,
|
|
2594
|
-
scope,
|
|
2595
|
-
admitted.role,
|
|
2596
|
-
record.residualTool,
|
|
2597
|
-
record.residualScan,
|
|
2598
|
-
publish,
|
|
2599
|
-
);
|
|
2600
|
-
}
|
|
2601
|
-
const accept = "sealedAcceptedOnly" in record && record.sealedAcceptedOnly === true
|
|
2602
|
-
? (
|
|
2603
|
-
roleOutcome: Extract<TerminalRoleOutcome, { kind: "accepted" | "audit_escalation" }>,
|
|
2604
|
-
) => roleOutcome.role === admitted.role && roleOutcome.kind === "accepted"
|
|
2605
|
-
: undefined;
|
|
2606
|
-
return settleSealedLedgerTerminal(admitted, authority, scope, publish, accept);
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
/**
|
|
2610
|
-
* Try to settle any registered seat. Undefined only for genuine absence.
|
|
2611
|
-
* Session malformation and publication exceptions keep their typed identity.
|
|
2612
|
-
*/
|
|
2613
|
-
export async function trySettleSeatTerminalResult(
|
|
2614
|
-
admitted: AdmittedRoleInvocation,
|
|
2615
|
-
authority: DurablePrincipalAuthority,
|
|
2616
|
-
scope?: SettlementCourtScope,
|
|
2617
|
-
): Promise<TerminalResult | undefined> {
|
|
2618
|
-
return settleSeat(admitted, authority, scope);
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
/**
|
|
2622
|
-
* Settle any registered seat. Throws when no lawful outcome is present.
|
|
2623
|
-
* Session-read and publication failures retain their typed identity.
|
|
2624
|
-
*/
|
|
2625
|
-
export async function settleSeatTerminalResult(
|
|
2626
|
-
admitted: AdmittedRoleInvocation,
|
|
2627
|
-
authority: DurablePrincipalAuthority,
|
|
2628
|
-
scope?: SettlementCourtScope,
|
|
2629
|
-
): Promise<TerminalResult> {
|
|
2630
|
-
const settled = await settleSeat(admitted, authority, scope);
|
|
2631
|
-
if (settled === undefined) {
|
|
2632
|
-
const label = admitted.role.charAt(0).toUpperCase() + admitted.role.slice(1);
|
|
2633
|
-
throw new Error(
|
|
2634
|
-
`${label} Role run completed without a lawful typed terminal result`,
|
|
2635
|
-
);
|
|
2636
|
-
}
|
|
2637
|
-
return settled;
|
|
1649
|
+
return settleSealedSeat(admitted, authority, scope, {
|
|
1650
|
+
acceptedOnly: "sealedAcceptedOnly" in record && record.sealedAcceptedOnly === true,
|
|
1651
|
+
});
|
|
2638
1652
|
}
|
|
2639
1653
|
|
|
2640
1654
|
/**
|
|
@@ -2716,12 +1730,13 @@ async function publishDeclaredSeatArtifacts(
|
|
|
2716
1730
|
roleOutcome: TerminalRoleOutcome,
|
|
2717
1731
|
coordinates: DurablePrincipalCoordinates,
|
|
2718
1732
|
entries: readonly SessionEntry[],
|
|
1733
|
+
recordAttemptHistory: boolean,
|
|
2719
1734
|
): Promise<TerminalArtifactRef[]> {
|
|
2720
1735
|
const face = seatArtifactFace(admitted.role);
|
|
2721
1736
|
const phase = face.reportPhase === true
|
|
2722
1737
|
? { phase: readAdmittedPath(admitted, "phase") }
|
|
2723
1738
|
: {};
|
|
2724
|
-
return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
|
|
1739
|
+
return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, recordAttemptHistory, {
|
|
2725
1740
|
report: {
|
|
2726
1741
|
role: admitted.role,
|
|
2727
1742
|
runId: admitted.runId,
|
|
@@ -2741,16 +1756,6 @@ async function publishDeclaredSeatArtifacts(
|
|
|
2741
1756
|
});
|
|
2742
1757
|
}
|
|
2743
1758
|
|
|
2744
|
-
/**
|
|
2745
|
-
* Shared accepted-settlement skeleton for seats that scan residual tool
|
|
2746
|
-
* candidates then project sealed ledger outcome (#502 DRY).
|
|
2747
|
-
* #757: sealed receipts pass through full decisiveFacts — one path, no per-seat projector.
|
|
2748
|
-
*/
|
|
2749
|
-
type SeatAcceptedSettlementSpec = {
|
|
2750
|
-
readonly role: TerminalRoleName;
|
|
2751
|
-
readonly toolName: string;
|
|
2752
|
-
};
|
|
2753
|
-
|
|
2754
1759
|
/** Latest top-level user message index; 0 when the session has none (initial attempt). */
|
|
2755
1760
|
function currentAttemptStartIndex(entries: readonly SessionEntry[]): number {
|
|
2756
1761
|
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
@@ -2762,93 +1767,8 @@ function currentAttemptStartIndex(entries: readonly SessionEntry[]): number {
|
|
|
2762
1767
|
return 0;
|
|
2763
1768
|
}
|
|
2764
1769
|
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
authority: DurablePrincipalAuthority,
|
|
2768
|
-
spec: SeatAcceptedSettlementSpec,
|
|
2769
|
-
scope?: SettlementCourtScope,
|
|
2770
|
-
): Promise<TerminalResult | undefined> {
|
|
2771
|
-
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
2772
|
-
const { sessionDirectory, sessionFile } = coordinates;
|
|
2773
|
-
const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
|
|
2774
|
-
const submissions = await recordedSubmissionPayloads(admitted, scope);
|
|
2775
|
-
// #843: collector shape (ledger closed first) plus current user-turn freshness.
|
|
2776
|
-
// Only the ledger-owned closure establishes this turn's success. A non-error
|
|
2777
|
-
// toolResult may be a candidate whose nested gate continued the conversation.
|
|
2778
|
-
// A current-attempt
|
|
2779
|
-
// residual without that success is this turn's own failure and must not be
|
|
2780
|
-
// masked by run-scoped stale acceptance (bare resume without courtAttemptId).
|
|
2781
|
-
// Same-turn accept-then-bounce keeps the success marker: terminal stays
|
|
2782
|
-
// accepted; rejection facts remain on payloads/gate (not latest-wins flip).
|
|
2783
|
-
const scanStart = currentAttemptStartIndex(entries);
|
|
2784
|
-
let thisAttemptHasSeatSuccess = false;
|
|
2785
|
-
let residual: BoundErroredToolCandidate | undefined;
|
|
2786
|
-
for (let index = entries.length - 1; index >= scanStart; index -= 1) {
|
|
2787
|
-
const entry = entries[index];
|
|
2788
|
-
if (entry?.type === "custom" && entry.customType === "ak-role-submission-closure"
|
|
2789
|
-
&& isRecord(entry.data) && entry.data.toolName === spec.toolName
|
|
2790
|
-
&& classifyPackagedRoleTerminalResult(entry.data).kind === "accepted") {
|
|
2791
|
-
thisAttemptHasSeatSuccess = true;
|
|
2792
|
-
}
|
|
2793
|
-
const message = entries[index]?.message;
|
|
2794
|
-
if (message?.role !== "toolResult") continue;
|
|
2795
|
-
if (message.toolName !== spec.toolName) continue;
|
|
2796
|
-
if (message.isError === false) continue;
|
|
2797
|
-
if (residual === undefined) {
|
|
2798
|
-
residual = boundErroredToolCandidate(
|
|
2799
|
-
entries,
|
|
2800
|
-
index,
|
|
2801
|
-
message,
|
|
2802
|
-
spec.toolName,
|
|
2803
|
-
);
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
const roleOutcome = await sealedLedgerOutcome(admitted, spec.role as TerminalRoleName, scope);
|
|
2807
|
-
if (roleOutcome !== undefined && (
|
|
2808
|
-
thisAttemptHasSeatSuccess || residual === undefined || (scope?.courtAttemptId !== undefined && scope.courtAttemptId.length > 0)
|
|
2809
|
-
)) {
|
|
2810
|
-
const navigator = extractNavigatorFact(entries);
|
|
2811
|
-
const artifacts = await publishDeclaredSeatArtifacts(
|
|
2812
|
-
admitted,
|
|
2813
|
-
roleOutcome,
|
|
2814
|
-
coordinates,
|
|
2815
|
-
entries,
|
|
2816
|
-
);
|
|
2817
|
-
return withSubmissions(
|
|
2818
|
-
await withOptionalGateProjection(
|
|
2819
|
-
{
|
|
2820
|
-
roleOutcome,
|
|
2821
|
-
navigator,
|
|
2822
|
-
artifacts,
|
|
2823
|
-
runId: admitted.runId,
|
|
2824
|
-
},
|
|
2825
|
-
sessionDirectory,
|
|
2826
|
-
detourGateContext(admitted, scope),
|
|
2827
|
-
),
|
|
2828
|
-
submissions,
|
|
2829
|
-
);
|
|
2830
|
-
}
|
|
2831
|
-
if (residual !== undefined) {
|
|
2832
|
-
const details = isRecord(residual.candidate)
|
|
2833
|
-
? residual.candidate
|
|
2834
|
-
: { candidate: residual.candidate };
|
|
2835
|
-
const failed = await settleFailureTerminalResult(
|
|
2836
|
-
admitted,
|
|
2837
|
-
{
|
|
2838
|
-
cause: "output",
|
|
2839
|
-
diagnostic: residual.diagnostic,
|
|
2840
|
-
details,
|
|
2841
|
-
},
|
|
2842
|
-
authority,
|
|
2843
|
-
scope ?? {},
|
|
2844
|
-
);
|
|
2845
|
-
return withSubmissions(failed, submissions);
|
|
2846
|
-
}
|
|
2847
|
-
return undefined;
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
/** Accepted-tool seats: one scan, tool name from the composition-root record. */
|
|
2851
|
-
export async function trySettleAcceptedSeatTerminalResult(
|
|
1770
|
+
/** Accepted-tool seats retain declaration checks and optional officer projection. */
|
|
1771
|
+
async function trySettleAcceptedSeatTerminalResult(
|
|
2852
1772
|
admitted: AdmittedRoleInvocation,
|
|
2853
1773
|
authority: DurablePrincipalAuthority,
|
|
2854
1774
|
scope?: SettlementCourtScope,
|
|
@@ -2857,10 +1777,7 @@ export async function trySettleAcceptedSeatTerminalResult(
|
|
|
2857
1777
|
if (toolName === undefined) {
|
|
2858
1778
|
throw new Error(`accepted-seat settlement is not declared for ${admitted.role}`);
|
|
2859
1779
|
}
|
|
2860
|
-
const settled = await
|
|
2861
|
-
role: admitted.role,
|
|
2862
|
-
toolName,
|
|
2863
|
-
}, scope);
|
|
1780
|
+
const settled = await settleSealedSeat(admitted, authority, scope, { acceptedOnly: false });
|
|
2864
1781
|
const record = packagedRoleMetadata(admitted.role);
|
|
2865
1782
|
if (
|
|
2866
1783
|
settled === undefined
|
|
@@ -2870,14 +1787,14 @@ export async function trySettleAcceptedSeatTerminalResult(
|
|
|
2870
1787
|
) {
|
|
2871
1788
|
return settled;
|
|
2872
1789
|
}
|
|
2873
|
-
return applySecretariatCountersignTerminal(admitted, authority, settled);
|
|
1790
|
+
return applySecretariatCountersignTerminal(admitted, authority, settled, scope);
|
|
2874
1791
|
}
|
|
2875
1792
|
|
|
2876
1793
|
/** One settlement dispatch. The registry `settlement` leaf selects the path. */
|
|
2877
1794
|
export async function trySettlePublicSeat(
|
|
2878
1795
|
admitted: AdmittedRoleInvocation,
|
|
2879
1796
|
authority: DurablePrincipalAuthority,
|
|
2880
|
-
scope:
|
|
1797
|
+
scope: SettlementCourtScope | undefined,
|
|
2881
1798
|
): Promise<TerminalResult | undefined> {
|
|
2882
1799
|
return settleSeat(admitted, authority, scope);
|
|
2883
1800
|
}
|
|
@@ -2966,9 +1883,22 @@ async function applySecretariatCountersignTerminal(
|
|
|
2966
1883
|
admitted: AdmittedRoleInvocation,
|
|
2967
1884
|
authority: DurablePrincipalAuthority,
|
|
2968
1885
|
settled: TerminalResult,
|
|
1886
|
+
scope?: SettlementCourtScope,
|
|
2969
1887
|
): Promise<TerminalResult> {
|
|
2970
1888
|
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
2971
|
-
|
|
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
|
+
}
|
|
2972
1902
|
const officer = countersignTerminalFromEntries(entries);
|
|
2973
1903
|
if (officer === undefined) return settled;
|
|
2974
1904
|
|
|
@@ -3011,15 +1941,25 @@ export async function attachPostAuditProjection(
|
|
|
3011
1941
|
admitted: AdmittedRoleInvocation,
|
|
3012
1942
|
authority: DurablePrincipalAuthority,
|
|
3013
1943
|
terminal: TerminalResult,
|
|
1944
|
+
scope?: SettlementCourtScope,
|
|
3014
1945
|
): Promise<TerminalResult> {
|
|
3015
1946
|
const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
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
|
+
}
|
|
3020
1960
|
const withGate = gate === undefined ? terminal : { ...terminal, gate };
|
|
3021
1961
|
if (admitted.role !== "secretariat") return withGate;
|
|
3022
|
-
return applySecretariatCountersignTerminal(admitted, authority, withGate);
|
|
1962
|
+
return applySecretariatCountersignTerminal(admitted, authority, withGate, scope);
|
|
3023
1963
|
}
|
|
3024
1964
|
|
|
3025
1965
|
/** One failed attempt to place a durable failure artifact (path is private layout). */
|
|
@@ -3037,17 +1977,10 @@ function publicationAttemptFromError(
|
|
|
3037
1977
|
error: unknown,
|
|
3038
1978
|
): PublicationAttempt {
|
|
3039
1979
|
if (error instanceof Error) {
|
|
3040
|
-
const identity: { name?: string; code?: string | number } = {
|
|
3041
|
-
name: error.name,
|
|
3042
|
-
};
|
|
3043
|
-
const code = (error as { code?: unknown }).code;
|
|
3044
|
-
if (typeof code === "string" || typeof code === "number") {
|
|
3045
|
-
identity.code = code;
|
|
3046
|
-
}
|
|
3047
1980
|
return {
|
|
3048
1981
|
path,
|
|
3049
1982
|
diagnostic: error.message || error.name || "write failed",
|
|
3050
|
-
identity,
|
|
1983
|
+
identity: thrownIdentity(error),
|
|
3051
1984
|
};
|
|
3052
1985
|
}
|
|
3053
1986
|
return { path, diagnostic: String(error) };
|
|
@@ -3104,7 +2037,7 @@ async function writeFailureJsonRetainingCause(
|
|
|
3104
2037
|
const candidates: string[] = [
|
|
3105
2038
|
...preferredCandidates,
|
|
3106
2039
|
// One unique name per fallback dir — collisions on fixed names cannot exhaust this.
|
|
3107
|
-
...uniqueFallbackDirs.map((dir) => join(dir, `${stem}.${randomUUID()}.json`)),
|
|
2040
|
+
...uniqueFallbackDirs.map((dir) => join(dir, stem === UNIQUE_ERROR_FALLBACK_STEM ? uniqueErrorFallbackName() : `${stem}.${randomUUID()}.json`)),
|
|
3108
2041
|
];
|
|
3109
2042
|
for (let i = 0; i < candidates.length; i += 1) {
|
|
3110
2043
|
const path = candidates[i]!;
|
|
@@ -3145,6 +2078,7 @@ export async function publishFailureArtifacts(
|
|
|
3145
2078
|
failure: ControlledFailure,
|
|
3146
2079
|
authority: DurablePrincipalAuthority,
|
|
3147
2080
|
onErrorPublished?: (path: string) => void,
|
|
2081
|
+
recordAttemptHistory = false,
|
|
3148
2082
|
): Promise<TerminalArtifactRef[]> {
|
|
3149
2083
|
const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
|
|
3150
2084
|
const { baseDir, attempt: baseAttempt } = await resolveFailureArtifactsBase(
|
|
@@ -3152,33 +2086,26 @@ export async function publishFailureArtifacts(
|
|
|
3152
2086
|
);
|
|
3153
2087
|
const priorIssues: PublicationAttempt[] =
|
|
3154
2088
|
baseAttempt === undefined ? [] : [baseAttempt];
|
|
3155
|
-
// #
|
|
3156
|
-
//
|
|
3157
|
-
//
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
publicationAttemptFromError(
|
|
3165
|
-
|
|
3166
|
-
error,
|
|
3167
|
-
),
|
|
3168
|
-
);
|
|
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
|
+
}
|
|
3169
2100
|
}
|
|
3170
|
-
// #
|
|
3171
|
-
//
|
|
3172
|
-
// strand the original controlled failure outside settlement — it rides
|
|
3173
|
-
// 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.
|
|
3174
2103
|
try {
|
|
3175
|
-
await
|
|
3176
|
-
kind: "failure",
|
|
3177
|
-
role: admitted.role,
|
|
3178
|
-
...failure,
|
|
3179
|
-
});
|
|
2104
|
+
await clearOppositeTerminalArtifactFace(admitted.runDirectory);
|
|
3180
2105
|
} catch (error) {
|
|
3181
|
-
priorIssues.push(publicationAttemptFromError(
|
|
2106
|
+
priorIssues.push(publicationAttemptFromError(
|
|
2107
|
+
roleRunArtifactsDirectory(admitted.runDirectory), error,
|
|
2108
|
+
));
|
|
3182
2109
|
}
|
|
3183
2110
|
|
|
3184
2111
|
// Prefer conventional names; unique fallback dirs keep colliding fixed paths
|
|
@@ -3191,23 +2118,23 @@ export async function publishFailureArtifacts(
|
|
|
3191
2118
|
);
|
|
3192
2119
|
const errorCandidates = underArtifacts
|
|
3193
2120
|
? [
|
|
3194
|
-
join(baseDir,
|
|
3195
|
-
join(baseDir,
|
|
3196
|
-
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),
|
|
3197
2124
|
]
|
|
3198
2125
|
: [
|
|
3199
|
-
join(baseDir,
|
|
3200
|
-
join(baseDir,
|
|
2126
|
+
join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
|
|
2127
|
+
join(baseDir, RUN_TERMINAL_ERROR_FILE),
|
|
3201
2128
|
];
|
|
3202
2129
|
const evidenceCandidates = underArtifacts
|
|
3203
2130
|
? [
|
|
3204
|
-
join(baseDir,
|
|
2131
|
+
join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
|
|
3205
2132
|
join(baseDir, "evidence.settlement.json"),
|
|
3206
2133
|
join(admitted.runDirectory, "evidence.settlement.json"),
|
|
3207
2134
|
]
|
|
3208
2135
|
: [
|
|
3209
2136
|
join(baseDir, "evidence.settlement.json"),
|
|
3210
|
-
join(baseDir,
|
|
2137
|
+
join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
|
|
3211
2138
|
];
|
|
3212
2139
|
|
|
3213
2140
|
const errorPayloadBase: Record<string, unknown> = {
|
|
@@ -3218,12 +2145,15 @@ export async function publishFailureArtifacts(
|
|
|
3218
2145
|
diagnostic: failure.diagnostic,
|
|
3219
2146
|
...(failure.identity === undefined ? {} : { identity: failure.identity }),
|
|
3220
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 }),
|
|
3221
2151
|
};
|
|
3222
2152
|
|
|
3223
2153
|
const errorWrite = await writeFailureJsonRetainingCause(
|
|
3224
2154
|
errorCandidates,
|
|
3225
2155
|
uniqueFallbackDirs,
|
|
3226
|
-
|
|
2156
|
+
UNIQUE_ERROR_FALLBACK_STEM,
|
|
3227
2157
|
errorPayloadBase,
|
|
3228
2158
|
priorIssues,
|
|
3229
2159
|
);
|
|
@@ -3234,12 +2164,7 @@ export async function publishFailureArtifacts(
|
|
|
3234
2164
|
sessionDirectory: sessionDirectory,
|
|
3235
2165
|
sessionFile: sessionFile,
|
|
3236
2166
|
admittedRequestPath: admitted.admittedRequestPath,
|
|
3237
|
-
attachments: admitted.attachments
|
|
3238
|
-
provenancePath: a.provenancePath,
|
|
3239
|
-
frozenPath: a.frozenPath,
|
|
3240
|
-
sha256: a.sha256,
|
|
3241
|
-
byteLength: a.byteLength,
|
|
3242
|
-
})),
|
|
2167
|
+
attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
|
|
3243
2168
|
...(failure.cause === undefined ? {} : { failureCause: failure.cause }),
|
|
3244
2169
|
};
|
|
3245
2170
|
const evidenceWrite = await writeFailureJsonRetainingCause(
|
|
@@ -3266,45 +2191,35 @@ export async function settleFailureTerminalResult(
|
|
|
3266
2191
|
failure: ControlledFailure,
|
|
3267
2192
|
authority: DurablePrincipalAuthority,
|
|
3268
2193
|
options: SettlementCourtScope & {
|
|
3269
|
-
readonly resume?: TerminalResume;
|
|
3270
2194
|
readonly onErrorPublished?: (path: string) => void;
|
|
3271
2195
|
} = {},
|
|
3272
2196
|
): Promise<TerminalResult> {
|
|
3273
2197
|
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
3274
2198
|
const { sessionDirectory, sessionFile } = coordinates;
|
|
3275
|
-
//
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
await
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
runId: admitted.runId,
|
|
3297
|
-
},
|
|
3298
|
-
sessionDirectory,
|
|
3299
|
-
detourGateContext(admitted, options),
|
|
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)}`,
|
|
3300
2220
|
);
|
|
3301
2221
|
}
|
|
3302
2222
|
}
|
|
3303
|
-
// Exact-session attendance only — never infer no-advice from caller omission.
|
|
3304
|
-
const navigator = await extractNavigatorFactFromAdmittedSession(sessionFile);
|
|
3305
|
-
// Private durable artifacts retain the original diagnostic identity (including run ID).
|
|
3306
|
-
const artifacts = await publishFailureArtifacts(admitted, failure, authority, options.onErrorPublished);
|
|
3307
|
-
const errorPath = artifacts.find((artifact) => artifact.kind === "error")?.path;
|
|
3308
2223
|
const decisiveFacts: Record<string, unknown> = {
|
|
3309
2224
|
...(failure.cause === undefined ? {} : { cause: failure.cause }),
|
|
3310
2225
|
diagnostic: failure.diagnostic,
|
|
@@ -3318,30 +2233,11 @@ export async function settleFailureTerminalResult(
|
|
|
3318
2233
|
if (failure.details !== undefined) {
|
|
3319
2234
|
decisiveFacts.secondaryEvidence = failure.details;
|
|
3320
2235
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
const roleOutcome: TerminalRoleOutcome = {
|
|
3327
|
-
kind: "failure",
|
|
3328
|
-
role: admitted.role,
|
|
3329
|
-
...(failure.cause === undefined ? {} : { cause: failure.cause }),
|
|
3330
|
-
diagnostic: failure.diagnostic,
|
|
3331
|
-
decisiveFacts,
|
|
3332
|
-
};
|
|
3333
|
-
const terminal = await withOptionalGateProjection(
|
|
3334
|
-
{
|
|
3335
|
-
roleOutcome,
|
|
3336
|
-
navigator,
|
|
3337
|
-
artifacts: [],
|
|
3338
|
-
resume: options.resume,
|
|
3339
|
-
},
|
|
3340
|
-
sessionDirectory,
|
|
3341
|
-
detourGateContext(admitted, options),
|
|
3342
|
-
);
|
|
3343
|
-
if (errorPath !== undefined) privateFailureErrorPaths.set(terminal, errorPath);
|
|
3344
|
-
return terminal;
|
|
2236
|
+
if (failure.packageFact !== undefined) {
|
|
2237
|
+
decisiveFacts.packageFact = failure.packageFact;
|
|
2238
|
+
}
|
|
2239
|
+
if (failure.stderr !== undefined) {
|
|
2240
|
+
decisiveFacts.stderr = failure.stderr;
|
|
3345
2241
|
}
|
|
3346
2242
|
const roleOutcome: TerminalRoleOutcome = {
|
|
3347
2243
|
kind: "failure",
|
|
@@ -3350,31 +2246,13 @@ export async function settleFailureTerminalResult(
|
|
|
3350
2246
|
diagnostic: failure.diagnostic,
|
|
3351
2247
|
decisiveFacts,
|
|
3352
2248
|
};
|
|
3353
|
-
// #478: ordinary controlled failure still surfaces accepted gate facts.
|
|
3354
2249
|
return withOptionalGateProjection(
|
|
3355
|
-
{
|
|
3356
|
-
roleOutcome,
|
|
3357
|
-
navigator,
|
|
3358
|
-
artifacts,
|
|
3359
|
-
runId: admitted.runId,
|
|
3360
|
-
},
|
|
2250
|
+
{ roleOutcome, navigator, artifacts, runId: admitted.runId },
|
|
3361
2251
|
sessionDirectory,
|
|
3362
2252
|
detourGateContext(admitted, options),
|
|
3363
2253
|
);
|
|
3364
2254
|
}
|
|
3365
2255
|
|
|
3366
|
-
/** Judge-named alias retained for #107 call sites. */
|
|
3367
|
-
export async function settleJudgeFailureTerminalResult(
|
|
3368
|
-
admitted: AdmittedJudgeInvocation,
|
|
3369
|
-
failure: ControlledFailure,
|
|
3370
|
-
authority: DurablePrincipalAuthority,
|
|
3371
|
-
options: SettlementCourtScope & {
|
|
3372
|
-
readonly resume?: TerminalResume;
|
|
3373
|
-
} = {},
|
|
3374
|
-
): Promise<TerminalResult> {
|
|
3375
|
-
return settleFailureTerminalResult(admitted, failure, authority, options);
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
2256
|
/**
|
|
3379
2257
|
* Emit one complete failure Terminal on stdout and one concise stderr diagnostic.
|
|
3380
2258
|
* Artifacts are already durable on the TerminalResult.
|
|
@@ -3389,9 +2267,11 @@ export function presentFailureTerminal(
|
|
|
3389
2267
|
io.stdout(formatTerminalResult(terminal));
|
|
3390
2268
|
if (terminal.roleOutcome.kind === "failure") {
|
|
3391
2269
|
if (io.omitFailureStderrDiagnostic) return;
|
|
2270
|
+
const hostStderr = terminal.roleOutcome.decisiveFacts.stderr;
|
|
3392
2271
|
io.stderr(formatFailureStderrDiagnostic({
|
|
3393
2272
|
...(terminal.roleOutcome.cause === undefined ? {} : { cause: terminal.roleOutcome.cause }),
|
|
3394
2273
|
diagnostic: terminal.roleOutcome.diagnostic,
|
|
2274
|
+
...(typeof hostStderr === "string" && hostStderr.length > 0 ? { stderr: hostStderr } : {}),
|
|
3395
2275
|
}));
|
|
3396
2276
|
return;
|
|
3397
2277
|
}
|