@akagilnc/pi-workflow-roles 0.1.5614 → 0.1.5650

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 (35) hide show
  1. package/dist/acp-host/production-host.js +960 -465
  2. package/dist/activation-ledger-topology.js +12 -0
  3. package/dist/external-host-turn-loop.js +281 -0
  4. package/dist/headless-host/production-host.js +900 -406
  5. package/dist/migrate-book-topology.js +8 -5
  6. package/dist/pi/role-turn-host.js +33 -3
  7. package/dist/public-cli/instruction-seat-run.js +177 -46
  8. package/dist/public-cli/invocation.js +5 -12
  9. package/dist/public-cli/main.js +541 -165
  10. package/dist/public-cli/post-admission.js +171 -18
  11. package/dist/public-cli/settlement.js +20 -10
  12. package/dist/report-ticket-tool.js +136 -0
  13. package/dist/role-run-placement.js +1 -2
  14. package/dist/role-run-relocation.js +24 -4
  15. package/package.json +1 -1
  16. package/resources/engine-dispatch.md +6 -3
  17. package/resources/engines/claude-code.md +4 -5
  18. package/resources/missing-ticket-reask.md +1 -0
  19. package/src/acp-host/role-turn-host.ts +41 -31
  20. package/src/activation-ledger-topology.ts +16 -0
  21. package/src/external-host-turn-loop.ts +103 -12
  22. package/src/headless-host/role-turn-host.ts +41 -25
  23. package/src/pi/role-turn-host.ts +37 -3
  24. package/src/public-cli/instruction-seat-run.ts +248 -50
  25. package/src/public-cli/invocation.ts +15 -27
  26. package/src/public-cli/post-admission.ts +250 -33
  27. package/src/public-cli/run-lifecycle.ts +5 -5
  28. package/src/public-cli/settlement.ts +38 -11
  29. package/src/public-cli/terminal.ts +6 -0
  30. package/src/report-ticket-tool.ts +176 -0
  31. package/src/role-envelope.ts +28 -3
  32. package/src/role-run-placement.ts +0 -2
  33. package/src/role-run-relocation.ts +45 -4
  34. package/src/role-runtime.ts +6 -0
  35. package/src/worker-submission-gates.ts +26 -3
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
3
 
