pi-subagents 0.81.0 → 0.82.1

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 (152) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/bundle/agents.js +3 -3
  4. package/bundle/child-tool-plan.js +2 -2
  5. package/bundle/{chunk-L5ZHE4L4.js → chunk-3JCDWF4V.js} +4 -4
  6. package/bundle/{chunk-LSRUTHT7.js → chunk-43SCHIMD.js} +1 -1
  7. package/bundle/{chunk-VJHOELQP.js → chunk-46XKCPJR.js} +193 -81
  8. package/bundle/{chunk-G5DKFLLI.js → chunk-4SDU2BZX.js} +15 -10
  9. package/bundle/{chunk-2ZSNH26H.js → chunk-4Y2ROR7L.js} +7 -7
  10. package/bundle/{chunk-V2JWDB5U.js → chunk-52UBALJL.js} +2 -2
  11. package/bundle/{chunk-3DC5KBVP.js → chunk-5TYMSZX3.js} +1 -1
  12. package/bundle/{chunk-NNLASTQE.js → chunk-6XJB7PIX.js} +20 -6
  13. package/bundle/{chunk-ZBC44JJ3.js → chunk-CIZQXJ5Q.js} +22 -9
  14. package/bundle/{chunk-KSWQVGDW.js → chunk-FXUXCX2K.js} +225 -18
  15. package/bundle/{chunk-563BHAZ5.js → chunk-G4GZD3OJ.js} +35 -26
  16. package/bundle/{chunk-ARRKBB2B.js → chunk-GJECHSAR.js} +2 -2
  17. package/bundle/{chunk-V3BIUPLU.js → chunk-IVAE5Z6Q.js} +256 -52
  18. package/bundle/{chunk-V6MR5TOQ.js → chunk-JYUKW2SY.js} +25 -19
  19. package/bundle/{chunk-BJN5LUQY.js → chunk-OYF6AFDY.js} +1 -1
  20. package/bundle/{chunk-CEKS3TFO.js → chunk-PBK2I7WY.js} +6 -6
  21. package/bundle/{chunk-TUWLXTAM.js → chunk-PDZWJCHD.js} +681 -320
  22. package/bundle/{chunk-EYNJ6VL6.js → chunk-PLCCBLPY.js} +2 -2
  23. package/bundle/{chunk-XMFLCKMQ.js → chunk-QE4AGIMG.js} +339 -132
  24. package/bundle/{chunk-74RT5YQE.js → chunk-RMQFMMAT.js} +123 -33
  25. package/bundle/{chunk-WSBNENFS.js → chunk-UCPP4GNU.js} +14 -4
  26. package/bundle/{chunk-2XYWN7QJ.js → chunk-UTT254JE.js} +2 -2
  27. package/bundle/{chunk-BPS2AGBZ.js → chunk-VSUSLDNW.js} +93 -52
  28. package/bundle/{chunk-ZP3HODFH.js → chunk-XSWOTIA3.js} +1 -1
  29. package/bundle/{chunk-3IF2PGW7.js → chunk-YKEWIB4M.js} +1 -1
  30. package/bundle/{chunk-26C3WLYQ.js → chunk-ZIV4J4HG.js} +498 -274
  31. package/bundle/control-channel.js +2 -2
  32. package/bundle/index.js +1 -1
  33. package/bundle/intercom-bridge.js +2 -2
  34. package/bundle/preflight.js +8 -8
  35. package/bundle/project-panes.js +3 -3
  36. package/docs/agents.md +37 -7
  37. package/docs/configuration.md +9 -1
  38. package/docs/tool-reference.md +3 -1
  39. package/docs/workflows.md +16 -0
  40. package/package.json +2 -2
  41. package/skills/council-mode/SKILL.md +1 -4
  42. package/skills/pi-subagents/SKILL.md +130 -107
  43. package/skills/pi-subagents/references/orchestration.md +124 -0
  44. package/skills/pi-subagents/references/prompting-and-roles.md +96 -295
  45. package/src/agents/agent-management.js +3 -3
  46. package/src/agents/agent-memory.js +3 -1
  47. package/src/agents/agents.js +6 -6
  48. package/src/agents/frontmatter.d.ts +2 -0
  49. package/src/agents/frontmatter.js +15 -1
  50. package/src/agents/skills.js +2 -1
  51. package/src/extension/config.js +3 -0
  52. package/src/extension/herdr-pi-bridge.js +17 -10
  53. package/src/extension/index.js +82 -2
  54. package/src/extension/reply-workflow-script.js +5 -2
  55. package/src/extension/schemas.js +3 -3
  56. package/src/extension/subagent-guide.js +1 -2
  57. package/src/intercom/native-supervisor-channel.js +20 -0
  58. package/src/missions/lifecycle.js +7 -1
  59. package/src/runs/background/active-async-capacity.d.ts +1 -0
  60. package/src/runs/background/active-async-capacity.js +9 -3
  61. package/src/runs/background/async-execution.js +20 -10
  62. package/src/runs/background/async-job-tracker.d.ts +6 -1
  63. package/src/runs/background/async-job-tracker.js +9 -4
  64. package/src/runs/background/async-status.d.ts +4 -0
  65. package/src/runs/background/async-status.js +22 -3
  66. package/src/runs/background/await-async-run.js +3 -7
  67. package/src/runs/background/boot-id.d.ts +12 -0
  68. package/src/runs/background/boot-id.js +47 -0
  69. package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
  70. package/src/runs/background/interrupted-elsewhere.js +68 -0
  71. package/src/runs/background/notify.d.ts +2 -0
  72. package/src/runs/background/notify.js +13 -0
  73. package/src/runs/background/result-delivery-ownership.d.ts +6 -0
  74. package/src/runs/background/result-delivery-ownership.js +19 -1
  75. package/src/runs/background/result-watcher.d.ts +3 -1
  76. package/src/runs/background/result-watcher.js +24 -8
  77. package/src/runs/background/run-child-session.d.ts +1 -6
  78. package/src/runs/background/run-child-session.js +1 -1
  79. package/src/runs/background/run-status.js +2 -1
  80. package/src/runs/background/stale-run-reconciler.d.ts +25 -0
  81. package/src/runs/background/stale-run-reconciler.js +52 -13
  82. package/src/runs/background/subagent-runner-bootstrap.js +3 -0
  83. package/src/runs/background/subagent-runner.d.ts +4 -0
  84. package/src/runs/background/subagent-runner.js +13 -5
  85. package/src/runs/background/subagent-wait.d.ts +1 -1
  86. package/src/runs/background/subagent-wait.js +6 -1
  87. package/src/runs/background/terminal-run-index.d.ts +2 -0
  88. package/src/runs/background/terminal-run-index.js +44 -3
  89. package/src/runs/foreground/async-stop-action.js +3 -1
  90. package/src/runs/foreground/execution.js +6 -3
  91. package/src/runs/foreground/foreground-history.d.ts +16 -2
  92. package/src/runs/foreground/foreground-history.js +133 -31
  93. package/src/runs/foreground/subagent-executor.d.ts +11 -0
  94. package/src/runs/foreground/subagent-executor.js +277 -74
  95. package/src/runs/shared/child-launch.d.ts +2 -2
  96. package/src/runs/shared/child-session.d.ts +3 -2
  97. package/src/runs/shared/child-session.js +51 -8
  98. package/src/runs/shared/child-tool-plan.d.ts +7 -0
  99. package/src/runs/shared/child-tool-plan.js +21 -6
  100. package/src/runs/shared/herdr-connection.d.ts +17 -8
  101. package/src/runs/shared/herdr-connection.js +19 -7
  102. package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
  103. package/src/runs/shared/herdr-external-adapters.js +1 -1
  104. package/src/runs/shared/herdr-machine.d.ts +12 -2
  105. package/src/runs/shared/herdr-machine.js +55 -9
  106. package/src/runs/shared/herdr-pane-host.d.ts +88 -0
  107. package/src/runs/shared/herdr-pane-host.js +155 -0
  108. package/src/runs/shared/herdr-placed-run.d.ts +27 -150
  109. package/src/runs/shared/herdr-placed-run.js +48 -395
  110. package/src/runs/shared/local-machine-access.d.ts +5 -0
  111. package/src/runs/shared/local-machine-access.js +28 -0
  112. package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
  113. package/src/runs/shared/orca-progress-tabs.js +1 -24
  114. package/src/runs/shared/parallel-utils.d.ts +1 -1
  115. package/src/runs/shared/placed-run-owner.d.ts +49 -0
  116. package/src/runs/shared/placed-run-owner.js +93 -0
  117. package/src/runs/shared/placement-host.d.ts +99 -0
  118. package/src/runs/shared/placement-host.js +157 -0
  119. package/src/runs/shared/rex-machine.d.ts +25 -0
  120. package/src/runs/shared/rex-machine.js +83 -0
  121. package/src/runs/shared/rex-pane-host.d.ts +62 -0
  122. package/src/runs/shared/rex-pane-host.js +327 -0
  123. package/src/shared/completion-owner.d.ts +5 -0
  124. package/src/shared/completion-owner.js +5 -0
  125. package/src/shared/session-identity.d.ts +2 -0
  126. package/src/shared/session-identity.js +26 -0
  127. package/src/shared/types.d.ts +45 -6
  128. package/src/tui/fleet.d.ts +1 -0
  129. package/src/tui/fleet.js +25 -15
  130. package/src/tui/render.d.ts +6 -0
  131. package/src/tui/render.js +3 -3
  132. package/src/tui/subagent-messages.js +1 -0
  133. package/src/workflows/scripted-workflow.d.ts +1 -1
  134. package/src/workflows/workflow-auto-resume.d.ts +32 -0
  135. package/src/workflows/workflow-auto-resume.js +102 -0
  136. package/src/workflows/workflow-launch-record.d.ts +27 -0
  137. package/src/workflows/workflow-launch-record.js +48 -0
  138. package/src/workflows/workflow-resume-claim.d.ts +82 -0
  139. package/src/workflows/workflow-resume-claim.js +210 -0
  140. package/src/workflows/workflow-resume.d.ts +56 -0
  141. package/src/workflows/workflow-resume.js +106 -0
  142. package/src/workflows/workflow-reuse.d.ts +15 -9
  143. package/src/workflows/workflow-reuse.js +77 -14
  144. package/src/workflows/workflow-revival.d.ts +2 -0
  145. package/src/workflows/workflow-revival.js +2 -1
  146. package/src/workflows/workflow-stop-cause.d.ts +6 -0
  147. package/src/workflows/workflow-stop-cause.js +8 -0
  148. package/skills/pi-subagents/references/constraints-and-recipes.md +0 -74
  149. package/skills/pi-subagents/references/execution-controls.md +0 -563
  150. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -165
  151. package/skills/pi-subagents/references/multi-lane-orchestration.md +0 -61
  152. package/skills/pi-subagents/references/review-and-validation.md +0 -73
