@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
@@ -25,10 +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 { readStrictPiSessionJsonl } from "../ledger-session-read.ts";
28
+ import { readLedgerSessionJsonlLines, readStrictPiSessionJsonl } from "../ledger-session-read.ts";
29
+ import { copyAndRecordHostDossier } from "../host-session-record.ts";
29
30
  import { projectThrownFailureLeaf, retainPackageFault } from "../public-cli/settlement.ts";
30
31
  import { describeErrorIdentity } from "../public-cli/run-lifecycle.ts";
31
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";
32
35
 
33
36
  /** Package-relative Internal role entrypoint (ADR 0052; same path as public-cli registry). */
34
37
  const INTERNAL_ROLE_ENTRYPOINT_RELATIVE = "extensions/role-runtime.ts";
@@ -450,18 +453,63 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
450
453
  );
451
454
  }
452
455
  const timeoutMs = request.timeoutMs ?? config.timeoutMs;
453
- return await spawnRunner(args, {
456
+ const result = await spawnRunner(args, {
454
457
  cwd: request.cwd,
455
458
  env,
456
459
  stdin,
457
460
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
458
461
  ...(request.signal === undefined ? {} : { signal: request.signal }),
459
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;
460
491
  },
461
492
  };
462
493
  }
463
494
 
464
- 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
+ }
465
513
 
466
514
  /** Last non-session entry id. Session headers are not parents of custom lines. */
467
515
  export function piSessionCustomParentId(
@@ -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,
@@ -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));
@@ -30,7 +30,6 @@ import {
30
30
  formatRunLeaf,
31
31
  isUnboundRunDirectory,
32
32
  listBookRunDirectories,
33
- roleRunArtifactsDirectory,
34
33
  roleRunPlacement,
35
34
  type RoleRunSubject,
36
35
  } from "../role-run-placement.ts";
@@ -60,6 +59,7 @@ import {
60
59
  rewriteRunDirectoryPathFields,
61
60
  rewriteRunDirectoryPathValue,
62
61
  } from "../role-run-relocation.ts";
62
+ import { readPageSync, renderCurrentSync, updateSectionSync, writeSectionSync } from "../run-dossier.ts";
63
63
  import {
64
64
  loadDoctorCase,
65
65
  } from "../doctor-evidence.ts";
