@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
@@ -5,16 +5,14 @@
5
5
  * Writes native-session-copy (or native-session-warning on retry failure) after CLI exit.
6
6
  * Log line write failures declare to stderr and never abort the leg.
7
7
  */
8
+ import { randomBytes } from "node:crypto";
8
9
  import { spawnSync } from "node:child_process";
9
10
  import {
10
- appendFileSync,
11
11
  copyFileSync,
12
- closeSync,
13
12
  existsSync,
14
- openSync,
15
- readFileSync,
16
13
  mkdirSync,
17
14
  readdirSync,
15
+ renameSync,
18
16
  rmSync,
19
17
  statSync,
20
18
  type Dirent,
@@ -22,11 +20,10 @@ import {
22
20
  import { homedir } from "node:os";
23
21
  import { dirname, join } from "node:path";
24
22
 
25
- import type { RoleTurnContinuation } from "./host-contracts.ts";
26
23
  import { sitianReport } from "./sitian-facade.ts";
27
24
  import type { SitianRecordInput } from "./sitian-contracts.ts";
28
25
 
29
- import { errorText } from "./unknown-value.ts";
26
+ import { errorText, isEnoent } from "./unknown-value.ts";
30
27
 
31
28
  /** Volume category under `<run>/session/<kind>/records.jsonl`. */
32
29
  export const HOST_SESSION_RECORD_KIND = "host-session" as const;
@@ -47,31 +44,41 @@ export function sitianReportSafe(input: SitianRecordInput): void {
47
44
 
48
45
  /** Recursively scan for Codex rollout file matching sessionId under sessionsDir. */
49
46
  function findCodexRollout(sessionsDir: string, sessionId: string): string | undefined {
50
- if (!existsSync(sessionsDir)) return undefined;
47
+ // Direct readdir: only ENOENT is absence. EACCES/EPERM keep their cause (#1161 C3-io).
51
48
  const matches: Array<{ path: string; mtime: number }> = [];
52
49
  function scan(dir: string, depth = 0): void {
53
50
  if (depth > 6) return;
54
51
  let entries: Dirent[];
55
52
  try {
56
53
  entries = readdirSync(dir, { withFileTypes: true });
57
- } catch {
58
- return;
54
+ } catch (error) {
55
+ if (isEnoent(error)) return;
56
+ throw error;
59
57
  }
60
58
  for (const entry of entries) {
61
59
  const fullPath = join(dir, entry.name);
62
60
  if (entry.isDirectory()) {
63
61
  scan(fullPath, depth + 1);
64
62
  } else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(sessionId)) {
63
+ // Ranking needs a real mtime. Silent mtime:0 would sort unreadable
64
+ // matches last and let an older readable sibling win (#1161 C3-io).
65
+ // ENOENT = vanished between readdir and stat (skip); other IO keeps cause.
65
66
  try {
66
67
  const stat = statSync(fullPath);
67
68
  matches.push({ path: fullPath, mtime: stat.mtimeMs });
68
- } catch {
69
- matches.push({ path: fullPath, mtime: 0 });
69
+ } catch (error) {
70
+ if (isEnoent(error)) continue;
71
+ throw error;
70
72
  }
71
73
  }
72
74
  }
73
75
  }
74
- scan(sessionsDir);
76
+ try {
77
+ scan(sessionsDir);
78
+ } catch (error) {
79
+ if (isEnoent(error)) return undefined;
80
+ throw error;
81
+ }
75
82
  if (matches.length === 0 || matches[0] === undefined) return undefined;
76
83
  matches.sort((a, b) => b.mtime - a.mtime);
77
84
  return matches[0]?.path;
@@ -118,55 +125,18 @@ function codexSessionsDirectory(home?: string): string {
118
125
  return join(process.env.CODEX_HOME || join(home || homedir(), ".codex"), "sessions");
119
126
  }
120
127
 
121
- /** Count each host activation, including Pi turns without an external copy. */
122
- export function recordRunStart(runDirectory: string): void {
123
- try { appendFileSync(join(runDirectory, ".run-starts"), "\n"); }
124
- catch (error) { declareHostSessionFailure(error); }
125
- }
126
-
127
128
  /**
128
- * Resolve the host dossier landing destination under `<run>/session/`.
129
- * Format: `<run>/session/<host>-<model>-<n>` (single file adds .jsonl; model slashes replaced with -).
130
- * Ordinal n is the start/resume count in the run (initial 1, each resume +1).
129
+ * Host original landing under `<run>/session/`: one file per host, never
130
+ * numbered — every exit overwrites the same name (#1161).
131
+ * codex / claude: `<host>.jsonl`; grok-build: directory `<host>`.
131
132
  */
132
133
  export function resolveHostDossierLandingPath(options: {
133
134
  readonly host: string;
134
- readonly model?: { readonly model?: string; readonly thinking?: string } | undefined;
135
135
  readonly sessionDirectory: string;
136
- readonly continuation: RoleTurnContinuation;
137
- }): { readonly landingPath: string; readonly ordinal: number; readonly sanitizedModel: string } {
138
- const sanitizedModel = (options.model?.model ?? "default").replace(/[\/\\]+/g, "-");
139
- let maxOrdinal = 0;
140
-
141
- if (existsSync(options.sessionDirectory)) {
142
- try {
143
- const entries = readdirSync(options.sessionDirectory);
144
- for (const entry of entries) {
145
- const match = entry.match(/-(\d+)(?:\.jsonl|\.failed)?$/);
146
- const first = match?.[1];
147
- if (first !== undefined) {
148
- const val = parseInt(first, 10);
149
- if (!isNaN(val) && val > maxOrdinal) {
150
- maxOrdinal = val;
151
- }
152
- }
153
- }
154
- } catch {
155
- // Ignore directory read errors
156
- }
157
- }
158
-
159
- const startsPath = join(dirname(options.sessionDirectory), ".run-starts");
160
- let starts = 0;
161
- if (existsSync(startsPath)) starts = readFileSync(startsPath, "utf8").length;
162
- const ordinal = Math.max(maxOrdinal + 1, starts, options.continuation.kind === "resume" ? 2 : 1);
163
-
164
- const baseName = `${options.host}-${sanitizedModel}-${ordinal}`;
165
- const landingPath = options.host === "grok-build"
166
- ? join(options.sessionDirectory, baseName)
167
- : join(options.sessionDirectory, `${baseName}.jsonl`);
168
-
169
- return { landingPath, ordinal, sanitizedModel };
136
+ }): string {
137
+ return options.host === "grok-build"
138
+ ? join(options.sessionDirectory, options.host)
139
+ : join(options.sessionDirectory, `${options.host}.jsonl`);
170
140
  }
171
141
 
172
142
  /** Record native session pointer line to Sitian as soon as session ID is known. */
@@ -212,29 +182,24 @@ function copyFileCloneOrFallback(src: string, dest: string): void {
212
182
  copyFileSync(src, dest);
213
183
  }
214
184
 
215
- /** Copy both grok-build originals: chat_history.jsonl and usage.json into destDir. */
185
+ /**
186
+ * Copy both grok-build originals: chat_history.jsonl and usage.json into destDir.
187
+ * Direct copy — no existsSync/access preflight. Real ENOENT/EACCES surface from
188
+ * the copy itself (Node fs: do not exists/access before open — #1161 C3-io).
189
+ */
216
190
  function copyGrokDossier(srcDir: string, destDir: string): void {
217
- const chatHistorySrc = join(srcDir, "chat_history.jsonl");
218
- if (!existsSync(chatHistorySrc)) {
219
- throw new Error(`Grok native chat_history.jsonl missing at ${chatHistorySrc}`);
220
- }
221
191
  mkdirSync(destDir, { recursive: true });
222
- const chatHistoryDest = join(destDir, "chat_history.jsonl");
223
- copyFileCloneOrFallback(chatHistorySrc, chatHistoryDest);
224
-
225
- const usageSrc = join(srcDir, "usage.json");
226
- if (!existsSync(usageSrc)) {
227
- throw new Error(`Grok native usage.json missing at ${usageSrc}`);
228
- }
229
- copyFileCloneOrFallback(usageSrc, join(destDir, "usage.json"));
192
+ copyFileCloneOrFallback(join(srcDir, "chat_history.jsonl"), join(destDir, "chat_history.jsonl"));
193
+ copyFileCloneOrFallback(join(srcDir, "usage.json"), join(destDir, "usage.json"));
230
194
  }
231
195
 
232
196
  /**
233
- * Copy native host session original to run session dossier after child process exits.
234
- * If copy fails, retries once.
235
- * On success, appends native-session-copy line to Sitian.
236
- * On repeated failure, appends native-session-warning line to Sitian.
237
- * Does not throw; turn outcome is never altered by copy success or failure.
197
+ * Copy the native host session original over the run's single landing path
198
+ * after the child process exits. Copies beside the landing path and swaps in
199
+ * on success, so a failed copy never destroys the previous good original.
200
+ * Retries once. Success appends a native-session-copy line, repeated failure a
201
+ * native-session-warning line. Does not throw; the turn outcome is never
202
+ * altered by copy success or failure.
238
203
  */
239
204
  export function copyAndRecordHostDossier(options: {
240
205
  readonly host: string;
@@ -242,102 +207,221 @@ export function copyAndRecordHostDossier(options: {
242
207
  readonly cwd: string;
243
208
  readonly sessionDirectory: string;
244
209
  readonly sessionParent: string;
245
- readonly continuation: RoleTurnContinuation;
246
- readonly model?: { readonly model?: string; readonly thinking?: string } | undefined;
247
210
  readonly home?: string | undefined;
248
211
  }): void {
249
- if (options.host === "pi" || options.host === "hermes") {
212
+ if (options.host === "hermes") {
250
213
  return;
251
214
  }
252
- const nativePath = resolveNativeSessionPath({
215
+ const report = (payload: Record<string, unknown>): void => sitianReportSafe({
216
+ level: "event",
217
+ kind: HOST_SESSION_RECORD_KIND,
253
218
  host: options.host,
254
- sessionId: options.sessionId,
255
219
  cwd: options.cwd,
256
- ...(options.home !== undefined ? { home: options.home } : {}),
220
+ sessionParent: options.sessionParent,
221
+ source: `${options.host}-dossier`,
222
+ payload,
257
223
  });
258
- if (nativePath === undefined && options.host !== "codex") return;
259
-
260
- let destination: ReturnType<typeof resolveHostDossierLandingPath>;
261
- try {
262
- destination = resolveHostDossierLandingPath(options);
263
- } catch (error) {
264
- sitianReportSafe({
265
- level: "event",
266
- kind: HOST_SESSION_RECORD_KIND,
267
- host: options.host,
268
- cwd: options.cwd,
269
- sessionParent: options.sessionParent,
270
- source: `${options.host}-dossier`,
271
- payload: {
272
- type: "native-session-warning",
273
- sessionId: options.sessionId,
274
- error: errorText(error),
275
- },
224
+ // Pi already lands at session/session.jsonl; record that landing through the
225
+ // same native-session-copy row current.json projects — no copy, no second
226
+ // original parser.
227
+ if (options.host === "pi") {
228
+ report({
229
+ type: "native-session-copy",
230
+ nativePath: options.sessionParent,
231
+ landingPath: options.sessionParent,
232
+ sessionId: options.sessionId,
276
233
  });
277
234
  return;
278
235
  }
279
- const { landingPath, ordinal, sanitizedModel } = destination;
236
+
237
+ const landingPath = resolveHostDossierLandingPath(options);
238
+ // Per-attempt staging/previous names: concurrent copies of the same landing
239
+ // must not share one `.copying` / `.previous` path (#1161 C2). Node's fs
240
+ // copy is not atomic; unique sibling + rename is the smallest ownership fix.
241
+ // Swap-failure restores that attempt's own `.previous.<token>` inline — no
242
+ // next-call orphan scan (J6: do not invent a second recovery mechanism).
243
+ //
244
+ // Native-path discovery (incl. Codex rollout scan) stays inside this attempt
245
+ // try: ADR 0086 one-retry + native-session-warning covers discover/copy
246
+ // failure alike. A throw outside would abort the host leg (#1161 C3-io).
280
247
 
281
248
  let lastError: unknown;
282
249
  let copySuccess = false;
250
+ let stagingForCleanup: string | undefined;
251
+ let nativePath: string | undefined;
283
252
  for (let attempt = 1; attempt <= 2; attempt++) {
253
+ const attemptToken = randomBytes(6).toString("hex");
254
+ const staging = `${landingPath}.copying.${attemptToken}`;
255
+ stagingForCleanup = staging;
284
256
  try {
257
+ nativePath = resolveNativeSessionPath({
258
+ host: options.host,
259
+ sessionId: options.sessionId,
260
+ cwd: options.cwd,
261
+ ...(options.home !== undefined ? { home: options.home } : {}),
262
+ });
263
+ if (nativePath === undefined && options.host !== "codex") {
264
+ stagingForCleanup = undefined;
265
+ return;
266
+ }
285
267
  if (options.host === "grok-build") {
286
- copyGrokDossier(nativePath!, landingPath);
268
+ copyGrokDossier(nativePath!, staging);
287
269
  } else {
288
- if (nativePath === undefined || !existsSync(nativePath)) {
270
+ // Undefined path = discovery found nothing (true absence). Present path:
271
+ // copy directly so EACCES/EPERM keep their cause — no existsSync wash.
272
+ if (nativePath === undefined) {
289
273
  throw new Error(`Native session file missing for ${options.host} session ${options.sessionId}`);
290
274
  }
291
275
  mkdirSync(dirname(landingPath), { recursive: true });
292
- copyFileCloneOrFallback(nativePath, landingPath);
276
+ copyFileCloneOrFallback(nativePath, staging);
277
+ }
278
+ if (options.host === "grok-build") {
279
+ // A directory cannot be renamed over: park the previous original aside,
280
+ // move the new one in, and put the old one back only if the landing is
281
+ // still absent. This attempt owns its `.previous.<token>` through
282
+ // success or failure — restore-if-absent, then clear the token when it
283
+ // is no longer the unique original (peer may have published landing).
284
+ // Never delete previous when restore failed and landing is still absent.
285
+ // Cleanup failure must not wash into native-session-copy success
286
+ // (#1161 C2 / 失败诚实). No orphan scan, no cross-call recovery, no queue.
287
+ const previous = `${landingPath}.previous.${attemptToken}`;
288
+ const hadPrevious = existsSync(landingPath);
289
+ let parked = false;
290
+ let keepUniquePrevious = false;
291
+ let cleanupError: unknown;
292
+ let swapFailure: unknown;
293
+ let restoreError: unknown;
294
+ try {
295
+ if (hadPrevious) {
296
+ renameSync(landingPath, previous);
297
+ parked = true;
298
+ }
299
+ try {
300
+ renameSync(staging, landingPath);
301
+ } catch (swapError) {
302
+ if (parked && existsSync(previous) && !existsSync(landingPath)) {
303
+ try {
304
+ renameSync(previous, landingPath);
305
+ } catch (error) {
306
+ restoreError = error;
307
+ // Restore failed; previous may still be the only old original.
308
+ if (existsSync(previous) && !existsSync(landingPath)) {
309
+ keepUniquePrevious = true;
310
+ }
311
+ }
312
+ }
313
+ swapFailure = swapError;
314
+ }
315
+ } finally {
316
+ if (parked && existsSync(previous) && !keepUniquePrevious) {
317
+ try {
318
+ rmSync(previous, { recursive: true, force: true });
319
+ } catch (error) {
320
+ cleanupError = error;
321
+ }
322
+ }
323
+ }
324
+ if (swapFailure !== undefined) {
325
+ // Keep every residual cause; previous cleanup names its path explicitly
326
+ // (Node errno usually embeds path — still pass residual so AggregateError
327
+ // never silently omits which leftover remains; #1161 C2).
328
+ lastError = combineCopyFailureCauses(
329
+ swapFailure,
330
+ restoreError,
331
+ cleanupError,
332
+ cleanupError === undefined ? undefined : previous,
333
+ );
334
+ // Ordinary swap fail with restore+cleanup success → ADR 0086 retry once.
335
+ // Restore or cleanup incomplete → keep residual paths; do not retry
336
+ // (retry would re-park / wash residual-previous causes).
337
+ if (restoreError !== undefined || cleanupError !== undefined) {
338
+ break;
339
+ }
340
+ try {
341
+ rmSync(staging, { recursive: true, force: true });
342
+ stagingForCleanup = undefined;
343
+ } catch (stagingCleanupError) {
344
+ lastError = combineCopyFailureCauses(
345
+ swapFailure,
346
+ undefined,
347
+ stagingCleanupError,
348
+ staging,
349
+ );
350
+ // Residual already recorded; clear so the final pass does not re-rm / duplicate.
351
+ stagingForCleanup = undefined;
352
+ break;
353
+ }
354
+ continue;
355
+ }
356
+ if (cleanupError !== undefined) {
357
+ // Swap landed but cleanup ownership is incomplete — leave the true
358
+ // cause with residual previous path; do not retry or claim copy.
359
+ lastError = combineCopyFailureCauses(cleanupError, undefined, cleanupError, previous);
360
+ stagingForCleanup = undefined;
361
+ break;
362
+ }
363
+ } else {
364
+ renameSync(staging, landingPath); // atomically replaces the previous file
293
365
  }
294
366
  copySuccess = true;
367
+ stagingForCleanup = undefined;
295
368
  break;
296
369
  } catch (error) {
297
370
  lastError = error;
371
+ try {
372
+ rmSync(staging, { recursive: true, force: true });
373
+ stagingForCleanup = undefined;
374
+ } catch (cleanupError) {
375
+ lastError = combineCopyFailureCauses(error, undefined, cleanupError, staging);
376
+ // Residual already recorded; clear so the final pass does not re-rm / duplicate.
377
+ stagingForCleanup = undefined;
378
+ break;
379
+ }
298
380
  }
299
381
  }
300
382
 
301
383
  if (copySuccess) {
302
- sitianReportSafe({
303
- level: "event",
304
- kind: HOST_SESSION_RECORD_KIND,
305
- host: options.host,
306
- cwd: options.cwd,
307
- sessionParent: options.sessionParent,
308
- source: `${options.host}-dossier`,
309
- payload: {
310
- type: "native-session-copy",
311
- nativePath,
312
- landingPath,
313
- ordinal,
314
- sessionId: options.sessionId,
315
- },
316
- });
384
+ report({ type: "native-session-copy", nativePath, landingPath, sessionId: options.sessionId });
317
385
  } else {
318
- // A partial first attempt must not masquerade as a complete native original.
319
- try { rmSync(landingPath, { recursive: options.host === "grok-build", force: true }); }
320
- catch { /* warning below reports the original copy failure */ }
321
- // Reserve the failed ordinal outside the native dossier landing namespace.
322
- try {
323
- mkdirSync(options.sessionDirectory, { recursive: true });
324
- closeSync(openSync(join(options.sessionDirectory, `${options.host}-${sanitizedModel}-${ordinal}.failed`), "a"));
325
- } catch { /* the warning below retains the original copy failure */ }
326
- sitianReportSafe({
327
- level: "event",
328
- kind: HOST_SESSION_RECORD_KIND,
329
- host: options.host,
330
- cwd: options.cwd,
331
- sessionParent: options.sessionParent,
332
- source: `${options.host}-dossier`,
333
- payload: {
334
- type: "native-session-warning",
335
- nativePath,
336
- landingPath,
337
- ordinal,
338
- sessionId: options.sessionId,
339
- error: errorText(lastError),
340
- },
386
+ // A partial attempt must not masquerade as a complete native original.
387
+ if (stagingForCleanup !== undefined) {
388
+ try {
389
+ rmSync(stagingForCleanup, { recursive: true, force: true });
390
+ } catch (cleanupError) {
391
+ lastError = combineCopyFailureCauses(lastError, undefined, cleanupError, stagingForCleanup);
392
+ }
393
+ }
394
+ report({
395
+ type: "native-session-warning",
396
+ nativePath,
397
+ landingPath,
398
+ sessionId: options.sessionId,
399
+ error: errorText(lastError),
341
400
  });
342
401
  }
343
402
  }
403
+
404
+ /** Fold swap / restore / staging-or-previous cleanup causes without washing any. */
405
+ function combineCopyFailureCauses(
406
+ primary: unknown,
407
+ restoreError: unknown,
408
+ cleanupError: unknown,
409
+ residualPath?: string,
410
+ ): unknown {
411
+ const errors: unknown[] = [primary];
412
+ const parts = [errorText(primary)];
413
+ if (restoreError !== undefined) {
414
+ errors.push(restoreError);
415
+ parts.push(`restore failed: ${errorText(restoreError)}`);
416
+ }
417
+ if (cleanupError !== undefined) {
418
+ if (cleanupError !== primary) errors.push(cleanupError);
419
+ parts.push(
420
+ residualPath === undefined
421
+ ? `cleanup failed: ${errorText(cleanupError)}`
422
+ : `cleanup failed: ${errorText(cleanupError)} (residual: ${residualPath})`,
423
+ );
424
+ }
425
+ if (errors.length === 1 && parts.length === 1) return primary;
426
+ return new AggregateError(errors, parts.join("; "), { cause: primary });
427
+ }
@@ -74,7 +74,7 @@ async function requireRunDirectory(candidate: string, display: string): Promise<
74
74
  * - absolute/relative path that realpath's to that same book runs slot
75
75
  *
76
76
  * Rejects project-tree projections and any path outside the project's ledger book runs.
77
- * Retained `run-state.json` must match basename identity and book binding.
77
+ * Retained state.jsonl run-state must match basename identity and book binding.
78
78
  */
79
79
  export async function resolveNotarySourceRunLocator(options: {
80
80
  readonly projectRoot: string;
@@ -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" },
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;