@@ -1,6 +1,8 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import { type AsyncJobState, type AsyncWidgetLayout, type ControlEvent, type SubagentState } from "../../shared/types.ts";
4
+ import type { RawDrainStatusObserver } from "../shared/readonly-drain-observation.ts";
5
+ import type { ResultDeliveryOwnership } from "./result-delivery-ownership.ts";
4
6
  interface AsyncJobTrackerOptions {
5
7
  completionRetentionMs?: number;
6
8
  /** Slow safety sweep for liveness repair when filesystem events are missed. */
@@ -12,6 +14,8 @@ interface AsyncJobTrackerOptions {
12
14
  platform?: NodeJS.Platform;
13
15
  onJobTerminal?: (job: AsyncJobState) => void;
14
16
  onJobCleanup?: (asyncId: string) => void;
17
+ /** Lets a run whose launching Pi exited hold its publication in the session that owns it. */
18
+ deliveryOwnerId?: ResultDeliveryOwnership["deliveryOwnerId"];
15
19
  watch?: typeof fs.watch;
16
20
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
17
21
  now?: () => number;
@@ -26,7 +30,8 @@ export declare function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">,
26
30
  handleStarted: (data: unknown) => void;
27
31
  handleComplete: (data: unknown) => void;
28
32
  resetJobs: (ctx?: ExtensionContext) => void;
29
- restoreActiveJobs: (ctx?: ExtensionContext) => void;
33
+ /** `observeStatus` also sees the statuses of other sessions' runs that the listing skips. */
34
+ restoreActiveJobs: (ctx?: ExtensionContext, observeStatus?: RawDrainStatusObserver) => void;
30
35
  dispose: () => void;
31
36
  };
32
37
  export {};
@@ -15,6 +15,7 @@ import { withWorkflowChildRunModel } from "./workflow-terminal-proof.js";
15
15
  import { validHostStepNodes } from "../shared/host-step-status.js";
16
16
  import { readProcessTerminal } from "./process-terminal.js";
17
17
  import { withCachedUiContext } from "../../shared/extension-context.js";
18
+ import { OWNING_SESSION_COMPLETION_OWNER } from "../../shared/completion-owner.js";
18
19
  const CONTROL_EVENT_READ_CHUNK_BYTES = 64 * 1024;
19
20
  const MAX_CONTROL_EVENT_LINE_BYTES = 1024 * 1024;
20
21
  const CONTROL_EVENT_SCAN_WINDOW_BYTES = 2 * 1024 * 1024;
@@ -560,8 +561,11 @@ export function createAsyncJobTracker(pi, state, asyncDirRoot, options = {}) {
560
561
  if (isTerminalJobStatus(job.status)) {
561
562
  let publication = terminalPublications.get(job.asyncId);
562
563
  if (!publication && status.mode !== "workflow" && status.processTerminal?.state === "pending"
563
- && status.runId === job.asyncId && status.sessionId === state.currentSessionId
564
- && status.completionOwnerId && status.completionOwnerId === state.completionOwnerId) {
564
+ && status.runId === job.asyncId && status.completionOwnerId
565
+ && ((status.sessionId === state.currentSessionId && status.completionOwnerId === state.completionOwnerId)
566
+ // Also covers a session inherited after /new or /resume: deliveryOwnerId checks run ownership.
567
+ || (typeof status.sessionId === "string"
568
+ && options.deliveryOwnerId?.(status.sessionId, status.completionOwnerId, status.completionOwnerProcess, status.parentWorkflowRunId) === OWNING_SESSION_COMPLETION_OWNER))) {
565
569
  publication = { instanceId: status.processTerminal.runnerProcessInstanceId, pending: true };
566
570
  terminalPublications.set(job.asyncId, publication);
567
571
  }
@@ -883,7 +887,7 @@ export function createAsyncJobTracker(pi, state, asyncDirRoot, options = {}) {
883
887
  rerenderWidget(ctx, []);
884
888
  }
885
889
  };
886
- const restoreActiveJobs = (ctx) => {
890
+ const restoreActiveJobs = (ctx, observeStatus) => {
887
891
  if (ctx?.hasUI)
888
892
  state.lastUiContext = ctx;
889
893
  state.statusProjectionSessionId = null;
@@ -891,7 +895,8 @@ export function createAsyncJobTracker(pi, state, asyncDirRoot, options = {}) {
891
895
  return;
892
896
  let runs;
893
897
  try {
894
- runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], sessionId: state.currentSessionId, resultsDir, kill: options.kill, now: options.now });
898
+ // A workflow whose Pi exited is repaired here, in the session that owns it, which then delivers its notice.
899
+ runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], sessionId: state.currentSessionId, resultsDir, kill: options.kill, now: options.now, adoptCompletionOwnerId: state.completionOwnerId }, observeStatus);
895
900
  }
