@akagilnc/pi-workflow-roles 0.1.5504 → 0.1.5575

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 (89) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +5965 -5994
  4. package/dist/analyst-gate-cycles-read.js +23 -55
  5. package/dist/analyst-ledger.js +21 -35
  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-pointer.js +38 -30
  9. package/dist/auditor-dossier-tool.js +4 -3
  10. package/dist/auditor-soul.js +3 -11
  11. package/dist/doctor-evidence.js +3 -3
  12. package/dist/engine-detour-usage.js +6 -14
  13. package/dist/headless-host/production-host.js +5316 -5324
  14. package/dist/host-descriptions.js +0 -4
  15. package/dist/host-session-record.js +266 -267
  16. package/dist/migrate-book-topology.js +1314 -696
  17. package/dist/notary-source-run.js +1 -1
  18. package/dist/packaged-role-registry.js +5 -44
  19. package/dist/pi/role-turn-host.js +53 -3
  20. package/dist/public-cli/auto-resume.js +8 -47
  21. package/dist/public-cli/instruction-seat-run.js +5 -3
  22. package/dist/public-cli/invocation.js +51 -56
  23. package/dist/public-cli/main.js +1916 -1748
  24. package/dist/public-cli/post-admission.js +96 -55
  25. package/dist/public-cli/run-lifecycle.js +27 -53
  26. package/dist/public-cli/settlement.js +157 -302
  27. package/dist/role-run-placement.js +1 -11
  28. package/dist/role-run-relocation.js +72 -50
  29. package/dist/run-dossier-files.js +30 -0
  30. package/dist/run-dossier.js +322 -0
  31. package/dist/run-terminal-artifacts.js +68 -197
  32. package/dist/run-ticket-number.js +16 -3
  33. package/dist/session-identity.js +49 -18
  34. package/dist/sitian-appender.js +13 -16
  35. package/dist/sitian-facade.js +11 -0
  36. package/dist/sitian-reader.js +44 -29
  37. package/dist/submission-gate.js +1 -1
  38. package/dist/ticket-provenance.js +22 -7
  39. package/package.json +1 -1
  40. package/scripts/build-package.mjs +2 -0
  41. package/src/acp-host/production-host.ts +1 -1
  42. package/src/acp-host/role-turn-host.ts +9 -2
  43. package/src/analyst-gate-cycles-read.ts +26 -60
  44. package/src/analyst-ledger.ts +27 -42
  45. package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
  46. package/src/analyst-metric-families/gate-cycles.ts +1 -1
  47. package/src/analyst-metric-families/round-timeline.ts +2 -5
  48. package/src/analyst-page.ts +3 -3
  49. package/src/archivist-record-entry.ts +2 -2
  50. package/src/archivist-record-pointer.ts +50 -36
  51. package/src/auditor-dossier-tool.ts +7 -5
  52. package/src/auditor-soul.ts +3 -9
  53. package/src/book-topology-migration-placement.ts +16 -3
  54. package/src/book-topology-record-class-migrators.ts +4 -2
  55. package/src/book-topology-runs-migrator.ts +6 -3
  56. package/src/doctor-contracts.ts +1 -1
  57. package/src/doctor-evidence.ts +2 -2
  58. package/src/engine-detour-tool.ts +1 -1
  59. package/src/engine-detour-usage.ts +6 -16
  60. package/src/external-host-turn-loop.ts +25 -0
  61. package/src/headless-host/production-host.ts +1 -4
  62. package/src/headless-host/role-turn-host.ts +54 -20
  63. package/src/host-descriptions.ts +0 -6
  64. package/src/host-session-record.ts +224 -140
  65. package/src/notary-source-run.ts +1 -1
  66. package/src/packaged-role-registry.ts +1 -50
  67. package/src/pi/adapter.ts +50 -7
  68. package/src/pi/role-turn-host.ts +51 -3
  69. package/src/public-cli/auto-resume.ts +7 -43
  70. package/src/public-cli/instruction-seat-run.ts +9 -2
  71. package/src/public-cli/invocation.ts +61 -87
  72. package/src/public-cli/post-admission.ts +111 -72
  73. package/src/public-cli/run-lifecycle.ts +27 -72
  74. package/src/public-cli/settlement.ts +193 -418
  75. package/src/public-cli/terminal.ts +1 -1
  76. package/src/role-run-placement.ts +2 -13
  77. package/src/role-run-relocation.ts +71 -68
  78. package/src/role-runtime.ts +6 -10
  79. package/src/run-dossier-files.ts +33 -0
  80. package/src/run-dossier.ts +357 -0
  81. package/src/run-terminal-artifacts.ts +83 -237
  82. package/src/run-ticket-number.ts +41 -11
  83. package/src/session-identity.ts +52 -20
  84. package/src/sitian-appender.ts +17 -18
  85. package/src/sitian-facade.ts +22 -0
  86. package/src/sitian-reader.ts +62 -32
  87. package/src/submission-gate.ts +1 -1
  88. package/src/ticket-provenance.ts +20 -7
  89. package/src/ticket-trajectory.ts +10 -10