4
4
  import type { RoleTurnHost, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
5
- import { sessionDirectoryOf } from "../role-run-placement.ts";
5
+ import { sessionDirectoryOf, sessionFileOf } from "../role-run-placement.ts";
6
6
  import {
7
7
  createSerializedRoleTurnHost,
8
8
  driveExternalRoleTurnRounds,
@@ -10,9 +10,12 @@ import {
10
10
  externalHostFailure as failure,
11
11
  raceAgainstHostAbort,
12
12
  recordTurnDelivery,
13
+ retainPackageFaultBesideLivePlacement,
14
+ syncTurnRequestLivePlacement,
15
+ trySyncTurnRequestLivePlacement,
13
16
  } from "../external-host-turn-loop.ts";
14
17
  import { describeErrorIdentity } from "../public-cli/run-lifecycle.ts";
15
- import { projectThrownFailureLeaf, retainPackageFault } from "../public-cli/settlement.ts";
18
+ import { projectThrownFailureLeaf } from "../public-cli/settlement.ts";
16
19
 
17
20
  import {
18
21
  copyAndRecordHostDossier,
@@ -260,7 +263,9 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
260
263
  let sessionId: string | undefined;
261
264
  let sessionOpened = false;
262
265
  let accepted = false;
263
- const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
266
+ // Re-resolve after mid-turn report-ticket may have moved principal (#1171).
267
+ const liveSessionParent = (): string =>
268
+ config.sessionIdentity.resolveSessionFile(request.principal);
264
269
  let outcome: RoleTurnResult = failure("session", "AcpNoOutcome", "no-outcome");
265
270
  let reportedFailure: RoleTurnResult["knownFailure"];
266
271
  let packageFailure: RoleTurnResult["knownFailure"];
@@ -321,7 +326,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
321
326
  ? loaded.sessionId
322
327
  : bindSessionId;
323
328
  if (config.hostName !== "hermes") recordNativeSessionPointer({
324
- host: config.hostName, sessionId: loadedId, cwd: request.cwd, sessionParent, home: request.home,
329
+ host: config.hostName, sessionId: loadedId, cwd: request.cwd, sessionParent: liveSessionParent(), home: request.home,
325
330
  });
326
331
  return loadedId;
327
332
  };
@@ -350,7 +355,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
350
355
  sessionReady = false;
351
356
  } else {
352
357
  if (config.hostName !== "hermes") recordNativeSessionPointer({
353
- host: config.hostName, sessionId, cwd: request.cwd, sessionParent, home: request.home,
358
+ host: config.hostName, sessionId, cwd: request.cwd, sessionParent: liveSessionParent(), home: request.home,
354
359
  });
355
360
  sessionOpened = true;
356
361
  await config.sessionIdentity.bind(request.principal, sessionId);
@@ -377,7 +382,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
377
382
  // MCP advertises tool.parameters; prepared.jsonSchema is the headless
378
383
  // draft-07 stamp of that clone — record the delivered parameters face.
379
384
  const { $schema: _headlessStamp, ...deliveredSchema } = prepared.jsonSchema;
380
- await recordTurnDelivery(request.runDirectory, {
385
+ await recordTurnDelivery(await syncTurnRequestLivePlacement(request), {
381
386
  systemPrompt: systemPromptOverride,
382
387
  outputSchema: deliveredSchema,
383
388
  }, "acp-host");
@@ -440,8 +445,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
440
445
  const native = connection?.result?.();
441
446
  reportedFailure = native?.knownFailure;
442
447
  outcome = native ?? { code: null, stderr: connection?.stderr?.() ?? "", timedOut: false };
443
- await retainPackageFault({
444
- runDirectory: request.runDirectory,
448
+ await retainPackageFaultBesideLivePlacement(request, {
445
449
  diagnostic: `ACP turn exception beside native host facts: ${describeErrorIdentity(error)}`,
446
450
  error,
447
451
  });
@@ -458,8 +462,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
458
462
  if (sessionId !== undefined && !accepted) {
459
463
  try { connection.notify("session/cancel", { sessionId }); }
460
464
  catch (error) {
461
- await retainPackageFault({
462
- runDirectory: request.runDirectory,
465
+ await retainPackageFaultBesideLivePlacement(request, {
463
466
  diagnostic: `session cancel failed beside host terminal: ${describeErrorIdentity(error)}`,
464
467
  error,
465
468
  });
@@ -467,8 +470,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
467
470
  }
468
471
  try { naturalTermination = await connection.close(); }
469
472
  catch (error) {
470
- await retainPackageFault({
471
- runDirectory: request.runDirectory,
473
+ await retainPackageFaultBesideLivePlacement(request, {
472
474
  diagnostic: `connection close failed beside host terminal: ${describeErrorIdentity(error)}`,
473
475
  error,
474
476
  });
@@ -489,31 +491,39 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
489
491
  || (naturalTermination !== undefined && (native.timedOut || native.signal !== undefined));
490
492
  let knownFailure = outcome.knownFailure;
491
493
  if (nativeFailed && knownFailure !== undefined && knownFailure !== reportedFailure) {
492
- if (knownFailure !== packageFailure) await retainPackageFault({
493
- runDirectory: request.runDirectory,
494
- diagnostic: knownFailure.diagnostic ?? "ACP package failure beside late native termination",
495
- error: knownFailure,
496
- });
494
+ if (knownFailure !== packageFailure) {
495
+ await retainPackageFaultBesideLivePlacement(request, {
496
+ diagnostic: knownFailure.diagnostic ?? "ACP package failure beside late native termination",
497
+ error: knownFailure,
498
+ });
499
+ }
497
500
  knownFailure = reportedFailure;
498
501
  }
499
502
  const { knownFailure: _nativeFailure, ...facts } = native;
500
503
  outcome = { ...facts, ...(knownFailure === undefined ? {} : { knownFailure }) };
501
504
  }
502
505
  if (config.hostName !== "hermes" && sessionOpened && sessionId !== undefined) {
503
- try {
504
- copyAndRecordHostDossier({
505
- host: config.hostName,
506
- sessionId,
507
- cwd: request.cwd,
508
- sessionDirectory: sessionDirectoryOf(request.runDirectory),
509
- sessionParent,
510
- ...(request.home !== undefined ? { home: request.home } : {}),
511
- });
512
- } catch (error) {
513
- await retainPackageFault({
514
- runDirectory: request.runDirectory,
515
- diagnostic: `host dossier copy failed beside host terminal: ${describeErrorIdentity(error)}`,
516
- error,
506
+ const live = await trySyncTurnRequestLivePlacement(request);
507
+ if ("runDirectory" in live) {
508
+ try {
509
+ copyAndRecordHostDossier({
510
+ host: config.hostName,
511
+ sessionId,
512
+ cwd: request.cwd,
513
+ sessionDirectory: sessionDirectoryOf(live.runDirectory),
514
+ sessionParent: sessionFileOf(live.runDirectory),
515
+ ...(request.home !== undefined ? { home: request.home } : {}),
516
+ });
517
+ } catch (error) {
518
+ await retainPackageFaultBesideLivePlacement(request, {
519
+ diagnostic: `host dossier copy failed beside host terminal: ${describeErrorIdentity(error)}`,
520
+ error,
521
+ });
522
+ }
523
+ } else {
524
+ await retainPackageFaultBesideLivePlacement(request, {
525
+ diagnostic: `host dossier copy skipped: live run path resolve failed beside host terminal: ${describeErrorIdentity(live.resolveError)}`,
526
+ error: live.resolveError,
517
527
  });
518
528
  }
519
529
  }
@@ -45,6 +45,22 @@ export function tryHomeFromAkRolesPath(path: string): string | undefined {
45
45
  return path.slice(0, match.index);
46
46
  }
47
47
 
48
+ /**
49
+ * Soft derive: book key owning a path under `.ak-roles/books/<bookKey>/…`.
50
+ * Same topology family as tryHomeFromAkRolesPath — no cross-book invent.
51
+ */
52
+ export function tryBookKeyFromAkRolesPath(path: string): string | undefined {
53
+ const home = tryHomeFromAkRolesPath(path);
54
+ if (home === undefined || home.length === 0) return undefined;
55
+ const booksRoot = join(resolveActivationLedgerHome(home), "books");
56
+ const rel = relative(booksRoot, resolve(path));
57
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
58
+ return undefined;
59
+ }
60
+ const bookKey = rel.split(sep)[0];
61
+ return bookKey === undefined || bookKey === "" ? undefined : bookKey;
62
+ }
63
+
48
64
  /**
49
65
  * Hard derive: package home owning a run/session path under `.ak-roles/`.
50
66
  * Fails typed when the path is not ledger topology (no HOME fallback).
@@ -3,6 +3,9 @@
3
3
  * One copy: serial, abort merge/race, closeRound re-ask rounds,
4
4
  * host-aborted, round-limit. Last hop = ExternalHostTurnDriver (four verbs).
5
5
  */
6
+ import { existsSync } from "node:fs";
7
+ import { basename } from "node:path";
8
+
6
9
  import type {
7
10
  RoleTurnHost,
8
11
  RoleTurnKnownFailure,
@@ -10,12 +13,105 @@ import type {
10
13
  RoleTurnResult,
11
14
  } from "./host-contracts.ts";
12
15
  import type { PreparedRoleTurn } from "./prepared-role-turn.ts";
16
+ import { tryBookKeyFromAkRolesPath } from "./activation-ledger-topology.ts";
13
17
  import { projectThrownFailureLeaf, retainPackageFault } from "./public-cli/settlement.ts";
14
- import { describeErrorIdentity } from "./public-cli/run-lifecycle.ts";
18
+ import { describeErrorIdentity, findRunDirectoryById } from "./public-cli/run-lifecycle.ts";
15
19
  import { deliveryLimitFromConfig } from "./receipt-delivery-policy.ts";
20
+ import { parseRunLeaf } from "./role-run-placement.ts";
21
+ import { projectTurnRequestLiveRunDirectory } from "./role-run-relocation.ts";
16
22
  import { reportRunRecord } from "./sitian-facade.ts";
17
23
  import { isOneShotWorkerReminderCode } from "./submission-errors.ts";
18
24
 
25
+ /**
26
+ * #1171: mid-turn report-ticket may have moved the leg. Prefer the recorded
27
+ * path when it still exists; otherwise re-locate by run id inside the book.
28
+ * Undefined means vanished and unlocatable — callers must not revive the dead leaf.
29
+ */
30
+ export async function resolveLiveRunDirectoryPath(
31
+ recordedPath: string,
32
+ home: string,
33
+ ): Promise<string | undefined> {
34
+ if (existsSync(recordedPath)) return recordedPath;
35
+ const parsed = parseRunLeaf(basename(recordedPath));
36
+ const bookKey = tryBookKeyFromAkRolesPath(recordedPath);
37
+ if (parsed === undefined || bookKey === undefined) return undefined;
38
+ return await findRunDirectoryById(
39
+ home,
40
+ parsed.runId,
41
+ bookKey,
42
+ parsed.role,
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Prefer the request path when it still exists; otherwise re-locate by run id
48
+ * (resolveLiveRunDirectoryPath). Mutates the live request so later turn-record /
49
+ * exit-copy / fault notes stay on the new leaf.
50
+ */
51
+ export async function syncTurnRequestLivePlacement(
52
+ request: RoleTurnRequest,
53
+ ): Promise<string> {
54
+ const live = await resolveLiveRunDirectoryPath(request.runDirectory, request.home);
55
+ if (live === undefined) {
56
+ const parsed = parseRunLeaf(basename(request.runDirectory));
57
+ throw new Error(
58
+ `admitted run ${parsed?.runId ?? "(unparseable)"} missing after host turn; not found under ${request.home}`,
59
+ );
60
+ }
61
+ if (live !== request.runDirectory) {
62
+ projectTurnRequestLiveRunDirectory(
63
+ request as {
64
+ runDirectory: string;
65
+ principal: { sessionDirectory?: string; sessionFile?: string };
66
+ },
67
+ live,
68
+ );
69
+ }
70
+ return live;
71
+ }
72
+
73
+ /**
74
+ * Soft live-path sync for cleanup / dossier work: lookup failure stays beside the
75
+ * caller and never revives a vanished leaf. Same authority as syncTurnRequestLivePlacement.
76
+ */
77
+ export async function trySyncTurnRequestLivePlacement(
78
+ request: RoleTurnRequest,
79
+ ): Promise<{ readonly runDirectory: string } | { readonly resolveError: unknown }> {
80
+ try {
81
+ return { runDirectory: await syncTurnRequestLivePlacement(request) };
82
+ } catch (resolveError) {
83
+ return { resolveError };
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Retain a package fault on the live leaf when locatable. Lookup failure must not
89
+ * replace the original fault or block later cleanup — reuse retainPackageFault when
90
+ * the leaf exists; otherwise present both causes on stderr (Pi transport path shape).
91
+ */
92
+ export async function retainPackageFaultBesideLivePlacement(
93
+ request: RoleTurnRequest,
94
+ input: { readonly diagnostic: string; readonly error?: unknown },
95
+ ): Promise<void> {
96
+ const live = await trySyncTurnRequestLivePlacement(request);
97
+ if ("resolveError" in live) {
98
+ const resolveDiagnostic =
99
+ `live run path resolve failed beside fault: ${describeErrorIdentity(live.resolveError)}`;
100
+ try {
101
+ process.stderr.write(`${input.diagnostic}\n`);
102
+ process.stderr.write(`${resolveDiagnostic}\n`);
103
+ } catch {
104
+ // Best-effort presentation beside an already-chosen terminal.
105
+ }
106
+ return;
107
+ }
108
+ await retainPackageFault({
109
+ runDirectory: live.runDirectory,
110
+ diagnostic: input.diagnostic,
111
+ ...(Object.hasOwn(input, "error") ? { error: input.error } : {}),
112
+ });
113
+ }
114
+
19
115
  /**
20
116
  * What one host start was given: one `turn-delivery` row of history.jsonl per start. History is
21
117
  * the volume whose write failure does not stop a run (#833), so a row that cannot be
@@ -187,8 +283,7 @@ export async function driveExternalRoleTurnRounds(
187
283
  try {
188
284
  closure = await prepared.closeRound();
189
285
  } catch (error) {
190
- await retainPackageFault({
191
- runDirectory: request.runDirectory,
286
+ await retainPackageFaultBesideLivePlacement(request, {
192
287
  diagnostic: `round closure failed beside host terminal: ${describeErrorIdentity(error)}`,
193
288
  error,
194
289
  });
@@ -199,8 +294,7 @@ export async function driveExternalRoleTurnRounds(
199
294
  if (closure.accepted) {
200
295
  try { await driver.afterAccepted?.(); }
201
296
  catch (error) {
202
- await retainPackageFault({
203
- runDirectory: request.runDirectory,
297
+ await retainPackageFaultBesideLivePlacement(request, {
204
298
  diagnostic: `post-acceptance close failed beside host terminal: ${describeErrorIdentity(error)}`,
205
299
  error,
206
300
  });
@@ -208,8 +302,7 @@ export async function driveExternalRoleTurnRounds(
208
302
  return result;
209
303
  }
210
304
  if ("failure" in closure) {
211
- await retainPackageFault({
212
- runDirectory: request.runDirectory,
305
+ await retainPackageFaultBesideLivePlacement(request, {
213
306
  diagnostic: `round closure failed beside host terminal: ${JSON.stringify(closure.failure)}`,
214
307
  });
215
308
  // This is an explicit envelope failure (e.g. required audit/ledger failure),
@@ -225,8 +318,7 @@ export async function driveExternalRoleTurnRounds(
225
318
  else {
226
319
  countedReasks += 1;
227
320
  if (countedReasks > countedReaskLimit) {
228
- await retainPackageFault({
229
- runDirectory: request.runDirectory,
321
+ await retainPackageFaultBesideLivePlacement(request, {
230
322
  diagnostic: `${driver.roundLimitName}: round-retry-limit`,
231
323
  });
232
324
  return result;
@@ -242,14 +334,13 @@ export async function driveExternalRoleTurnRounds(
242
334
  * failure stays primary. Ordinary cleanup is handled at its owning seam. */
243
335
  export async function disposeExternalRoleTurn(
244
336
  prepared: Pick<PreparedRoleTurn, "dispose">,
245
- request: Pick<RoleTurnRequest, "runDirectory">,
337
+ request: RoleTurnRequest,
246
338
  outcome: RoleTurnResult,
247
339
  ): Promise<RoleTurnResult> {
248
340
  try {
249
341
  await prepared.dispose?.();
250
342
  } catch (error) {
251
- await retainPackageFault({
252
- runDirectory: request.runDirectory,
343
+ await retainPackageFaultBesideLivePlacement(request, {
253
344
  diagnostic: `required envelope shutdown failed beside host terminal: ${describeErrorIdentity(error)}`,
254
345
  error,
255
346
  });
@@ -11,7 +11,7 @@ import { tmpdir } from "node:os";
11
11
  import { dirname, isAbsolute, join, resolve } from "node:path";
12
12
 
13
13
  import type { RoleTurnHost, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
14
- import { sessionDirectoryOf } from "../role-run-placement.ts";
14
+ import { sessionDirectoryOf, sessionFileOf } from "../role-run-placement.ts";
15
15
  import {
16
16
  createSerializedRoleTurnHost,
17
17
  driveExternalRoleTurnRounds,
@@ -20,9 +20,12 @@ import {
20
20
  hostAbortedError,
21
21
  isHostAbortedError,
22
22
  recordTurnDelivery,
23
+ retainPackageFaultBesideLivePlacement,
24
+ syncTurnRequestLivePlacement,
25
+ trySyncTurnRequestLivePlacement,
23
26
  } from "../external-host-turn-loop.ts";
24
27
  import { describeErrorIdentity } from "../public-cli/run-lifecycle.ts";
25
- import { projectThrownFailureLeaf, retainPackageFault } from "../public-cli/settlement.ts";
28
+ import { projectThrownFailureLeaf } from "../public-cli/settlement.ts";
26
29
  import {
27
30
  renderSystemPromptOverride,
28
31
  resolveBoundHostSessionId,
@@ -467,7 +470,9 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
467
470
  // CLI start-up inputs (system prompt / schema / MCP config files) are not
468
471
  // dossier: they live in a throwaway directory, and what was delivered is
469
472
  // recorded once per actual start as a history.jsonl `turn-delivery` row.
470
- const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
473
+ // Re-resolve after mid-turn report-ticket may have moved principal (#1171).
474
+ const liveSessionParent = (): string =>
475
+ config.sessionIdentity.resolveSessionFile(request.principal);
471
476
  const inputsDirectory = await mkdtemp(join(tmpdir(), "ak-role-headless-"));
472
477
  const systemPromptPath = join(inputsDirectory, "system-prompt.txt");
473
478
  let mcpConfigPath: string | undefined;
@@ -500,8 +505,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
500
505
  try {
501
506
  await rm(inputsDirectory, { recursive: true, force: true });
502
507
  } catch (cleanupError) {
503
- await retainPackageFault({
504
- runDirectory: request.runDirectory,
508
+ await retainPackageFaultBesideLivePlacement(request, {
505
509
  diagnostic: `headless turn inputs cleanup failed beside setup failure: ${describeErrorIdentity(cleanupError)}`,
506
510
  error: cleanupError,
507
511
  });
@@ -552,13 +556,13 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
552
556
  host: config.hostName,
553
557
  sessionId,
554
558
  cwd: request.cwd,
555
- sessionParent,
559
+ sessionParent: liveSessionParent(),
556
560
  home: request.home,
557
561
  }) !== undefined;
558
562
  }
559
563
 
560
564
  // What this CLI start was given: one history record per start.
561
- await recordTurnDelivery(request.runDirectory, {
565
+ await recordTurnDelivery(await syncTurnRequestLivePlacement(request), {
562
566
  systemPrompt,
563
567
  ...(startedWithSchema === undefined ? {} : { outputSchema: startedWithSchema }),
564
568
  }, "headless-host");
@@ -587,7 +591,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
587
591
  if (!codex && typeof event === "object" && event !== null && "session_id" in event
588
592
  && typeof event.session_id === "string" && event.session_id !== "" && event.session_id !== sessionId) {
589
593
  sessionId = event.session_id;
590
- recordNativeSessionPointer({ host: config.hostName, sessionId, cwd: request.cwd, sessionParent, home: request.home });
594
+ recordNativeSessionPointer({ host: config.hostName, sessionId, cwd: request.cwd, sessionParent: liveSessionParent(), home: request.home });
591
595
  }
592
596
  if (codex && !codexIdObserved) {
593
597
  const tid = codexObserver?.result().threadId;
@@ -597,7 +601,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
597
601
  host: config.hostName,
598
602
  sessionId: tid,
599
603
  cwd: request.cwd,
600
- sessionParent,
604
+ sessionParent: liveSessionParent(),
601
605
  home: request.home,
602
606
  }) !== undefined;
603
607
  }
@@ -608,7 +612,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
608
612
  exitedSessionId = roundSessionId;
609
613
  if (codex && roundSessionId !== undefined && roundSessionId !== "" && !pointerRecorded) {
610
614
  try {
611
- recordNativeSessionPointer({ host: config.hostName, sessionId: roundSessionId, cwd: request.cwd, sessionParent, home: request.home });
615
+ recordNativeSessionPointer({ host: config.hostName, sessionId: roundSessionId, cwd: request.cwd, sessionParent: liveSessionParent(), home: request.home });
612
616
  } catch (error) {
613
617
  spawned.packageErrors = [...spawned.packageErrors, error];
614
618
  }
@@ -626,8 +630,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
626
630
  }
627
631
 
628
632
  for (const error of spawned.packageErrors) {
629
- await retainPackageFault({
630
- runDirectory: request.runDirectory,
633
+ await retainPackageFaultBesideLivePlacement(request, {
631
634
  diagnostic: `headless transport handling failed beside host terminal: ${describeErrorIdentity(error)}`,
632
635
  error,
633
636
  });
@@ -635,8 +638,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
635
638
 
636
639
  try {
637
640
  const noteBeside = async (error: unknown, what: string): Promise<void> => {
638
- await retainPackageFault({
639
- runDirectory: request.runDirectory,
641
+ await retainPackageFaultBesideLivePlacement(request, {
640
642
  diagnostic: `${what}: ${describeErrorIdentity(error)}`,
641
643
  error,
642
644
  });
@@ -729,8 +731,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
729
731
  }
730
732
  return deliveredFromSpawned(spawned);
731
733
  } catch (error) {
732
- await retainPackageFault({
733
- runDirectory: request.runDirectory,
734
+ await retainPackageFaultBesideLivePlacement(request, {
734
735
  diagnostic: `required turn handling failed beside host terminal: ${describeErrorIdentity(error)}`,
735
736
  error,
736
737
  });
@@ -742,24 +743,39 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
742
743
  },
743
744
  });
744
745
  } finally {
746
+ // Cleanup must not depend on live-path sync: lookup failure stays beside
747
+ // the original cleanup/copy work and never skips rm(inputsDirectory).
748
+ const live = await trySyncTurnRequestLivePlacement(request);
749
+ const liveRunDirectory = "runDirectory" in live ? live.runDirectory : undefined;
750
+ if (liveRunDirectory === undefined && "resolveError" in live) {
751
+ await retainPackageFaultBesideLivePlacement(request, {
752
+ diagnostic: `headless cleanup live path resolve failed beside host terminal: ${describeErrorIdentity(live.resolveError)}`,
753
+ error: live.resolveError,
754
+ });
755
+ }
745
756
  try {
746
757
  await rm(inputsDirectory, { recursive: true, force: true });
747
758
  } catch (error) {
748
- await retainPackageFault({
749
- runDirectory: request.runDirectory,
759
+ await retainPackageFaultBesideLivePlacement(request, {
750
760
  diagnostic: `headless turn inputs cleanup failed beside host terminal: ${describeErrorIdentity(error)}`,
751
761
  error,
752
762
  });
753
763
  }
754
764
  try {
755
- if (exitedSessionId !== undefined && exitedSessionId !== "") copyAndRecordHostDossier({
756
- host: config.hostName, sessionId: exitedSessionId, cwd: request.cwd,
757
- sessionDirectory: sessionDirectoryOf(request.runDirectory), sessionParent,
758
- ...(request.home !== undefined ? { home: request.home } : {}),
759
- });
765
+ if (
766
+ exitedSessionId !== undefined
767
+ && exitedSessionId !== ""
768
+ && liveRunDirectory !== undefined
769
+ ) {
770
+ copyAndRecordHostDossier({
771
+ host: config.hostName, sessionId: exitedSessionId, cwd: request.cwd,
772
+ sessionDirectory: sessionDirectoryOf(liveRunDirectory),
773
+ sessionParent: sessionFileOf(liveRunDirectory),
774
+ ...(request.home !== undefined ? { home: request.home } : {}),
775
+ });
776
+ }
760
777
  } catch (error) {
761
- await retainPackageFault({
762
- runDirectory: request.runDirectory,
778
+ await retainPackageFaultBesideLivePlacement(request, {
763
779
  diagnostic: `host dossier copy failed beside host terminal: ${describeErrorIdentity(error)}`,
764
780
  error,
765
781
  });
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { execFile, spawn } from "node:child_process";
7
7
  import { constants } from "node:fs";
8
+ import { existsSync } from "node:fs";
8
9
  import { access, appendFile, realpath } from "node:fs/promises";
9
10
  import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
10
11
  import { platform } from "node:process";
@@ -27,6 +28,12 @@ import { projectActivationFlags } from "../role-activation-flags.ts";
27
28
  import { encodeUserDialogueStdin } from "../user-dialogue-stdin.ts";
28
29
  import { readLedgerSessionJsonlLines, readStrictPiSessionJsonl } from "../ledger-session-read.ts";
29
30
  import { copyAndRecordHostDossier } from "../host-session-record.ts";
31
+ import {
32
+ resolveLiveRunDirectoryPath,
33
+ retainPackageFaultBesideLivePlacement,
34
+ syncTurnRequestLivePlacement,
35
+ } from "../external-host-turn-loop.ts";
36
+ import { homeFromRunDirectory } from "../activation-ledger-topology.ts";
30
37
  import { projectThrownFailureLeaf, retainPackageFault } from "../public-cli/settlement.ts";
31
38
  import { describeErrorIdentity } from "../public-cli/run-lifecycle.ts";
32
39
  import { RECEIPT_DELIVERY_LIMIT_ENV } from "../receipt-delivery-policy.ts";
@@ -358,7 +365,34 @@ export function createDefaultPiSpawnRunner(options: {
358
365
  if (stdinDeliveryError !== undefined) packageErrors.push(stdinDeliveryError);
359
366
  for (const error of packageErrors) {
360
367
  const diagnostic = `Pi transport handling failed beside host terminal: ${describeErrorIdentity(error)}`;
361
- const runDirectory = spawnOptions.env.AK_ROLE_RUN_DIR;
368
+ // #1171 F4: spawn-time path may have been renamed; reuse
369
+ // resolveLiveRunDirectoryPath (same authority as syncTurnRequestLivePlacement).
370
+ const spawnDir = spawnOptions.env.AK_ROLE_RUN_DIR;
371
+ let runDirectory: string | undefined;
372
+ if (typeof spawnDir === "string" && spawnDir.trim() !== "") {
373
+ try {
374
+ runDirectory = await resolveLiveRunDirectoryPath(
375
+ spawnDir,
376
+ homeFromRunDirectory(spawnDir),
377
+ );
378
+ } catch (resolveError) {
379
+ // Resolve threw beside an already-closed child: keep both causes
380
+ // on the existing retain/stderr presentation path (失败诚实).
381
+ runDirectory = existsSync(spawnDir) ? spawnDir : undefined;
382
+ const resolveDiagnostic =
383
+ `live run path resolve failed beside Pi transport fault: ${describeErrorIdentity(resolveError)}`;
384
+ if (runDirectory !== undefined) {
385
+ await retainPackageFault({
386
+ runDirectory,
387
+ diagnostic: resolveDiagnostic,
388
+ error: resolveError,
389
+ });
390
+ } else {
391
+ try { process.stderr.write(`${resolveDiagnostic}\n`); }
392
+ catch { /* Best-effort presentation beside the already closed child. */ }
393
+ }
394
+ }
395
+ }
362
396
  if (runDirectory !== undefined) {
363
397
  await retainPackageFault({ runDirectory, diagnostic, error });
364
398
  } else {
@@ -474,6 +508,7 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
474
508
  // bind projects host.sessions; copy records the landing. Host adapters do
475
509
  // not write current.json — public bind/settlement seams own that render.
476
510
  try {
511
+ await syncTurnRequestLivePlacement(request);
477
512
  const { sessionFile, sessionDirectory } = config.principalAuthority.decode(request.principal);
478
513
  const sessionId = await readPiSessionHeaderId(sessionFile);
479
514
  if (sessionId !== undefined) {
@@ -491,8 +526,7 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
491
526
  });
492
527
  }
493
528
  } catch (error) {
494
- await retainPackageFault({
495
- runDirectory: request.runDirectory,
529
+ await retainPackageFaultBesideLivePlacement(request, {
496
530
  diagnostic: `pi host dossier record failed beside host terminal: ${describeErrorIdentity(error)}`,
497
531
  error,
498
532
  });