896
901
  catch (error) {
897
902
  console.error(`Failed to restore active async jobs from '${asyncDirRoot}':`, error);
@@ -142,6 +142,10 @@ interface AsyncRunListOptions {
142
142
  includeNested?: boolean;
143
143
  /** Explicit repair/debug escape hatch. Normal runtime paths must not set this. */
144
144
  repairScan?: boolean;
145
+ /** This process's completion owner, adopted by an owner-exited workflow repair of a run the session owns; needs `sessionId`. */
146
+ adoptCompletionOwnerId?: string;
147
+ /** Receives the warning for a skipped corrupt terminal status, so an open TUI can show it as a notice instead of console output. */
148
+ onCorruptStatus?: (warning: string) => void;
145
149
  }
146
150
  type TargetedAsyncRunResolution = {
147
151
  kind: "exact";
@@ -37,6 +37,9 @@ function isAsyncStatusIsolationError(asyncDir, error) {
37
37
  || message.startsWith(`Invalid host step '${statusPath}`)
38
38
  || /^(workflowChildren|Invalid workflowChildren)/.test(message);
39
39
  }
40
+ // Status path -> last logged signature. A corrupt run is re-scanned on every
41
+ // status poll, so the line repeats only when the file or the outcome changes.
42
+ const reportedCorruptStatus = new Map();
40
43
  function isolateCorruptActiveRun(asyncDir, runId, error, now) {
41
44
  const statusPath = path.join(asyncDir, "status.json");
42
45
  const processTerminal = readProcessTerminal(asyncDir, { runId });
@@ -61,7 +64,20 @@ function isolateCorruptActiveRun(asyncDir, runId, error, now) {
61
64
  markerAction = `failed to release active marker: ${getErrorMessage(releaseError)}`;
62
65
  }
63
66
  }
64
- console.error(`[pi-subagents] Skipping corrupt active async run '${runId}' at '${statusPath}': ${getErrorMessage(error)}; ${markerAction}.`);
67
+ const line = `[pi-subagents] Skipping corrupt active async run '${runId}' at '${statusPath}': ${getErrorMessage(error)}; ${markerAction}.`;
68
+ let fileSignature = "";
69
+ try {
70
+ const stat = fs.statSync(statusPath);
71
+ fileSignature = `${stat.size}:${stat.mtimeMs}`;
72
+ }
73
+ catch {
74
+ // A missing file still gets the line once; its message carries the cause.
75
+ }
76
+ const signature = `${fileSignature}\n${line}`;
77
+ if (reportedCorruptStatus.get(statusPath) === signature)
78
+ return;
79
+ reportedCorruptStatus.set(statusPath, signature);
80
+ console.error(line);
65
81
  }
66
82
  function isNotFoundError(error) {
67
83
  return typeof error === "object"
@@ -388,7 +404,7 @@ export function listAsyncRuns(asyncDirRoot, options = {}, observeStatus) {
388
404
  }
389
405
  if (wantsTerminal) {
390
406
  for (const sessionId of owners ?? [undefined]) {
391
- for (const entry of readRecentTerminalRunIndex(asyncDirRoot, { sessionId, ...(options.entryLimit !== undefined ? { limit: options.entryLimit } : {}) }))
407
+ for (const entry of readRecentTerminalRunIndex(asyncDirRoot, { sessionId, onCorruptStatus: options.onCorruptStatus, ...(options.entryLimit !== undefined ? { limit: options.entryLimit } : {}) }))
392
408
  indexed.add(entry);
393
409
  }
394
410
  }
@@ -425,16 +441,19 @@ export function listAsyncRuns(asyncDirRoot, options = {}, observeStatus) {
425
441
  let status;
426
442
  try {
427
443
  // Reconciliation can rewrite state; session-scoped discovery does not own foreign runs.
444
+ let owned = false;
428
445
  if (owners) {
429
446
  const stored = readStatus(asyncDir);
430
447
  if (stored && !owners.has(stored.sessionId)) {
431
448
  observeStatus?.(stored);
432
449
  continue;
433
450
  }
451
+ owned = stored !== null;
434
452
  }
453
+ // Only a run this session owns may adopt the caller's completion owner.
435
454
  const reconciliation = options.reconcile === false
436
455
  ? undefined
437
- : reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now }, observeStatus);
456
+ : reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now, adoptCompletionOwnerId: owned ? options.adoptCompletionOwnerId : undefined }, observeStatus);
438
457
  status = (reconciliation?.status ?? readStatus(asyncDir));
