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
@@ -4,6 +4,7 @@ import { writeAtomicJson } from "../../shared/atomic-json.js";
4
4
  import { resultFilePath, resultPayloadPathForSessionRun, writeAsyncResultFile } from "./result-files.js";
5
5
  import { updateActiveRunIndex } from "./active-run-index.js";
6
6
  import { readProcessTerminal } from "./process-terminal.js";
7
+ import { OWNING_SESSION_COMPLETION_OWNER } from "../../shared/completion-owner.js";
7
8
  import { readStatus } from "../../shared/utils.js";
8
9
  import { DIRS } from "../../shared/types.js";
9
10
  import { resolveEffectiveThinking } from "../../shared/model-info.js";
@@ -11,6 +12,8 @@ import { normalizeParallelGroups } from "./parallel-groups.js";
11
12
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.js";
12
13
  import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.js";
13
14
  import { currentPidNamespaceScope } from "./pid-namespace.js";
15
+ import { currentBootId } from "./boot-id.js";
16
+ import { WORKFLOW_STOP_CAUSE_OWNER_EXITED } from "../../workflows/workflow-stop-cause.js";
14
17
  import { isTerminalAsyncState } from "./workflow-terminal-proof.js";
15
18
  function getErrorMessage(error) {
16
19
  return error instanceof Error ? error.message : String(error);
@@ -215,9 +218,14 @@ function settledWorkflowChildStatus(asyncDir, workflowRunId, step, now) {
215
218
  delete settled.stopped;
216
219
  return settled;
217
220
  }
218
- function buildFailedRepair(status, asyncDir, now, reason) {
221
+ function buildFailedRepair(status, asyncDir, now, reason, ownerExited = false, adoptCompletionOwnerId) {
219
222
  const runId = status.runId || path.basename(asyncDir);
220
223
  const pid = typeof status.pid === "number" ? status.pid : "unknown";
224
+ // A dead workflow host is the Pi process itself; its journal keeps the finished children for a relaunch.
225
+ const exitedWorkflow = ownerExited && status.mode === "workflow" ? status.workflow : undefined;
226
+ // Its owner process is dead, so nothing else can deliver its notice: an owning session that repairs it
227
+ // adopts it, and a repair elsewhere leaves it to whichever session owns the run.
228
+ const completionOwnerId = exitedWorkflow ? adoptCompletionOwnerId || OWNING_SESSION_COMPLETION_OWNER : status.completionOwnerId;
221
229
  const terminal = readProcessTerminal(asyncDir, { runId }) ?? status.processTerminal;
222
230
  const runnerExit = terminal?.state === "observed" || terminal?.state === "unknown"
223
231
  ? terminal.instances?.find((instance) => instance.kind === "runner" && instance.processInstanceId === terminal.runnerProcessInstanceId)
@@ -225,7 +233,9 @@ function buildFailedRepair(status, asyncDir, now, reason) {
225
233
  const exitText = runnerExit?.kind === "runner"
226
234
  ? `exited with code ${runnerExit.exitCode ?? "none"}${runnerExit.signal ? ` (signal ${runnerExit.signal})` : ""}`
227
235
  : "exited or disappeared";
228
- const baseMessage = reason ?? `Async runner process ${pid} ${exitText} before writing a result. Marked run failed by stale-run reconciliation.`;
236
+ const baseMessage = reason ?? (exitedWorkflow
237
+ ? `The Pi process (PID ${pid}) that ran workflow ${runId} exited before the workflow finished. Marked run failed by stale-run reconciliation. Its finished children are kept.`
238
+ : `Async runner process ${pid} ${exitText} before writing a result. Marked run failed by stale-run reconciliation.`);
229
239
  const diagnostics = readRunnerStartupDiagnostics(asyncDir);
230
240
  const message = diagnostics ? `${baseMessage}\n\nRunner stderr tail:\n${diagnostics}` : baseMessage;
231
241
  const steps = status.steps?.length ? status.steps : [{ agent: "subagent", status: "running" }];
@@ -263,6 +273,9 @@ function buildFailedRepair(status, asyncDir, now, reason) {
263
273
  lastUpdate: now,
264
274
  endedAt: now,
265
275
  steps: repairedSteps,
276
+ // Read from the pre-repair status: the repair time would make an old crash look new.
277
+ ...(exitedWorkflow ? { workflow: { ...exitedWorkflow, stopCause: WORKFLOW_STOP_CAUSE_OWNER_EXITED, ownerExitedAt: status.lastUpdate ?? status.startedAt } } : {}),
278
+ ...(completionOwnerId ? { completionOwnerId } : {}),
266
279
  };
267
280
  const resultAgent = repairedSteps[status.currentStep ?? 0]?.agent ?? repairedSteps[0]?.agent ?? "subagent";
268
281
  return {
@@ -272,7 +285,9 @@ function buildFailedRepair(status, asyncDir, now, reason) {
272
285
  id: runId,
273
286
  agent: resultAgent,
274
287
  mode: status.mode,
275
- ...(status.completionOwnerId ? { completionOwnerId: status.completionOwnerId } : {}),
288
+ ...(exitedWorkflow ? { workflow: { stopCause: WORKFLOW_STOP_CAUSE_OWNER_EXITED } } : {}),
289
+ ...(completionOwnerId ? { completionOwnerId } : {}),
290
+ ...(status.completionOwnerProcess ? { completionOwnerProcess: status.completionOwnerProcess } : {}),
276
291
  ...(status.parentWorkflowRunId ? { parentWorkflowRunId: status.parentWorkflowRunId } : {}),
277
292
  ...(status.workflowKey ? { workflowKey: status.workflowKey } : {}),
278
293
  success: false,
@@ -298,8 +313,8 @@ function buildFailedRepair(status, asyncDir, now, reason) {
298
313
  },
299
314
  };
300
315
  }
301
- function writeFailedRepair(asyncDir, status, resultPath, now, reason) {
302
- const repair = buildFailedRepair(status, asyncDir, now, reason);
316
+ function writeFailedRepair(asyncDir, status, resultPath, now, reason, ownerExited = false, adoptCompletionOwnerId) {
317
+ const repair = buildFailedRepair(status, asyncDir, now, reason, ownerExited, adoptCompletionOwnerId);
303
318
  if (repair.result.sessionId)
304
319
  writeAsyncResultFile(resultPath, repair.result);
305
320
  writeAtomicJson(path.join(asyncDir, "status.json"), repair.status);
@@ -386,6 +401,32 @@ export function checkPidLiveness(pid, kill = process.kill, probeZombie = false)
386
401
  return "unknown";
387
402
  }
388
403
  }
404
+ export function isRecordedPid(value) {
405
+ if (!value || typeof value !== "object")
406
+ return false;
407
+ const { pid, pidNamespaceScope, bootId } = value;
408
+ return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0
409
+ && (pidNamespaceScope === undefined || typeof pidNamespaceScope === "string")
410
+ && (bootId === undefined || typeof bootId === "string");
411
+ }
412
+ /**
413
+ * Liveness of a pid saved in a status file. A pid saved under another boot is dead without a
414
+ * probe, but only when both sides are in the same pid namespace or neither records one: a
415
+ * macOS host reading a Linux container's runs must not compare their boot ids.
416
+ */
417
+ export function checkRecordedPidLiveness(recorded, observation = { observedScope: currentPidNamespaceScope(), observedBootId: currentBootId() }) {
418
+ const { observedScope, observedBootId } = observation;
419
+ if (recorded.pidNamespaceScope === observedScope && recorded.bootId && observedBootId && recorded.bootId !== observedBootId)
420
+ return "dead";
421
+ // An observer without a scope (macOS/Windows host sharing a container's temp root) cannot match a recorded one.
422
+ const scopeMismatch = recorded.pidNamespaceScope !== undefined && recorded.pidNamespaceScope !== observedScope;
423
+ // A local zombie is evidence about this pid only when its namespace is known to match.
424
+ const scopeVerified = recorded.pidNamespaceScope !== undefined && recorded.pidNamespaceScope === observedScope;
425
+ const liveness = observation.probe
426
+ ? observation.probe(recorded.pid)
427
+ : checkPidLiveness(recorded.pid, observation.kill, scopeVerified);
428
+ return liveness === "dead" && scopeMismatch ? "unknown" : liveness;
429
+ }
389
430
  export function reconcileAsyncRun(asyncDir, options = {}, observeStatus) {
390
431
  const now = options.now?.() ?? Date.now();
391
432
  const status = readStatus(asyncDir);
@@ -450,13 +491,11 @@ export function reconcileAsyncRun(asyncDir, options = {}, observeStatus) {
450
491
  return { status: null, repaired: false, resultPath };
451
492
  }
452
493
  }
453
- const observedScope = options.pidNamespaceScope ? options.pidNamespaceScope() : currentPidNamespaceScope();
454
- // An observer without a scope (macOS/Windows host sharing a container's temp root) cannot match a recorded one.
455
- const pidScopeMismatch = effectiveStatus.pidNamespaceScope !== undefined && effectiveStatus.pidNamespaceScope !== observedScope;
456
- // A local zombie is evidence about this runner only when its namespace is known to match.
457
- const pidScopeVerified = effectiveStatus.pidNamespaceScope !== undefined && effectiveStatus.pidNamespaceScope === observedScope;
458
- const observedLiveness = checkPidLiveness(effectiveStatus.pid, options.kill, pidScopeVerified);
459
- const liveness = observedLiveness === "dead" && pidScopeMismatch ? "unknown" : observedLiveness;
494
+ const liveness = checkRecordedPidLiveness({ pid: effectiveStatus.pid, pidNamespaceScope: effectiveStatus.pidNamespaceScope, bootId: effectiveStatus.bootId }, {
495
+ kill: options.kill,
496
+ observedScope: options.pidNamespaceScope ? options.pidNamespaceScope() : currentPidNamespaceScope(),
497
+ observedBootId: options.bootId ? options.bootId() : currentBootId(),
498
+ });
460
499
  if (liveness !== "dead") {
461
500
  const staleAfterMs = options.staleAlivePidMs ?? 24 * 60 * 60 * 1000;
462
501
  const lastUpdate = effectiveStatus.lastUpdate ?? effectiveStatus.startedAt;
@@ -466,5 +505,5 @@ export function reconcileAsyncRun(asyncDir, options = {}, observeStatus) {
466
505
  const message = `Async runner PID ${effectiveStatus.pid} ${probe}; status has not updated for ${now - lastUpdate}ms, so stale-run reconciliation marked the run failed because PID ownership is unverified.`;
467
506
  return writeFailedRepair(asyncDir, effectiveStatus, resultPath, now, message);
468
507
  }
469
- return writeFailedRepair(asyncDir, effectiveStatus, resultPath, now);
508
+ return writeFailedRepair(asyncDir, effectiveStatus, resultPath, now, undefined, true, options.adoptCompletionOwnerId);
470
509
  }
@@ -14,6 +14,7 @@ import { acquireSessionLease } from "../shared/session-lease.js";
14
14
  import { readProcessTerminalCandidate, writeProcessTerminalCandidate, markProcessTerminalCandidateLeaseRelease } from "./process-terminal-candidate.js";
15
15
  import { persistRunnerStartupFailure } from "./runner-startup-failure.js";
16
16
  import { currentPidNamespaceScope } from "./pid-namespace.js";
17
+ import { primeBootId } from "./boot-id.js";
17
18
  function isRecord(value) {
18
19
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
19
20
  }
@@ -103,6 +104,8 @@ async function waitForStartupControl(controlPath, token, action, timeoutMs = 30_
103
104
  export async function runConfiguredSubagent(rawConfig, options = {}) {
104
105
  validateSubagentRunConfig(rawConfig);
105
106
  const config = rawConfig;
107
+ // The runner never reads the boot id from the OS (sysctl on macOS); Pi passes it to direct runners only.
108
+ primeBootId(typeof config.bootId === "string" && config.bootId ? config.bootId : undefined);
106
109
  let lease;
107
110
  let startupCommitted = config.revivalLease === undefined && config.launchBarrierToken === undefined;
108
111
  let executionLoaded = false;
@@ -4,6 +4,7 @@ import { type RunnerSubagentStep as SubagentStep, type RunnerStep } from "../sha
4
4
  import type { InheritedChildRuntime } from "../shared/child-launch.ts";
5
5
  import type { ChildSessionFactory, DefaultChildSessionFactoryOptions } from "../shared/child-session.ts";
6
6
  import { type ChildEvent, type SteerDelivery, type StepSteerHandler } from "./run-child-session.ts";
7
+ import type { RecordedPid } from "./stale-run-reconciler.ts";
7
8
  import type { SessionLeaseRequest } from "../shared/session-lease.ts";
8
9
  import { createHerdrExternalAdapter, type HerdrExternalAdapterId, type HerdrExternalResult } from "../shared/herdr-external-adapters.ts";
9
10
  import { type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
@@ -25,6 +26,7 @@ export interface SubagentRunConfig {
25
26
  asyncDir: string;
26
27
  sessionId?: string | null;
27
28
  completionOwnerId?: string;
29
+ completionOwnerProcess?: RecordedPid;
28
30
  piPackageRoot?: string;
29
31
  /** Test seam: module the runner imports its `ChildSessionFactory` from. */
30
32
  childSessionFactoryModule?: string;
@@ -76,6 +78,8 @@ export interface SubagentRunConfig {
76
78
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
77
79
  runnerProcessInstanceId?: string;
78
80
  launchBarrierToken?: string;
81
+ /** Pi's boot id, set only for a direct (non-launcher) runner; the runner never reads its own. */
82
+ bootId?: string;
79
83
  parentWorkflowRunId?: string;
80
84
  workflowKey?: string;
81
85
  lane?: WorkflowLaneMetadata;
@@ -49,7 +49,7 @@ import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDi
49
49
  import { isContextOverflow } from "../shared/model-resolution.js";
50
50
  import { processTerminalPath, writeProcessTerminalCandidate } from "./process-terminal.js";
51
51
  import { persistRunnerStartupFailure } from "./runner-startup-failure.js";
52
- import { currentPidNamespaceScope } from "./pid-namespace.js";
52
+ import { currentRecordedPid } from "./boot-id.js";
53
53
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, unconsumedSteerReason, updateSteeringTarget } from "./steering.js";
54
54
  import { PROMPT_REDACTED, SUBAGENT_STOPPED_MESSAGE, SUBAGENT_TIMED_OUT_MESSAGE, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getAgentDir, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.js";
55
55
  import { planAbortRecovery } from "../shared/abort-recovery.js";
@@ -77,6 +77,7 @@ import { resolveCodexExecLaunch } from "../shared/codex-exec-adapter.js";
77
77
  import { resolveCursorAgentLaunch } from "../shared/cursor-agent-adapter.js";
78
78
  import { resolveExternalCliRunnerStatus } from "../shared/external-cli-contract.js";
79
79
  import { formatHerdrMachineHint, prepareHerdrMachineExternalCliRun } from "../shared/herdr-machine.js";
80
+ import { isRexLocalMachine } from "../shared/rex-machine.js";
80
81
  import { HerdrExternalNeedsAttentionError, createHerdrExternalAdapter, prepareInternalHerdrExternalAdapter } from "../shared/herdr-external-adapters.js";
81
82
  import { runExternalJob } from "../shared/external-job-runner.js";
82
83
  import { createOrcaProgressTab } from "../shared/orca-progress-tabs.js";
@@ -522,6 +523,7 @@ export async function runSingleStepInner(step, ctx) {
522
523
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
523
524
  inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
524
525
  requiredExtensions: step.requiredExtensions ?? ctx.inheritedChildRuntime?.requiredExtensions,
526
+ agentName: step.agent,
525
527
  permissionRules: step.permissionRules,
526
528
  }));
527
529
  }
@@ -608,6 +610,8 @@ export async function runSingleStepInner(step, ctx) {
608
610
  return omitUndefinedProperties({ agent: step.agent, context: step.context, output: message, error: message, exitCode: 1, stopped: stopped || undefined, timedOut: stopped ? undefined : true });
609
611
  }
610
612
  if (step.machine) {
613
+ if (step.machine.provider !== "herdr")
614
+ throw new Error("Rex supports native Pi children only.");
611
615
  const runner = resolveExternalCliRunnerStatus({ ...step.runner, machine: step.machine });
612
616
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
613
617
  const placedRunId = `${ctx.id}-${ctx.flatIndex}`;
@@ -781,7 +785,8 @@ export async function runSingleStepInner(step, ctx) {
781
785
  let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
782
786
  let toolBudgetBlocked = false;
783
787
  let actualLaunchContractDigest = step.launchContractDigest;
784
- const mutationSnapshot = step.machine ? { source: "tracked-files", trackedOnly: true, cwd: step.cwd ?? ctx.cwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(step.cwd ?? ctx.cwd);
788
+ // A machine 'local' child edits this disk, so its local Git evidence is authoritative, as for an unplaced run.
789
+ const mutationSnapshot = step.machine && !isRexLocalMachine(step.machine) ? { source: "tracked-files", trackedOnly: true, cwd: step.cwd ?? ctx.cwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(step.cwd ?? ctx.cwd);
785
790
  let finalMutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
786
791
  let contextOverflow = false;
787
792
  let launchWarningsEmitted = false;
@@ -1004,7 +1009,7 @@ export async function runSingleStepInner(step, ctx) {
1004
1009
  ? formatEmptyTerminalAssistantResponseError(run.messages)
1005
1010
  : undefined;
1006
1011
  const remoteGitChanged = run.nativeMachine?.initialGit && run.nativeMachine.finalGit ? run.nativeMachine.initialGit.head !== run.nativeMachine.finalGit.head || run.nativeMachine.initialGit.dirty !== run.nativeMachine.finalGit.dirty : undefined;
1007
- const mutationEvidence = run.nativeMachine ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1012
+ const mutationEvidence = run.nativeMachine && !isRexLocalMachine(step.machine) ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1008
1013
  finalMutationEvidence = mutationEvidence;
1009
1014
  const mutationAttemptObserved = run.observedMutationAttempt === true || mutationEvidence.attemptedMutation === true;
1010
1015
  // Validated structured output is persistable file content even when the final text is only
@@ -1669,6 +1674,7 @@ export async function runSubagent(config, childSessions) {
1669
1674
  ...(config.toolCallId ? { toolCallId: config.toolCallId } : {}),
1670
1675
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
1671
1676
  ...(config.completionOwnerId ? { completionOwnerId: config.completionOwnerId } : {}),
1677
+ ...(config.completionOwnerProcess ? { completionOwnerProcess: config.completionOwnerProcess } : {}),
1672
1678
  mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
1673
1679
  ...(config.nestedSelf ? { isNested: true } : {}),
1674
1680
  state: "running",
@@ -1680,8 +1686,7 @@ export async function runSubagent(config, childSessions) {
1680
1686
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
1681
1687
  ...(config.toolBudget ? { toolBudget: initialToolBudgetState(config.toolBudget) } : {}),
1682
1688
  ...(config.usageBudget ? { usageBudget: usageBudgetState(config.usageBudget, undefined) } : {}),
1683
- pid: process.pid,
1684
- pidNamespaceScope: currentPidNamespaceScope(),
1689
+ ...currentRecordedPid(),
1685
1690
  cwd,
1686
1691
  currentStep: 0,
1687
1692
  chainStepCount: steps.length,
@@ -1909,6 +1914,8 @@ export async function runSubagent(config, childSessions) {
1909
1914
  cwd,
1910
1915
  sessionId: config.sessionId,
1911
1916
  completionOwnerId: config.completionOwnerId,
1917
+ completionOwnerProcess: config.completionOwnerProcess,
1918
+ parentWorkflowRunId: config.parentWorkflowRunId,
1912
1919
  sessionFile: statusPayload.sessionFile ?? latestSessionFile,
1913
1920
  }), (filePath, payload) => publishRunResult(filePath, () => writePendingAsyncResultFile(filePath, payload)));
1914
1921
  };
@@ -4797,6 +4804,7 @@ export async function runSubagent(config, childSessions) {
4797
4804
  runtimeAcknowledgedExtensions: singleRuntimeAcknowledgedExtensions,
4798
4805
  sessionId: config.sessionId,
4799
4806
  completionOwnerId: config.completionOwnerId,
4807
+ completionOwnerProcess: config.completionOwnerProcess,
4800
4808
  sessionFile: effectiveSessionFile,
4801
4809
  intercomTarget: config.controlIntercomTarget,
4802
4810
  shareUrl,
@@ -54,7 +54,7 @@ export interface SubagentWaitParams {
54
54
  all?: boolean;
55
55
  /** Give up after this many milliseconds. Defaults to waitTool.defaultTimeoutMs, then 30 minutes. */
56
56
  timeoutMs?: number;
57
- /** False keeps a blocking wait open through idle attention; supervisor/contact requests still stop the wait. */
57
+ /** False keeps a blocking wait open through all attention except supervisor/contact requests, such as idle time, a tool held open, or repeated tool failures. */
58
58
  stopOnAttention?: boolean;
59
59
  }
60
60
  /** Minimal event-bus surface wait subscribes to (matches pi.events). */
@@ -707,8 +707,13 @@ export async function waitForSubagents(params, signal, deps) {
707
707
  const supervisorAttentionHint = relevantAttention.some(hasSupervisorTool)
708
708
  ? " Reply to any pending supervisor request. If subagent_supervisor({ action: \"pending\" }) is empty, check intercom({ action: \"pending\" }) because an external intercom tool may own the request."
709
709
  : "";
710
+ // Only when watchdog attention alone ended the wait: stopOnAttention: false would have kept blocking.
711
+ const keepWaitingHint = stopOnAttention && supervisorAttentionHint === ""
712
+ && (waitForAll || finishedAsyncCount + providerFinishedCount === 0)
713
+ ? " These runs are still running and none is waiting on a supervisor reply. To keep waiting through this kind of attention, call bg_wait again with stopOnAttention: false; supervisor requests still end that wait."
714
+ : "";
710
715
  const attentionNote = relevantAttention.length > 0
711
- ? ` ${relevantAttention.length} run(s) need attention: ${relevantAttention.map((run) => run.id).join(", ")} —${supervisorAttentionHint} inspect with subagent({ action: "status" }) then steer a top-level live async child, resume a paused/completed/failed child, or interrupt explicitly.`
716
+ ? ` ${relevantAttention.length} run(s) need attention: ${relevantAttention.map((run) => run.id).join(", ")} —${supervisorAttentionHint} inspect with subagent({ action: "status" }) then steer a top-level live async child, resume a paused/completed/failed child, or interrupt explicitly.${keepWaitingHint}`
712
717
  : "";
713
718
  const stillRunning = active.filter((run) => initialAsyncIds.has(run.id)).length
714
719
  + providerActive.filter((item) => initialProviderIds.has(backgroundWorkIdentity(item))).length;
@@ -5,4 +5,6 @@ export declare function readTerminalRunToolCallIndex(asyncDirRoot: string, toolC
5
5
  export declare function readRecentTerminalRunIndex(asyncDirRoot: string, options?: {
6
6
  sessionId?: string;
7
7
  limit?: number;
8
+ endedAfter?: number;
9
+ onCorruptStatus?: (warning: string) => void;
8
10
  }): string[];
@@ -12,6 +12,35 @@ const TOOL_CALL_INDEX_DIR = "@tool-calls";
12
12
  function isTerminalState(state) {
13
13
  return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped" || state === "rejected";
14
14
  }
15
+ function isCorruptStatusError(error, statusPath) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ return message.startsWith(`Failed to parse async status file '${statusPath}'`)
18
+ || message.startsWith(`Failed to validate async status file '${statusPath}'`);
19
+ }
20
+ // A corrupt indexed status is retried on every read. Emit a warning only when
21
+ // its observed file identity or parse outcome changes, like active-run diagnostics.
22
+ const reportedCorruptTerminalStatus = new Map();
23
+ function warnCorruptTerminalStatus(runId, statusPath, error, onCorruptStatus) {
24
+ let fileSignature = "unavailable";
25
+ try {
26
+ const stat = fs.statSync(statusPath);
27
+ fileSignature = `${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}:${stat.ino}`;
28
+ }
29
+ catch (statError) {
30
+ if (statError.code !== "ENOENT")
31
+ throw statError;
32
+ }
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ const signature = `${fileSignature}\n${message}`;
35
+ if (reportedCorruptTerminalStatus.get(statusPath) === signature)
36
+ return;
37
+ reportedCorruptTerminalStatus.set(statusPath, signature);
38
+ const warning = `Skipping indexed terminal run '${runId}' with corrupt status at '${statusPath}': ${message}`;
39
+ if (onCorruptStatus)
40
+ onCorruptStatus(warning);
41
+ else
42
+ console.error(`[pi-subagents] ${warning}`);
43
+ }
15
44
  function indexRoot(asyncDirRoot) {
16
45
  return path.join(asyncDirRoot, TERMINAL_RUN_INDEX_DIR);
17
46
  }
@@ -99,8 +128,10 @@ function sessionDirs(asyncDirRoot, sessionId) {
99
128
  throw error;
100
129
  }
101
130
  }
102
- function recentMarkerFiles(dirs, limit) {
131
+ function recentMarkerFiles(dirs, limit, endedAfter) {
103
132
  return dirs.flatMap(markerFiles)
133
+ // The name starts with the run's end time, so an old run is skipped without reading its marker.
134
+ .filter((marker) => endedAfter === undefined || !(Number(marker.name.slice(0, TIMESTAMP_WIDTH)) < endedAfter))
104
135
  .sort((left, right) => right.name.localeCompare(left.name))
105
136
  .slice(0, limit)
106
137
  .map((marker) => path.join(marker.dir, marker.name));
@@ -135,7 +166,7 @@ export function readTerminalRunToolCallIndex(asyncDirRoot, toolCallId) {
135
166
  }
136
167
  export function readRecentTerminalRunIndex(asyncDirRoot, options = {}) {
137
168
  const limit = options.limit === undefined ? Number.POSITIVE_INFINITY : Math.max(0, Math.floor(options.limit));
138
- const candidates = recentMarkerFiles(sessionDirs(asyncDirRoot, options.sessionId), limit);
169
+ const candidates = recentMarkerFiles(sessionDirs(asyncDirRoot, options.sessionId), limit, options.endedAfter);
139
170
  const runIds = [];
140
171
  const seen = new Set();
141
172
  for (const marker of candidates) {
@@ -152,7 +183,17 @@ export function readRecentTerminalRunIndex(asyncDirRoot, options = {}) {
152
183
  continue;
153
184
  }
154
185
  const asyncDir = path.join(asyncDirRoot, entry.runId);
155
- const status = readStatus(asyncDir);
186
+ const statusPath = path.join(asyncDir, "status.json");
187
+ let status;
188
+ try {
189
+ status = readStatus(asyncDir);
190
+ }
191
+ catch (error) {
192
+ if (!isCorruptStatusError(error, statusPath))
193
+ throw error;
194
+ warnCorruptTerminalStatus(entry.runId, statusPath, error, options.onCorruptStatus);
195
+ continue;
196
+ }
156
197
  if (!status || !isTerminalState(status.state) || status.sessionId !== entry.sessionId || (status.runId && status.runId !== entry.runId)) {
157
198
  removeInvalidMarker(marker);
158
199
  continue;
@@ -44,6 +44,8 @@ function sealPausedRun(asyncDir, status, resultsDir) {
44
44
  sessionId: status.sessionId,
45
45
  asyncDir,
46
46
  ...(status.completionOwnerId ? { completionOwnerId: status.completionOwnerId } : {}),
47
+ ...(status.completionOwnerProcess ? { completionOwnerProcess: status.completionOwnerProcess } : {}),
48
+ ...(status.parentWorkflowRunId ? { parentWorkflowRunId: status.parentWorkflowRunId } : {}),
47
49
  ...(status.toolCallId ? { toolCallId: status.toolCallId } : {}),
48
50
  results: (status.steps ?? []).map((step) => ({
49
51
  agent: step.agent,
@@ -146,7 +148,7 @@ export function stopAsyncRun(state, runId, kill, location, childId) {
146
148
  if (!target)
147
149
  return null;
148
150
  const status = reconcileAsyncRun(target.asyncDir, { kill }).status;
149
- // A workflow relaunched with options.reuseFrom stops the exact child run it re-attached, whichever session launched it.
151
+ // A relaunched workflow stops the exact child run it re-attached from its relaunch chain, whichever session launched it.
150
152
  if (state.currentSessionId && !ownsRun(state.currentSessionId, status?.sessionId) && location?.reattachedRunId !== target.asyncId) {
151
153
  return {
152
154
  content: [{ type: "text", text: `Async run '${target.asyncId}' was not found in the active session.` }],
@@ -46,6 +46,7 @@ import { buildInProcessChildLaunch, createReportedChildSessionInput } from "../s
46
46
  import { childSessionFactory, childSessionHasQueuedMessages, projectChildSessionEventForJson } from "../shared/child-session.js";
47
47
  import { reconcileAttemptUsage } from "../shared/usage-reconciliation.js";
48
48
  import { clearProviderRetry, observeProviderDiagnostics, snapshotProviderDiagnostics } from "../../shared/provider-diagnostics.js";
49
+ import { isRexLocalMachine } from "../shared/rex-machine.js";
49
50
  const artifactOutputByResult = new WeakMap();
50
51
  const acceptanceOutputByResult = new WeakMap();
51
52
  function emptyUsage() {
@@ -441,7 +442,9 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
441
442
  };
442
443
  return result;
443
444
  }
444
- const mutationSnapshot = options.machine ? { source: "tracked-files", trackedOnly: true, cwd: options.cwd ?? runtimeCwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(options.cwd ?? runtimeCwd);
445
+ // A machine 'local' child edits this disk, so its local Git evidence is authoritative, as for an unplaced run.
446
+ const mutationCwd = isRexLocalMachine(options.machine) ? options.machine.cwd : options.cwd ?? runtimeCwd;
447
+ const mutationSnapshot = options.machine && !isRexLocalMachine(options.machine) ? { source: "tracked-files", trackedOnly: true, cwd: mutationCwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(mutationCwd);
445
448
  let structuredOutputToolInvoked = false;
446
449
  let structuredOutputMessageStartIndex;
447
450
  let toolAvailabilityError;
@@ -1259,7 +1262,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
1259
1262
  toolAvailabilityError = toolDiagnosticError;
1260
1263
  result.runtimeAcknowledgedExtensions = capture.runtimeAcknowledgedExtensions();
1261
1264
  if (session?.machineEvidence)
1262
- result.nativeMachine = { provider: "herdr", machineId: session.machineEvidence.machineId, ...(session.machineEvidence.initial ? { initialGit: session.machineEvidence.initial } : {}), ...(session.machineEvidence.final ? { finalGit: session.machineEvidence.final } : {}) };
1265
+ result.nativeMachine = { provider: session.machineEvidence.provider, machineId: session.machineEvidence.machineId, ...(session.machineEvidence.initial ? { initialGit: session.machineEvidence.initial } : {}), ...(session.machineEvidence.final ? { finalGit: session.machineEvidence.final } : {}) };
1263
1266
  let closeError = result.error ?? toolDiagnosticError ?? assistantError;
1264
1267
  const promptErrorMessage = promptError === undefined ? undefined : promptError instanceof Error ? promptError.message : String(promptError);
1265
1268
  if (promptError !== undefined)
@@ -1533,7 +1536,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
1533
1536
  durationMs: progress.durationMs,
1534
1537
  };
1535
1538
  const remoteGitChanged = result.nativeMachine?.initialGit && result.nativeMachine.finalGit ? result.nativeMachine.initialGit.head !== result.nativeMachine.finalGit.head || result.nativeMachine.initialGit.dirty !== result.nativeMachine.finalGit.dirty : undefined;
1536
- const mutationEvidence = result.nativeMachine ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, options.cwd ?? runtimeCwd);
1539
+ const mutationEvidence = result.nativeMachine && !isRexLocalMachine(options.machine) ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, mutationCwd);
1537
1540
  const acceptanceOutput = getFinalOutput(result.messages ?? []);
1538
1541
  const structuredText = result.structuredOutput === undefined ? undefined : JSON.stringify(result.structuredOutput, null, 2);
1539
1542
  let fullOutput = stripAcceptanceReport(acceptanceOutput);
@@ -1,11 +1,25 @@
1
- import type { SubagentState } from "../../shared/types.ts";
1
+ import type { ForegroundResumeChild, ForegroundResumeRun, SubagentState } from "../../shared/types.ts";
2
2
  export declare const MAX_REMEMBERED_FOREGROUND_RUNS = 50;
3
- export declare function persistForegroundRunHistory(state: SubagentState, options?: {
3
+ export declare const FOREGROUND_OWNER_EXITED_STOP_ERROR = "Subagent stopped because the Pi process that ran it exited before the run finished.";
4
+ /**
5
+ * Saves one foreground run to its own history file. A run with a `running` child is a launch-time
6
+ * record; the run's final save replaces it.
7
+ */
8
+ export declare function persistForegroundRun(run: ForegroundResumeRun, options?: {
4
9
  resultsDir?: string;
5
10
  limit?: number;
6
11
  }): void;
12
+ /** Drops this process's `running` record for a launch that threw, so it never loads as interrupted. */
13
+ export declare function discardRunningForegroundRun(runId: string, options?: {
14
+ resultsDir?: string;
15
+ }): void;
16
+ /** For a record with a `running` child: its owning Pi process is gone. */
17
+ export declare function ownerConfirmedDead(run: ForegroundResumeRun): boolean;
18
+ /** State text for a remembered child; a child cut off by a Pi exit names the resume target. */
19
+ export declare function foregroundChildStateLabel(run: Pick<ForegroundResumeRun, "runId">, child: Pick<ForegroundResumeChild, "status" | "error" | "sessionFile">): string;
7
20
  export declare function restoreForegroundRunHistory(state: SubagentState, options?: {
8
21
  resultsDir?: string;
9
22
  sessionId?: string | null;
10
23
  limit?: number;
24
+ observeForeign?: (run: ForegroundResumeRun) => void;
11
25
  }): number;