@akagilnc/pi-workflow-roles 0.1.5483 → 0.1.5571

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +6312 -6376
  4. package/dist/analyst-gate-cycles-read.js +28 -78
  5. package/dist/analyst-ledger.js +26 -42
  6. package/dist/analyst-metric-families/acceptance-success-rework.js +25 -1
  7. package/dist/analyst-metric-families/round-timeline.js +2 -5
  8. package/dist/archivist-record-entry.js +4 -3
  9. package/dist/archivist-record-pointer.js +71 -36
  10. package/dist/atomic-write.js +2 -2
  11. package/dist/auditor-dossier-tool.js +11 -6
  12. package/dist/auditor-soul.js +5 -12
  13. package/dist/collector-config.js +6 -5
  14. package/dist/doctor-evidence.js +8 -4
  15. package/dist/engine-detour-usage.js +8 -22
  16. package/dist/headless-host/production-host.js +5969 -6011
  17. package/dist/host-descriptions.js +0 -4
  18. package/dist/host-session-record.js +266 -267
  19. package/dist/ledger-session-read.js +5 -0
  20. package/dist/migrate-book-topology.js +1369 -722
  21. package/dist/navigator-public-session.js +13 -17
  22. package/dist/notary-source-run.js +8 -16
  23. package/dist/packaged-role-registry.js +5 -56
  24. package/dist/pi/durable-principal.js +2 -3
  25. package/dist/pi/role-turn-host.js +79 -15
  26. package/dist/public-cli/auto-resume.js +8 -47
  27. package/dist/public-cli/instruction-seat-run.js +9 -7
  28. package/dist/public-cli/invocation.js +67 -100
  29. package/dist/public-cli/main.js +2689 -2532
  30. package/dist/public-cli/post-admission.js +96 -55
  31. package/dist/public-cli/run-lifecycle.js +44 -100
  32. package/dist/public-cli/settlement.js +184 -429
  33. package/dist/public-cli/terminal.js +0 -12
  34. package/dist/role-run-placement.js +53 -16
  35. package/dist/role-run-relocation.js +111 -52
  36. package/dist/run-dossier-files.js +30 -0
  37. package/dist/run-dossier.js +322 -0
  38. package/dist/run-terminal-artifacts.js +69 -202
  39. package/dist/run-ticket-number.js +49 -7
  40. package/dist/session-assistant-usage.js +9 -18
  41. package/dist/session-identity.js +49 -19
  42. package/dist/sitian-appender.js +29 -18
  43. package/dist/sitian-facade.js +11 -0
  44. package/dist/sitian-reader.js +44 -29
  45. package/dist/submission-gate.js +1 -1
  46. package/dist/submission-ledger.js +4 -4
  47. package/dist/ticket-provenance-contracts.js +2 -8
  48. package/dist/ticket-provenance.js +25 -9
  49. package/package.json +1 -1
  50. package/scripts/build-package.mjs +2 -0
  51. package/src/acp-host/production-host.ts +1 -1
  52. package/src/acp-host/role-turn-host.ts +11 -4
  53. package/src/analyst-gate-cycles-read.ts +31 -88
  54. package/src/analyst-ledger.ts +36 -51
  55. package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
  56. package/src/analyst-metric-families/gate-cycles.ts +1 -1
  57. package/src/analyst-metric-families/round-timeline.ts +2 -5
  58. package/src/analyst-page.ts +3 -3
  59. package/src/archivist-record-entry.ts +6 -5
  60. package/src/archivist-record-pointer.ts +92 -42
  61. package/src/atomic-write.ts +3 -1
  62. package/src/auditor-dossier-tool.ts +14 -8
  63. package/src/auditor-soul.ts +5 -10
  64. package/src/book-topology-migration-placement.ts +16 -3
  65. package/src/book-topology-record-class-migrators.ts +4 -2
  66. package/src/book-topology-runs-migrator.ts +6 -3
  67. package/src/collector-config.ts +5 -3
  68. package/src/doctor-contracts.ts +1 -1
  69. package/src/doctor-evidence.ts +7 -3
  70. package/src/engine-detour-tool.ts +1 -1
  71. package/src/engine-detour-usage.ts +12 -25
  72. package/src/external-host-turn-loop.ts +25 -0
  73. package/src/factory-board.ts +16 -81
  74. package/src/headless-host/production-host.ts +1 -4
  75. package/src/headless-host/role-turn-host.ts +56 -21
  76. package/src/host-descriptions.ts +0 -6
  77. package/src/host-session-record.ts +224 -140
  78. package/src/ledger-session-read.ts +6 -0
  79. package/src/navigator-public-session.ts +13 -18
  80. package/src/navigator-work-context.ts +3 -1
  81. package/src/notary-role.ts +2 -1
  82. package/src/notary-source-run.ts +8 -18
  83. package/src/packaged-role-registry.ts +1 -65
  84. package/src/pi/adapter.ts +50 -7
  85. package/src/pi/durable-principal.ts +2 -4
  86. package/src/pi/role-turn-host.ts +85 -14
  87. package/src/public-cli/auto-resume.ts +7 -43
  88. package/src/public-cli/instruction-seat-run.ts +13 -6
  89. package/src/public-cli/invocation.ts +90 -144
  90. package/src/public-cli/post-admission.ts +111 -72
  91. package/src/public-cli/run-lifecycle.ts +47 -115
  92. package/src/public-cli/settlement.ts +221 -589
  93. package/src/public-cli/terminal.ts +1 -61
  94. package/src/role-envelope.ts +4 -2
  95. package/src/role-run-placement.ts +65 -17
  96. package/src/role-run-relocation.ts +110 -70
  97. package/src/role-runtime.ts +46 -69
  98. package/src/run-dossier-files.ts +33 -0
  99. package/src/run-dossier.ts +357 -0
  100. package/src/run-terminal-artifacts.ts +84 -240
  101. package/src/run-ticket-number.ts +82 -14
  102. package/src/session-assistant-usage.ts +8 -20
  103. package/src/session-identity.ts +52 -21
  104. package/src/sitian-appender.ts +33 -26
  105. package/src/sitian-facade.ts +22 -0
  106. package/src/sitian-reader.ts +62 -32
  107. package/src/submission-gate.ts +1 -1
  108. package/src/submission-ledger.ts +4 -4
  109. package/src/ticket-provenance-contracts.ts +3 -6
  110. package/src/ticket-provenance.ts +23 -11
  111. package/src/ticket-snapshot.ts +3 -2
  112. package/src/ticket-trajectory.ts +95 -109
@@ -7,12 +7,14 @@ import { randomUUID } from "node:crypto";
7
7
  import { dirname, join } from "node:path";
8
8
  import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
9
9
 