439
458
  if (options.reconcile === false)
440
459
  observeStatus?.(status);
@@ -2,9 +2,8 @@ import * as fs from "node:fs";
2
2
  import { readStatus } from "../../shared/utils.js";
3
3
  import { workflowAwaitedAsyncResultPath } from "./async-execution.js";
4
4
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.js";
5
- import { currentPidNamespaceScope } from "./pid-namespace.js";
6
5
  import { resultPayloadPathForSessionRun, resultSnapshotReplaced, withResultRunLease } from "./result-files.js";
7
- import { checkPidLiveness } from "./stale-run-reconciler.js";
6
+ import { checkRecordedPidLiveness } from "./stale-run-reconciler.js";
8
7
  function errorMessage(error) {
9
8
  return error instanceof Error ? error.message : String(error);
10
9
  }
@@ -13,11 +12,8 @@ function runnerExitedWithoutResult(asyncDir, runId, resultPath) {
13
12
  const status = readStatus(asyncDir);
14
13
  if (!status || (status.state !== "running" && status.state !== "queued") || typeof status.pid !== "number")
15
14
  return false;
16
- const observedScope = currentPidNamespaceScope();
17
- if (status.pidNamespaceScope !== undefined && status.pidNamespaceScope !== observedScope)
18
- return false;
19
- const pidScopeVerified = status.pidNamespaceScope !== undefined && status.pidNamespaceScope === observedScope;
20
- if (checkPidLiveness(status.pid, undefined, pidScopeVerified) !== "dead")
15
+ // A pid from another namespace never reads as dead here, so the runner is not presumed gone.
16
+ if (checkRecordedPidLiveness({ pid: status.pid, pidNamespaceScope: status.pidNamespaceScope, bootId: status.bootId }) !== "dead")
21
17
  return false;
22
18
  return !fs.existsSync(resultPath) && !(status.sessionId && resultPayloadPathForSessionRun(asyncDir, status.sessionId, runId));
23
19
  }