@@ -9,8 +9,12 @@ import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
9
9
 
10
10
  import { sessionFileOf } from "../role-run-placement.ts";
11
11
 
12
- import { writeHardenedArtifactFile } from "./auto-resume.ts";
13
- import { sitianReport } from "../sitian-facade.ts";
12
+ import { sitianReport, type RecordPointer } from "../sitian-facade.ts";
13
+ import {
14
+ ATTEMPT_HISTORY_IDENTITY_FIELD,
15
+ TERMINAL_COURT_ATTEMPT_FIELD,
16
+ writeRunTerminal,
17
+ } from "../run-terminal-artifacts.ts";
14
18
 
15
19
  import {
16
20
  readAttemptScopedSubmissionRows,
@@ -38,7 +42,6 @@ import {
38
42
  import type { DoctorCaseCost } from "../doctor-contracts.ts";
39
43
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "../dossier-resolution.ts";
40
44
  import {
41
- ensureRunArtifactsDir,
42
45
  homeFromRunDirectory,
43
46
  type AdmittedRoleInvocation,
44
47
  } from "./invocation.ts";
@@ -55,7 +58,6 @@ import {
55
58
  packagedRoleAcceptedOutputTool,
56
59
  packagedRoleMetadata,
57
60
  type PackagedArtifactFace,
58
- type PackagedArtifactLeaf,
59
61
  } from "../packaged-role-registry.ts";
60
62
  import {
61
63
  NO_RECEIPT_LIFECYCLE_ENTRY_TYPE,
@@ -70,18 +72,8 @@ import type {
70
72
  DurablePrincipalAuthority,
71
73
  DurablePrincipalCoordinates,
72
74
  } from "../host-contracts.ts";
73
- import { roleRunArtifactsDirectory } from "../role-run-placement.ts";
74
- import {
75
- listSeamOwnedUniqueErrorFacePaths,
76
- uniqueErrorFallbackName,
77
- UNIQUE_ERROR_FALLBACK_STEM,
78
- RUN_TERMINAL_ARTIFACT_FILES,
79
- RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS,
80
- RUN_TERMINAL_REPORT_FILE,
81
- RUN_TERMINAL_ERROR_FILE,
82
- RUN_TERMINAL_EVIDENCE_FILE,
83
- RUN_TERMINAL_ERROR_SETTLEMENT_FILE,
84
- } from "../run-terminal-artifacts.ts";
75
+ import { readStateRowsSync, runCurrentPath } from "../run-dossier.ts";
76
+ import { reportRunRecord } from "../sitian-facade.ts";
85
77
 
86
78
  /** Ledger reads use the run's machine home — not ambient process HOME (child write vs parent settle). */
87
79
  function sealedLedgerHome(admitted: Pick<AdmittedRoleInvocation, "runDirectory">): string {
@@ -107,32 +99,6 @@ export type SettlementCourtScope = {
107
99
  readonly notePackageFault?: (diagnostic: string) => void | Promise<void>;
108
100
  };
109
101
 
110
- /**
111
- * Hardened artifacts directory. A planted symlink at the run directory or the
112
- * artifacts path must not receive a durable run artifact.
113
- */
114
- export async function ensureRealArtifactsDirectory(runDirectory: string): Promise<string> {
115
- const runStat = await lstat(runDirectory);
116
- if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
117
- throw new Error("run artifact retention: run directory is not a real directory");
118
- }
119
- const artifactsDir = roleRunArtifactsDirectory(runDirectory);
120
- try {
121
- const existing = await lstat(artifactsDir);
122
- if (existing.isSymbolicLink() || !existing.isDirectory()) {
123
- throw new Error("run artifact retention: artifacts path is not a real directory");
124
- }
125
- } catch (error) {
126
- if (!isEnoent(error)) throw error;
127
- await mkdir(artifactsDir, { recursive: true });
128
- const created = await lstat(artifactsDir);
129
- if (created.isSymbolicLink() || !created.isDirectory()) {
130
- throw new Error("run artifact retention: artifacts directory is not a real directory");
131
- }
132
- }
133
- return artifactsDir;
134
- }
135
-
136
102
  /**
137
103
  * One retention path for a package fault beside an already chosen terminal.
138
104
  * Session append is first when the caller has that channel. The artifact file
@@ -156,13 +122,14 @@ export async function retainPackageFault(input: {
156
122
  ...(Object.hasOwn(input, "error") ? { failure: projectThrownFailureLeaf(input.error) } : {}),
157
123
  };
158
124
  let retentionFailure: string | undefined;
159
- const writeArtifact = async (appendFailure?: ControlledFailure): Promise<void> => {
160
- const artifactsDir = await ensureRealArtifactsDirectory(input.runDirectory);
161
- 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", {
162
129
  version: 1,
163
130
  ...payload,
164
131
  ...(appendFailure === undefined ? {} : { retentionFailure: appendFailure }),
165
- });
132
+ }, "settlement");
166
133
  };
167
134
  if (input.appendSession !== undefined) {
168
135
  try {
@@ -171,18 +138,18 @@ export async function retainPackageFault(input: {
171
138
  retentionFailure =
172
139
  `post-dispatch diagnostic session append failed (best-effort continue): dossier=${describeErrorIdentity(appendError)}`;
173
140
  try {
174
- await writeArtifact(projectThrownFailureLeaf(appendError));
175
- } catch (artifactError) {
141
+ writeLog(projectThrownFailureLeaf(appendError));
142
+ } catch (logError) {
176
143
  retentionFailure =
177
- `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)}`;
178
145
  }
179
146
  }
180
147
  } else {
181
148
  try {
182
- await writeArtifact();
183
- } catch (artifactError) {
149
+ writeLog();
150
+ } catch (logError) {
184
151
  retentionFailure =
185
- `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)}`;
186
153
  }
187
154
  }
188
155
  const stderr = input.stderr ?? ((text: string) => {
@@ -460,13 +427,29 @@ async function settleNoReceiptTerminal(
460
427
  kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt",
461
428
  ...facts, decisiveFacts: facts,
462
429
  };
430
+ let attemptHistoryIdentity: string | undefined;
463
431
  if (scope?.recordAttemptHistory === true) {
464
- await appendRunAttemptHistory(
465
- { role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile },
432
+ const pointer = await appendRunAttemptHistory(
433
+ attemptHistorySource(admitted, coordinates),
466
434
  roleOutcome,
467
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
+ });
468
452
  }
469
- if (scope?.previewOnly !== true) await clearOppositeTerminalArtifactFace(admitted.runDirectory);
470
453
  return attachEngineDetourToolUsage({
471
454
  roleOutcome,
472
455
  navigator: await extractNavigatorFactFromAdmittedSession(
@@ -526,7 +509,7 @@ export type ControlledFailure = RoleTurnKnownFailure & {
526
509
  */
527
510
  readonly packageFact?: PackageSideFact;
528
511
  /**
529
- * 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
530
513
  * or artifact write can fail; the original bytes stay on this object.
531
514
  */
532
515
  readonly stderr?: string;
@@ -555,7 +538,7 @@ export type PackageSideFact = {
555
538
  readonly cancelName?: string;
556
539
  /** An exception caught after the host already reported this call. */
557
540
  readonly thrown?: RoleTurnKnownFailure & { readonly diagnostic: string };
558
- /** A durable stderr.log write that failed while handling this call. */
541
+ /** A durable stderr log-line write that failed while handling this call. */
559
542
  readonly stderrLogWriteFailure?: unknown;
560
543
  };
561
544
 
@@ -1052,12 +1035,23 @@ type AttemptHistorySource = {
1052
1035
  readonly sessionFile: string;
1053
1036
  };
1054
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
+
1055
1049
  /** Append the complete attempt to the package ledger before overwriting pointer artifacts (#419). */
1056
1050
  export async function appendRunAttemptHistory(
1057
1051
  source: AttemptHistorySource,
1058
1052
  outcome: AttemptHistoryOutcome,
1059
- ): Promise<void> {
1060
- sitianReport({
1053
+ ): Promise<RecordPointer> {
1054
+ return sitianReport({
1061
1055
  level: "event",
1062
1056
  kind: "attempt-history",
1063
1057
  subject: { runId: source.runId },
@@ -1316,120 +1310,109 @@ async function extractNavigatorFactFromAdmittedSession(
1316
1310
  }
1317
1311
 
1318
1312
  /**
1319
- * Remove one face path (file or directory collision plant).
1320
- * Only missing target is silent — other errno stay loud (失败诚实).
1321
- * recursive: publication may have occupied a face name as a directory (EISDIR plant).
1322
- */
1323
- async function removeFaceIfPresent(path: string): Promise<void> {
1324
- try {
1325
- await rm(path, { recursive: true, force: true });
1326
- } catch (error) {
1327
- // force:true already ignores ENOENT; ENOTDIR = face path not enterable
1328
- // (artifacts-as-file mid-path) — same absent-face semantics as the reader.
1329
- // Other errno stay loud (失败诚实).
1330
- if (isMissingPathError(error)) return;
1331
- throw error;
1332
- }
1333
- }
1334
-
1335
- /**
1336
- * #953: artifact face reflects the current terminal only.
1337
- * Reader contract owns the full adoptable set (conventional trio + fixed
1338
- * fallbacks + seam-owned unique). Clear every seam-owned face before the
1339
- * new current publishes — including the conventional name about to be
1340
- * rewritten. Retaining that name left directory collision plants in place
1341
- * (supported publish input); writer then fell back while the reader stopped
1342
- * at EISDIR on the stale conventional path and never adopted the fallback.
1343
- * report / error / empty share one method (no per-branch face list).
1344
- * empty publishes nothing; does not create a no_receipt-shaped public artifact.
1345
- * Non-terminal materials stay. Face names may be directory collision plants —
1346
- * remove recursively. Unique ownership is listSeamOwnedUniqueErrorFacePaths:
1347
- * parent runs/ faces clear only when body.runId binds; unparseable runId → none.
1348
- * Path absence (ENOENT/ENOTDIR) is silent; other delete failures stay loud.
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).
1349
1320
  */
1350
- async function clearOppositeTerminalArtifactFace(
1351
- runDirectory: string,
1352
- ): Promise<void> {
1353
- const artifactsDir = roleRunArtifactsDirectory(runDirectory);
1354
- for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
1355
- await removeFaceIfPresent(join(artifactsDir, file));
1356
- }
1357
- // Fixed fallbacks + unique clear too; subsequent publish writes the new
1358
- // durable path after this clear (conventional first when writable).
1359
- for (const relative of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
1360
- await removeFaceIfPresent(join(runDirectory, relative));
1361
- }
1362
- for (const path of await listSeamOwnedUniqueErrorFacePaths(runDirectory)) {
1363
- await removeFaceIfPresent(path);
1364
- }
1365
- }
1366
-
1367
- async function ensureTerminalArtifactFace(
1368
- runDirectory: string,
1369
- ): Promise<string> {
1370
- const artifactsDir = await ensureRunArtifactsDir(runDirectory);
1371
- await clearOppositeTerminalArtifactFace(runDirectory);
1372
- return artifactsDir;
1373
- }
1374
-
1375
- type AcceptedArtifactAttachment = {
1376
- readonly provenancePath: string;
1377
- readonly frozenPath: string;
1378
- readonly sha256: string;
1379
- readonly byteLength: number;
1380
- };
1381
-
1382
- function acceptedArtifactAttachmentRefs(
1383
- attachments: readonly AcceptedArtifactAttachment[],
1384
- ): AcceptedArtifactAttachment[] {
1385
- return attachments.map((a) => ({
1386
- provenancePath: a.provenancePath,
1387
- frozenPath: a.frozenPath,
1388
- sha256: a.sha256,
1389
- byteLength: a.byteLength,
1390
- }));
1391
- }
1392
-
1393
- /**
1394
- * Sole success-terminal artifact publisher (#953): append attempt history,
1395
- * refresh the public terminal face, write report/evidence, return refs.
1396
- * Seat-specific structured fields stay in callers; do not fork this flow.
1397
- */
1398
- async function publishAcceptedTerminalArtifacts(
1321
+ async function publishAcceptedTerminal(
1399
1322
  admitted: AdmittedRoleInvocation,
1400
1323
  roleOutcome: TerminalRoleOutcome,
1401
1324
  coordinates: DurablePrincipalCoordinates,
1402
- recordAttemptHistory: boolean,
1403
- bodies: {
1404
- readonly report: Record<string, unknown>;
1405
- readonly evidence: Record<string, unknown>;
1406
- },
1325
+ scope: SettlementCourtScope | undefined,
1326
+ report: Record<string, unknown>,
1407
1327
  ): Promise<TerminalArtifactRef[]> {
1408
1328
  // #419: a dispatched turn appends before rewriting last-write-wins views;
1409
1329
  // a later projection may refresh views but must not invent another attempt.
1410
- if (recordAttemptHistory) {
1411
- await appendRunAttemptHistory(
1412
- { 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),
1413
1334
  roleOutcome,
1414
1335
  );
1336
+ attemptHistoryIdentity = pointer.identity;
1337
+ } else {
1338
+ attemptHistoryIdentity = await belongingAttemptHistoryIdentity(
1339
+ admitted.runDirectory,
1340
+ scope,
1341
+ );
1415
1342
  }
1416
- const artifactsDir = await ensureTerminalArtifactFace(admitted.runDirectory);
1417
- const reportPath = join(artifactsDir, RUN_TERMINAL_REPORT_FILE);
1418
- const evidencePath = join(artifactsDir, RUN_TERMINAL_EVIDENCE_FILE);
1419
- await writeFile(
1420
- reportPath,
1421
- `${JSON.stringify(bodies.report, null, 2)}\n`,
1422
- "utf8",
1423
- );
1424
- await writeFile(
1425
- evidencePath,
1426
- `${JSON.stringify(bodies.evidence, null, 2)}\n`,
1427
- "utf8",
1428
- );
1429
- return [
1430
- { kind: "report", path: reportPath },
1431
- { kind: "evidence", path: evidencePath },
1432
- ];
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;
1433
1416
  }
1434
1417
 
1435
1418
  type LawfulSessionRead =
@@ -1491,7 +1474,7 @@ async function finishLawfulSeat(
1491
1474
  ): Promise<TerminalResult> {
1492
1475
  const artifacts = scope?.previewOnly === true
1493
1476
  ? []
1494
- : await publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries, scope?.recordAttemptHistory === true);
1477
+ : await publishDeclaredSeatTerminal(admitted, roleOutcome, coordinates, entries, scope);
1495
1478
  const terminal = await attachEngineDetourToolUsage({
1496
1479
  roleOutcome,
1497
1480
  navigator: extractNavigatorFact(entries),
@@ -1547,9 +1530,9 @@ function extractDoctorCandidateFacts(
1547
1530
  return {};
1548
1531
  }
1549
1532
 
1550
- const EMPTY_ARTIFACT_FACE: PackagedArtifactFace = { leaves: [] };
1533
+ const EMPTY_ARTIFACT_FACE: PackagedArtifactFace = {};
1551
1534
 
1552
- /** 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. */
1553
1536
  function seatArtifactFace(role: string): PackagedArtifactFace {
1554
1537
  const record = packagedRoleMetadata(role);
1555
1538
  if (record !== undefined && "artifactFace" in record && record.artifactFace !== undefined) {
@@ -1558,32 +1541,6 @@ function seatArtifactFace(role: string): PackagedArtifactFace {
1558
1541
  return EMPTY_ARTIFACT_FACE;
1559
1542
  }
1560
1543
 
1561
- function readAdmittedPath(source: object, path: string): unknown {
1562
- let current: unknown = source;
1563
- for (const part of path.split(".")) {
1564
- if (typeof current !== "object" || current === null) return undefined;
1565
- current = (current as Record<string, unknown>)[part];
1566
- }
1567
- return current;
1568
- }
1569
-
1570
- function evidenceLeaves(
1571
- admitted: AdmittedRoleInvocation,
1572
- leaves: readonly PackagedArtifactLeaf[],
1573
- ): Record<string, unknown> {
1574
- const evidence: Record<string, unknown> = {};
1575
- for (const leaf of leaves) {
1576
- if (leaf.callerProvenance === true) {
1577
- if (!admitted.instructionEmpty) evidence[leaf.key] = admitted.instruction;
1578
- continue;
1579
- }
1580
- const value = readAdmittedPath(admitted, leaf.from ?? leaf.key);
1581
- if (value === undefined && leaf.omitUndefined === true) continue;
1582
- evidence[leaf.key] = leaf.copyArray === true && Array.isArray(value) ? [...value] : value;
1583
- }
1584
- return evidence;
1585
- }
1586
-
1587
1544
  function doctorReportFacts(
1588
1545
  face: PackagedArtifactFace,
1589
1546
  roleOutcome: TerminalRoleOutcome,
@@ -1597,34 +1554,25 @@ function doctorReportFacts(
1597
1554
  };
1598
1555
  }
1599
1556
 
1600
- async function publishDeclaredSeatArtifacts(
1557
+ async function publishDeclaredSeatTerminal(
1601
1558
  admitted: AdmittedRoleInvocation,
1602
1559
  roleOutcome: TerminalRoleOutcome,
1603
1560
  coordinates: DurablePrincipalCoordinates,
1604
1561
  entries: readonly SessionEntry[],
1605
- recordAttemptHistory: boolean,
1562
+ scope: SettlementCourtScope | undefined,
1606
1563
  ): Promise<TerminalArtifactRef[]> {
1607
1564
  const face = seatArtifactFace(admitted.role);
1608
1565
  const phase = face.reportPhase === true
1609
- ? { phase: readAdmittedPath(admitted, "phase") }
1566
+ ? { phase: (admitted as { phase?: unknown }).phase }
1610
1567
  : {};
1611
- return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, recordAttemptHistory, {
1612
- report: {
1613
- role: admitted.role,
1614
- runId: admitted.runId,
1615
- ...phase,
1616
- outcome: roleOutcome,
1617
- ...doctorReportFacts(face, roleOutcome, entries),
1618
- },
1619
- evidence: {
1620
- runId: admitted.runId,
1621
- ...(face.evidenceRole === true ? { role: admitted.role } : {}),
1622
- ...evidenceLeaves(admitted, face.leaves),
1623
- sessionDirectory: coordinates.sessionDirectory,
1624
- sessionFile: coordinates.sessionFile,
1625
- admittedRequestPath: admitted.admittedRequestPath,
1626
- attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
1627
- },
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),
1628
1576
  });
1629
1577
  }
1630
1578
 
@@ -1807,7 +1755,7 @@ async function applySecretariatCountersignTerminal(
1807
1755
  /**
1808
1756
  * After the audit gate returns, attach the secretariat officer fact onto the
1809
1757
  * terminal this turn already settled. Does not publish again — a second publish
1810
- * would append another attempt-history row for the same attempt.
1758
+ * would settle the same attempt twice.
1811
1759
  */
1812
1760
  export async function attachPostAuditCountersignFact(
1813
1761
  admitted: AdmittedRoleInvocation,
@@ -1819,182 +1767,41 @@ export async function attachPostAuditCountersignFact(
1819
1767
  return applySecretariatCountersignTerminal(admitted, authority, terminal, scope);
1820
1768
  }
1821
1769
 
1822
- /** One failed attempt to place a durable failure artifact (path is private layout). */
1823
- type PublicationAttempt = {
1824
- readonly path: string;
1825
- readonly diagnostic: string;
1826
- readonly identity?: {
1827
- readonly name?: string;
1828
- readonly code?: string | number;
1829
- };
1830
- };
1831
-
1832
- function publicationAttemptFromError(
1833
- path: string,
1834
- error: unknown,
1835
- ): PublicationAttempt {
1836
- if (error instanceof Error) {
1837
- return {
1838
- path,
1839
- diagnostic: error.message || error.name || "write failed",
1840
- identity: thrownIdentity(error),
1841
- };
1842
- }
1843
- return { path, diagnostic: String(error) };
1844
- }
1845
-
1846
1770
  /**
1847
- * Directories eligible for open-ended unique failure-artifact placement.
1848
- * Always includes the ledger runs/ parent of the run directory so an
1849
- * unwritable run tree cannot strand the original controlled failure.
1771
+ * Replace the leg's terminal with the controlled failure. A write failure
1772
+ * propagates: the caller notes it beside the host terminal.
1850
1773
  */
1851
- function uniqueFailureFallbackDirs(
1852
- runDirectory: string,
1853
- baseDir: string,
1854
- ): string[] {
1855
- const dirs: string[] = [];
1856
- for (const dir of [baseDir, runDirectory, dirname(runDirectory)]) {
1857
- if (!dirs.includes(dir)) dirs.push(dir);
1858
- }
1859
- return dirs;
1860
- }
1861
-
1862
- /**
1863
- * Resolve a writable artifacts base directory. If `artifacts/` cannot be created
1864
- * (e.g. a file occupies that name), fall back to the run directory itself.
1865
- */
1866
- async function resolveFailureArtifactsBase(
1867
- runDirectory: string,
1868
- ): Promise<{ baseDir: string; attempt?: PublicationAttempt }> {
1869
- const artifactsDir = roleRunArtifactsDirectory(runDirectory);
1870
- try {
1871
- await ensureRunArtifactsDir(runDirectory);
1872
- return { baseDir: artifactsDir };
1873
- } catch (error) {
1874
- return {
1875
- baseDir: runDirectory,
1876
- attempt: publicationAttemptFromError(artifactsDir, error),
1877
- };
1878
- }
1879
- }
1880
-
1881
- /**
1882
- * Write JSON across preferred paths, then unique open-ended fallbacks.
1883
- * Finite fixed names must not be able to exhaust durability and strand the
1884
- * original controlled failure outside settlement.
1885
- */
1886
- async function writeFailureJsonRetainingCause(
1887
- preferredCandidates: readonly string[],
1888
- uniqueFallbackDirs: readonly string[],
1889
- stem: string,
1890
- basePayload: Readonly<Record<string, unknown>>,
1891
- priorIssues: readonly PublicationAttempt[],
1892
- ): Promise<{ path: string; issues: PublicationAttempt[] }> {
1893
- const issues: PublicationAttempt[] = [...priorIssues];
1894
- const candidates: string[] = [
1895
- ...preferredCandidates,
1896
- // One unique name per fallback dir — collisions on fixed names cannot exhaust this.
1897
- ...uniqueFallbackDirs.map((dir) => join(dir, stem === UNIQUE_ERROR_FALLBACK_STEM ? uniqueErrorFallbackName() : `${stem}.${randomUUID()}.json`)),
1898
- ];
1899
- for (let i = 0; i < candidates.length; i += 1) {
1900
- const path = candidates[i]!;
1901
- const payload =
1902
- issues.length === 0
1903
- ? basePayload
1904
- : { ...basePayload, publicationIssues: issues };
1905
- try {
1906
- await writeFile(
1907
- path,
1908
- `${JSON.stringify(payload, null, 2)}\n`,
1909
- "utf8",
1910
- );
1911
- return { path, issues };
1912
- } catch (error) {
1913
- issues.push(publicationAttemptFromError(path, error));
1914
- }
1915
- }
1916
- const last = issues.at(-1);
1917
- const error = new Error(
1918
- last?.diagnostic ?? "unable to write durable failure artifact",
1919
- ) as Error & {
1920
- code?: string | number;
1921
- publicationAttempts?: PublicationAttempt[];
1922
- };
1923
- if (last?.identity?.name !== undefined && last.identity.name !== "") {
1924
- error.name = last.identity.name;
1925
- }
1926
- if (last?.identity?.code !== undefined) {
1927
- error.code = last.identity.code;
1928
- }
1929
- error.publicationAttempts = issues;
1930
- throw error;
1931
- }
1932
-
1933
- export async function publishFailureArtifacts(
1774
+ export async function publishFailureTerminal(
1934
1775
  admitted: AdmittedRoleInvocation,
1935
1776
  failure: ControlledFailure,
1936
- authority: DurablePrincipalAuthority,
1777
+ coordinates: DurablePrincipalCoordinates,
1937
1778
  onErrorPublished?: (path: string) => void,
1938
- recordAttemptHistory = false,
1779
+ scope?: SettlementCourtScope,
1939
1780
  ): Promise<TerminalArtifactRef[]> {
1940
- const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
1941
- const { baseDir, attempt: baseAttempt } = await resolveFailureArtifactsBase(
1942
- admitted.runDirectory,
1943
- );
1944
- const priorIssues: PublicationAttempt[] =
1945
- baseAttempt === undefined ? [] : [baseAttempt];
1946
- // #419: a dispatched failure joins history before fixed-name views change.
1947
- // Re-projecting an existing failure does not append. History write failure
1948
- // rides publicationIssues rather than stranding the controlled failure.
1949
- 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) {
1950
1788
  try {
1951
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile }, {
1952
- kind: "failure", role: admitted.role, ...failure,
1953
- });
1789
+ const pointer = await appendRunAttemptHistory(
1790
+ attemptHistorySource(admitted, coordinates),
1791
+ failureOutcome,
1792
+ );
1793
+ attemptHistoryIdentity = pointer.identity;
1954
1794
  } catch (error) {
1955
- priorIssues.push(publicationAttemptFromError(sessionFile, error));
1795
+ historyFailure = error;
1796
+ historyFailed = true;
1956
1797
  }
1798
+ } else {
1799
+ attemptHistoryIdentity = await belongingAttemptHistoryIdentity(
1800
+ admitted.runDirectory,
1801
+ scope,
1802
+ );
1957
1803
  }
1958
- // #953: clear the prior success face, but a failure here must not strand
1959
- // the original controlled cause; preserve the issue beside the fallback.
1960
- try {
1961
- await clearOppositeTerminalArtifactFace(admitted.runDirectory);
1962
- } catch (error) {
1963
- priorIssues.push(publicationAttemptFromError(
1964
- roleRunArtifactsDirectory(admitted.runDirectory), error,
1965
- ));
1966
- }
1967
-
1968
- // Prefer conventional names; unique fallback dirs keep colliding fixed paths
1969
- // from stranding the original failure outside settlement. Include the ledger
1970
- // runs/ parent so a locked run directory (EACCES) cannot exhaust durability.
1971
- const underArtifacts = baseDir === roleRunArtifactsDirectory(admitted.runDirectory);
1972
- const uniqueFallbackDirs = uniqueFailureFallbackDirs(
1973
- admitted.runDirectory,
1974
- baseDir,
1975
- );
1976
- const errorCandidates = underArtifacts
1977
- ? [
1978
- join(baseDir, RUN_TERMINAL_ERROR_FILE),
1979
- join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
1980
- join(admitted.runDirectory, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
1981
- ]
1982
- : [
1983
- join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
1984
- join(baseDir, RUN_TERMINAL_ERROR_FILE),
1985
- ];
1986
- const evidenceCandidates = underArtifacts
1987
- ? [
1988
- join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
1989
- join(baseDir, "evidence.settlement.json"),
1990
- join(admitted.runDirectory, "evidence.settlement.json"),
1991
- ]
1992
- : [
1993
- join(baseDir, "evidence.settlement.json"),
1994
- join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
1995
- ];
1996
-
1997
- const errorPayloadBase: Record<string, unknown> = {
1804
+ writeRunTerminal(admitted.runDirectory, "error", {
1998
1805
  kind: "error",
1999
1806
  role: admitted.role,
2000
1807
  runId: admitted.runId,
@@ -2005,38 +1812,12 @@ export async function publishFailureArtifacts(
2005
1812
  // This package's own facts, kept beside the host's report.
2006
1813
  ...(failure.packageFact === undefined ? {} : { packageFact: failure.packageFact }),
2007
1814
  ...(failure.stderr === undefined ? {} : { stderr: failure.stderr }),
2008
- };
2009
-
2010
- const errorWrite = await writeFailureJsonRetainingCause(
2011
- errorCandidates,
2012
- uniqueFallbackDirs,
2013
- UNIQUE_ERROR_FALLBACK_STEM,
2014
- errorPayloadBase,
2015
- priorIssues,
2016
- );
2017
- onErrorPublished?.(errorWrite.path);
2018
-
2019
- const evidencePayload: Record<string, unknown> = {
2020
- runId: admitted.runId,
2021
- sessionDirectory: sessionDirectory,
2022
- sessionFile: sessionFile,
2023
- admittedRequestPath: admitted.admittedRequestPath,
2024
- attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
2025
- ...(failure.cause === undefined ? {} : { failureCause: failure.cause }),
2026
- };
2027
- const evidenceWrite = await writeFailureJsonRetainingCause(
2028
- evidenceCandidates,
2029
- uniqueFallbackDirs,
2030
- "evidence",
2031
- evidencePayload,
2032
- // Evidence records the same publication collisions observed placing the error body.
2033
- errorWrite.issues,
2034
- );
2035
-
2036
- return [
2037
- { kind: "error", path: errorWrite.path },
2038
- { kind: "evidence", path: evidenceWrite.path },
2039
- ];
1815
+ ...terminalBelongingFields(attemptHistoryIdentity, scope),
1816
+ });
1817
+ const path = runCurrentPath(admitted.runDirectory);
1818
+ onErrorPublished?.(path);
1819
+ if (historyFailed) throw historyFailure;
1820
+ return [{ kind: "error", path }];
2040
1821
  }
2041
1822
 
2042
1823
  /**
@@ -2062,13 +1843,7 @@ export async function settleFailureTerminalResult(
2062
1843
  let artifacts: TerminalArtifactRef[] = [];
2063
1844
  if (options.previewOnly !== true) {
2064
1845
  try {
2065
- artifacts = await publishFailureArtifacts(
2066
- admitted,
2067
- failure,
2068
- authority,
2069
- options.onErrorPublished,
2070
- options.recordAttemptHistory === true,
2071
- );
1846
+ artifacts = await publishFailureTerminal(admitted, failure, coordinates, options.onErrorPublished, options);
2072
1847
  } catch (error) {
2073
1848
  await noteSettlementFault(
2074
1849
  admitted.runDirectory,