@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
@@ -25,22 +25,9 @@ export const LEGACY_REVIEW_OUTPUT_ROLES = new Map<string, string>([
25
25
  ["ak_auditor_output", "auditor"],
26
26
  ]);
27
27
 
28
- /**
29
- * Success-face fields that still differ by seat. Presence and omission match
30
- * the former per-seat publishers. Key order is not part of the contract.
31
- */
32
- export type PackagedArtifactLeaf = {
33
- readonly key: string;
34
- readonly from?: string;
35
- readonly omitUndefined?: true;
36
- readonly copyArray?: true;
37
- readonly callerProvenance?: true;
38
- };
39
-
28
+ /** Success-terminal facts that still differ by seat. */
40
29
  export type PackagedArtifactFace = {
41
30
  readonly reportPhase?: true;
42
- readonly evidenceRole?: true;
43
- readonly leaves: readonly PackagedArtifactLeaf[];
44
31
  readonly doctorReportFacts?: true;
45
32
  };
46
33
 
@@ -126,13 +113,6 @@ export const PUBLIC_ROLE_RECORDS = [
126
113
  settlement: "sealed",
127
114
  artifactFace: {
128
115
  reportPhase: true,
129
- evidenceRole: true,
130
- leaves: [
131
- { key: "phase" },
132
- { key: "packetPath" },
133
- { key: "prerequisitesPath", omitUndefined: true },
134
- { key: "prerequisites" },
135
- ],
136
116
  },
137
117
  boardPlacement: "marshal",
138
118
  activationFlags: [
@@ -164,11 +144,6 @@ export const PUBLIC_ROLE_RECORDS = [
164
144
  settlement: "sealed",
165
145
  artifactFace: {
166
146
  reportPhase: true,
167
- evidenceRole: true,
168
- leaves: [
169
- { key: "phase" },
170
- { key: "taskPath" },
171
- ],
172
147
  },
173
148
  activationFlags: [
174
149
  { field: "taskPath", flag: "ak-coder-task", binds: "input" },
@@ -203,13 +178,6 @@ export const PUBLIC_ROLE_RECORDS = [
203
178
  /** Publish only an accepted ledger outcome; audit escalation stays unsettled here. */
204
179
  sealedAcceptedOnly: true,
205
180
  artifactFace: {
206
- evidenceRole: true,
207
- leaves: [
208
- { key: "baseRevision" },
209
- { key: "lens" },
210
- { key: "authorityRefs", copyArray: true },
211
- { key: "callerProvenance", callerProvenance: true },
212
- ],
213
181
  },
214
182
  /** Frozen base/lens/authority become the initial prompt; instruction follows. */
215
183
  transportPrompt: "skill-args",
@@ -242,12 +210,6 @@ export const PUBLIC_ROLE_RECORDS = [
242
210
  outputTool: COLLECTOR_OUTPUT_TOOL,
243
211
  settlement: "sealed",
244
212
  artifactFace: {
245
- evidenceRole: true,
246
- leaves: [
247
- { key: "prNumber", omitUndefined: true },
248
- { key: "repository", from: "repository.canonical" },
249
- { key: "manifestDigest" },
250
- ],
251
213
  },
252
214
  boardPlacement: "collector",
253
215
  /** Taishi acceptance is a typed groups array, not a status leaf. */
@@ -277,12 +239,6 @@ export const PUBLIC_ROLE_RECORDS = [
277
239
  auditTool: DOCTOR_AUDIT_TOOL_NAME,
278
240
  settlement: "sealed",
279
241
  artifactFace: {
280
- evidenceRole: true,
281
- leaves: [
282
- { key: "issueNumber" },
283
- { key: "caseRunsPath" },
284
- { key: "caseIdentity" },
285
- ],
286
242
  doctorReportFacts: true,
287
243
  },
288
244
  /** Navigator subject is the doctor case, not the case-path file bytes. */
@@ -309,11 +265,6 @@ export const PUBLIC_ROLE_RECORDS = [
309
265
  outputTool: MERGER_OUTPUT_TOOL_NAME,
310
266
  settlement: "sealed",
311
267
  artifactFace: {
312
- evidenceRole: true,
313
- leaves: [
314
- { key: "mergerInputPath" },
315
- { key: "derived" },
316
- ],
317
268
  },
318
269
  activationFlags: [
319
270
  { field: "inputPath", from: "mergerInputPath", flag: "ak-merger-input", binds: "input" },
@@ -334,8 +285,6 @@ export const PUBLIC_ROLE_RECORDS = [
334
285
  sourceRunStored: "raw",
335
286
  argvResult: "source-run",
336
287
  sameParent: "source-locator",
337
- /** Infrastructure failure at this stage has no accepted gate cycle to project. */
338
- skipGateOnInfrastructureStage: true,
339
288
  /** 符宝郎 is a review officer and inherits the gatekeeper model. */
340
289
  reviewOfficer: true,
341
290
  provinceConfig: true,
@@ -443,8 +392,6 @@ export const PUBLIC_ROLE_RECORDS = [
443
392
  sameParent: "gate-pointer",
444
393
  /** Resume restores an optional source-run path onto the admitted face. */
445
394
  resumeSourcePath: true,
446
- /** Infrastructure failure at this stage has no accepted gate cycle to project. */
447
- skipGateOnInfrastructureStage: true,
448
395
  /** 台院 is a review officer and inherits the gatekeeper model. */
449
396
  reviewOfficer: true,
450
397
  provinceConfig: true,
@@ -476,8 +423,6 @@ export const PUBLIC_ROLE_RECORDS = [
476
423
  presentSettled: "always",
477
424
  summonResume: true,
478
425
  admission: "instruction",
479
- /** Infrastructure failure at this stage has no accepted gate cycle to project. */
480
- skipGateOnInfrastructureStage: true,
481
426
  sameParent: "none",
482
427
  /** Province model root. Officers name this seat via modelInheritsFrom. */
483
428
  provinceConfig: true,
@@ -725,15 +670,6 @@ export function packagedAuditToolName(role: string): string | undefined {
725
670
  return record.auditTool;
726
671
  }
727
672
 
728
- /** Infrastructure-failure stage that must not be re-read as an accepted gate cycle. */
729
- export function packagedSkipsGateOnInfrastructureStage(stage: unknown): boolean {
730
- return typeof stage === "string" && PUBLIC_ROLE_RECORDS.some((record) =>
731
- "skipGateOnInfrastructureStage" in record
732
- && record.skipGateOnInfrastructureStage === true
733
- && record.role === stage
734
- );
735
- }
736
-
737
673
  /** Durable officer entry whose name is the seat role. */
738
674
  export function packagedDurableOfficerEntry(officer: unknown): boolean {
739
675
  return typeof officer === "string" && PUBLIC_ROLE_RECORDS.some((record) =>
package/src/pi/adapter.ts CHANGED
@@ -18,6 +18,10 @@ import type {
18
18
  import { createOAuthKeepalive, type OAuthKeepaliveOptions } from "../oauth-keepalive.ts";
19
19
  import { createRoleRuntimeExtension, type RoleRuntimeDependencies } from "../role-runtime.ts";
20
20
  import { renderAgentStartMaterials } from "../agent-start-materials.ts";
21
+ import { runDirectoryFromHostContext } from "../host-contracts.ts";
22
+ import { sitianReportSafe } from "../host-session-record.ts";
23
+ import { isTerminatingToolName } from "../package-contracts/terminating-tools.ts";
24
+ import { sessionFileOf } from "../role-run-placement.ts";
21
25
  import { projectCorrectableExecuteRejection } from "../submission-correctable-error.ts";
22
26
  import {
23
27
  WorkerCommitReminderError,
@@ -148,6 +152,12 @@ export function createPiRoleHostAdapter(
148
152
  options: { transcriptFromContext?: (context: ExtensionContext) => string; oauthKeepalive?: OAuthKeepaliveOptions } = {},
149
153
  ): PiRoleHostAdapter {
150
154
  const keepalive = createOAuthKeepalive(options.oauthKeepalive);
155
+ // Pi chains the before_agent_start handlers, each seeing the prompt the one before left; the
156
+ // prompt Pi sends is the one the last registered handler leaves. Only that handler's result is
157
+ // what a start was given, so only it records.
158
+ let beforeAgentStartHandlers = 0;
159
+ // Parameters of registered terminating tools — the schema Pi was handed.
160
+ const registeredToolParameters = new Map<string, unknown>();
151
161
  // Decode transport exactly once before Pi's native handler chain. Pi retains
152
162
  // its own transform/image propagation and per-handler error isolation.
153
163
  pi.on("input", (value) => {
@@ -160,10 +170,13 @@ export function createPiRoleHostAdapter(
160
170
  },
161
171
  registerFlag: (name, definition) => pi.registerFlag(name, definition),
162
172
  getFlag: (name) => pi.getFlag(name),
163
- registerTool: (tool) => pi.registerTool(toPiToolDefinition(
164
- tool,
165
- (context) => projectPiContext(context, options.transcriptFromContext),
166
- )),
173
+ registerTool: (tool) => {
174
+ registeredToolParameters.set(tool.name, tool.parameters);
175
+ return pi.registerTool(toPiToolDefinition(
176
+ tool,
177
+ (context) => projectPiContext(context, options.transcriptFromContext),
178
+ ));
179
+ },
167
180
  getAllTools: () => pi.getAllTools().map(({ name, sourceInfo }) => ({
168
181
  name,
169
182
  ...(sourceInfo?.path === undefined ? {} : { sourceInfo: { path: sourceInfo.path } }),
@@ -174,10 +187,40 @@ export function createPiRoleHostAdapter(
174
187
  const context = (value: ExtensionContext) => projectPiContext(value, options.transcriptFromContext);
175
188
  if (registration[0] === "before_agent_start") {
176
189
  const [, handler] = registration;
190
+ const position = beforeAgentStartHandlers;
191
+ beforeAgentStartHandlers += 1;
177
192
  pi.on("before_agent_start", (value, ctx) => {
178
- const result = handler({ prompt: value.prompt, systemPrompt: value.systemPrompt, systemPromptOptions: value.systemPromptOptions }, context(ctx));
179
- if (result instanceof Promise) return result.then((settled) => foldBeforeAgentStartReturn(settled, value.systemPrompt));
180
- return foldBeforeAgentStartReturn(result, value.systemPrompt);
193
+ const projected = context(ctx);
194
+ const fold = (settled: unknown) => {
195
+ const folded = foldBeforeAgentStartReturn(settled, value.systemPrompt);
196
+ const runDirectory = runDirectoryFromHostContext(projected);
197
+ if (runDirectory !== undefined && position === beforeAgentStartHandlers - 1) {
198
+ const delivered = (folded as { systemPrompt?: string } | undefined)?.systemPrompt ?? value.systemPrompt;
199
+ // Record the same parameters object Pi was handed — not a
200
+ // headless draft-07 projection of another host's transport.
201
+ let outputSchema: unknown;
202
+ for (const [name, parameters] of registeredToolParameters) {
203
+ if (!isTerminatingToolName(name)) continue;
204
+ outputSchema = parameters;
205
+ break;
206
+ }
207
+ // History write failures do not stop a run (#833): declared, and Pi starts.
208
+ sitianReportSafe({
209
+ level: "event",
210
+ kind: "turn-delivery",
211
+ sessionParent: sessionFileOf(runDirectory),
212
+ source: "pi-adapter",
213
+ payload: {
214
+ systemPrompt: delivered,
215
+ ...(outputSchema === undefined ? {} : { outputSchema }),
216
+ },
217
+ });
218
+ }
219
+ return folded;
220
+ };
221
+ const result = handler({ prompt: value.prompt, systemPrompt: value.systemPrompt, systemPromptOptions: value.systemPromptOptions }, projected);
222
+ if (result instanceof Promise) return result.then(fold);
223
+ return fold(result);
181
224
  });
182
225
  } else if (registration[0] === "input") {
183
226
  const [, handler] = registration;
@@ -1,5 +1,3 @@
1
- import { join } from "node:path";
2
-
3
1
  import type {
4
2
  DurablePrincipal,
5
3
  DurablePrincipalAuthority,
@@ -8,7 +6,7 @@ import type {
8
6
  } from "../host-contracts.ts";
9
7
  import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
10
8
  import { resolveActivationLedgerHome } from "../activation-ledger-topology.ts";
11
- import { roleRunPlacement } from "../role-run-placement.ts";
9
+ import { roleRunPlacement, sessionFileIn } from "../role-run-placement.ts";
12
10
 
13
11
  type PiDurablePrincipal = DurablePrincipal & {
14
12
  readonly sessionDirectory: string;
@@ -69,7 +67,7 @@ export const piDurablePrincipalAuthority: DurablePrincipalAuthority = {
69
67
  sessionFile:
70
68
  typeof record.sessionFile === "string" && record.sessionFile.trim() !== ""
71
69
  ? record.sessionFile
72
- : join(record.sessionDirectory, "session.jsonl"),
70
+ : sessionFileIn(record.sessionDirectory),
73
71
  };
74
72
  },
75
73
  };
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { execFile, spawn } from "node:child_process";
7
7
  import { constants } from "node:fs";
8
- import { access, appendFile, readFile, realpath } from "node:fs/promises";
8
+ import { access, appendFile, realpath } from "node:fs/promises";
9
9
  import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
10
10
  import { platform } from "node:process";
11
11
  import { promisify } from "node:util";
@@ -25,9 +25,13 @@ import { ExplicitInternalActivationError } from "../host-contracts.ts";
25
25
  import { applyEngineChildEnv, ENGINE_MODEL_FLAG_NAME, normalizeEngineName } from "../engine-detour.ts";
26
26
  import { projectActivationFlags } from "../role-activation-flags.ts";
27
27
  import { encodeUserDialogueStdin } from "../user-dialogue-stdin.ts";
28
+ import { readLedgerSessionJsonlLines, readStrictPiSessionJsonl } from "../ledger-session-read.ts";
29
+ import { copyAndRecordHostDossier } from "../host-session-record.ts";
28
30
  import { projectThrownFailureLeaf, retainPackageFault } from "../public-cli/settlement.ts";
29
31
  import { describeErrorIdentity } from "../public-cli/run-lifecycle.ts";
30
32
  import { RECEIPT_DELIVERY_LIMIT_ENV } from "../receipt-delivery-policy.ts";
33
+ import { createSessionIdentityAuthority } from "../session-identity.ts";
34
+ import { sitianReport } from "../sitian-facade.ts";
31
35
 
32
36
  /** Package-relative Internal role entrypoint (ADR 0052; same path as public-cli registry). */
33
37
  const INTERNAL_ROLE_ENTRYPOINT_RELATIVE = "extensions/role-runtime.ts";
@@ -449,22 +453,96 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
449
453
  );
450
454
  }
451
455
  const timeoutMs = request.timeoutMs ?? config.timeoutMs;
452
- return await spawnRunner(args, {
456
+ const result = await spawnRunner(args, {
453
457
  cwd: request.cwd,
454
458
  env,
455
459
  stdin,
456
460
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
457
461
  ...(request.signal === undefined ? {} : { signal: request.signal }),
458
462
  });
463
+ // Deliver already-written Pi host facts. Pi lands at session/session.jsonl;
464
+ // bind projects host.sessions; copy records the landing. Host adapters do
465
+ // not write current.json — public bind/settlement seams own that render.
466
+ try {
467
+ const { sessionFile, sessionDirectory } = config.principalAuthority.decode(request.principal);
468
+ const sessionId = await readPiSessionHeaderId(sessionFile);
469
+ if (sessionId !== undefined) {
470
+ await createSessionIdentityAuthority(config.principalAuthority, "pi").bind(
471
+ request.principal,
472
+ sessionId,
473
+ );
474
+ copyAndRecordHostDossier({
475
+ host: "pi",
476
+ sessionId,
477
+ cwd: request.cwd,
478
+ sessionDirectory,
479
+ sessionParent: sessionFile,
480
+ ...(request.home !== undefined ? { home: request.home } : {}),
481
+ });
482
+ }
483
+ } catch (error) {
484
+ await retainPackageFault({
485
+ runDirectory: request.runDirectory,
486
+ diagnostic: `pi host dossier record failed beside host terminal: ${describeErrorIdentity(error)}`,
487
+ error,
488
+ });
489
+ }
490
+ return result;
459
491
  },
460
492
  };
461
493
  }
462
494
 
463
- import { sitianReport } from "../sitian-facade.ts";
495
+ /**
496
+ * Session header id from the Pi volume the child (or fixture) already wrote.
497
+ * Reuses the ledger line kernel: a later malformed line must not erase a valid
498
+ * header already present (ADR 0086 originals are not validated whole-file).
499
+ */
500
+ async function readPiSessionHeaderId(sessionFile: string): Promise<string | undefined> {
501
+ try {
502
+ for (const line of await readLedgerSessionJsonlLines(sessionFile)) {
503
+ const entry = line.row;
504
+ if (entry === undefined || entry.type !== "session") continue;
505
+ if (typeof entry.id === "string" && entry.id.trim() !== "") return entry.id;
506
+ }
507
+ } catch (error) {
508
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
509
+ throw error;
510
+ }
511
+ return undefined;
512
+ }
513
+
514
+ /** Last non-session entry id. Session headers are not parents of custom lines. */
515
+ export function piSessionCustomParentId(
516
+ entries: readonly { id?: unknown; type?: unknown }[],
517
+ ): string | null {
518
+ let parentId: string | null = null;
519
+ for (const entry of entries) {
520
+ if (typeof entry.id === "string" && entry.type !== "session") parentId = entry.id;
521
+ }
522
+ return parentId;
523
+ }
524
+
525
+ /** One Pi custom JSONL line: type, customType, data, id, parentId, timestamp. */
526
+ export function formatPiSessionCustomEntry(input: {
527
+ readonly customType: string;
528
+ readonly data: unknown;
529
+ readonly parentId: string | null;
530
+ readonly timestamp: string;
531
+ }): string {
532
+ return `${JSON.stringify({
533
+ type: "custom",
534
+ customType: input.customType,
535
+ data: input.data,
536
+ id: randomUUID(),
537
+ parentId: input.parentId,
538
+ timestamp: input.timestamp,
539
+ })}\n`;
540
+ }
464
541
 
465
542
  /**
466
543
  * Append one custom JSONL entry to the durable principal's session file.
467
544
  * Pi session codec only — AK artifact O_EXCL retention stays in public-cli.
545
+ * Read is raw JSON.parse (SyntaxError propagates). Sitian full-traversal stays elsewhere.
468
546
  */
469
547
  export async function appendPiSessionCustomEntry(
470
548
  authority: DurablePrincipalAuthority,
@@ -473,21 +551,14 @@ export async function appendPiSessionCustomEntry(
473
551
  data: unknown,
474
552
  ): Promise<void> {
475
553
  const { sessionFile } = authority.decode(principal);
476
- const text = await readFile(sessionFile, "utf8");
477
- let parentId: string | null = null;
478
- for (const line of text.trim().split("\n").filter(Boolean)) {
479
- const entry = JSON.parse(line) as { id?: unknown; type?: unknown };
480
- if (typeof entry.id === "string" && entry.type !== "session") parentId = entry.id;
481
- }
554
+ const entries = await readStrictPiSessionJsonl(sessionFile) as { id?: unknown; type?: unknown }[];
482
555
  const timestamp = new Date().toISOString();
483
- const pointerLine = `${JSON.stringify({
484
- type: "custom",
556
+ const pointerLine = formatPiSessionCustomEntry({
485
557
  customType,
486
558
  data,
487
- id: randomUUID(),
488
- parentId,
559
+ parentId: piSessionCustomParentId(entries),
489
560
  timestamp,
490
- })}\n`;
561
+ });
491
562
  await appendFile(sessionFile, pointerLine, "utf8");
492
563
  sitianReport({
493
564
  level: "event",
@@ -11,10 +11,6 @@
11
11
  * path continues: same budget, same call-local count semantics. No failure-type
12
12
  * classification — every thrown value is treated identically.
13
13
  */
14
- import { constants as fsConstants } from "node:fs";
15
- import { randomUUID } from "node:crypto";
16
- import { open } from "node:fs/promises";
17
- import { join } from "node:path";
18
14
  import type {
19
15
  DurablePrincipal,
20
16
  DurablePrincipalAuthority,
@@ -33,14 +29,13 @@ import { processCancelSignalName } from "./process-cancel.ts";
33
29
  import { isLawfulTypedTerminalOutcome, formatTerminalResult, type TerminalArtifactRef, type TerminalResult, type TerminalRoleName } from "./terminal.ts";
34
30
  import {
35
31
  attachRecordedSubmissions,
36
- ensureRealArtifactsDirectory,
37
32
  presentFailureTerminal,
38
33
  retainPackageFault,
39
34
  } from "./settlement.ts";
40
35
 
41
- export { ensureRealArtifactsDirectory };
42
36
  import type { CliIo } from "./cli-io.ts";
43
37
  import { serializeThrownValue } from "../serialize-thrown-value.ts";
38
+ import { reportRunRecord } from "../sitian-facade.ts";
44
39
 
45
40
  import { errorText } from "../unknown-value.ts";
46
41
 
@@ -155,41 +150,11 @@ function jsonSafeReplacer(): (key: string, value: unknown) => unknown {
155
150
  }
156
151
 
157
152
  /**
158
- * Retain one throwing dispatch attempt's complete exception as an independent
159
- * per-attempt file under the run's artifacts directory, then leave an
160
- * addressable pointer in the session principal (custom entry). Exclusive-create
161
- * open (O_EXCL) with a per-attempt unique name enforces 史必追加 (#419): a later
162
- * attempt can never overwrite an earlier attempt's file.
153
+ * Retain one throwing dispatch attempt's complete exception as a line in the
154
+ * run's log.jsonl, then leave an addressable pointer in the session principal
155
+ * (custom entry). The log only appends (史必追加, #419): a later attempt can
156
+ * never overwrite an earlier attempt's record.
163
157
  */
164
- /**
165
- * Hardened create-once JSON write shared by every durable artifact this loop
166
- * retains directly (dispatch-error dumps, lawful-persist-failure error/
167
- * evidence records): O_EXCL (fail loud on a colliding name, never overwrite)
168
- * + O_NOFOLLOW where the platform provides it (a planted symlink is never
169
- * followed) — mirrors settlement.ts's own hardened artifact writers.
170
- */
171
- export async function writeHardenedArtifactFile(
172
- artifactsDir: string,
173
- namePrefix: string,
174
- payload: Record<string, unknown>,
175
- ): Promise<string> {
176
- const filePath = join(artifactsDir, `${namePrefix}-${randomUUID()}.json`);
177
- const body = `${JSON.stringify(payload, jsonSafeReplacer(), 2)}\n`;
178
- const noFollowFlag =
179
- typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
180
- const handle = await open(
181
- filePath,
182
- fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollowFlag,
183
- 0o600,
184
- );
185
- try {
186
- await handle.writeFile(body, "utf8");
187
- } finally {
188
- await handle.close();
189
- }
190
- return filePath;
191
- }
192
-
193
158
  async function retainDispatchError(
194
159
  admitted: { runDirectory: string; principal: DurablePrincipal },
195
160
  principalAuthority: DurablePrincipalAuthority,
@@ -197,14 +162,13 @@ async function retainDispatchError(
197
162
  attempt: number,
198
163
  error: unknown,
199
164
  ): Promise<{ file: string; pointerError?: unknown }> {
200
- const artifactsDir = await ensureRealArtifactsDirectory(admitted.runDirectory);
201
165
  // Whole-object dump: everything the thrown value carries, nothing picked.
202
- const filePath = await writeHardenedArtifactFile(artifactsDir, `dispatch-error-attempt-${attempt}`, {
166
+ const filePath = reportRunRecord(admitted.runDirectory, "dispatch-exception", JSON.parse(JSON.stringify({
203
167
  version: 1,
204
168
  attempt,
205
169
  recordedAt: new Date().toISOString(),
206
170
  error: serializeThrownValue(error),
207
- });
171
+ }, jsonSafeReplacer())) as Record<string, unknown>, "auto-resume").recordFile;
208
172
  // Addressable pointer in the dossier (卷宗): Pi session custom-entry codec
209
173
  // (appendPiSessionCustomEntry). Lease still owned here with run-writer.
210
174
  let pointerLease: RunWriterLease;
@@ -56,6 +56,7 @@ import {
56
56
  type RunDirectoryRelocation,
57
57
  } from "./invocation.ts";
58
58
  import {
59
+ bindDiaristAssertedTicketNumber,
59
60
  presentControlledFailure,
60
61
  packageFaultNoteFor,
61
62
  prepareSummonsResumeMaterials,
@@ -83,7 +84,7 @@ import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.ts";
83
84
  import {
84
85
  presentStructuralRejection,
85
86
  readBoundSessionEntries,
86
- attachPostAuditProjection,
87
+ attachPostAuditCountersignFact,
87
88
  trySettlePublicSeat,
88
89
  type SettlementCourtScope,
89
90
  } from "./settlement.ts";
@@ -321,6 +322,8 @@ async function bindAndRelocateDiarist(
321
322
  authority: DurablePrincipalAuthority,
322
323
  lease?: RunWriterLease,
323
324
  ): Promise<RunDirectoryRelocation | undefined> {
325
+ // The diarist's asserted ticketNumber binds here, in the public call's process.
326
+ await bindDiaristAssertedTicketNumber(admitted);
324
327
  const boardTicket = await readBoardTicketNumber(admitted.runDirectory);
325
328
  if (boardTicket === undefined) return undefined;
326
329
  if (admitted.ticketNumber === undefined) {
@@ -1096,7 +1099,11 @@ export async function continueParentAfterChild(
1096
1099
  && heldUnreadableTerminal(stopped.terminal)
1097
1100
  && stopped.terminal !== undefined
1098
1101
  ) {
1099
- const parentTerminal = await trySettlePublicSeat(admitted, env.principalAuthority, undefined);
1102
+ const parentTerminal = await trySettlePublicSeat(
1103
+ admitted,
1104
+ env.principalAuthority,
1105
+ { ...await readCurrentCourt(admitted.runDirectory), ...packageFaultScope(admitted, env, io) },
1106
+ );
1100
1107
  if (
1101
1108
  parentTerminal?.roleOutcome.kind === "accepted"
1102
1109
  || parentTerminal?.roleOutcome.kind === "audit_escalation"
@@ -1115,7 +1122,7 @@ export async function continueParentAfterChild(
1115
1122
  const terminal = await trySettlePublicSeat(
1116
1123
  admitted,
1117
1124
  env.principalAuthority,
1118
- packageFaultScope(admitted, env, io),
1125
+ { ...await readCurrentCourt(admitted.runDirectory), ...packageFaultScope(admitted, env, io) },
1119
1126
  );
1120
1127
  if (terminal?.roleOutcome.kind !== "accepted") throw new Error("pending submission is not recorded");
1121
1128
  return auditSubmittedRole({ exitCode: 0, admitted, terminal }, env, io, resolved.admitted, latestQueuePayload(resolved.terminal));
@@ -1399,10 +1406,10 @@ async function auditSubmittedRole(
1399
1406
  }
1400
1407
  }
1401
1408
  // The turn already settled this court (payloads, usage, autoResumeCount, history).
1402
- // Audit only adds gate rounds and, for 中书省, the officer fact. A second
1403
- // settle would republish the same attempt.
1409
+ // Audit only adds, for 中书省, the officer fact. A second settle would
1410
+ // republish the same attempt.
1404
1411
  if (turn.terminal === undefined) throw new Error(`audited ${admitted.role} submission did not settle`);
1405
- const terminal = await attachPostAuditProjection(
1412
+ const terminal = await attachPostAuditCountersignFact(
1406
1413
  admitted,
1407
1414
  env.principalAuthority,
1408
1415
  turn.terminal,