@@ -0,0 +1,12 @@
1
+ import type { RecordedPid } from "./stale-run-reconciler.ts";
2
+ /**
3
+ * Identity of the current machine boot, read once per process. A pid recorded under a
4
+ * different boot id cannot still be running. Undefined when the platform has none.
5
+ */
6
+ export declare function currentBootId(): string | undefined;
7
+ /** This process as a recorded pid, without the fields the platform does not have. */
8
+ export declare function currentRecordedPid(): RecordedPid;
9
+ /** Fixes the boot id without reading the OS; runners take Pi's value from their config. */
10
+ export declare function primeBootId(value: string | undefined): void;
11
+ /** Clears the cache so the next read calls `nextReader` (default: the OS reader). */
12
+ export declare function resetBootIdForTesting(nextReader?: () => string | undefined): void;
@@ -0,0 +1,47 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import { currentPidNamespaceScope } from "./pid-namespace.js";
4
+ function readBootIdFromOs() {
5
+ try {
6
+ if (process.platform === "linux")
7
+ return fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf-8").trim() || undefined;
8
+ if (process.platform === "darwin") {
9
+ return execFileSync("sysctl", ["-n", "kern.bootsessionuuid"], { encoding: "utf-8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
10
+ }
11
+ }
12
+ catch {
13
+ // An unreadable boot id only means liveness falls back to probing the pid.
14
+ }
15
+ return undefined;
16
+ }
17
+ // Process-wide, so the bundled extension and the compiled runner modules share one value.
18
+ const BOOT_ID_KEY = Symbol.for("pi-subagents.boot-id.v1");
19
+ const runtime = globalThis;
20
+ let reader = readBootIdFromOs;
21
+ /**
22
+ * Identity of the current machine boot, read once per process. A pid recorded under a
23
+ * different boot id cannot still be running. Undefined when the platform has none.
24
+ */
25
+ export function currentBootId() {
26
+ const cached = runtime[BOOT_ID_KEY];
27
+ if (cached)
28
+ return cached.value;
29
+ const value = reader();
30
+ runtime[BOOT_ID_KEY] = { value };
31
+ return value;
32
+ }
33
+ /** This process as a recorded pid, without the fields the platform does not have. */
34
+ export function currentRecordedPid() {
35
+ const pidNamespaceScope = currentPidNamespaceScope();
36
+ const bootId = currentBootId();
37
+ return { pid: process.pid, ...(pidNamespaceScope ? { pidNamespaceScope } : {}), ...(bootId ? { bootId } : {}) };
38
+ }
39
+ /** Fixes the boot id without reading the OS; runners take Pi's value from their config. */
40
+ export function primeBootId(value) {
41
+ runtime[BOOT_ID_KEY] = { value };
42
+ }
43
+ /** Clears the cache so the next read calls `nextReader` (default: the OS reader). */
44
+ export function resetBootIdForTesting(nextReader) {
45
+ reader = nextReader ?? readBootIdFromOs;
46
+ delete runtime[BOOT_ID_KEY];
47
+ }
@@ -0,0 +1,22 @@
1
+ import type { ForegroundResumeRun } from "../../shared/types.ts";
2
+ import type { RawDrainStatusObserver } from "../shared/readonly-drain-observation.ts";
3
+ interface InterruptedElsewhereCollector {
4
+ /** Observer for the async statuses session start already reads; keeps foreign ones that may be interrupted. */
5
+ observeStatus: RawDrainStatusObserver;
6
+ /** Receives the foreground records of other sessions that session start already reads. */
7
+ observeForegroundRun: (run: ForegroundResumeRun) => void;
8
+ /** One line per other session that can be reopened and has runs whose Pi is dead. */
9
+ lines: () => string[];
10
+ }
11
+ /**
12
+ * Collects runs of other sessions in this folder that were cut off because their Pi died, from what
13
+ * session start reads anyway. Read-only: nothing is written to those runs or sessions. The cheap
14
+ * filters run as records arrive; `lines` reads each session file's header once, then probes each
15
+ * candidate's pid once.
16
+ */
17
+ export declare function createInterruptedElsewhereCollector(input: {
18
+ currentSessionId: string;
19
+ cwd: string;
20
+ now: number;
21
+ }): InterruptedElsewhereCollector;
22
+ export {};
@@ -0,0 +1,68 @@
1
+ import { resolvePiSessionId } from "../../shared/session-identity.js";
2
+ import { ownerConfirmedDead } from "../foreground/foreground-history.js";
3
+ import { ownerSessionIds } from "../shared/run-ownership.js";
4
+ import { checkRecordedPidLiveness } from "./stale-run-reconciler.js";
5
+ /** Runs interrupted longer ago than this are not pointed to. */
6
+ const INTERRUPTED_ELSEWHERE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
7
+ function interruptedElsewhereLine(reopenId, count) {
8
+ const command = `\`pi --session ${reopenId}\``;
9
+ return count === 1
10
+ ? `1 subagent run was interrupted in another session. Reopen it to resume it: ${command}`
11
+ : `${count} subagent runs were interrupted in another session. Reopen it to resume them: ${command}`;
12
+ }
13
+ /**
14
+ * Collects runs of other sessions in this folder that were cut off because their Pi died, from what
15
+ * session start reads anyway. Read-only: nothing is written to those runs or sessions. The cheap
16
+ * filters run as records arrive; `lines` reads each session file's header once, then probes each
17
+ * candidate's pid once.
18
+ */
19
+ export function createInterruptedElsewhereCollector(input) {
20
+ const ownSessions = ownerSessionIds(input.currentSessionId);
21
+ // sessionId -> runId -> whether its owner is dead; a run seen from both sources counts once.
22
+ const candidates = new Map();
23
+ const recent = (at) => typeof at === "number" && input.now - at <= INTERRUPTED_ELSEWHERE_MAX_AGE_MS;
24
+ const add = (sessionId, runId, ownerDead) => {
25
+ if (ownSessions.has(sessionId))
26
+ return;
27
+ const runs = candidates.get(sessionId) ?? new Map();
28
+ if (!runs.has(runId))
29
+ runs.set(runId, ownerDead);
30
+ candidates.set(sessionId, runs);
31
+ };
32
+ return {
33
+ observeStatus(observed) {
34
+ // The listing passes the status.json values it read.
35
+ const status = observed;
36
+ if (!status || typeof status.sessionId !== "string" || typeof status.runId !== "string" || (status.state !== "running" && status.state !== "queued"))
37
+ return;
38
+ if (status.parentWorkflowRunId !== undefined || status.cwd !== input.cwd || !recent(status.lastUpdate ?? status.startedAt))
39
+ return;
40
+ const { pid, pidNamespaceScope, bootId } = status;
41
+ if (typeof pid !== "number")
42
+ return;
43
+ add(status.sessionId, status.runId, () => checkRecordedPidLiveness({ pid, pidNamespaceScope, bootId }) === "dead");
44
+ },
45
+ observeForegroundRun(run) {
46
+ if (!run.sessionId || run.parentWorkflowRunId !== undefined || run.cwd !== input.cwd || !recent(run.updatedAt))
47
+ return;
48
+ add(run.sessionId, run.runId, () => ownerConfirmedDead(run));
49
+ },
50
+ lines() {
51
+ const lines = [];
52
+ for (const [sessionId, runs] of candidates) {
53
+ // Only a persisted session can be reopened; its id is then its file path, whose header names
54
+ // the id `pi --session` resolves, so the command needs no quoting in any shell.
55
+ const reopenId = resolvePiSessionId(sessionId);
56
+ if (!reopenId)
57
+ continue;
58
+ let interrupted = 0;
59
+ for (const ownerDead of runs.values())
60
+ if (ownerDead())
61
+ interrupted += 1;
62
+ if (interrupted > 0)
63
+ lines.push(interruptedElsewhereLine(reopenId, interrupted));
64
+ }
65
+ return lines;
66
+ },
67
+ };
68
+ }
@@ -43,6 +43,8 @@ export interface SubagentNotifyDetails {
43
43
  /** The finished run's id; for a workflow it equals workflowRunId. */
44
44
  runId?: string;
45
45
  workflowRunId?: string;
46
+ /** What happens next to a workflow that Pi's exit or a runtime replacement stopped; decided once at delivery. */
47
+ resumeNotice?: string;
46
48
  childRuns?: Array<{
47
49
  runId: string;
48
50
  workflowKey?: string;
@@ -15,6 +15,8 @@ import { COMPLETION_ACTION } from "../../shared/parent-wake.js";
15
15
  import { resolveSubagentResultStatus } from "../../intercom/result-intercom.js";
16
16
  import { isUnexplainedProcessSignal } from "../shared/process-signal.js";
17
17
  import { capSummaryOutputs } from "../shared/single-output.js";
18
+ import { describeWorkflowResume } from "../../workflows/workflow-resume-claim.js";
19
+ import { isReusableWorkflowStopCause } from "../../workflows/workflow-stop-cause.js";
18
20
  const CHILD_OUTPUT_PREVIEW_MAX_BYTES = 2_000;
19
21
  const ERROR_LINE_MAX_BYTES = 300;
20
22
  const CHILD_OUTPUT_PREVIEW_COUNT = 8;
@@ -187,10 +189,13 @@ function formatCorrelationLines(details) {
187
189
  return [
188
190
  details.asyncDir ? `Retention-managed async directory: ${boundedSafeText(details.asyncDir)}` : undefined,
189
191
  details.workflowRunId ? `Workflow run: ${details.workflowRunId}` : undefined,
192
+ details.resumeNotice,
190
193
  details.childRuns?.length ? `Child runs: ${details.childRuns.map(formatChildRun).join(", ")}` : undefined,
191
194
  details.reconciledFromDetachedChild ? `Reconciled detached child: ${details.reconciledFromDetachedChild}` : undefined,
192
195
  ].filter((line) => line !== undefined);
193
196
  }
197
+ // The forms of describeWorkflowResume, so a re-render keeps the line out of the result preview.
198
+ const RESUME_NOTICE_PREFIXES = ["Auto-resumed as ", "Resumed as ", "Relaunched as ", "Not auto-resumed: ", "Resume by hand: ", "Relaunch it by hand with "];
194
199
  const WATCHDOG_BLOCKERS_HEADING = "Watchdog blockers:";
195
200
  function formatWatchdogBlockerLines(details) {
196
201
  if (!details.watchdogBlockers?.length)
@@ -295,6 +300,9 @@ export function parseSubagentNotifyContent(content) {
295
300
  const workflowRunIndex = body.findIndex((line) => line.startsWith("Workflow run: "));
296
301
  const childRunsIndex = body.findIndex((line) => line.startsWith("Child runs: "));
297
302
  const reconciledIndex = body.findIndex((line) => line.startsWith("Reconciled detached child: "));
303
+ // Only the slot right after `Workflow run:` carries the resume line; the same text elsewhere is output.
304
+ const resumeNoticeLine = workflowRunIndex >= 0 ? body[workflowRunIndex + 1] : undefined;
305
+ const resumeNotice = resumeNoticeLine && RESUME_NOTICE_PREFIXES.some((prefix) => resumeNoticeLine.startsWith(prefix)) ? resumeNoticeLine : undefined;
298
306
  const watchdogIndex = body.findIndex((line) => line === WATCHDOG_BLOCKERS_HEADING);
299
307
  const watchdogBlockers = watchdogIndex >= 0 ? parseWatchdogBlockerLines(body.slice(watchdogIndex + 1)) : [];
300
308
  const metadataIndexes = [sessionIndex, handoffIndex, workflowRunIndex, childRunsIndex, reconciledIndex, watchdogIndex].filter((index) => index >= 0);
@@ -332,6 +340,7 @@ export function parseSubagentNotifyContent(content) {
332
340
  resultPreview,
333
341
  ...(handoffPath ? { handoffPath } : {}),
334
342
  ...(workflowRunId ? { workflowRunId } : {}),
343
+ ...(resumeNotice ? { resumeNotice } : {}),
335
344
  ...(childRuns?.length ? { childRuns } : {}),
336
345
  ...(reconciledFromDetachedChild ? { reconciledFromDetachedChild } : {}),
337
346
  ...(watchdogBlockers.length ? { watchdogBlockers } : {}),
@@ -614,6 +623,9 @@ export function buildCompletionDetails(result) {
614
623
  : undefined;
615
624
  const reconciledFromDetachedChild = typeof result.reconciledFromDetachedChild === "string" ? result.reconciledFromDetachedChild : undefined;
616
625
  const asyncDir = nonemptyPath(result.asyncDir);
626
+ // Read here, once per delivery; a re-render parses the line back instead of reading the run again.
627
+ const stopCause = result.workflow?.stopCause;
628
+ const resumeNotice = workflowRunId && asyncDir && isReusableWorkflowStopCause(stopCause) ? describeWorkflowResume(asyncDir, workflowRunId) : undefined;
617
629
  const watchdogBlockers = [];
618
630
  const watchdogConcerns = [];
619
631
  const collectWatchdogFindings = (owner, progress) => {
@@ -653,6 +665,7 @@ export function buildCompletionDetails(result) {
653
665
  ...(handoffPath ? { handoffPath } : {}),
654
666
  ...(rawRunId ? { runId: rawRunId } : {}),
655
667
  ...(workflowRunId ? { workflowRunId } : {}),
668
+ ...(resumeNotice ? { resumeNotice } : {}),
656
669
  ...(childRuns.length ? { childRuns } : {}),
657
670
  ...(childOutputs?.length ? { childOutputs } : {}),
658
671
  ...(reconciledFromDetachedChild ? { reconciledFromDetachedChild } : {}),
@@ -2,6 +2,12 @@ import type { SubagentState } from "../../shared/types.ts";
2
2
  type ResultDeliveryState = Pick<SubagentState, "currentSessionId" | "completionOwnerId">;
3
3
  export interface ResultDeliveryOwnership {
4
4
  owns(sessionId: string, completionOwnerId: unknown): boolean;
5
+ /**
6
+ * The completion owner a result is delivered under: `owning-session` when its own owner is another Pi
7
+ * process that is confirmed dead and this session owns the run, so nothing else can ever deliver it;
8
+ * otherwise `completionOwnerId`.
9
+ */
10
+ deliveryOwnerId(sessionId: string | undefined, completionOwnerId: string | null | undefined, ownerProcess: unknown, parentWorkflowRunId: unknown): string | null | undefined;
5
11
  claimedSessionIds(): readonly string[];
6
12
  }
7
13
  export declare function createResultDeliveryOwnership(state: ResultDeliveryState): ResultDeliveryOwnership;
@@ -1,5 +1,9 @@
1
1
  import { ownsRun } from "../shared/run-ownership.js";
2
2
  import { inheritedSessionIds } from "../../shared/session-succession.js";
3
+ import { OWNING_SESSION_COMPLETION_OWNER } from "../../shared/completion-owner.js";
4
+ import { checkRecordedPidLiveness, isRecordedPid } from "./stale-run-reconciler.js";
5
+ // A dead process cannot come back, so only that verdict is kept.
6
+ const deadCompletionOwnerIds = new Set();
3
7
  export function createResultDeliveryOwnership(state) {
4
8
  const currentOwner = () => typeof state.completionOwnerId === "string" && state.completionOwnerId
5
9
  ? state.completionOwnerId
@@ -7,10 +11,24 @@ export function createResultDeliveryOwnership(state) {
7
11
  return {
8
12
  owns(sessionId, completionOwnerId) {
9
13
  const owner = currentOwner();
10
- if (!owner || completionOwnerId !== owner)
14
+ if (!owner || (completionOwnerId !== owner && completionOwnerId !== OWNING_SESSION_COMPLETION_OWNER))
11
15
  return false;
12
16
  return ownsRun(state.currentSessionId, sessionId);
13
17
  },
18
+ deliveryOwnerId(sessionId, completionOwnerId, ownerProcess, parentWorkflowRunId) {
19
+ const owner = currentOwner();
20
+ // A workflow child is reported by its workflow, which has its own owner-exited notice.
21
+ if (!owner || typeof completionOwnerId !== "string" || completionOwnerId === owner
22
+ || completionOwnerId === OWNING_SESSION_COMPLETION_OWNER || parentWorkflowRunId
23
+ || !ownsRun(state.currentSessionId, sessionId))
24
+ return completionOwnerId;
25
+ if (!deadCompletionOwnerIds.has(completionOwnerId)) {
26
+ if (!isRecordedPid(ownerProcess) || checkRecordedPidLiveness(ownerProcess) !== "dead")
27
+ return completionOwnerId;
28
+ deadCompletionOwnerIds.add(completionOwnerId);
29
+ }
30
+ return OWNING_SESSION_COMPLETION_OWNER;
31
+ },
14
32
  claimedSessionIds() {
15
33
  if (!currentOwner() || !state.currentSessionId)
16
34
  return [];
@@ -31,7 +31,7 @@ type ResultWatcherDeps = {
31
31
  resultScanLogging?: "all" | "activity" | "off";
32
32
  platform?: NodeJS.Platform;
33
33
  /** Shared current/predecessor session ownership used by the notifier. */
34
- ownership?: Pick<ResultDeliveryOwnership, "owns" | "claimedSessionIds">;
34
+ ownership?: Pick<ResultDeliveryOwnership, "owns" | "claimedSessionIds" | "deliveryOwnerId">;
35
35
  /** Called once the parent has this run's result: the notifier accepted it, or an earlier delivery already did. */
36
36
  onResultDelivered?: (runId: string) => void;
37
37
  };
@@ -69,6 +69,8 @@ type ResultFileData = CompletionNotification & {
69
69
  intercomTarget?: string;
70
70
  parallelHandoff?: ParallelHandoffReference;
71
71
  notificationDeliveredAt?: unknown;
72
+ completionOwnerProcess?: unknown;
73
+ parentWorkflowRunId?: unknown;
72
74
  };
73
75
  /**
74
76
  * Watches persisted async results for the session currently owned by this
@@ -19,25 +19,31 @@ const RETRY_DELAY_MS = 100;
19
19
  // lease attempt instead of parking this event loop while a runner publishes the same run.
20
20
  const CLEANUP_LEASE_WAIT_MS = 0;
21
21
  const SLOW_RESULT_SCAN_MS = 500;
22
- function jsonStringProperty(raw, property) {
23
- const matches = raw.matchAll(new RegExp(`"${property}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`, "g"));
22
+ /** The last `"property": <value>` whose value matches `valuePattern`, parsed without reading the whole file; malformed reads as none. */
23
+ function jsonPropertyValue(raw, property, valuePattern) {
24
24
  let encoded;
25
- for (const match of matches)
25
+ for (const match of raw.matchAll(new RegExp(`"${property}"\\s*:\\s*(${valuePattern})`, "g")))
26
26
  encoded = match[1];
27
27
  if (!encoded)
28
28
  return undefined;
29
29
  try {
30
- const value = JSON.parse(encoded);
31
- return typeof value === "string" && value ? value : undefined;
30
+ return JSON.parse(encoded);
32
31
  }
33
32
  catch {
34
33
  return undefined;
35
34
  }
36
35
  }
36
+ function jsonStringProperty(raw, property) {
37
+ const value = jsonPropertyValue(raw, property, `"(?:\\\\.|[^"\\\\])*"`);
38
+ return typeof value === "string" && value ? value : undefined;
39
+ }
37
40
  function resultFileIdentity(raw, file) {
38
41
  return {
39
42
  sessionId: jsonStringProperty(raw, "sessionId"),
40
43
  completionOwnerId: jsonStringProperty(raw, "completionOwnerId"),
44
+ // Recorded as a flat object, so a brace-free match is the whole value.
45
+ completionOwnerProcess: jsonPropertyValue(raw, "completionOwnerProcess", `\\{[^{}]*\\}`),
46
+ parentWorkflowRunId: jsonStringProperty(raw, "parentWorkflowRunId"),
41
47
  runId: file.replace(/\.json$/i, ""),
42
48
  };
43
49
  }
@@ -105,6 +111,9 @@ export function createResultWatcher(pi, state, resultsDir, completionTtlMs, deps
105
111
  // While deliverPendingResults runs, completions skip the batch delay.
106
112
  let deliveringPending = 0;
107
113
  const identityCache = new Map();
114
+ // Result file -> last logged mission-sync error. A failed sync is retried
115
+ // every pass, so the same error is logged once per result file.
116
+ const reportedMissionSyncFailures = new Map();
108
117
  let deliveryActive = true;
109
118
  let deliveryEpoch = 0;
110
119
  let resultScanTimer = null;
@@ -116,6 +125,7 @@ export function createResultWatcher(pi, state, resultsDir, completionTtlMs, deps
116
125
  && typeof completionOwnerId === "string"
117
126
  && completionOwnerId === state.completionOwnerId);
118
127
  const claimedSessionIds = () => deps.ownership?.claimedSessionIds() ?? [];
128
+ const deliveryOwnerId = deps.ownership?.deliveryOwnerId ?? ((_sessionId, completionOwnerId) => completionOwnerId);
119
129
  const ownsCompletion = (sessionId, completionOwnerId, epoch) => {
120
130
  if (!deliveryActive || epoch !== deliveryEpoch)
121
131
  return false;
@@ -223,7 +233,7 @@ export function createResultWatcher(pi, state, resultsDir, completionTtlMs, deps
223
233
  // files keep their existing diagnostics and compatibility behavior.
224
234
  if (!identity.sessionId)
225
235
  return true;
226
- if (ownsResult(identity.sessionId, identity.completionOwnerId))
236
+ if (ownsResult(identity.sessionId, deliveryOwnerId(identity.sessionId, identity.completionOwnerId, identity.completionOwnerProcess, identity.parentWorkflowRunId)))
227
237
  return true;
228
238
  // Acknowledgment removes the observer index; gating on the mission binding instead
229
239
  // re-ran mission sync for an unowned result on every scan.
@@ -306,7 +316,7 @@ export function createResultWatcher(pi, state, resultsDir, completionTtlMs, deps
306
316
  if (typeof data.sessionId !== "string" || !data.sessionId)
307
317
  return;
308
318
  const sessionId = data.sessionId;
309
- const completionOwnerId = data.completionOwnerId;
319
+ const completionOwnerId = deliveryOwnerId(sessionId, data.completionOwnerId, data.completionOwnerProcess, data.parentWorkflowRunId);
310
320
  const runId = typeof data.runId === "string" && data.runId
311
321
  ? data.runId
312
322
  : typeof data.id === "string" && data.id ? data.id : file.replace(/\.json$/i, "");
@@ -329,10 +339,15 @@ export function createResultWatcher(pi, state, resultsDir, completionTtlMs, deps
329
339
  let observerSucceeded = true;
330
340
  try {
331
341
  syncMissionFromAsyncCompletion({ ...data, runId });
342
+ reportedMissionSyncFailures.delete(file);
332
343
  }
333
344
  catch (error) {
334
345
  observerSucceeded = false;
335
- console.error(`Mission completion sync failed for '${resultPath}':`, error);
346
+ const message = error instanceof Error ? error.message : String(error);
347
+ if (reportedMissionSyncFailures.get(file) !== message) {
348
+ reportedMissionSyncFailures.set(file, message);
349
+ console.error(`Mission completion sync failed for '${resultPath}':`, error);
350
+ }
336
351
  }
337
352
  try {
338
353
  deps.observeCompletion?.({ ...data, runId });
@@ -484,6 +499,7 @@ export function createResultWatcher(pi, state, resultsDir, completionTtlMs, deps
484
499
  ...data,
485
500
  id: data.id ?? runId,
486
501
  runId,
502
+ completionOwnerId,
487
503
  triggerTurn,
488
504
  intercomDelivered,
489
505
  ...(nestedChildren?.length ? { nestedChildren } : {}),
@@ -91,12 +91,7 @@ export interface RunChildSessionResult {
91
91
  toolCount: number;
92
92
  durationMs: number;
93
93
  model?: string;
94
- nativeMachine?: {
95
- provider: "herdr";
96
- machineId: string;
97
- initialGit?: import("../../shared/types.ts").HerdrRemoteGitStatus;
98
- finalGit?: import("../../shared/types.ts").HerdrRemoteGitStatus;
99
- };
94
+ nativeMachine?: import("../../shared/types.ts").SingleResult["nativeMachine"];
100
95
  error?: string;
101
96
  finalOutput: string;
102
97
  outputState: SubagentOutputState;
@@ -526,7 +526,7 @@ export function runChildSession(input) {
526
526
  toolCount,
527
527
  durationMs: Date.now() - startedAt,
528
528
  model,
529
- nativeMachine: session?.machineEvidence ? { provider: "herdr", machineId: session.machineEvidence.machineId, ...(session.machineEvidence.initial ? { initialGit: session.machineEvidence.initial } : {}), ...(session.machineEvidence.final ? { finalGit: session.machineEvidence.final } : {}) } : undefined,
529
+ nativeMachine: session?.machineEvidence ? { provider: session.machineEvidence.provider, machineId: session.machineEvidence.machineId, ...(session.machineEvidence.initial ? { initialGit: session.machineEvidence.initial } : {}), ...(session.machineEvidence.final ? { finalGit: session.machineEvidence.final } : {}) } : undefined,
530
530
  error: stopped ? stopMessage() : timedOut ? (error ?? timeoutMessage()) : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal && !commandError) ? undefined : finalError,
531
531
  finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage() : error ?? timeoutMessage()) : finalOutput,
532
532
  outputState: finalOutput.trim() ? "present" : "absent",
@@ -36,6 +36,7 @@ import { workflowAsyncChildSteeringGuidance } from "../shared/workflow-async-chi
36
36
  import { formatWorkflowKeyRevival, projectWorkflowKeyRevival } from "../../workflows/workflow-revival.js";
37
37
  import { capSummaryOutputs } from "../shared/single-output.js";
38
38
  import { ownsRun } from "../shared/run-ownership.js";
39
+ import { foregroundChildStateLabel } from "../foreground/foreground-history.js";
39
40
  /** A step whose structured output has a path line needs only a short inline preview. */
40
41
  const STRUCTURED_PREVIEW_WITH_PATH_CHARS = 500;
41
42
  function formatProcessTerminal(value) {
@@ -200,7 +201,7 @@ function formatRememberedForegroundStatus(run) {
200
201
  for (const child of run.children) {
201
202
  const output = rememberedForegroundChildOutput(child).trim().split(/\r?\n/).find((line) => line.trim());
202
203
  const parts = [
203
- `${child.index + 1}. ${child.sessionName?.trim() || child.agent} ${child.status}`,
204
+ `${child.index + 1}. ${child.sessionName?.trim() || child.agent} ${foregroundChildStateLabel(run, child)}`,
204
205
  child.exitCode !== undefined ? `exit ${child.exitCode}` : undefined,
205
206
  child.detachedReason ? `detached: ${child.detachedReason}` : undefined,
206
207
  child.acceptance ? `acceptance: ${child.acceptance.status}` : undefined,
@@ -24,6 +24,12 @@ interface ReconcileAsyncRunOptions {
24
24
  missingStatusGraceMs?: number;
25
25
  staleAlivePidMs?: number;
26
26
  pidNamespaceScope?: () => string | undefined;
27
+ bootId?: () => string | undefined;
28
+ /**
29
+ * This process's completion owner, for an owner-exited workflow repair to adopt so the session that
30
+ * reopened it delivers its notice. Only callers whose session owns the run pass it.
31
+ */
32
+ adoptCompletionOwnerId?: string;
27
33
  }
28
34
  interface ReconcileAsyncRunResult {
29
35
  status: AsyncStatus | null;
@@ -33,5 +39,24 @@ interface ReconcileAsyncRunResult {
33
39
  }
34
40
  export declare function reconcileNestedAsyncDescendants(route: NestedRoute, options?: ReconcileAsyncRunOptions): void;
35
41
  export declare function checkPidLiveness(pid: number, kill?: KillFn, probeZombie?: boolean): PidLiveness;
42
+ export interface RecordedPid {
43
+ pid: number;
44
+ pidNamespaceScope?: string;
45
+ bootId?: string;
46
+ }
47
+ export declare function isRecordedPid(value: unknown): value is RecordedPid;
48
+ export interface RecordedPidObservation {
49
+ observedScope: string | undefined;
50
+ observedBootId: string | undefined;
51
+ kill?: KillFn;
52
+ /** Replaces the default `checkPidLiveness` probe. */
53
+ probe?: (pid: number) => PidLiveness;
54
+ }
55
+ /**
56
+ * Liveness of a pid saved in a status file. A pid saved under another boot is dead without a
57
+ * probe, but only when both sides are in the same pid namespace or neither records one: a
58
+ * macOS host reading a Linux container's runs must not compare their boot ids.
59
+ */
60
+ export declare function checkRecordedPidLiveness(recorded: RecordedPid, observation?: RecordedPidObservation): PidLiveness;
36
61
  export declare function reconcileAsyncRun(asyncDir: string, options?: ReconcileAsyncRunOptions, observeStatus?: RawDrainStatusObserver): ReconcileAsyncRunResult;
37
62
  export {};