@akagilnc/pi-workflow-roles 0.1.5504 → 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 (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
@@ -1,13 +1,13 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
- import { basename, dirname, join } from "node:path";
4
- import { isRecord, errorText } from "./unknown-value.ts";
1
+ import { basename, join } from "node:path";
2
+ import { isRecord } from "./unknown-value.ts";
3
+ import { latestOfficerPointersFromRecords, RUN_HISTORY_FILE } from "./run-dossier.ts";
4
+ import { OFFICER_POINTER_RECORD_KIND } from "./run-dossier-files.ts";
5
+ import { readSitianRecords, reportRunRecord } from "./sitian-facade.ts";
5
6
 
6
7
  import { homeFromRunDirectory } from "./activation-ledger-topology.ts";
7
8
  import { findRunDirectoryById, readRoleRunIdentity } from "./public-cli/run-lifecycle.ts";
8
9
  import { rewriteRunDirectoryPathValue } from "./role-run-relocation.ts";
9
- import { parseRunLeaf, runDirectoryOfSessionFile, runDirectoryFromSessionDirectory } from "./role-run-placement.ts";
10
- import { sitianVolumeDirectory } from "./sitian-appender.ts";
10
+ import { parseRunLeaf, runDirectoryOfSessionFile } from "./role-run-placement.ts";
11
11
 
12
12
  /** Typed parent-side pointer to an independent officer run 正本 (ADR 0079 / #675). */
13
13
  export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer" as const;
@@ -27,21 +27,17 @@ export type DirectOfficerRunPointer = {
27
27
  readonly submissionToolCallId?: string;
28
28
  };
29
29
 
30
- function directOfficerRunPointerFile(
31
- parentSessionFile: string,
32
- officer: DirectOfficerRunPointer["officer"],
33
- ): string {
34
- return join(sitianVolumeDirectory(dirname(parentSessionFile), "auditor-roles"), `${officer}.pointer.json`);
35
- }
30
+ /** History record kind of a booked officer pointer (appender routes it to history.jsonl). */
31
+ export { OFFICER_POINTER_RECORD_KIND };
36
32
 
37
33
  /**
38
- * Book a typed pointer under parent session/auditor-roles (same nest owner as
39
- * createRecordSession). Never fabricates user/assistant/toolResult rows (#675).
40
- * Directory placement stays with the archivist record entry (ADR 0018 / 0065).
34
+ * Book a typed pointer as one history record of the parent run; the settlement
35
+ * seam projects the latest per officer into current.json `officers`.
36
+ * Never fabricates user/assistant/toolResult rows (#675).
41
37
  *
42
- * Stable leaf per officer under one parent (#753 gate-round accounting):
43
- * same-parent re-summons upsert the same pointer instead of minting N files that
44
- * each re-scan the full officer session and multiply gate-cycle counts.
38
+ * Latest record wins per officer under one parent (#753 gate-round accounting):
39
+ * a same-parent re-summons supersedes the earlier pointer instead of adding a
40
+ * second one that re-scans the full officer session and multiplies gate-cycle counts.
45
41
  */
46
42
  export function bookDirectOfficerRunPointer(options: {
47
43
  readonly parentSessionFile: string;
@@ -50,8 +46,6 @@ export function bookDirectOfficerRunPointer(options: {
50
46
  readonly runDirectory?: string;
51
47
  readonly submissionToolCallId?: string;
52
48
  }): DirectOfficerRunPointer {
53
- const pointerFile = directOfficerRunPointerFile(options.parentSessionFile, options.officer);
54
- mkdirSync(dirname(pointerFile), { recursive: true });
55
49
  const submissionToolCallId = options.submissionToolCallId?.trim() ?? "";
56
50
  const pointer: DirectOfficerRunPointer = {
57
51
  version: 1,
@@ -63,32 +57,26 @@ export function bookDirectOfficerRunPointer(options: {
63
57
  : {}),
64
58
  ...(submissionToolCallId === "" ? {} : { submissionToolCallId }),
65
59
  };
66
- writeFileSync(
67
- pointerFile,
68
- `${JSON.stringify(pointer)}\n`,
69
- "utf8",
70
- );
60
+ reportRunRecord(runDirectoryOfSessionFile(options.parentSessionFile), OFFICER_POINTER_RECORD_KIND, pointer, "submission-gate");
71
61
  return pointer;
72
62
  }
73
63
 
74
64
  /** Shared strict pointer IO; internal audit wraps unreadability as a re-summons. */
75
- export async function resolveOfficerSessionFromPointerFile(pointerPath: string): Promise<{
65
+ export async function resolveOfficerPointer(
66
+ raw: unknown,
67
+ parentRunDirectory: string,
68
+ ): Promise<{
76
69
  sessionFile: string;
77
70
  officer?: DirectOfficerRunPointer["officer"];
78
71
  runDirectory?: string;
79
72
  submissionToolCallId?: string;
80
73
  }> {
81
- let raw: unknown;
82
- try {
83
- raw = JSON.parse(await readFile(pointerPath, "utf8"));
84
- } catch (error) {
85
- throw new Error(`direct officer run pointer unreadable in ${pointerPath}: ${errorText(error)}`, { cause: error });
86
- }
74
+ const where = `${parentRunDirectory} officers`;
87
75
  if (!isRecord(raw) || raw.kind !== DIRECT_OFFICER_RUN_POINTER_KIND || raw.version !== 1) {
88
- throw new Error(`direct officer run pointer has unknown shape in ${pointerPath}`);
76
+ throw new Error(`direct officer run pointer has unknown shape in ${where}`);
89
77
  }
90
78
  if (typeof raw.sessionFile !== "string" || raw.sessionFile.trim() === "") {
91
- throw new Error(`direct officer run pointer missing sessionFile in ${pointerPath}`);
79
+ throw new Error(`direct officer run pointer missing sessionFile in ${where}`);
92
80
  }
93
81
  const officer = raw.officer === "inspector" || raw.officer === "notary"
94
82
  || raw.officer === "auditor" || raw.officer === "countersign" ? raw.officer : undefined;
@@ -97,7 +85,6 @@ export async function resolveOfficerSessionFromPointerFile(pointerPath: string):
97
85
  let sessionFile = raw.sessionFile;
98
86
  const recordedRunDirectory = runDirectory ?? runDirectoryOfSessionFile(sessionFile);
99
87
  const leaf = parseRunLeaf(basename(recordedRunDirectory));
100
- const parentRunDirectory = runDirectoryFromSessionDirectory(dirname(dirname(pointerPath)));
101
88
  const parentIdentity = await readRoleRunIdentity(parentRunDirectory);
102
89
  if (leaf !== undefined && parentIdentity !== undefined) {
103
90
  const currentRunDirectory = await findRunDirectoryById(
@@ -118,13 +105,40 @@ export async function resolveOfficerSessionFromPointerFile(pointerPath: string):
118
105
  };
119
106
  }
120
107
 
108
+ /**
109
+ * The latest pointer booked per officer on one run, as stored. History absent → empty.
110
+ * Canonical malformed diagnostics are not discarded: a damaged officer source must
111
+ * not wash into lawful zero rounds (#1161 O1).
112
+ */
113
+ export async function readBookedOfficerPointers(parentRunDirectory: string): Promise<Readonly<Record<string, unknown>>> {
114
+ const { records, diagnostics } = await readSitianRecords(join(parentRunDirectory, RUN_HISTORY_FILE));
115
+ if (diagnostics.length > 0) {
116
+ throw new Error(
117
+ `${RUN_HISTORY_FILE} has ${diagnostics.length} malformed row(s); officer pointers unreadable: ${parentRunDirectory}`,
118
+ );
119
+ }
120
+ return latestOfficerPointersFromRecords(records);
121
+ }
122
+
123
+ /** Every pointer booked on one run, in officer-name order. No pointers → []. */
124
+ export async function readOfficerPointers(parentRunDirectory: string): Promise<readonly Awaited<ReturnType<typeof resolveOfficerPointer>>[]> {
125
+ const officers = await readBookedOfficerPointers(parentRunDirectory);
126
+ return Promise.all(
127
+ Object.keys(officers).sort().map((name) => resolveOfficerPointer(officers[name], parentRunDirectory)),
128
+ );
129
+ }
130
+
121
131
  /** Missing or unreadable is not a pass: the internal audit caller re-runs the gate. */
122
132
  export async function readDirectOfficerRunPointer(
123
133
  parentSessionFile: string,
124
134
  officer: DirectOfficerRunPointer["officer"],
125
135
  ): Promise<DirectOfficerRunPointer | undefined> {
126
136
  try {
127
- const pointer = await resolveOfficerSessionFromPointerFile(directOfficerRunPointerFile(parentSessionFile, officer));
137
+ const parentRunDirectory = runDirectoryOfSessionFile(parentSessionFile);
138
+ const pointer = await resolveOfficerPointer(
139
+ (await readBookedOfficerPointers(parentRunDirectory))[officer],
140
+ parentRunDirectory,
141
+ );
128
142
  if (pointer.officer !== officer) return undefined;
129
143
  return { ...pointer, officer, version: 1, kind: DIRECT_OFFICER_RUN_POINTER_KIND };
130
144
  } catch {
@@ -1,9 +1,9 @@
1
1
  import { join, resolve } from "node:path";
2
2
 
3
+ import { RUN_HISTORY_FILE, RUN_LOG_FILE, runCurrentPath } from "./run-dossier.ts";
3
4
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
5
  import type { HostContext } from "./host-contracts.ts";
5
6
  import {
6
- roleRunArtifactsDirectory,
7
7
  runDirectoryOfSessionFile,
8
8
  sessionFileOf,
9
9
  } from "./role-run-placement.ts";
@@ -13,10 +13,11 @@ export const AUDITOR_DOSSIER_TOOL_NAME = "ak_get_run_dossier" as const;
13
13
 
14
14
  export type AuditorDossierLocation = {
15
15
  readonly runDirectory: string;
16
- readonly admittedRequest: string;
16
+ readonly current: string;
17
17
  readonly parentSessionCandidate: string;
18
18
  readonly attachments: string;
19
- readonly artifacts: string;
19
+ readonly log: string;
20
+ readonly history: string;
20
21
  };
21
22
 
22
23
  /** Resolve the exact run binding already carried by the parent record session. */
@@ -47,10 +48,11 @@ export function createAuditorDossierTool(
47
48
  // #836: whole run directory pointer — officer finds materials; no leaf preference (A7.2).
48
49
  const details: AuditorDossierLocation = {
49
50
  runDirectory,
50
- admittedRequest: join(runDirectory, "admitted-request.json"),
51
+ current: runCurrentPath(runDirectory),
51
52
  parentSessionCandidate: sessionFileOf(runDirectory),
52
53
  attachments: join(runDirectory, "attachments"),
53
- artifacts: roleRunArtifactsDirectory(runDirectory),
54
+ log: join(runDirectory, RUN_LOG_FILE),
55
+ history: join(runDirectory, RUN_HISTORY_FILE),
54
56
  };
55
57
  return {
56
58
  content: [{ type: "text", text: JSON.stringify(details) }],
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { readPageSync } from "./run-dossier.ts";
2
3
  import { basename, join } from "node:path";
3
4
 
4
5
  import { parseRunLeaf } from "./role-run-placement.ts";
@@ -122,15 +123,8 @@ export async function readAuditorResumeBinding(runDirectory: string): Promise<
122
123
  | { readonly subject: AuditorSoulRole; readonly sourceRunDirectory: string }
123
124
  | undefined
124
125
  > {
125
- let raw: unknown;
126
- try {
127
- raw = JSON.parse(await readFile(join(runDirectory, "admitted-request.json"), "utf8")) as unknown;
128
- } catch (error) {
129
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
130
- throw error;
131
- }
132
- if (!isRecord(raw)) return undefined;
133
- const record = raw as { sourceRunPath?: unknown; auditorSubject?: unknown };
126
+ const record = readPageSync(runDirectory, "admitted");
127
+ if (record === undefined) return undefined;
134
128
  if (typeof record.sourceRunPath !== "string" || record.sourceRunPath.trim() === "") return undefined;
135
129
  const subject = isAuditorSoulRole(record.auditorSubject)
136
130
  ? record.auditorSubject
@@ -15,9 +15,10 @@ import { isUnboundRunDirectory, roleRunPlacement } from "./role-run-placement.ts
15
15
  export { isUnboundRunDirectory };
16
16
  import {
17
17
  MIGRATION_TICKET_DERIVATION_PAGE,
18
- readBoardTicketNumber,
18
+ readMigrationBoardTicketNumber,
19
19
  readMigrationDerivedTicketNumber,
20
20
  } from "./run-ticket-number.ts";
21
+ import { readPageSync } from "./run-dossier.ts";
21
22
 
22
23
  import { isRecord, isEnoent } from "./unknown-value.ts";
23
24
 
@@ -83,11 +84,23 @@ export function ticketNumberFromWorktreeBasename(
83
84
 
84
85
  /**
85
86
  * projectRoot from a retained run's durable pages.
86
- * ENOENT → try next page / undefined; bad JSON and non-ENOENT IO propagate.
87
+ * New落点 (state rows) first, then pre-#1161 page files at the migration boundary.
88
+ * ENOENT → try next / undefined; bad JSON and non-ENOENT IO propagate.
87
89
  */
88
90
  async function readProjectRootFromRun(
89
91
  runDirectory: string,
90
92
  ): Promise<{ readonly projectRoot: string; readonly sourcePage: string } | undefined> {
93
+ for (const section of ["admitted", "invocation", "runState"] as const) {
94
+ try {
95
+ const page = readPageSync(runDirectory, section);
96
+ const projectRoot = page?.projectRoot;
97
+ if (typeof projectRoot === "string" && projectRoot.length > 0) {
98
+ return { projectRoot, sourcePage: `state.jsonl:${section}` };
99
+ }
100
+ } catch (error) {
101
+ if (!isEnoent(error)) throw error;
102
+ }
103
+ }
91
104
  for (const page of ["admitted-request.json", "invocation.json", "run-state.json"] as const) {
92
105
  const path = join(runDirectory, page);
93
106
  try {
@@ -123,7 +136,7 @@ export async function resolveMigratingRunTicket(runDirectory: string): Promise<{
123
136
  readonly ticketNumber: number | undefined;
124
137
  readonly derivation: MigratingRunTicketDerivation | undefined;
125
138
  }> {
126
- const boardTicket = await readBoardTicketNumber(runDirectory);
139
+ const boardTicket = await readMigrationBoardTicketNumber(runDirectory);
127
140
  if (boardTicket !== undefined) {
128
141
  return {
129
142
  ticketNumber: boardTicket,
@@ -31,12 +31,14 @@ import {
31
31
  type BookTopologyPartitionMigrator,
32
32
  type MigrationItemOutcome,
33
33
  } from "./book-topology-migration.ts";
34
- import { S4_SUBMISSION_LEDGER_KINDS } from "./sitian-appender.ts";
35
34
  import { projectTicketProvenanceHeader } from "./ticket-provenance-contracts.ts";
36
35
 
37
36
  import { sha256Hex } from "./sha256.ts";
38
37
  import { isRecord, isEnoent } from "./unknown-value.ts";
39
38
 
39
+ /** Kinds the retired #852-era submission-ledger volume held (migration of stored runs only). */
40
+ const LEGACY_SUBMISSION_LEDGER_KINDS = new Set(["candidate", "roundContext", "outcome", "sealed", "post-seal-anomaly"]);
41
+
40
42
  const TICKET_PROVENANCE = "ticket-provenance";
41
43
  const SUBMISSION_LEDGER = "submission-ledger";
42
44
  const ATTEMPT_HISTORY = "attempt-history";
@@ -210,7 +212,7 @@ function runIdFromPayload(payload: unknown): string | undefined {
210
212
  function recordClassOfKind(kind: unknown): RecordClass | undefined {
211
213
  if (kind === TICKET_PROVENANCE) return TICKET_PROVENANCE;
212
214
  if (kind === ATTEMPT_HISTORY) return ATTEMPT_HISTORY;
213
- if (typeof kind === "string" && S4_SUBMISSION_LEDGER_KINDS.has(kind)) return SUBMISSION_LEDGER;
215
+ if (typeof kind === "string" && LEGACY_SUBMISSION_LEDGER_KINDS.has(kind)) return SUBMISSION_LEDGER;
214
216
  return undefined;
215
217
  }
216
218
 
@@ -26,9 +26,10 @@ import {
26
26
  rewriteRoleRunDurablePages,
27
27
  type RunDirectoryPathRewrite,
28
28
  } from "./role-run-relocation.ts";
29
+ import { renderCurrentSync } from "./run-dossier.ts";
29
30
  import {
30
31
  MIGRATION_TICKET_DERIVATION_PAGE,
31
- readBoardTicketNumber,
32
+ readMigrationBoardTicketNumber,
32
33
  } from "./run-ticket-number.ts";
33
34
 
34
35
  import { isEnoent } from "./unknown-value.ts";
@@ -151,7 +152,7 @@ async function planBookMoves(
151
152
  // #863: board typed ticket on an unbound leaf → place under that ticket.
152
153
  // No board ticket → stay unbound. Never invent from prose or derivation.
153
154
  const boardTicket = leaf.isDirectory
154
- ? await readBoardTicketNumber(leaf.sourcePath)
155
+ ? await readMigrationBoardTicketNumber(leaf.sourcePath)
155
156
  : undefined;
156
157
  if (parsed !== undefined && boardTicket !== undefined) {
157
158
  targetPath = destinationRunDirectory(
@@ -318,7 +319,7 @@ export async function relocateBoardBoundUnboundRunsInBook(
318
319
  const parsed = parseRunLeaf(entry.name);
319
320
  if (parsed === undefined) continue;
320
321
  const sourcePath = join(unboundRuns, entry.name);
321
- const boardTicket = await readBoardTicketNumber(sourcePath);
322
+ const boardTicket = await readMigrationBoardTicketNumber(sourcePath);
322
323
  if (boardTicket === undefined) continue;
323
324
  const targetPath = destinationRunDirectory(
324
325
  booksDirectory,
@@ -367,6 +368,8 @@ export async function relocateBoardBoundUnboundRunsInBook(
367
368
  for (const move of planned) {
368
369
  await mkdir(dirname(move.targetPath), { recursive: true });
369
370
  await rename(move.sourcePath, move.targetPath);
371
+ // Same live relocate seam: derived host.original follows the new run directory.
372
+ renderCurrentSync(move.targetPath);
370
373
  }
371
374
 
372
375
  return planned.map((move) => ({
@@ -49,7 +49,7 @@ export type DoctorSubmission =
49
49
  // #836: runtime cost is a fact beside the role payload, never merged into it —
50
50
  // DoctorOutput is the accepted payload itself, identical to DoctorSubmission.
51
51
  export type DoctorOutput = DoctorSubmission;
52
- export type DoctorEvidenceEntry = { id: string; kind: "session" | "stderr"; byteLength: number; contentLength: number; sha256: string; content: string };
52
+ export type DoctorEvidenceEntry = { id: string; kind: "session" | "log"; byteLength: number; contentLength: number; sha256: string; content: string };
53
53
  export type DoctorCase = { version: 1; identity: DoctorCaseIdentity; evidence: DoctorEvidenceEntry[]; cost: DoctorCaseCost };
54
54
 
55
55
  // status 的合法词写在 description。交卷原样入账(validateDoctorSubmissionShape),
@@ -7,7 +7,7 @@ import { AcceptedDetailsContractError, acceptedFacts, isTerminatingToolName, val
7
7
 
8
8
  import { isRecord, isMissingPathError } from "./unknown-value.ts";
9
9
 
10
- async function discoverCaseFiles(root: string): Promise<string[]> { const found: string[] = []; async function walk(dir: string, depth: number) { for (const item of await readdir(dir, { withFileTypes: true })) { const path = resolve(dir, item.name); if (item.isDirectory()) { await walk(path, depth + 1); } else if (item.isFile() && (item.name.endsWith(".jsonl") || (item.name === "stderr.log" && depth === 1))) found.push(path); } } await walk(root, 0); return found.sort(); }
10
+ async function discoverCaseFiles(root: string): Promise<string[]> { const found: string[] = []; async function walk(dir: string, depth: number) { for (const item of await readdir(dir, { withFileTypes: true })) { const path = resolve(dir, item.name); if (item.isDirectory()) { await walk(path, depth + 1); } else if (item.isFile() && ((item.name.endsWith(".jsonl") && item.name !== "state.jsonl" && !(item.name === "log.jsonl" && depth === 1)) || (item.name === "log.jsonl" && depth === 1))) found.push(path); } } await walk(root, 0); return found.sort(); }
11
11
  function sourceList(count: number, sources: string[]) { return { count, sources: [...new Set(sources)].sort() }; }
12
12
  function accumulate(metric: DoctorCount, value: number, source: string) { metric.count += value; if (value) metric.sources.push(source); }
13
13
  function timestamp(row: Record<string, unknown>) { return typeof row.timestamp === "string" && Number.isFinite(Date.parse(row.timestamp)) ? row.timestamp : undefined; }
@@ -49,7 +49,7 @@ export async function loadDoctorCase(runsPath: string, projectRoot?: string): Pr
49
49
  if (!match) throw new Error("Doctor case must be an .ak-roles/books/<book>/<n>/runs directory");
50
50
  const evidence: DoctorEvidenceEntry[] = [], sessions: DoctorCaseCost["sessions"] = [], statuses: DoctorCaseCost["statuses"] = [], commits: DoctorCaseCost["commits"] = [];
51
51
  const turns: DoctorCount = { count: 0, sources: [] }, calls: DoctorCount = { count: 0, sources: [] }, tokens: DoctorCount = { count: 0, sources: [] };
52
- for (const path of await discoverCaseFiles(root)) { const id = relative(root, path).split(sep).join("/"); const bytes = await readFile(path); const content = bytes.toString("utf8"); const kind = id.endsWith(".jsonl") ? "session" : "stderr"; evidence.push({ id, kind, byteLength: bytes.byteLength, contentLength: content.length, sha256: sha256Hex(bytes), content }); if (kind === "stderr") continue; const result = deriveSession(content, id); sessions.push(result.session); statuses.push(...result.statuses); commits.push(...result.commits); accumulate(turns, result.turns, id); accumulate(calls, result.calls, id); accumulate(tokens, result.tokens, id); }
52
+ for (const path of await discoverCaseFiles(root)) { const id = relative(root, path).split(sep).join("/"); const bytes = await readFile(path); const content = bytes.toString("utf8"); const kind = id.split("/").length === 2 && id.endsWith("/log.jsonl") ? "log" : "session"; evidence.push({ id, kind, byteLength: bytes.byteLength, contentLength: content.length, sha256: sha256Hex(bytes), content }); if (kind === "log") continue; const result = deriveSession(content, id); sessions.push(result.session); statuses.push(...result.statuses); commits.push(...result.commits); accumulate(turns, result.turns, id); accumulate(calls, result.calls, id); accumulate(tokens, result.tokens, id); }
53
53
  const runDirs = (await readdir(root, { withFileTypes: true })).filter((item) => item.isDirectory()).map((item) => item.name).sort(); const legs = evidence.filter((entry) => entry.kind === "session").map((entry) => entry.id); const retryDirs = runDirs.filter((name) => /(?:^|[-_])retry(?:[-_]|$)/i.test(name)); const rawBytes = evidence.filter((entry) => entry.kind === "session").reduce((sum, entry) => sum + entry.byteLength, 0);
54
54
  const cost: DoctorCaseCost = { invocations: sourceList(runDirs.length, runDirs), legs: sourceList(legs.length, legs), modelApiTurns: sourceList(turns.count, turns.sources), outputTokens: sourceList(tokens.count, tokens.sources), toolCalls: sourceList(calls.count, calls.sources), retries: { ...sourceList(retryDirs.length, retryDirs), evidence: "literal run-dir naming" }, statuses, commits, sessions, outputBytes: { ...sourceList(rawBytes, legs), payload: "raw JSONL bytes", providerWireBytes: "unavailable" } };
55
55
  return { version: 1, identity: { issueNumber: Number(match[1]), runsPath: await stableRunsIdentity(root) }, evidence, cost };
@@ -125,7 +125,7 @@ export function createEngineDetourToolDefinition(input: {
125
125
  : undefined;
126
126
  const runId = runDirectory === undefined ? undefined : runIdFromRunDirectory(runDirectory);
127
127
  // Public-invocation scope + selected host from Host envelope only — never
128
- // courtAttemptId, never sidecar file, never pre-spawn invocation.json I/O.
128
+ // courtAttemptId, never sidecar file, never pre-spawn current.json I/O.
129
129
  const invocationScopeId =
130
130
  typeof ctx.invocationScopeId === "string" && ctx.invocationScopeId.trim() !== ""
131
131
  ? ctx.invocationScopeId.trim()
@@ -15,6 +15,7 @@
15
15
  * `ak-role resume`) mints a new one. Never courtAttemptId, never Pi session
16
16
  * toolResult join keys, and never a detour-owned sidecar file.
17
17
  */
18
+ import { readPageSync, RUN_LOG_FILE } from "./run-dossier.ts";
18
19
  import { randomUUID } from "node:crypto";
19
20
  import { readFileSync } from "node:fs";
20
21
  import { join } from "node:path";
@@ -25,7 +26,6 @@ export {
25
26
  } from "./role-run-placement.ts";
26
27
 
27
28
  import { ENGINE_DETOUR_TOOL_NAME } from "./engine-detour.ts";
28
- import { sitianRunVolumeDirectory, sitianVolumeRecordsFile } from "./sitian-appender.ts";
29
29
  import {
30
30
  readSitianRecords,
31
31
  resolveSitianRecordPath,
@@ -42,13 +42,11 @@ export const ENGINE_DETOUR_CALL_KIND = "engine-detour-call" as const;
42
42
  export const ENGINE_DETOUR_TOOL_USAGE_FACT_KEY = "engineDetourToolUsage" as const;
43
43
 
44
44
  /**
45
- * Run-relative sitian volume path for one detour call.
45
+ * Run-relative path of the log that holds one detour call's record.
46
46
  * Openable once the run directory is known from the terminal's run id.
47
47
  * Contains no runId bytes itself (#108 + #537).
48
48
  */
49
- export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE = sitianVolumeRecordsFile(
50
- sitianRunVolumeDirectory("", ENGINE_DETOUR_CALL_KIND),
51
- );
49
+ export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE = RUN_LOG_FILE;
52
50
 
53
51
  /** stdout UTF-8 byte length (ticket-frozen metric; empty stdout is real 0). */
54
52
  export function engineDetourStdoutByteLength(stdout: string): number {
@@ -253,22 +251,14 @@ export async function readEngineDetourToolUsage(input: {
253
251
  return { callCount: calls.length, calls };
254
252
  }
255
253
 
256
- /** One shared invocation.json reader (ENOENT → undefined; other errors propagate). */
254
+ /** One shared state.jsonl invocation-row reader (absent → undefined; other errors propagate). */
257
255
  function readInvocationRecord(
258
256
  runDirectory: string,
259
257
  ): Record<string, unknown> | undefined {
260
- try {
261
- const raw = JSON.parse(
262
- readFileSync(join(runDirectory, "invocation.json"), "utf8"),
263
- ) as unknown;
264
- return isRecord(raw) ? raw : undefined;
265
- } catch (error) {
266
- if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return undefined;
267
- throw error;
268
- }
258
+ return readPageSync(runDirectory, "invocation");
269
259
  }
270
260
 
271
- /** True when invocation.json carries a non-empty engine axis. */
261
+ /** True when the invocation section carries a non-empty engine axis. */
272
262
  export async function readInvocationEngineMounted(
273
263
  runDirectory: string,
274
264
  ): Promise<boolean> {
@@ -13,8 +13,33 @@ import type { PreparedRoleTurn } from "./prepared-role-turn.ts";
13
13
  import { projectThrownFailureLeaf, retainPackageFault } from "./public-cli/settlement.ts";
14
14
  import { describeErrorIdentity } from "./public-cli/run-lifecycle.ts";
15
15
  import { deliveryLimitFromConfig } from "./receipt-delivery-policy.ts";
16
+ import { reportRunRecord } from "./sitian-facade.ts";
16
17
  import { isOneShotWorkerReminderCode } from "./submission-errors.ts";
17
18
 
19
+ /**
20
+ * What one host start was given: one `turn-delivery` row of history.jsonl per start. History is
21
+ * the volume whose write failure does not stop a run (#833), so a row that cannot be
22
+ * written is noted and the host still starts.
23
+ */
24
+ export async function recordTurnDelivery(
25
+ runDirectory: string,
26
+ delivered: { readonly systemPrompt: string; readonly outputSchema?: unknown },
27
+ source: string,
28
+ ): Promise<void> {
29
+ try {
30
+ reportRunRecord(runDirectory, "turn-delivery", {
31
+ systemPrompt: delivered.systemPrompt,
32
+ ...(delivered.outputSchema === undefined ? {} : { outputSchema: delivered.outputSchema }),
33
+ }, source);
34
+ } catch (error) {
35
+ await retainPackageFault({
36
+ runDirectory,
37
+ diagnostic: `turn delivery record failed beside host start: ${describeErrorIdentity(error)}`,
38
+ error,
39
+ });
40
+ }
41
+ }
42
+
18
43
  export type ExternalPreparedTurn = Pick<PreparedRoleTurn, "prompt" | "abortSignal" | "closeRound">;
19
44
 
20
45
  export type ExternalHostRoundOutcome =
@@ -36,10 +36,7 @@ export function createProductionHeadlessRoleTurnHost(
36
36
  options: ProductionHeadlessHostOptions,
37
37
  ): RoleTurnHost {
38
38
  const { packageRoot, principalAuthority, description, hostName } = options;
39
- const sessionIdentity = createSessionIdentityAuthority(
40
- principalAuthority,
41
- description.sessionBindingFile,
42
- );
39
+ const sessionIdentity = createSessionIdentityAuthority(principalAuthority, hostName);
43
40
  const roleRuntimeDependencies = createRoleRuntimeDependencies(packageRoot);
44
41
 
45
42
  const innerFor = (operatorHome: string): RoleTurnHost =>
@@ -6,7 +6,8 @@
6
6
  import { spawn, spawnSync } from "node:child_process";
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { existsSync } from "node:fs";
9
- import { writeFile } from "node:fs/promises";
9
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
10
+ import { tmpdir } from "node:os";
10
11
  import { dirname, isAbsolute, join, resolve } from "node:path";
11
12
 
12
13
  import type { RoleTurnHost, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
@@ -18,6 +19,7 @@ import {
18
19
  externalHostFailure as failure,
19
20
  hostAbortedError,
20
21
  isHostAbortedError,
22
+ recordTurnDelivery,
21
23
  } from "../external-host-turn-loop.ts";
22
24
  import { describeErrorIdentity } from "../public-cli/run-lifecycle.ts";
23
25
  import { projectThrownFailureLeaf, retainPackageFault } from "../public-cli/settlement.ts";
@@ -462,31 +464,51 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
462
464
 
463
465
  // config.env owns package-root/child env; do not re-spread process.env over it.
464
466
  const env: NodeJS.ProcessEnv = { ...process.env, ...(config.env ?? {}) };
465
- const systemPromptPath = join(request.runDirectory, "headless-system-prompt.txt");
466
- await writeFile(systemPromptPath, systemPrompt, "utf8");
467
+ // CLI start-up inputs (system prompt / schema / MCP config files) are not
468
+ // dossier: they live in a throwaway directory, and what was delivered is
469
+ // recorded once per actual start as a history.jsonl `turn-delivery` row.
470
+ const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
471
+ const inputsDirectory = await mkdtemp(join(tmpdir(), "ak-role-headless-"));
472
+ const systemPromptPath = join(inputsDirectory, "system-prompt.txt");
467
473
  let mcpConfigPath: string | undefined;
468
474
  let outputSchemaPath: string | undefined;
469
- if (codex) {
470
- // #1148: Codex --output-schema requires a native strict transport schema.
471
- // Project from the unique open declaration; do not alter package receipt rules.
472
- if (prepared.terminatingToolName !== NAVIGATOR_OUTPUT_TOOL_NAME) {
473
- outputSchemaPath = join(request.runDirectory, "headless-output-schema.json");
475
+ let startedWithSchema: unknown;
476
+ // A setup failure after mkdtemp must not leave the prompt material behind.
477
+ try {
478
+ await writeFile(systemPromptPath, systemPrompt, "utf8");
479
+ let deliveredSchema: unknown;
480
+ if (codex) {
481
+ // #1148: Codex --output-schema requires a native strict transport schema.
482
+ // Project from the unique open declaration; do not alter package receipt rules.
483
+ if (prepared.terminatingToolName !== NAVIGATOR_OUTPUT_TOOL_NAME) {
484
+ outputSchemaPath = join(inputsDirectory, "output-schema.json");
485
+ deliveredSchema = closeJsonSchemaForCodex(prepared.jsonSchema);
486
+ await writeFile(outputSchemaPath, `${JSON.stringify(deliveredSchema, null, 2)}\n`, "utf8");
487
+ }
488
+ } else {
489
+ mcpConfigPath = join(inputsDirectory, "mcp-config.json");
474
490
  await writeFile(
475
- outputSchemaPath,
476
- `${JSON.stringify(closeJsonSchemaForCodex(prepared.jsonSchema), null, 2)}\n`,
491
+ mcpConfigPath,
492
+ `${JSON.stringify(headlessMcpConfigDocument(prepared.mcpServers), null, 2)}\n`,
477
493
  "utf8",
478
494
  );
495
+ // #959: navigator prose exit — no closed JSON schema on claude either.
496
+ if (prepared.terminatingToolName !== NAVIGATOR_OUTPUT_TOOL_NAME) deliveredSchema = prepared.jsonSchema;
479
497
  }
480
- } else {
481
- mcpConfigPath = join(request.runDirectory, "headless-mcp-config.json");
482
- await writeFile(
483
- mcpConfigPath,
484
- `${JSON.stringify(headlessMcpConfigDocument(prepared.mcpServers), null, 2)}\n`,
485
- "utf8",
486
- );
498
+ startedWithSchema = deliveredSchema;
499
+ } catch (setupError) {
500
+ try {
501
+ await rm(inputsDirectory, { recursive: true, force: true });
502
+ } catch (cleanupError) {
503
+ await retainPackageFault({
504
+ runDirectory: request.runDirectory,
505
+ diagnostic: `headless turn inputs cleanup failed beside setup failure: ${describeErrorIdentity(cleanupError)}`,
506
+ error: cleanupError,
507
+ });
508
+ }
509
+ throw setupError;
487
510
  }
488
511
 
489
- const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
490
512
  let exitedSessionId: string | undefined;
491
513
  try {
492
514
  outcome = await driveExternalRoleTurnRounds(prepared, request, {
@@ -535,6 +557,11 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
535
557
  }) !== undefined;
536
558
  }
537
559
 
560
+ // What this CLI start was given: one history record per start.
561
+ await recordTurnDelivery(request.runDirectory, {
562
+ systemPrompt,
563
+ ...(startedWithSchema === undefined ? {} : { outputSchema: startedWithSchema }),
564
+ }, "headless-host");
538
565
  let spawned: HeadlessSpawnResult;
539
566
  try {
540
567
  spawned = await spawnHeadlessTurn({
@@ -715,12 +742,19 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
715
742
  },
716
743
  });
717
744
  } finally {
745
+ try {
746
+ await rm(inputsDirectory, { recursive: true, force: true });
747
+ } catch (error) {
748
+ await retainPackageFault({
749
+ runDirectory: request.runDirectory,
750
+ diagnostic: `headless turn inputs cleanup failed beside host terminal: ${describeErrorIdentity(error)}`,
751
+ error,
752
+ });
753
+ }
718
754
  try {
719
755
  if (exitedSessionId !== undefined && exitedSessionId !== "") copyAndRecordHostDossier({
720
756
  host: config.hostName, sessionId: exitedSessionId, cwd: request.cwd,
721
757
  sessionDirectory: sessionDirectoryOf(request.runDirectory), sessionParent,
722
- continuation: request.continuation,
723
- ...(request.model !== undefined ? { model: request.model } : {}),
724
758
  ...(request.home !== undefined ? { home: request.home } : {}),
725
759
  });
726
760
  } catch (error) {
@@ -15,8 +15,6 @@ import type { HeadlessHostDescription } from "./headless-host/description.ts";
15
15
  export type HostIdentityDescription = Readonly<{
16
16
  /** Binary path segments relative to the operator home. */
17
17
  binaryFromHome: readonly string[];
18
- /** Durable host session binding filename beside the session principal. */
19
- sessionBindingFile: string;
20
18
  }>;
21
19
 
22
20
  /** Resolve a host description's agent binary under the operator home. */
@@ -45,7 +43,6 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
45
43
  }),
46
44
  modelPassing: "set_model",
47
45
  setModelId: "bare",
48
- sessionBindingFile: "grok-acp-session.json",
49
46
  }),
50
47
  /**
51
48
  * Operator home `~/.hermes`, native session/load resume, `acp` subcommand.
@@ -63,7 +60,6 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
63
60
  }),
64
61
  modelPassing: "set_model",
65
62
  setModelId: "provider:model",
66
- sessionBindingFile: "hermes-acp-session.json",
67
63
  seatProfileSoul: Object.freeze({
68
64
  flag: "-p",
69
65
  namePrefix: "ak-",
@@ -84,7 +80,6 @@ export const HEADLESS_HOST_DESCRIPTIONS: Readonly<Record<string, HeadlessHostDes
84
80
  "claude": Object.freeze({
85
81
  protocol: "claude-print",
86
82
  binaryFromHome: Object.freeze([".local", "bin", "claude"]),
87
- sessionBindingFile: "claude-headless-session.json",
88
83
  fixedArgs: Object.freeze([
89
84
  // Live NDJSON events → sitian host-session records (#811); last line is the result receipt.
90
85
  "--output-format", "stream-json",
@@ -109,7 +104,6 @@ export const HEADLESS_HOST_DESCRIPTIONS: Readonly<Record<string, HeadlessHostDes
109
104
  "codex": Object.freeze({
110
105
  protocol: "codex-exec",
111
106
  binaryFromHome: Object.freeze([".local", "bin", "codex"]),
112
- sessionBindingFile: "codex-headless-session.json",
113
107
  }),
114
108
  });
115
109