@@ -137,7 +137,6 @@ export type AdmittedRoleInvocationBase = {
137
137
  readonly runDirectory: string;
138
138
  /** Host-issued opaque durable principal (coordinates only via authority.decode). */
139
139
  readonly principal: DurablePrincipal;
140
- readonly admittedRequestPath: string;
141
140
  /**
142
141
  * Optional opaque invocation correlation restored from a prior admitted page
143
142
  * (ADR 0049 host channel). Admission does not mint ticket-binding ids.
@@ -318,7 +317,6 @@ export type AdmissionPlacement = {
318
317
  readonly sessionDirectory: string;
319
318
  readonly sessionFile: string;
320
319
  readonly runDirectory: string;
321
- readonly artifactsDirectory: string;
322
320
  readonly attachmentsDirectory: string;
323
321
  readonly ledgerHome: string;
324
322
  readonly bookKey: string;
@@ -356,25 +354,20 @@ export function issueAdmissionPlacement(
356
354
  }
357
355
 
358
356
  /**
359
- * Unique admitted-request.json persistence projection: top-level sessionDirectory/sessionFile
357
+ * Unique `admitted` section projection of current.json: top-level sessionDirectory/sessionFile
360
358
  * (base wire shape). Memory Admitted keeps only the opaque principal — never dual-carry.
361
359
  */
362
- async function writeAdmittedRequestPersistence(
363
- admittedRequestPath: string,
360
+ function writeAdmittedRequestPersistence(
361
+ runDirectory: string,
364
362
  body: Record<string, unknown>,
365
363
  coordinates: { readonly sessionDirectory: string; readonly sessionFile: string },
366
- ): Promise<void> {
364
+ ): void {
367
365
  const { principal: _omitPrincipal, ...rest } = body;
368
- const projection = {
366
+ writeSectionSync(runDirectory, "admitted", {
369
367
  ...rest,
370
368
  sessionDirectory: coordinates.sessionDirectory,
371
369
  sessionFile: coordinates.sessionFile,
372
- };
373
- await writeFile(
374
- admittedRequestPath,
375
- `${JSON.stringify(projection, null, 2)}\n`,
376
- "utf8",
377
- );
370
+ });
378
371
  }
379
372
 
380
373
  /**
@@ -403,7 +396,7 @@ function effectiveModelLedgerFields(
403
396
  export { homeFromRunDirectory };
404
397
 
405
398
  /**
406
- * Persist one `invocation.json` identity page for the public run.
399
+ * Persist the `invocation` identity section of current.json for the public run.
407
400
  * Admission is the sole source for every field; this is the only identity
408
401
  * projection and callers never provide an independent ledger shape.
409
402
  * When an effective model is known at admission, provider/model (and thinking
@@ -426,11 +419,7 @@ async function writeRoleInvocationLedger(
426
419
  ...(source.ticketNumber === undefined ? {} : { ticketNumber: source.ticketNumber }),
427
420
  ...effectiveModelLedgerFields(effectiveModel),
428
421
  };
429
- await writeFile(
430
- join(source.runDirectory, "invocation.json"),
431
- `${JSON.stringify(identity, null, 2)}\n`,
432
- "utf8",
433
- );
422
+ writeSectionSync(source.runDirectory, "invocation", identity);
434
423
  }
435
424
 
436
425
  /**
@@ -450,7 +439,7 @@ export async function recordEffectiveInvocationModel(
450
439
  host?: string,
451
440
  engineModel?: string | null,
452
441
  ): Promise<void> {
453
- await updateDurableJsonPage(join(runDirectory, "invocation.json"), (current) => {
442
+ updateSectionSync(runDirectory, "invocation", (current) => {
454
443
  const next: Record<string, unknown> = { ...current };
455
444
  if (model !== undefined) {
456
445
  next.provider = model.provider;
@@ -478,33 +467,19 @@ export async function recordEffectiveInvocationModel(
478
467
  });
479
468
  }
480
469
 
481
- /**
482
- * Read one durable JSON object, let the caller keep its field rules, and write
483
- * the page back. `undefined` means the caller decided not to change the page.
484
- */
485
- async function updateDurableJsonPage(
486
- path: string,
487
- update: (current: Record<string, unknown>) => Record<string, unknown> | undefined,
488
- ): Promise<void> {
489
- const current = JSON.parse(await readFile(path, "utf8")) as Record<string, unknown>;
490
- const next = update(current);
491
- if (next === undefined) return;
492
- await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
493
- }
494
-
495
- /** Merge observed launch-time fields into the single existing invocation.json identity page. */
470
+ /** Merge observed launch-time fields into the single existing invocation section of current.json. */
496
471
  async function mergeInvocationIdentityPage(
497
472
  runDirectory: string,
498
473
  fields: Record<string, unknown>,
499
474
  ): Promise<void> {
500
- await updateDurableJsonPage(join(runDirectory, "invocation.json"), (current) => ({
475
+ updateSectionSync(runDirectory, "invocation", (current) => ({
501
476
  ...current,
502
477
  ...fields,
503
478
  }));
504
479
  }
505
480
 
506
481
  /**
507
- * Persist parent --source-run path onto admitted-request for officer resume lookup (#747).
482
+ * Persist parent --source-run path onto the admitted section for officer resume lookup (#747).
508
483
  * Reuses the existing notary sourceRunPath key; does not invent a new field name.
509
484
  */
510
485
  export async function persistAdmittedSourceRunPath(
@@ -515,7 +490,7 @@ export async function persistAdmittedSourceRunPath(
515
490
  if (sourceRunPath.trim() === "") {
516
491
  throw new Error("persistAdmittedSourceRunPath requires a non-empty sourceRunPath");
517
492
  }
518
- await updateDurableJsonPage(admitted.admittedRequestPath, (current) => {
493
+ updateSectionSync(admitted.runDirectory, "admitted", (current) => {
519
494
  if (typeof current.sourceRunPath === "string" && current.sourceRunPath !== sourceRunPath) {
520
495
  throw new Error(
521
496
  `persistAdmittedSourceRunPath refuses to replace ${current.sourceRunPath} with ${sourceRunPath}`,
@@ -533,7 +508,7 @@ export async function persistAdmittedSourceRunPath(
533
508
 
534
509
  /**
535
510
  * Bind a post-admission resolved ticketNumber onto the in-memory admitted
536
- * object and both durable pages (invocation.json + admitted-request.json).
511
+ * object and both durable sections (invocation + admitted).
537
512
  * A later typed role receipt replaces an earlier assertion (#1025).
538
513
  */
539
514
  export async function bindAdmittedTicketNumber(
@@ -551,7 +526,7 @@ export async function recordAdmittedCorrelation(
551
526
  correlationId: string,
552
527
  ): Promise<void> {
553
528
  let correlationIds: string[] = [];
554
- await updateDurableJsonPage(admitted.admittedRequestPath, (current) => {
529
+ updateSectionSync(admitted.runDirectory, "admitted", (current) => {
555
530
  const prior = [
556
531
  ...(Array.isArray(current.correlationIds)
557
532
  ? current.correlationIds.filter((value): value is string =>
@@ -598,8 +573,7 @@ export async function relocateAdmittedRunToTicket(
598
573
  const principal = authority.seal(target);
599
574
 
600
575
  if (admitted.role !== "diarist") {
601
- const parentPage = JSON.parse(await readFile(admitted.admittedRequestPath, "utf8")) as Record<string, unknown>;
602
- const childRunIds = parentPage.childDiaristRunIds;
576
+ const childRunIds = readPageSync(oldRunDirectory, "admitted")?.childDiaristRunIds;
603
577
  for (const childRunId of Array.isArray(childRunIds) ? childRunIds : []) {
604
578
  if (typeof childRunId !== "string") continue;
605
579
  const childDirectory = join(dirname(oldRunDirectory), formatRunLeaf(childRunId, "diarist"));
@@ -616,6 +590,16 @@ export async function relocateAdmittedRunToTicket(
616
590
  await rehomeUnboundTicketProvenance(childDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
617
591
  ensureRoleRunDirectory(ledgerHome, dirname(childTarget.runDirectory));
618
592
  await rename(childDirectory, childTarget.runDirectory);
593
+ // Finished child legs will not settle again; refresh derived host.original
594
+ // after the rename commit. A derived render fault must not undo or block
595
+ // the committed placement (#1161 L1) — note and keep the true cause.
596
+ try {
597
+ renderCurrentSync(childTarget.runDirectory);
598
+ } catch (error) {
599
+ process.stderr.write(
600
+ `[invocation] current.json render refused after child relocate; placement kept: ${childTarget.runDirectory}: ${errorText(error)}\n`,
601
+ );
602
+ }
619
603
  }
620
604
  }
621
605
 
@@ -629,8 +613,9 @@ export async function relocateAdmittedRunToTicket(
629
613
  // Persisted paths are resolved from typed run identity on read.
630
614
  await rename(oldRunDirectory, target.runDirectory);
631
615
 
632
- // rename moved the open lock inode with the directory. Transfer cleanup
633
- // ownership immediately after the commit.
616
+ // rename moved the open lock inode with the directory. Transfer cleanup and
617
+ // admitted path ownership immediately after the commit — before any derived
618
+ // render that may refuse (#1161 L1 / BASE order).
634
619
  heldLease?.relocate(target.runDirectory);
635
620
 
636
621
  const admittedRecord = admitted as unknown as Record<string, unknown>;
@@ -638,7 +623,6 @@ export async function relocateAdmittedRunToTicket(
638
623
  admittedRecord,
639
624
  [
640
625
  "runDirectory",
641
- "admittedRequestPath",
642
626
  "taskPath",
643
627
  "packetPath",
644
628
  "prerequisitesPath",
@@ -657,6 +641,11 @@ export async function relocateAdmittedRunToTicket(
657
641
  }
658
642
  (admitted as { principal: DurablePrincipal }).principal = principal;
659
643
 
644
+ // current.json projects host.original from this run directory. Ownership is
645
+ // already transferred; a refuse still carries the true cause to the caller
646
+ // (#1161 L1) without leaving the leg under the unbound path.
647
+ renderCurrentSync(target.runDirectory);
648
+
660
649
  return { oldRunDirectory, newRunDirectory: target.runDirectory };
661
650
  }
662
651
 
@@ -665,7 +654,7 @@ export async function recordChildDiaristRun(
665
654
  parent: AdmittedRoleInvocation,
666
655
  childRunId: string,
667
656
  ): Promise<void> {
668
- await updateDurableJsonPage(parent.admittedRequestPath, (page) => {
657
+ updateSectionSync(parent.runDirectory, "admitted", (page) => {
669
658
  const existing = Array.isArray(page.childDiaristRunIds)
670
659
  ? page.childDiaristRunIds.filter((runId): runId is string => typeof runId === "string")
671
660
  : [];
@@ -687,9 +676,8 @@ export async function bindTicketNumberOnRunDirectory(
687
676
  ticketNumber,
688
677
  "bindTicketNumberOnRunDirectory",
689
678
  );
690
- const admittedPath = join(runDirectory, "admitted-request.json");
691
679
  let unchanged = false;
692
- await updateDurableJsonPage(admittedPath, (admitted) => {
680
+ updateSectionSync(runDirectory, "admitted", (admitted) => {
693
681
  if (admitted.ticketNumber === ticketNumber) {
694
682
  unchanged = true;
695
683
  return undefined;
@@ -712,7 +700,7 @@ export async function recordLaunchedPiIdentity(
712
700
  }
713
701
 
714
702
  /**
715
- * Observed role-package launch provenance written onto the same invocation.json page.
703
+ * Observed role-package launch provenance written onto the same state.jsonl invocation row.
716
704
  * Values are field observations from the public CLI activation seam — never fixed schema markers.
717
705
  */
718
706
  export type LaunchedRolePackageIdentity = {
@@ -1303,10 +1291,16 @@ async function freezeRegularFileAttachment(
1303
1291
 
1304
1292
  /** Freeze attachments only — ticket binding is the shared LLM seat path (#635). */
1305
1293
  async function freezeAttachments(
1294
+ ledgerHome: string,
1306
1295
  attachmentPaths: readonly string[],
1307
1296
  attachmentsDirectory: string,
1308
1297
  ): Promise<readonly FrozenAttachment[]> {
1309
1298
  const attachments: FrozenAttachment[] = [];
1299
+ // attachments/ appears only when something is frozen; created through the
1300
+ // ledger's symlink-rejecting tree walk, never a plain mkdir.
1301
+ if (attachmentPaths.length > 0) {
1302
+ ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1303
+ }
1310
1304
  for (let i = 0; i < attachmentPaths.length; i += 1) {
1311
1305
  const frozen = await freezeRegularFileAttachment(
1312
1306
  attachmentPaths[i]!,
@@ -1332,7 +1326,7 @@ export async function freezeAttachmentsIntoRun(
1332
1326
  const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
1333
1327
  const attachmentsDirectory = join(runDirectory, "attachments", summonsKey);
1334
1328
  ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1335
- return freezeAttachments(attachmentPaths, attachmentsDirectory);
1329
+ return freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory);
1336
1330
  }
1337
1331
 
1338
1332
  /** Freeze prepared bytes and metadata through one path for birth or retained runs. */
@@ -1673,7 +1667,7 @@ export async function admitPublicRole(
1673
1667
  { cause: error },
1674
1668
  );
1675
1669
  }
1676
- frozenAttachments = await freezeAttachments(attachmentPaths, placed.attachmentsDirectory);
1670
+ frozenAttachments = await freezeAttachments(placed.ledgerHome, attachmentPaths, placed.attachmentsDirectory);
1677
1671
  return {
1678
1672
  issueNumber,
1679
1673
  caseRunsPath,
@@ -1832,7 +1826,7 @@ export type AdmitJudgeInvocationOptions = {
1832
1826
  /** Injectable clock/id for tests. */
1833
1827
  createRunId?: () => string;
1834
1828
  principalAuthority: DurablePrincipalAuthority;
1835
- /** Effective model for this invocation — written onto invocation.json. */
1829
+ /** Effective model for this invocation — written onto the state.jsonl invocation row. */
1836
1830
  model?: InvocationEffectiveModel;
1837
1831
  /** Typed ticket already on this summons (起居录 / parent board). Never parsed from prose. */
1838
1832
  assertedTicketNumber?: number;
@@ -1904,7 +1898,7 @@ async function placeRoleAdmission(options: {
1904
1898
  });
1905
1899
  const attachments = options.freezeAttachments === false
1906
1900
  ? []
1907
- : await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1901
+ : await freezeAttachments(ledgerHome, options.attachmentPaths, attachmentsDirectory);
1908
1902
  return {
1909
1903
  runId,
1910
1904
  bookKey,
@@ -1954,9 +1948,8 @@ async function persistPlacedAdmission(
1954
1948
  },
1955
1949
  placed: PlacedRoleAdmission,
1956
1950
  model: InvocationEffectiveModel | undefined,
1957
- ): Promise<string> {
1958
- const admittedRequestPath = join(placed.runDirectory, "admitted-request.json");
1959
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
1951
+ ): Promise<void> {
1952
+ writeAdmittedRequestPersistence(placed.runDirectory, admitted, {
1960
1953
  sessionDirectory: placed.sessionDirectory,
1961
1954
  sessionFile: placed.sessionFile,
1962
1955
  });
@@ -1969,12 +1962,11 @@ async function persistPlacedAdmission(
1969
1962
  admitted.role,
1970
1963
  model,
1971
1964
  );
1972
- return admittedRequestPath;
1973
1965
  }
1974
1966
 
1975
1967
  /**
1976
1968
  * Shared admission: project check, placement, attachment freeze,
1977
- * admitted-request and invocation ledger write.
1969
+ * admitted and invocation section write.
1978
1970
  * Countersign passes deferPersistence so same-ticket lookup can reserve coordinates
1979
1971
  * before materializeCountersignInvocation writes the page.
1980
1972
  * Seats whose extra facts are known before placement pass them as admittedFields.
@@ -1988,7 +1980,7 @@ async function admitStandardMaterialInvocation<
1988
1980
  >(
1989
1981
  role: R,
1990
1982
  options: AdmitInspectorInvocationOptions & {
1991
- /** Reserve coordinates; skip freeze, placement disk, and admitted-request write. */
1983
+ /** Reserve coordinates; skip freeze, placement disk, and admitted section write. */
1992
1984
  readonly deferPersistence?: boolean;
1993
1985
  /** Skip attachment freeze. Gleaner-left admits no caller attachments. */
1994
1986
  readonly freezeAttachments?: boolean;
@@ -2039,9 +2031,7 @@ async function admitStandardMaterialInvocation<
2039
2031
  attachments: persistedAttachmentRefs(placed.attachments),
2040
2032
  ...placed.ticketFields,
2041
2033
  };
2042
- const admittedRequestPath = defer
2043
- ? join(placed.runDirectory, "admitted-request.json")
2044
- : await persistPlacedAdmission(admitted, placed, options.model);
2034
+ if (!defer) await persistPlacedAdmission(admitted, placed, options.model);
2045
2035
  return {
2046
2036
  role,
2047
2037
  runId: placed.runId,
@@ -2052,7 +2042,6 @@ async function admitStandardMaterialInvocation<
2052
2042
  attachments: placed.attachments,
2053
2043
  runDirectory: placed.runDirectory,
2054
2044
  principal: placed.principal,
2055
- admittedRequestPath,
2056
2045
  ...correlationFields,
2057
2046
  ...placed.ticketFields,
2058
2047
  ...admittedFields,
@@ -2145,7 +2134,7 @@ export type AdmitCountersignInvocationOptions = {
2145
2134
  /** Injectable clock/id for tests. */
2146
2135
  createRunId?: () => string;
2147
2136
  principalAuthority: DurablePrincipalAuthority;
2148
- /** Effective model for this invocation — written onto invocation.json. */
2137
+ /** Effective model for this invocation — written onto the state.jsonl invocation row. */
2149
2138
  model?: InvocationEffectiveModel;
2150
2139
  correlationId?: string;
2151
2140
  /** Same-ticket lookup may select an existing run before a new run is persisted. */
@@ -2175,10 +2164,6 @@ export async function materializeCountersignInvocation(
2175
2164
  });
2176
2165
  const placement = placed;
2177
2166
  (admitted as { runDirectory: string }).runDirectory = placement.runDirectory;
2178
- (admitted as { admittedRequestPath: string }).admittedRequestPath = join(
2179
- placement.runDirectory,
2180
- "admitted-request.json",
2181
- );
2182
2167
  (admitted as { principal: DurablePrincipal }).principal = placement.principal;
2183
2168
  const ticketFields = ticketAdmissionFields(options.ticketNumber);
2184
2169
  if (ticketFields.ticketNumber !== undefined) {
@@ -2189,7 +2174,7 @@ export async function materializeCountersignInvocation(
2189
2174
  admitted.runDirectory,
2190
2175
  );
2191
2176
  (admitted as { attachments: readonly FrozenAttachment[] }).attachments = attachments;
2192
- await writeAdmittedRequestPersistence(admitted.admittedRequestPath, admitted, {
2177
+ writeAdmittedRequestPersistence(admitted.runDirectory, admitted, {
2193
2178
  sessionDirectory: placement.sessionDirectory,
2194
2179
  sessionFile: placement.sessionFile,
2195
2180
  });
@@ -2200,7 +2185,7 @@ export async function materializeCountersignInvocation(
2200
2185
  );
2201
2186
  }
2202
2187
 
2203
- /** Load admitted-request.json written at admission (Navigator work-context seam). */
2188
+ /** Load the admitted section written at admission (Navigator work-context seam). */
2204
2189
  export async function loadAdmittedJudgeRequest(
2205
2190
  runDirectory: string,
2206
2191
  ): Promise<{
@@ -2209,11 +2194,8 @@ export async function loadAdmittedJudgeRequest(
2209
2194
  attachments: readonly FrozenAttachment[];
2210
2195
  } | undefined> {
2211
2196
  try {
2212
- const raw = JSON.parse(
2213
- await readFile(join(runDirectory, "admitted-request.json"), "utf8"),
2214
- ) as unknown;
2215
- if (!isRecord(raw)) return undefined;
2216
- const record = raw;
2197
+ const record = readPageSync(runDirectory, "admitted");
2198
+ if (record === undefined) return undefined;
2217
2199
  if (!packagedPublicInstructionSubject(record.role)) return undefined;
2218
2200
  if (typeof record.instruction !== "string") return undefined;
2219
2201
  if (typeof record.instructionEmpty !== "boolean") return undefined;
@@ -2228,14 +2210,6 @@ export async function loadAdmittedJudgeRequest(
2228
2210
  }
2229
2211
  }
2230
2212
 
2231
- export async function ensureRunArtifactsDir(runDirectory: string): Promise<string> {
2232
- const directory = roleRunArtifactsDirectory(runDirectory);
2233
- return ensureRoleRunDirectory(
2234
- resolveActivationLedgerHome(homeFromRunDirectory(runDirectory)),
2235
- directory,
2236
- );
2237
- }
2238
-
2239
2213
  export type AdmitFixerInvocationOptions = {
2240
2214
  home: string;
2241
2215
  principalAuthority: DurablePrincipalAuthority;
@@ -2247,7 +2221,7 @@ export type AdmitFixerInvocationOptions = {
2247
2221
  prerequisitesPath?: string;
2248
2222
  project?: string;
2249
2223
  createRunId?: () => string;
2250
- /** Effective model for this invocation — written onto invocation.json. */
2224
+ /** Effective model for this invocation — written onto the state.jsonl invocation row. */
2251
2225
  model?: InvocationEffectiveModel;
2252
2226
  /** Typed ticket already on this summons. Placement uses it; code does not infer one. */
2253
2227
  assertedTicketNumber?: number;
@@ -2402,7 +2376,7 @@ export type AdmitReviewerInvocationOptions = {
2402
2376
  project?: string;
2403
2377
  createRunId?: () => string;
2404
2378
  correlationId?: string;
2405
- /** Effective model for this invocation — written onto invocation.json. */
2379
+ /** Effective model for this invocation — written onto the state.jsonl invocation row. */
2406
2380
  model?: InvocationEffectiveModel;
2407
2381
  /** Typed ticket already on this summons. Placement uses it; code does not infer one. */
2408
2382
  assertedTicketNumber?: number;
@@ -2455,7 +2429,7 @@ export type AdmitMergerInvocationOptions = {
2455
2429
  attachmentPaths: readonly string[];
2456
2430
  project?: string;
2457
2431
  createRunId?: () => string;
2458
- /** Effective model for this invocation — written onto invocation.json. */
2432
+ /** Effective model for this invocation — written onto the state.jsonl invocation row. */
2459
2433
  model?: InvocationEffectiveModel;
2460
2434
  /** Typed ticket already on this summons. Placement uses it; code does not infer one. */
2461
2435
  assertedTicketNumber?: number;