10
- import { writeHardenedArtifactFile } from "./auto-resume.ts";
10
+ import { sessionFileOf } from "../role-run-placement.ts";
11
+
12
+ import { sitianReport, type RecordPointer } from "../sitian-facade.ts";
11
13
  import {
12
- readAnalystGateCyclesFromAuditorRoles,
13
- type AnalystGateCycleRound,
14
- } from "../analyst-gate-cycles-read.ts";
15
- import { sitianReport } from "../sitian-facade.ts";
14
+ ATTEMPT_HISTORY_IDENTITY_FIELD,
15
+ TERMINAL_COURT_ATTEMPT_FIELD,
16
+ writeRunTerminal,
17
+ } from "../run-terminal-artifacts.ts";
16
18
 
17
19
  import {
18
20
  readAttemptScopedSubmissionRows,
@@ -26,6 +28,7 @@ import {
26
28
  processCancelDiagnostic,
27
29
  type CatchableProcessSignal,
28
30
  } from "./process-cancel.ts";
31
+ import { readStrictPiSessionJsonl } from "../ledger-session-read.ts";
29
32
  import { serializeThrownValue } from "../serialize-thrown-value.ts";
30
33
  import { describeErrorIdentity } from "./run-lifecycle.ts";
31
34
  import {
@@ -39,7 +42,6 @@ import {
39
42
  import type { DoctorCaseCost } from "../doctor-contracts.ts";
40
43
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "../dossier-resolution.ts";
41
44
  import {
42
- ensureRunArtifactsDir,
43
45
  homeFromRunDirectory,
44
46
  type AdmittedRoleInvocation,
45
47
  } from "./invocation.ts";
@@ -55,9 +57,7 @@ import {
55
57
  packagedDurableOfficerEntry,
56
58
  packagedRoleAcceptedOutputTool,
57
59
  packagedRoleMetadata,
58
- packagedSkipsGateOnInfrastructureStage,
59
60
  type PackagedArtifactFace,
60
- type PackagedArtifactLeaf,
61
61
  } from "../packaged-role-registry.ts";
62
62
  import {
63
63
  NO_RECEIPT_LIFECYCLE_ENTRY_TYPE,
@@ -72,18 +72,8 @@ import type {
72
72
  DurablePrincipalAuthority,
73
73
  DurablePrincipalCoordinates,
74
74
  } from "../host-contracts.ts";
75
- import { roleRunArtifactsDirectory } from "../role-run-placement.ts";
76
- import {
77
- listSeamOwnedUniqueErrorFacePaths,
78
- uniqueErrorFallbackName,
79
- UNIQUE_ERROR_FALLBACK_STEM,
80
- RUN_TERMINAL_ARTIFACT_FILES,
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,
86
- } from "../run-terminal-artifacts.ts";
75
+ import { readStateRowsSync, runCurrentPath } from "../run-dossier.ts";
76
+ import { reportRunRecord } from "../sitian-facade.ts";
87
77
 
88
78
  /** Ledger reads use the run's machine home — not ambient process HOME (child write vs parent settle). */
89
79
  function sealedLedgerHome(admitted: Pick<AdmittedRoleInvocation, "runDirectory">): string {
@@ -109,32 +99,6 @@ export type SettlementCourtScope = {
109
99
  readonly notePackageFault?: (diagnostic: string) => void | Promise<void>;
110
100
  };
111
101
 
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
102
  /**
139
103
  * One retention path for a package fault beside an already chosen terminal.
140
104
  * Session append is first when the caller has that channel. The artifact file
@@ -158,13 +122,14 @@ export async function retainPackageFault(input: {
158
122
  ...(Object.hasOwn(input, "error") ? { failure: projectThrownFailureLeaf(input.error) } : {}),
159
123
  };
160
124
  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", {
125
+ // The run's log is the fallback after the session append throws, and the only
126
+ // channel when the caller has none.
127
+ const writeLog = (appendFailure?: ControlledFailure): void => {
128
+ reportRunRecord(input.runDirectory, "post-admission-diagnostic", {
164
129
  version: 1,
165
130
  ...payload,
166
131
  ...(appendFailure === undefined ? {} : { retentionFailure: appendFailure }),
167
- });
132
+ }, "settlement");
168
133
  };
169
134
  if (input.appendSession !== undefined) {
170
135
  try {
@@ -173,18 +138,18 @@ export async function retainPackageFault(input: {
173
138
  retentionFailure =
174
139
  `post-dispatch diagnostic session append failed (best-effort continue): dossier=${describeErrorIdentity(appendError)}`;
175
140
  try {
176
- await writeArtifact(projectThrownFailureLeaf(appendError));
177
- } catch (artifactError) {
141
+ writeLog(projectThrownFailureLeaf(appendError));
142
+ } catch (logError) {
178
143
  retentionFailure =
179
- `post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; artifact=${describeErrorIdentity(artifactError)}`;
144
+ `post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; log=${describeErrorIdentity(logError)}`;
180
145
  }
181
146
  }
182
147
  } else {
183
148
  try {
184
- await writeArtifact();
185
- } catch (artifactError) {
149
+ writeLog();
150
+ } catch (logError) {
186
151
  retentionFailure =
187
- `post-dispatch diagnostic durable retention failed (best-effort continue): artifact=${describeErrorIdentity(artifactError)}`;
152
+ `post-dispatch diagnostic durable retention failed (best-effort continue): log=${describeErrorIdentity(logError)}`;
188
153
  }
189
154
  }
190
155
  const stderr = input.stderr ?? ((text: string) => {
@@ -217,7 +182,7 @@ export function ledgerReadScope(
217
182
  ): { home: string; sessionParent: string; attemptId?: string } {
218
183
  return {
219
184
  home: sealedLedgerHome(admitted),
220
- sessionParent: join(admitted.runDirectory, "session", "session.jsonl"),
185
+ sessionParent: sessionFileOf(admitted.runDirectory),
221
186
  ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
222
187
  ? {}
223
188
  : { attemptId: scope.courtAttemptId }),
@@ -462,14 +427,30 @@ async function settleNoReceiptTerminal(
462
427
  kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt",
463
428
  ...facts, decisiveFacts: facts,
464
429
  };
430
+ let attemptHistoryIdentity: string | undefined;
465
431
  if (scope?.recordAttemptHistory === true) {
466
- await appendRunAttemptHistory(
467
- { role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile },
432
+ const pointer = await appendRunAttemptHistory(
433
+ attemptHistorySource(admitted, coordinates),
468
434
  roleOutcome,
469
435
  );
436
+ attemptHistoryIdentity = pointer.identity;
437
+ } else {
438
+ attemptHistoryIdentity = await belongingAttemptHistoryIdentity(
439
+ admitted.runDirectory,
440
+ scope,
441
+ );
442
+ }
443
+ if (scope?.previewOnly !== true) {
444
+ // Belonging settlement only (#1161 甲 / R2): never inherit another court's
445
+ // latest terminal pointer, and never invent a history row on re-projection (N1).
446
+ writeRunTerminal(admitted.runDirectory, "no_receipt", {
447
+ role: admitted.role,
448
+ runId: admitted.runId,
449
+ outcome: roleOutcome,
450
+ ...terminalBelongingFields(attemptHistoryIdentity, scope),
451
+ });
470
452
  }
471
- if (scope?.previewOnly !== true) await clearOppositeTerminalArtifactFace(admitted.runDirectory);
472
- return withOptionalGateProjection({
453
+ return attachEngineDetourToolUsage({
473
454
  roleOutcome,
474
455
  navigator: await extractNavigatorFactFromAdmittedSession(
475
456
  coordinates.sessionFile,
@@ -478,7 +459,7 @@ async function settleNoReceiptTerminal(
478
459
  ),
479
460
  artifacts: [],
480
461
  runId: admitted.runId,
481
- }, coordinates.sessionDirectory, detourGateContext(admitted, scope));
462
+ }, coordinates.sessionDirectory, detourUsageContext(admitted, scope));
482
463
  }
483
464
 
484
465
  /** Transitional host-session reads remain only for non-sealed failure and audit evidence. */
@@ -495,8 +476,6 @@ import {
495
476
  adviceNavigatorFact,
496
477
  type ControlledFailureCause,
497
478
  type TerminalArtifactRef,
498
- type TerminalGateFact,
499
- type TerminalGateSeat,
500
479
  type TerminalNavigatorFact,
501
480
  type TerminalResult,
502
481
  type TerminalRoleName,
@@ -530,7 +509,7 @@ export type ControlledFailure = RoleTurnKnownFailure & {
530
509
  */
531
510
  readonly packageFact?: PackageSideFact;
532
511
  /**
533
- * Host stderr that is not already this failure's diagnostic. A stderr.log
512
+ * Host stderr that is not already this failure's diagnostic. A stderr log
534
513
  * or artifact write can fail; the original bytes stay on this object.
535
514
  */
536
515
  readonly stderr?: string;
@@ -559,7 +538,7 @@ export type PackageSideFact = {
559
538
  readonly cancelName?: string;
560
539
  /** An exception caught after the host already reported this call. */
561
540
  readonly thrown?: RoleTurnKnownFailure & { readonly diagnostic: string };
562
- /** A durable stderr.log write that failed while handling this call. */
541
+ /** A durable stderr log-line write that failed while handling this call. */
563
542
  readonly stderrLogWriteFailure?: unknown;
564
543
  };
565
544
 
@@ -1034,12 +1013,7 @@ type SessionEntry = {
1034
1013
  export async function readBoundSessionEntries(
1035
1014
  sessionFile: string,
1036
1015
  ): 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);
1041
- }
1042
- return entries;
1016
+ return await readStrictPiSessionJsonl(sessionFile) as SessionEntry[];
1043
1017
  }
1044
1018
 
1045
1019
  /**
@@ -1061,12 +1035,23 @@ type AttemptHistorySource = {
1061
1035
  readonly sessionFile: string;
1062
1036
  };
1063
1037
 
1038
+ function attemptHistorySource(
1039
+ admitted: Pick<AdmittedRoleInvocation, "role" | "runId">,
1040
+ coordinates: Pick<DurablePrincipalCoordinates, "sessionFile">,
1041
+ ): AttemptHistorySource {
1042
+ return {
1043
+ role: admitted.role,
1044
+ runId: admitted.runId,
1045
+ sessionFile: coordinates.sessionFile,
1046
+ };
1047
+ }
1048
+
1064
1049
  /** Append the complete attempt to the package ledger before overwriting pointer artifacts (#419). */
1065
1050
  export async function appendRunAttemptHistory(
1066
1051
  source: AttemptHistorySource,
1067
1052
  outcome: AttemptHistoryOutcome,
1068
- ): Promise<void> {
1069
- sitianReport({
1053
+ ): Promise<RecordPointer> {
1054
+ return sitianReport({
1070
1055
  level: "event",
1071
1056
  kind: "attempt-history",
1072
1057
  subject: { runId: source.runId },
@@ -1151,73 +1136,6 @@ function parseNavigatorAttendanceDetails(
1151
1136
  };
1152
1137
  }
1153
1138
 
1154
- /**
1155
- * Project direct and historical paired gate rounds onto the public Terminal.
1156
- * actualSeats derive only from accepted receipts, never expected/missing seats.
1157
- */
1158
- export function projectTerminalGateFact(
1159
- rounds: readonly AnalystGateCycleRound[],
1160
- ): TerminalGateFact | undefined {
1161
- if (rounds.length === 0) return undefined;
1162
- const seen = new Set<TerminalGateSeat>();
1163
- for (const round of rounds) {
1164
- if (round.origin.kind === "historical_dispatch") seen.add("gatekeeper");
1165
- seen.add(round.officer);
1166
- }
1167
- const actualSeats = (["gatekeeper", "inspector", "notary"] as const).filter(
1168
- (seat) => seen.has(seat),
1169
- );
1170
- return {
1171
- actualSeats,
1172
- rounds: rounds.map((round) => ({
1173
- roundIndex: round.roundIndex,
1174
- dispatch:
1175
- round.origin.kind === "direct"
1176
- ? { kind: "direct" as const, officer: round.officer }
1177
- : {
1178
- kind: "historical_dispatch" as const,
1179
- officer: round.officer,
1180
- ...(round.origin.reason === undefined
1181
- ? {}
1182
- : { reason: round.origin.reason }),
1183
- },
1184
- officer: {
1185
- seat: round.officer,
1186
- status: round.status,
1187
- findings: round.findings,
1188
- },
1189
- })),
1190
- };
1191
- }
1192
-
1193
- /**
1194
- * Read gate facts from the run's session/auditor-roles nest (#446/#478).
1195
- * Missing directories → undefined (no-gate zero change).
1196
- * Damaged discovered volumes propagate — never wash to "no gate".
1197
- */
1198
- export async function extractGateFactFromSessionDirectory(
1199
- sessionDirectory: string,
1200
- options: {
1201
- readonly runDirectory?: string;
1202
- readonly parentSessionFile?: string;
1203
- } = {},
1204
- ): Promise<TerminalGateFact | undefined> {
1205
- const directories = [join(sessionDirectory, "auditor-roles")];
1206
- const parentSessionFile =
1207
- options.parentSessionFile ?? join(sessionDirectory, "session.jsonl");
1208
- const rounds = await readAnalystGateCyclesFromAuditorRoles(directories, {
1209
- parentSessionFile,
1210
- });
1211
- return projectTerminalGateFact(rounds);
1212
- }
1213
-
1214
- /**
1215
- * Attach optional gate projection onto a settled Terminal base.
1216
- * Shared by every settle path so auditor-roles is scanned once here only.
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.
1220
- */
1221
1139
  /**
1222
1140
  * #537: project this-invocation ak_engine_detour usage onto decisiveFacts.
1223
1141
  * Absent when engine is not mounted; callCount 0 when mounted with zero calls.
@@ -1231,16 +1149,15 @@ async function attachEngineDetourToolUsage<
1231
1149
  >(
1232
1150
  base: T,
1233
1151
  sessionDirectory: string,
1234
- gateContext: {
1152
+ detourContext: {
1235
1153
  readonly runDirectory?: string;
1236
- readonly courtAttemptId?: string;
1237
1154
  readonly invocationScopeId?: string;
1238
1155
  readonly notePackageFault?: SettlementCourtScope["notePackageFault"];
1239
1156
  } = {},
1240
1157
  ): Promise<T> {
1241
1158
  const runDirectory =
1242
- typeof gateContext.runDirectory === "string" && gateContext.runDirectory.length > 0
1243
- ? gateContext.runDirectory
1159
+ typeof detourContext.runDirectory === "string" && detourContext.runDirectory.length > 0
1160
+ ? detourContext.runDirectory
1244
1161
  : runDirectoryFromSessionDirectory(sessionDirectory);
1245
1162
  let engineMounted = false;
1246
1163
  try {
@@ -1248,9 +1165,9 @@ async function attachEngineDetourToolUsage<
1248
1165
  } catch (error) {
1249
1166
  await noteSettlementFault(
1250
1167
  runDirectory,
1251
- gateContext.notePackageFault === undefined
1168
+ detourContext.notePackageFault === undefined
1252
1169
  ? undefined
1253
- : { notePackageFault: gateContext.notePackageFault },
1170
+ : { notePackageFault: detourContext.notePackageFault },
1254
1171
  `engine mount read failed beside host terminal: ${describeErrorIdentity(error)}`,
1255
1172
  );
1256
1173
  return base;
@@ -1260,9 +1177,9 @@ async function attachEngineDetourToolUsage<
1260
1177
  // Public-invocation scope from the shared Host envelope (settlement scope), never
1261
1178
  // courtAttemptId and never a detour sidecar file.
1262
1179
  const invocationScopeId =
1263
- typeof gateContext.invocationScopeId === "string" &&
1264
- gateContext.invocationScopeId.length > 0
1265
- ? gateContext.invocationScopeId
1180
+ typeof detourContext.invocationScopeId === "string" &&
1181
+ detourContext.invocationScopeId.length > 0
1182
+ ? detourContext.invocationScopeId
1266
1183
  : undefined;
1267
1184
 
1268
1185
  const sessionFile = sessionFileFromSessionDirectory(sessionDirectory);
@@ -1277,9 +1194,9 @@ async function attachEngineDetourToolUsage<
1277
1194
  } catch (error) {
1278
1195
  await noteSettlementFault(
1279
1196
  runDirectory,
1280
- gateContext.notePackageFault === undefined
1197
+ detourContext.notePackageFault === undefined
1281
1198
  ? undefined
1282
- : { notePackageFault: gateContext.notePackageFault },
1199
+ : { notePackageFault: detourContext.notePackageFault },
1283
1200
  `engine detour read failed beside host terminal: ${describeErrorIdentity(error)}`,
1284
1201
  );
1285
1202
  return base;
@@ -1290,20 +1207,17 @@ async function attachEngineDetourToolUsage<
1290
1207
  };
1291
1208
  }
1292
1209
 
1293
- /** Gate + detour projection context from admitted run + settlement scope. */
1294
- function detourGateContext(
1210
+ /** Engine-detour usage context from admitted run + settlement scope. */
1211
+ function detourUsageContext(
1295
1212
  admitted: { readonly runDirectory: string },
1296
1213
  scope?: SettlementCourtScope,
1297
1214
  ): {
1298
1215
  readonly runDirectory: string;
1299
- readonly courtAttemptId?: string;
1300
1216
  readonly invocationScopeId?: string;
1217
+ readonly notePackageFault?: SettlementCourtScope["notePackageFault"];
1301
1218
  } {
1302
1219
  return {
1303
1220
  runDirectory: admitted.runDirectory,
1304
- ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
1305
- ? {}
1306
- : { courtAttemptId: scope.courtAttemptId }),
1307
1221
  ...(scope?.invocationScopeId === undefined || scope.invocationScopeId.length === 0
1308
1222
  ? {}
1309
1223
  : { invocationScopeId: scope.invocationScopeId }),
@@ -1311,54 +1225,6 @@ function detourGateContext(
1311
1225
  };
1312
1226
  }
1313
1227
 
1314
- async function withOptionalGateProjection<
1315
- T extends {
1316
- roleOutcome: TerminalRoleOutcome;
1317
- navigator: TerminalNavigatorFact;
1318
- artifacts: readonly TerminalArtifactRef[];
1319
- },
1320
- >(
1321
- base: T,
1322
- sessionDirectory: string,
1323
- gateContext: {
1324
- readonly runDirectory?: string;
1325
- readonly parentSessionFile?: string;
1326
- readonly courtAttemptId?: string;
1327
- readonly invocationScopeId?: string;
1328
- readonly notePackageFault?: SettlementCourtScope["notePackageFault"];
1329
- } = {},
1330
- ): Promise<T & { gate?: TerminalGateFact }> {
1331
- // A gate transport failure is already represented by typed evidence and has no
1332
- // accepted gate cycle to project. Re-reading that rejected receipt as an
1333
- // accepted cycle would replace the original failure with a projection error.
1334
- const secondaryEvidence = base.roleOutcome.kind === "failure"
1335
- ? base.roleOutcome.decisiveFacts.secondaryEvidence
1336
- : undefined;
1337
- const skipGate =
1338
- isRecord(secondaryEvidence)
1339
- && secondaryEvidence.kind === "role_infrastructure_failure"
1340
- && packagedSkipsGateOnInfrastructureStage(secondaryEvidence.stage);
1341
-
1342
- let next: T & { gate?: TerminalGateFact } = base;
1343
- if (!skipGate) {
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
- }
1357
- }
1358
-
1359
- return attachEngineDetourToolUsage(next, sessionDirectory, gateContext);
1360
- }
1361
-
1362
1228
  function routePlaybookFailureMessage(entries: readonly SessionEntry[]): string | undefined {
1363
1229
  for (let i = entries.length - 1; i >= 0; i -= 1) {
1364
1230
  const entry = entries[i];
@@ -1444,120 +1310,109 @@ async function extractNavigatorFactFromAdmittedSession(
1444
1310
  }
1445
1311
 
1446
1312
  /**
1447
- * Remove one face path (file or directory collision plant).
1448
- * Only missing target is silent — other errno stay loud (失败诚实).
1449
- * recursive: publication may have occupied a face name as a directory (EISDIR plant).
1450
- */
1451
- async function removeFaceIfPresent(path: string): Promise<void> {
1452
- try {
1453
- await rm(path, { recursive: true, force: true });
1454
- } catch (error) {
1455
- // force:true already ignores ENOENT; ENOTDIR = face path not enterable
1456
- // (artifacts-as-file mid-path) — same absent-face semantics as the reader.
1457
- // Other errno stay loud (失败诚实).
1458
- if (isMissingPathError(error)) return;
1459
- throw error;
1460
- }
1461
- }
1462
-
1463
- /**
1464
- * #953: artifact face reflects the current terminal only.
1465
- * Reader contract owns the full adoptable set (conventional trio + fixed
1466
- * fallbacks + seam-owned unique). Clear every seam-owned face before the
1467
- * new current publishes — including the conventional name about to be
1468
- * rewritten. Retaining that name left directory collision plants in place
1469
- * (supported publish input); writer then fell back while the reader stopped
1470
- * at EISDIR on the stale conventional path and never adopted the fallback.
1471
- * report / error / empty share one method (no per-branch face list).
1472
- * empty publishes nothing; does not create a no_receipt-shaped public artifact.
1473
- * Non-terminal materials stay. Face names may be directory collision plants —
1474
- * remove recursively. Unique ownership is listSeamOwnedUniqueErrorFacePaths:
1475
- * parent runs/ faces clear only when body.runId binds; unparseable runId → none.
1476
- * Path absence (ENOENT/ENOTDIR) is silent; other delete failures stay loud.
1477
- */
1478
- async function clearOppositeTerminalArtifactFace(
1479
- runDirectory: string,
1480
- ): Promise<void> {
1481
- const artifactsDir = roleRunArtifactsDirectory(runDirectory);
1482
- for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
1483
- await removeFaceIfPresent(join(artifactsDir, file));
1484
- }
1485
- // Fixed fallbacks + unique clear too; subsequent publish writes the new
1486
- // durable path after this clear (conventional first when writable).
1487
- for (const relative of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
1488
- await removeFaceIfPresent(join(runDirectory, relative));
1489
- }
1490
- for (const path of await listSeamOwnedUniqueErrorFacePaths(runDirectory)) {
1491
- await removeFaceIfPresent(path);
1492
- }
1493
- }
1494
-
1495
- async function ensureTerminalArtifactFace(
1496
- runDirectory: string,
1497
- ): Promise<string> {
1498
- const artifactsDir = await ensureRunArtifactsDir(runDirectory);
1499
- await clearOppositeTerminalArtifactFace(runDirectory);
1500
- return artifactsDir;
1501
- }
1502
-
1503
- type AcceptedArtifactAttachment = {
1504
- readonly provenancePath: string;
1505
- readonly frozenPath: string;
1506
- readonly sha256: string;
1507
- readonly byteLength: number;
1508
- };
1509
-
1510
- function acceptedArtifactAttachmentRefs(
1511
- attachments: readonly AcceptedArtifactAttachment[],
1512
- ): AcceptedArtifactAttachment[] {
1513
- return attachments.map((a) => ({
1514
- provenancePath: a.provenancePath,
1515
- frozenPath: a.frozenPath,
1516
- sha256: a.sha256,
1517
- byteLength: a.byteLength,
1518
- }));
1519
- }
1520
-
1521
- /**
1522
- * Sole success-terminal artifact publisher (#953): append attempt history,
1523
- * refresh the public terminal face, write report/evidence, return refs.
1524
- * Seat-specific structured fields stay in callers; do not fork this flow.
1313
+ * Sole success-terminal publisher (#953): replace the leg's terminal in
1314
+ * current.json. The report carries the outcome's verdict facts; the role's
1315
+ * submitted payloads live in history.jsonl. The terminal names the
1316
+ * attempt-history row that belongs to this settlement — from this settle's
1317
+ * append when recording, otherwise that court's prior terminal row in
1318
+ * state.jsonl (#1161 甲 / R2). Never content-match payloads, never inherit the
1319
+ * leg's latest terminal, never invent a history row on re-projection (N1).
1525
1320
  */
1526
- async function publishAcceptedTerminalArtifacts(
1321
+ async function publishAcceptedTerminal(
1527
1322
  admitted: AdmittedRoleInvocation,
1528
1323
  roleOutcome: TerminalRoleOutcome,
1529
1324
  coordinates: DurablePrincipalCoordinates,
1530
- recordAttemptHistory: boolean,
1531
- bodies: {
1532
- readonly report: Record<string, unknown>;
1533
- readonly evidence: Record<string, unknown>;
1534
- },
1325
+ scope: SettlementCourtScope | undefined,
1326
+ report: Record<string, unknown>,
1535
1327
  ): Promise<TerminalArtifactRef[]> {
1536
1328
  // #419: a dispatched turn appends before rewriting last-write-wins views;
1537
1329
  // 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 },
1330
+ let attemptHistoryIdentity: string | undefined;
1331
+ if (scope?.recordAttemptHistory === true) {
1332
+ const pointer = await appendRunAttemptHistory(
1333
+ attemptHistorySource(admitted, coordinates),
1541
1334
  roleOutcome,
1542
1335
  );
1336
+ attemptHistoryIdentity = pointer.identity;
1337
+ } else {
1338
+ attemptHistoryIdentity = await belongingAttemptHistoryIdentity(
1339
+ admitted.runDirectory,
1340
+ scope,
1341
+ );
1543
1342
  }
1544
- const artifactsDir = await ensureTerminalArtifactFace(admitted.runDirectory);
1545
- const reportPath = join(artifactsDir, RUN_TERMINAL_REPORT_FILE);
1546
- const evidencePath = join(artifactsDir, RUN_TERMINAL_EVIDENCE_FILE);
1547
- await writeFile(
1548
- reportPath,
1549
- `${JSON.stringify(bodies.report, null, 2)}\n`,
1550
- "utf8",
1551
- );
1552
- await writeFile(
1553
- evidencePath,
1554
- `${JSON.stringify(bodies.evidence, null, 2)}\n`,
1555
- "utf8",
1556
- );
1557
- return [
1558
- { kind: "report", path: reportPath },
1559
- { kind: "evidence", path: evidencePath },
1560
- ];
1343
+ writeRunTerminal(admitted.runDirectory, "report", {
1344
+ ...report,
1345
+ ...terminalBelongingFields(attemptHistoryIdentity, scope),
1346
+ });
1347
+ return [{ kind: "report", path: runCurrentPath(admitted.runDirectory) }];
1348
+ }
1349
+
1350
+ /** 甲 pointer + court tag carried on the terminal body for later re-projection. */
1351
+ function terminalBelongingFields(
1352
+ attemptHistoryIdentity: string | undefined,
1353
+ scope: SettlementCourtScope | undefined,
1354
+ ): Record<string, string> {
1355
+ const courtAttemptId = scope?.courtAttemptId;
1356
+ return {
1357
+ ...(attemptHistoryIdentity === undefined
1358
+ ? {}
1359
+ : { [ATTEMPT_HISTORY_IDENTITY_FIELD]: attemptHistoryIdentity }),
1360
+ ...(courtAttemptId === undefined || courtAttemptId.length === 0
1361
+ ? {}
1362
+ : { [TERMINAL_COURT_ATTEMPT_FIELD]: courtAttemptId }),
1363
+ };
1364
+ }
1365
+
1366
+ /**
1367
+ * Belonging attempt-history identity for a non-recording re-projection (#1161 甲 / R2).
1368
+ * Court-tagged settle recovers from that court's own prior terminal row in
1369
+ * state.jsonl. Untagged normal rewrite (bare resume after a sealed face, no open
1370
+ * court) keeps only the face being rewritten — the last terminal row's own
1371
+ * identity. If that face has none (approved gap), return undefined: never walk
1372
+ * older terminals for a non-empty pointer, never invent-append (N1), never
1373
+ * subject.attemptId on attempt-history. Bare resume therefore cannot restore
1374
+ * court B's pointer while the current face is A (or A's gap). Court-tagged
1375
+ * lookup that finds no row still returns undefined — the approved gap.
1376
+ */
1377
+ async function belongingAttemptHistoryIdentity(
1378
+ runDirectory: string,
1379
+ scope: SettlementCourtScope | undefined,
1380
+ ): Promise<string | undefined> {
1381
+ const courtAttemptId = scope?.courtAttemptId;
1382
+ let rows: readonly Record<string, unknown>[];
1383
+ try {
1384
+ rows = readStateRowsSync(runDirectory);
1385
+ } catch (error) {
1386
+ // Documented: history/state failure domain does not block the terminal face
1387
+ // (#1161 / main history disposition). True cause must leave a trace.
1388
+ await noteSettlementFault(
1389
+ runDirectory,
1390
+ scope,
1391
+ `terminal state read failed beside terminal projection: ${describeErrorIdentity(error)}`,
1392
+ );
1393
+ return undefined;
1394
+ }
1395
+ if (courtAttemptId !== undefined && courtAttemptId.length > 0) {
1396
+ let found: string | undefined;
1397
+ for (const row of rows) {
1398
+ if (row.kind !== "terminal" || !isRecord(row.payload)) continue;
1399
+ const body = row.payload.body;
1400
+ if (!isRecord(body) || body[TERMINAL_COURT_ATTEMPT_FIELD] !== courtAttemptId) continue;
1401
+ const identity = body[ATTEMPT_HISTORY_IDENTITY_FIELD];
1402
+ if (typeof identity === "string" && identity.length > 0) found = identity;
1403
+ }
1404
+ return found;
1405
+ }
1406
+ // Untagged: only the current face (last terminal). Gap face stays a gap.
1407
+ for (let index = rows.length - 1; index >= 0; index -= 1) {
1408
+ const row = rows[index]!;
1409
+ if (row.kind !== "terminal" || !isRecord(row.payload)) continue;
1410
+ const body = row.payload.body;
1411
+ if (!isRecord(body)) return undefined;
1412
+ const identity = body[ATTEMPT_HISTORY_IDENTITY_FIELD];
1413
+ return typeof identity === "string" && identity.length > 0 ? identity : undefined;
1414
+ }
1415
+ return undefined;
1561
1416
  }
1562
1417
 
1563
1418
  type LawfulSessionRead =
@@ -1619,13 +1474,13 @@ async function finishLawfulSeat(
1619
1474
  ): Promise<TerminalResult> {
1620
1475
  const artifacts = scope?.previewOnly === true
1621
1476
  ? []
1622
- : await publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries, scope?.recordAttemptHistory === true);
1623
- const terminal = await withOptionalGateProjection({
1477
+ : await publishDeclaredSeatTerminal(admitted, roleOutcome, coordinates, entries, scope);
1478
+ const terminal = await attachEngineDetourToolUsage({
1624
1479
  roleOutcome,
1625
1480
  navigator: extractNavigatorFact(entries),
1626
1481
  artifacts,
1627
1482
  runId: admitted.runId,
1628
- }, coordinates.sessionDirectory, detourGateContext(admitted, scope));
1483
+ }, coordinates.sessionDirectory, detourUsageContext(admitted, scope));
1629
1484
  return attachRecordedSubmissions(admitted, terminal, scope);
1630
1485
  }
1631
1486
 
@@ -1675,9 +1530,9 @@ function extractDoctorCandidateFacts(
1675
1530
  return {};
1676
1531
  }
1677
1532
 
1678
- const EMPTY_ARTIFACT_FACE: PackagedArtifactFace = { leaves: [] };
1533
+ const EMPTY_ARTIFACT_FACE: PackagedArtifactFace = {};
1679
1534
 
1680
- /** Artifact face for one seat. Absent means report/evidence carry only shared leaves. */
1535
+ /** Terminal face for one seat. Absent means the report carries only the shared facts. */
1681
1536
  function seatArtifactFace(role: string): PackagedArtifactFace {
1682
1537
  const record = packagedRoleMetadata(role);
1683
1538
  if (record !== undefined && "artifactFace" in record && record.artifactFace !== undefined) {
@@ -1686,32 +1541,6 @@ function seatArtifactFace(role: string): PackagedArtifactFace {
1686
1541
  return EMPTY_ARTIFACT_FACE;
1687
1542
  }
1688
1543
 
1689
- function readAdmittedPath(source: object, path: string): unknown {
1690
- let current: unknown = source;
1691
- for (const part of path.split(".")) {
1692
- if (typeof current !== "object" || current === null) return undefined;
1693
- current = (current as Record<string, unknown>)[part];
1694
- }
1695
- return current;
1696
- }
1697
-
1698
- function evidenceLeaves(
1699
- admitted: AdmittedRoleInvocation,
1700
- leaves: readonly PackagedArtifactLeaf[],
1701
- ): Record<string, unknown> {
1702
- const evidence: Record<string, unknown> = {};
1703
- for (const leaf of leaves) {
1704
- if (leaf.callerProvenance === true) {
1705
- if (!admitted.instructionEmpty) evidence[leaf.key] = admitted.instruction;
1706
- continue;
1707
- }
1708
- const value = readAdmittedPath(admitted, leaf.from ?? leaf.key);
1709
- if (value === undefined && leaf.omitUndefined === true) continue;
1710
- evidence[leaf.key] = leaf.copyArray === true && Array.isArray(value) ? [...value] : value;
1711
- }
1712
- return evidence;
1713
- }
1714
-
1715
1544
  function doctorReportFacts(
1716
1545
  face: PackagedArtifactFace,
1717
1546
  roleOutcome: TerminalRoleOutcome,
@@ -1725,34 +1554,25 @@ function doctorReportFacts(
1725
1554
  };
1726
1555
  }
1727
1556
 
1728
- async function publishDeclaredSeatArtifacts(
1557
+ async function publishDeclaredSeatTerminal(
1729
1558
  admitted: AdmittedRoleInvocation,
1730
1559
  roleOutcome: TerminalRoleOutcome,
1731
1560
  coordinates: DurablePrincipalCoordinates,
1732
1561
  entries: readonly SessionEntry[],
1733
- recordAttemptHistory: boolean,
1562
+ scope: SettlementCourtScope | undefined,
1734
1563
  ): Promise<TerminalArtifactRef[]> {
1735
1564
  const face = seatArtifactFace(admitted.role);
1736
1565
  const phase = face.reportPhase === true
1737
- ? { phase: readAdmittedPath(admitted, "phase") }
1566
+ ? { phase: (admitted as { phase?: unknown }).phase }
1738
1567
  : {};
1739
- return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, recordAttemptHistory, {
1740
- report: {
1741
- role: admitted.role,
1742
- runId: admitted.runId,
1743
- ...phase,
1744
- outcome: roleOutcome,
1745
- ...doctorReportFacts(face, roleOutcome, entries),
1746
- },
1747
- evidence: {
1748
- runId: admitted.runId,
1749
- ...(face.evidenceRole === true ? { role: admitted.role } : {}),
1750
- ...evidenceLeaves(admitted, face.leaves),
1751
- sessionDirectory: coordinates.sessionDirectory,
1752
- sessionFile: coordinates.sessionFile,
1753
- admittedRequestPath: admitted.admittedRequestPath,
1754
- attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
1755
- },
1568
+ // Payloads are the role's submitted words: they live in history.jsonl, not here.
1569
+ const { payloads: _payloads, ...verdict } = roleOutcome as TerminalRoleOutcome & { payloads?: unknown };
1570
+ return publishAcceptedTerminal(admitted, roleOutcome, coordinates, scope, {
1571
+ role: admitted.role,
1572
+ runId: admitted.runId,
1573
+ ...phase,
1574
+ outcome: verdict,
1575
+ ...doctorReportFacts(face, roleOutcome, entries),
1756
1576
  });
1757
1577
  }
1758
1578
 
@@ -1933,211 +1753,55 @@ async function applySecretariatCountersignTerminal(
1933
1753
  }
1934
1754
 
1935
1755
  /**
1936
- * After the audit gate returns, attach gate rounds and the secretariat officer
1937
- * fact onto the terminal this turn already settled. Does not publish again —
1938
- * a second publish would append another attempt-history row for the same attempt.
1756
+ * After the audit gate returns, attach the secretariat officer fact onto the
1757
+ * terminal this turn already settled. Does not publish again — a second publish
1758
+ * would settle the same attempt twice.
1939
1759
  */
1940
- export async function attachPostAuditProjection(
1760
+ export async function attachPostAuditCountersignFact(
1941
1761
  admitted: AdmittedRoleInvocation,
1942
1762
  authority: DurablePrincipalAuthority,
1943
1763
  terminal: TerminalResult,
1944
1764
  scope?: SettlementCourtScope,
1945
1765
  ): Promise<TerminalResult> {
1946
- const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
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
- }
1960
- const withGate = gate === undefined ? terminal : { ...terminal, gate };
1961
- if (admitted.role !== "secretariat") return withGate;
1962
- return applySecretariatCountersignTerminal(admitted, authority, withGate, scope);
1963
- }
1964
-
1965
- /** One failed attempt to place a durable failure artifact (path is private layout). */
1966
- type PublicationAttempt = {
1967
- readonly path: string;
1968
- readonly diagnostic: string;
1969
- readonly identity?: {
1970
- readonly name?: string;
1971
- readonly code?: string | number;
1972
- };
1973
- };
1974
-
1975
- function publicationAttemptFromError(
1976
- path: string,
1977
- error: unknown,
1978
- ): PublicationAttempt {
1979
- if (error instanceof Error) {
1980
- return {
1981
- path,
1982
- diagnostic: error.message || error.name || "write failed",
1983
- identity: thrownIdentity(error),
1984
- };
1985
- }
1986
- return { path, diagnostic: String(error) };
1987
- }
1988
-
1989
- /**
1990
- * Directories eligible for open-ended unique failure-artifact placement.
1991
- * Always includes the ledger runs/ parent of the run directory so an
1992
- * unwritable run tree cannot strand the original controlled failure.
1993
- */
1994
- function uniqueFailureFallbackDirs(
1995
- runDirectory: string,
1996
- baseDir: string,
1997
- ): string[] {
1998
- const dirs: string[] = [];
1999
- for (const dir of [baseDir, runDirectory, dirname(runDirectory)]) {
2000
- if (!dirs.includes(dir)) dirs.push(dir);
2001
- }
2002
- return dirs;
2003
- }
2004
-
2005
- /**
2006
- * Resolve a writable artifacts base directory. If `artifacts/` cannot be created
2007
- * (e.g. a file occupies that name), fall back to the run directory itself.
2008
- */
2009
- async function resolveFailureArtifactsBase(
2010
- runDirectory: string,
2011
- ): Promise<{ baseDir: string; attempt?: PublicationAttempt }> {
2012
- const artifactsDir = roleRunArtifactsDirectory(runDirectory);
2013
- try {
2014
- await ensureRunArtifactsDir(runDirectory);
2015
- return { baseDir: artifactsDir };
2016
- } catch (error) {
2017
- return {
2018
- baseDir: runDirectory,
2019
- attempt: publicationAttemptFromError(artifactsDir, error),
2020
- };
2021
- }
1766
+ if (admitted.role !== "secretariat") return terminal;
1767
+ return applySecretariatCountersignTerminal(admitted, authority, terminal, scope);
2022
1768
  }
2023
1769
 
2024
1770
  /**
2025
- * Write JSON across preferred paths, then unique open-ended fallbacks.
2026
- * Finite fixed names must not be able to exhaust durability and strand the
2027
- * original controlled failure outside settlement.
1771
+ * Replace the leg's terminal with the controlled failure. A write failure
1772
+ * propagates: the caller notes it beside the host terminal.
2028
1773
  */
2029
- async function writeFailureJsonRetainingCause(
2030
- preferredCandidates: readonly string[],
2031
- uniqueFallbackDirs: readonly string[],
2032
- stem: string,
2033
- basePayload: Readonly<Record<string, unknown>>,
2034
- priorIssues: readonly PublicationAttempt[],
2035
- ): Promise<{ path: string; issues: PublicationAttempt[] }> {
2036
- const issues: PublicationAttempt[] = [...priorIssues];
2037
- const candidates: string[] = [
2038
- ...preferredCandidates,
2039
- // One unique name per fallback dir — collisions on fixed names cannot exhaust this.
2040
- ...uniqueFallbackDirs.map((dir) => join(dir, stem === UNIQUE_ERROR_FALLBACK_STEM ? uniqueErrorFallbackName() : `${stem}.${randomUUID()}.json`)),
2041
- ];
2042
- for (let i = 0; i < candidates.length; i += 1) {
2043
- const path = candidates[i]!;
2044
- const payload =
2045
- issues.length === 0
2046
- ? basePayload
2047
- : { ...basePayload, publicationIssues: issues };
2048
- try {
2049
- await writeFile(
2050
- path,
2051
- `${JSON.stringify(payload, null, 2)}\n`,
2052
- "utf8",
2053
- );
2054
- return { path, issues };
2055
- } catch (error) {
2056
- issues.push(publicationAttemptFromError(path, error));
2057
- }
2058
- }
2059
- const last = issues.at(-1);
2060
- const error = new Error(
2061
- last?.diagnostic ?? "unable to write durable failure artifact",
2062
- ) as Error & {
2063
- code?: string | number;
2064
- publicationAttempts?: PublicationAttempt[];
2065
- };
2066
- if (last?.identity?.name !== undefined && last.identity.name !== "") {
2067
- error.name = last.identity.name;
2068
- }
2069
- if (last?.identity?.code !== undefined) {
2070
- error.code = last.identity.code;
2071
- }
2072
- error.publicationAttempts = issues;
2073
- throw error;
2074
- }
2075
-
2076
- export async function publishFailureArtifacts(
1774
+ export async function publishFailureTerminal(
2077
1775
  admitted: AdmittedRoleInvocation,
2078
1776
  failure: ControlledFailure,
2079
- authority: DurablePrincipalAuthority,
1777
+ coordinates: DurablePrincipalCoordinates,
2080
1778
  onErrorPublished?: (path: string) => void,
2081
- recordAttemptHistory = false,
1779
+ scope?: SettlementCourtScope,
2082
1780
  ): Promise<TerminalArtifactRef[]> {
2083
- const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
2084
- const { baseDir, attempt: baseAttempt } = await resolveFailureArtifactsBase(
2085
- admitted.runDirectory,
2086
- );
2087
- const priorIssues: PublicationAttempt[] =
2088
- baseAttempt === undefined ? [] : [baseAttempt];
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) {
1781
+ // #419: a dispatched failure joins history before the terminal changes. A history
1782
+ // write failure never strands the failure terminal; it is rethrown after it.
1783
+ let historyFailure: unknown;
1784
+ let historyFailed = false;
1785
+ const failureOutcome = { kind: "failure" as const, role: admitted.role, ...failure };
1786
+ let attemptHistoryIdentity: string | undefined;
1787
+ if (scope?.recordAttemptHistory === true) {
2093
1788
  try {
2094
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile }, {
2095
- kind: "failure", role: admitted.role, ...failure,
2096
- });
1789
+ const pointer = await appendRunAttemptHistory(
1790
+ attemptHistorySource(admitted, coordinates),
1791
+ failureOutcome,
1792
+ );
1793
+ attemptHistoryIdentity = pointer.identity;
2097
1794
  } catch (error) {
2098
- priorIssues.push(publicationAttemptFromError(sessionFile, error));
1795
+ historyFailure = error;
1796
+ historyFailed = true;
2099
1797
  }
1798
+ } else {
1799
+ attemptHistoryIdentity = await belongingAttemptHistoryIdentity(
1800
+ admitted.runDirectory,
1801
+ scope,
1802
+ );
2100
1803
  }
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.
2103
- try {
2104
- await clearOppositeTerminalArtifactFace(admitted.runDirectory);
2105
- } catch (error) {
2106
- priorIssues.push(publicationAttemptFromError(
2107
- roleRunArtifactsDirectory(admitted.runDirectory), error,
2108
- ));
2109
- }
2110
-
2111
- // Prefer conventional names; unique fallback dirs keep colliding fixed paths
2112
- // from stranding the original failure outside settlement. Include the ledger
2113
- // runs/ parent so a locked run directory (EACCES) cannot exhaust durability.
2114
- const underArtifacts = baseDir === roleRunArtifactsDirectory(admitted.runDirectory);
2115
- const uniqueFallbackDirs = uniqueFailureFallbackDirs(
2116
- admitted.runDirectory,
2117
- baseDir,
2118
- );
2119
- const errorCandidates = underArtifacts
2120
- ? [
2121
- join(baseDir, RUN_TERMINAL_ERROR_FILE),
2122
- join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
2123
- join(admitted.runDirectory, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
2124
- ]
2125
- : [
2126
- join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
2127
- join(baseDir, RUN_TERMINAL_ERROR_FILE),
2128
- ];
2129
- const evidenceCandidates = underArtifacts
2130
- ? [
2131
- join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
2132
- join(baseDir, "evidence.settlement.json"),
2133
- join(admitted.runDirectory, "evidence.settlement.json"),
2134
- ]
2135
- : [
2136
- join(baseDir, "evidence.settlement.json"),
2137
- join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
2138
- ];
2139
-
2140
- const errorPayloadBase: Record<string, unknown> = {
1804
+ writeRunTerminal(admitted.runDirectory, "error", {
2141
1805
  kind: "error",
2142
1806
  role: admitted.role,
2143
1807
  runId: admitted.runId,
@@ -2148,38 +1812,12 @@ export async function publishFailureArtifacts(
2148
1812
  // This package's own facts, kept beside the host's report.
2149
1813
  ...(failure.packageFact === undefined ? {} : { packageFact: failure.packageFact }),
2150
1814
  ...(failure.stderr === undefined ? {} : { stderr: failure.stderr }),
2151
- };
2152
-
2153
- const errorWrite = await writeFailureJsonRetainingCause(
2154
- errorCandidates,
2155
- uniqueFallbackDirs,
2156
- UNIQUE_ERROR_FALLBACK_STEM,
2157
- errorPayloadBase,
2158
- priorIssues,
2159
- );
2160
- onErrorPublished?.(errorWrite.path);
2161
-
2162
- const evidencePayload: Record<string, unknown> = {
2163
- runId: admitted.runId,
2164
- sessionDirectory: sessionDirectory,
2165
- sessionFile: sessionFile,
2166
- admittedRequestPath: admitted.admittedRequestPath,
2167
- attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
2168
- ...(failure.cause === undefined ? {} : { failureCause: failure.cause }),
2169
- };
2170
- const evidenceWrite = await writeFailureJsonRetainingCause(
2171
- evidenceCandidates,
2172
- uniqueFallbackDirs,
2173
- "evidence",
2174
- evidencePayload,
2175
- // Evidence records the same publication collisions observed placing the error body.
2176
- errorWrite.issues,
2177
- );
2178
-
2179
- return [
2180
- { kind: "error", path: errorWrite.path },
2181
- { kind: "evidence", path: evidenceWrite.path },
2182
- ];
1815
+ ...terminalBelongingFields(attemptHistoryIdentity, scope),
1816
+ });
1817
+ const path = runCurrentPath(admitted.runDirectory);
1818
+ onErrorPublished?.(path);
1819
+ if (historyFailed) throw historyFailure;
1820
+ return [{ kind: "error", path }];
2183
1821
  }
2184
1822
 
2185
1823
  /**
@@ -2205,13 +1843,7 @@ export async function settleFailureTerminalResult(
2205
1843
  let artifacts: TerminalArtifactRef[] = [];
2206
1844
  if (options.previewOnly !== true) {
2207
1845
  try {
2208
- artifacts = await publishFailureArtifacts(
2209
- admitted,
2210
- failure,
2211
- authority,
2212
- options.onErrorPublished,
2213
- options.recordAttemptHistory === true,
2214
- );
1846
+ artifacts = await publishFailureTerminal(admitted, failure, coordinates, options.onErrorPublished, options);
2215
1847
  } catch (error) {
2216
1848
  await noteSettlementFault(
2217
1849
  admitted.runDirectory,
@@ -2246,10 +1878,10 @@ export async function settleFailureTerminalResult(
2246
1878
  diagnostic: failure.diagnostic,
2247
1879
  decisiveFacts,
2248
1880
  };
2249
- return withOptionalGateProjection(
1881
+ return attachEngineDetourToolUsage(
2250
1882
  { roleOutcome, navigator, artifacts, runId: admitted.runId },
2251
1883
  sessionDirectory,
2252
- detourGateContext(admitted, options),
1884
+ detourUsageContext(admitted, options),
2253
1885
  );
2254
1886
  }
2255
1887