pi-subagents 0.78.0 → 0.79.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 (188) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/docs/agents.md +4 -2
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +4 -0
  5. package/docs/observability.md +11 -3
  6. package/docs/tool-reference.md +12 -1
  7. package/docs/workflows.md +6 -4
  8. package/package.json +1 -1
  9. package/skills/pi-subagents/SKILL.md +1 -0
  10. package/skills/pi-subagents/references/execution-controls.md +2 -0
  11. package/src/extension/declaration-pinning.d.ts.map +1 -1
  12. package/src/extension/declaration-pinning.js +15 -1
  13. package/src/extension/declaration-pinning.js.map +1 -1
  14. package/src/extension/index.d.ts.map +1 -1
  15. package/src/extension/index.js +19 -4
  16. package/src/extension/index.js.map +1 -1
  17. package/src/extension/public-execution.d.ts +1 -0
  18. package/src/extension/public-execution.d.ts.map +1 -1
  19. package/src/extension/public-execution.js +3 -0
  20. package/src/extension/public-execution.js.map +1 -1
  21. package/src/extension/rpc.d.ts.map +1 -1
  22. package/src/extension/rpc.js +5 -3
  23. package/src/extension/rpc.js.map +1 -1
  24. package/src/extension/schemas.d.ts +2 -0
  25. package/src/extension/schemas.d.ts.map +1 -1
  26. package/src/extension/schemas.js +1 -0
  27. package/src/extension/schemas.js.map +1 -1
  28. package/src/extension/tool-activation.d.ts +1 -0
  29. package/src/extension/tool-activation.d.ts.map +1 -1
  30. package/src/extension/tool-activation.js +57 -18
  31. package/src/extension/tool-activation.js.map +1 -1
  32. package/src/extension/upgrade-notice.d.ts.map +1 -1
  33. package/src/extension/upgrade-notice.js +7 -1
  34. package/src/extension/upgrade-notice.js.map +1 -1
  35. package/src/intercom/native-supervisor-channel.d.ts +2 -1
  36. package/src/intercom/native-supervisor-channel.d.ts.map +1 -1
  37. package/src/intercom/native-supervisor-channel.js +117 -9
  38. package/src/intercom/native-supervisor-channel.js.map +1 -1
  39. package/src/runs/background/async-execution.d.ts.map +1 -1
  40. package/src/runs/background/async-execution.js +13 -16
  41. package/src/runs/background/async-execution.js.map +1 -1
  42. package/src/runs/background/async-job-tracker.d.ts.map +1 -1
  43. package/src/runs/background/async-job-tracker.js +8 -0
  44. package/src/runs/background/async-job-tracker.js.map +1 -1
  45. package/src/runs/background/async-resume.d.ts.map +1 -1
  46. package/src/runs/background/async-resume.js +2 -1
  47. package/src/runs/background/async-resume.js.map +1 -1
  48. package/src/runs/background/async-status-snapshot.d.ts.map +1 -1
  49. package/src/runs/background/async-status-snapshot.js +4 -2
  50. package/src/runs/background/async-status-snapshot.js.map +1 -1
  51. package/src/runs/background/async-status.d.ts.map +1 -1
  52. package/src/runs/background/async-status.js +16 -8
  53. package/src/runs/background/async-status.js.map +1 -1
  54. package/src/runs/background/inspect-rpc.d.ts +5 -0
  55. package/src/runs/background/inspect-rpc.d.ts.map +1 -1
  56. package/src/runs/background/inspect-rpc.js +20 -3
  57. package/src/runs/background/inspect-rpc.js.map +1 -1
  58. package/src/runs/background/result-delivery-ownership.d.ts +0 -2
  59. package/src/runs/background/result-delivery-ownership.d.ts.map +1 -1
  60. package/src/runs/background/result-delivery-ownership.js +5 -21
  61. package/src/runs/background/result-delivery-ownership.js.map +1 -1
  62. package/src/runs/background/result-files.d.ts +5 -3
  63. package/src/runs/background/result-files.d.ts.map +1 -1
  64. package/src/runs/background/result-files.js +10 -6
  65. package/src/runs/background/result-files.js.map +1 -1
  66. package/src/runs/background/result-watcher.d.ts.map +1 -1
  67. package/src/runs/background/result-watcher.js +15 -8
  68. package/src/runs/background/result-watcher.js.map +1 -1
  69. package/src/runs/background/resume-guidance.d.ts.map +1 -1
  70. package/src/runs/background/resume-guidance.js +3 -2
  71. package/src/runs/background/resume-guidance.js.map +1 -1
  72. package/src/runs/background/run-child-session.d.ts.map +1 -1
  73. package/src/runs/background/run-child-session.js +7 -4
  74. package/src/runs/background/run-child-session.js.map +1 -1
  75. package/src/runs/background/run-status.d.ts.map +1 -1
  76. package/src/runs/background/run-status.js +11 -4
  77. package/src/runs/background/run-status.js.map +1 -1
  78. package/src/runs/background/stale-run-reconciler.d.ts.map +1 -1
  79. package/src/runs/background/stale-run-reconciler.js +53 -5
  80. package/src/runs/background/stale-run-reconciler.js.map +1 -1
  81. package/src/runs/background/subagent-runner.d.ts.map +1 -1
  82. package/src/runs/background/subagent-runner.js +15 -5
  83. package/src/runs/background/subagent-runner.js.map +1 -1
  84. package/src/runs/background/workflow-terminal-proof.d.ts +18 -1
  85. package/src/runs/background/workflow-terminal-proof.d.ts.map +1 -1
  86. package/src/runs/background/workflow-terminal-proof.js +27 -0
  87. package/src/runs/background/workflow-terminal-proof.js.map +1 -1
  88. package/src/runs/foreground/async-dismiss-action.d.ts.map +1 -1
  89. package/src/runs/foreground/async-dismiss-action.js +2 -1
  90. package/src/runs/foreground/async-dismiss-action.js.map +1 -1
  91. package/src/runs/foreground/async-steering-action.d.ts.map +1 -1
  92. package/src/runs/foreground/async-steering-action.js +3 -2
  93. package/src/runs/foreground/async-steering-action.js.map +1 -1
  94. package/src/runs/foreground/async-stop-action.d.ts +1 -0
  95. package/src/runs/foreground/async-stop-action.d.ts.map +1 -1
  96. package/src/runs/foreground/async-stop-action.js +3 -1
  97. package/src/runs/foreground/async-stop-action.js.map +1 -1
  98. package/src/runs/foreground/command-action.d.ts.map +1 -1
  99. package/src/runs/foreground/command-action.js +2 -1
  100. package/src/runs/foreground/command-action.js.map +1 -1
  101. package/src/runs/foreground/execution.d.ts.map +1 -1
  102. package/src/runs/foreground/execution.js +18 -7
  103. package/src/runs/foreground/execution.js.map +1 -1
  104. package/src/runs/foreground/subagent-executor.d.ts +2 -0
  105. package/src/runs/foreground/subagent-executor.d.ts.map +1 -1
  106. package/src/runs/foreground/subagent-executor.js +98 -32
  107. package/src/runs/foreground/subagent-executor.js.map +1 -1
  108. package/src/runs/shared/async-status-projection.d.ts +4 -0
  109. package/src/runs/shared/async-status-projection.d.ts.map +1 -1
  110. package/src/runs/shared/async-status-projection.js +23 -1
  111. package/src/runs/shared/async-status-projection.js.map +1 -1
  112. package/src/runs/shared/child-commands.d.ts +2 -0
  113. package/src/runs/shared/child-commands.d.ts.map +1 -1
  114. package/src/runs/shared/child-commands.js +4 -0
  115. package/src/runs/shared/child-commands.js.map +1 -1
  116. package/src/runs/shared/child-runtime-config.d.ts +6 -0
  117. package/src/runs/shared/child-runtime-config.d.ts.map +1 -1
  118. package/src/runs/shared/child-runtime-config.js +6 -0
  119. package/src/runs/shared/child-runtime-config.js.map +1 -1
  120. package/src/runs/shared/child-session.d.ts +6 -6
  121. package/src/runs/shared/child-session.d.ts.map +1 -1
  122. package/src/runs/shared/child-session.js +111 -51
  123. package/src/runs/shared/child-session.js.map +1 -1
  124. package/src/runs/shared/long-running-guard.d.ts +3 -1
  125. package/src/runs/shared/long-running-guard.d.ts.map +1 -1
  126. package/src/runs/shared/long-running-guard.js +75 -17
  127. package/src/runs/shared/long-running-guard.js.map +1 -1
  128. package/src/runs/shared/model-resolution.d.ts +5 -1
  129. package/src/runs/shared/model-resolution.d.ts.map +1 -1
  130. package/src/runs/shared/model-resolution.js +8 -1
  131. package/src/runs/shared/model-resolution.js.map +1 -1
  132. package/src/runs/shared/nested-events.d.ts.map +1 -1
  133. package/src/runs/shared/nested-events.js +2 -0
  134. package/src/runs/shared/nested-events.js.map +1 -1
  135. package/src/runs/shared/pi-spawn.d.ts +5 -1
  136. package/src/runs/shared/pi-spawn.d.ts.map +1 -1
  137. package/src/runs/shared/pi-spawn.js +86 -30
  138. package/src/runs/shared/pi-spawn.js.map +1 -1
  139. package/src/runs/shared/run-ownership.d.ts +4 -0
  140. package/src/runs/shared/run-ownership.d.ts.map +1 -0
  141. package/src/runs/shared/run-ownership.js +9 -0
  142. package/src/runs/shared/run-ownership.js.map +1 -0
  143. package/src/runs/shared/subagent-control.d.ts +1 -0
  144. package/src/runs/shared/subagent-control.d.ts.map +1 -1
  145. package/src/runs/shared/subagent-control.js +2 -1
  146. package/src/runs/shared/subagent-control.js.map +1 -1
  147. package/src/runs/shared/subagent-prompt-runtime.d.ts +45 -18
  148. package/src/runs/shared/subagent-prompt-runtime.d.ts.map +1 -1
  149. package/src/runs/shared/subagent-prompt-runtime.js +160 -59
  150. package/src/runs/shared/subagent-prompt-runtime.js.map +1 -1
  151. package/src/shared/file-lease.d.ts +7 -1
  152. package/src/shared/file-lease.d.ts.map +1 -1
  153. package/src/shared/file-lease.js +8 -2
  154. package/src/shared/file-lease.js.map +1 -1
  155. package/src/shared/fork-context.d.ts +3 -1
  156. package/src/shared/fork-context.d.ts.map +1 -1
  157. package/src/shared/fork-context.js +23 -13
  158. package/src/shared/fork-context.js.map +1 -1
  159. package/src/shared/session-succession.d.ts +5 -0
  160. package/src/shared/session-succession.d.ts.map +1 -0
  161. package/src/shared/session-succession.js +25 -0
  162. package/src/shared/session-succession.js.map +1 -0
  163. package/src/shared/types.d.ts +15 -1
  164. package/src/shared/types.d.ts.map +1 -1
  165. package/src/shared/types.js +3 -0
  166. package/src/shared/types.js.map +1 -1
  167. package/src/shared/utils.d.ts +11 -0
  168. package/src/shared/utils.d.ts.map +1 -1
  169. package/src/shared/utils.js +17 -0
  170. package/src/shared/utils.js.map +1 -1
  171. package/src/slash/slash-commands.d.ts.map +1 -1
  172. package/src/slash/slash-commands.js +2 -2
  173. package/src/slash/slash-commands.js.map +1 -1
  174. package/src/watchdog/register-main.js +2 -2
  175. package/src/watchdog/register-main.js.map +1 -1
  176. package/src/watchdog/runtime.d.ts.map +1 -1
  177. package/src/watchdog/runtime.js +3 -5
  178. package/src/watchdog/runtime.js.map +1 -1
  179. package/src/workflows/scripted-workflow.d.ts +1 -1
  180. package/src/workflows/scripted-workflow.d.ts.map +1 -1
  181. package/src/workflows/workflow-child-summary.d.ts +5 -0
  182. package/src/workflows/workflow-child-summary.d.ts.map +1 -1
  183. package/src/workflows/workflow-child-summary.js +10 -0
  184. package/src/workflows/workflow-child-summary.js.map +1 -1
  185. package/src/workflows/workflow-reuse.d.ts +6 -1
  186. package/src/workflows/workflow-reuse.d.ts.map +1 -1
  187. package/src/workflows/workflow-reuse.js +22 -1
  188. package/src/workflows/workflow-reuse.js.map +1 -1
@@ -68,6 +68,7 @@ import { canQueueRetainedAsyncFollowUp, steerAsyncRun } from "./async-steering-a
68
68
  import { resolveWorkflowForegroundSteeringTarget, steerWorkflowForegroundTarget, steerWorkflowRun, } from "./workflow-foreground-steering.js";
69
69
  import { stopAsyncRun } from "./async-stop-action.js";
70
70
  import { dismissRecoveredWorkflow } from "./async-dismiss-action.js";
71
+ import { inheritedSessionIds } from "../../shared/session-succession.js";
71
72
  import { promotePausedWorkflowIfSettled, reconcileDetachedWorkflowChildCompletion } from "./workflow-detach-reconcile.js";
72
73
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.js";
73
74
  import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.js";
@@ -94,7 +95,7 @@ import { getInspectorPlugins } from "../../inspectors/plugins.js";
94
95
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.js";
95
96
  import { previewSimpleWorkflowRun, runWorkflowScript, validateWorkflowScript, WorkflowScriptError } from "../../workflows/scripted-workflow.js";
96
97
  import { formatIncrementalChildCompletion, incrementalChildCompletionTriggersTurn } from "../background/notify.js";
97
- import { appendWorkflowChildJournal, findWorkflowReuseSource, matchWorkflowReuse, workflowChildFingerprint, workflowScriptDigest, workflowStopCause, WORKFLOW_RUNTIME_REPLACED_RELAUNCH_NOTICE } from "../../workflows/workflow-reuse.js";
98
+ import { appendWorkflowChildJournal, findWorkflowReuseSource, matchWorkflowReuse, readNamedWorkflowReuseSource, workflowChildFingerprint, workflowRuntimeReplacedRelaunchNotice, workflowScriptDigest, workflowStopCause } from "../../workflows/workflow-reuse.js";
98
99
  import { executeWorkflowHostCommand, resolveWorkflowHostOutputClaimPath } from "../../workflows/host-command.js";
99
100
  import { buildWorkflowReceipt, readWorkflowReceipt, workflowReceiptPath, resolveWorkflowReceiptResumeEntry, writeWorkflowReceipt } from "../../workflows/workflow-receipt.js";
100
101
  import { formatWorkflowKeyRevival, recordWorkflowRevival, withWorkflowRevivals } from "../../workflows/workflow-revival.js";
@@ -270,11 +271,12 @@ function nestedResolutionScopeForExecutor(deps) {
270
271
  ...(address ? { descendantOf: { parentRunId: address.parentRunId, ...(address.parentStepIndex !== undefined ? { parentStepIndex: address.parentStepIndex } : {}) } } : {}),
271
272
  };
272
273
  }
273
- function trustedSessionRootsForStatus(ctx, deps) {
274
+ function trustedSessionRoots(parentSessionFile, deps) {
274
275
  const roots = deps.config.defaultSessionDir ? [path.resolve(deps.expandTilde(deps.config.defaultSessionDir))] : [];
275
- const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
276
+ // Inherited runs keep their children under the root of the session that launched them.
276
277
  if (parentSessionFile)
277
- roots.push(deps.getSubagentSessionRoot(parentSessionFile));
278
+ for (const sessionFile of [parentSessionFile, ...inheritedSessionIds(parentSessionFile)])
279
+ roots.push(deps.getSubagentSessionRoot(sessionFile));
278
280
  return [...new Set(roots)];
279
281
  }
280
282
  /** Children follow the launching session's trust; hosts older than Pi's trust concept keep Pi's default. */
@@ -1129,7 +1131,7 @@ function pathWithin(base, candidate) {
1129
1131
  const resolvedCandidate = path.resolve(candidate);
1130
1132
  return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
1131
1133
  }
1132
- function validateNestedSessionFile(run, trustedSessionRoots) {
1134
+ function validateNestedSessionFile(run, trustedSessionRoots, resolveSource, visited = []) {
1133
1135
  const sessionFile = nestedRunSessionFile(run);
1134
1136
  if (!sessionFile)
1135
1137
  throw new Error(`Nested run '${run.id}' does not have a persisted session file to resume from.`);
@@ -1151,7 +1153,24 @@ function validateNestedSessionFile(run, trustedSessionRoots) {
1151
1153
  throw new Error(`Nested run '${run.id}' session file is outside trusted nested session roots: ${sessionFile}`);
1152
1154
  }
1153
1155
  if (!realSessionFile.split(path.sep).includes(run.id)) {
1154
- throw new Error(`Nested run '${run.id}' session file is not under that nested run's session directory: ${sessionFile}`);
1156
+ // A resume replacement continues its source's session; accept it only when that source,
1157
+ // resolved in the caller's own scope, is one the caller could resume itself (the states
1158
+ // resolveNestedResumeTarget refuses), validates on its own and owns this same file.
1159
+ const sourceId = run.revivedFrom;
1160
+ const chain = [...visited, run.id];
1161
+ const source = sourceId && chain.length <= 8 && !chain.includes(sourceId) ? resolveSource(sourceId) : undefined;
1162
+ let sourceFile;
1163
+ if (source && source.state !== "running" && source.state !== "queued" && source.state !== "stopped") {
1164
+ try {
1165
+ sourceFile = validateNestedSessionFile(source, trustedSessionRoots, resolveSource, chain);
1166
+ }
1167
+ catch {
1168
+ sourceFile = undefined;
1169
+ }
1170
+ }
1171
+ if (sourceFile !== realSessionFile) {
1172
+ throw new Error(`Nested run '${run.id}' session file is not under that nested run's session directory: ${sessionFile}`);
1173
+ }
1155
1174
  }
1156
1175
  return realSessionFile;
1157
1176
  }
@@ -1165,7 +1184,7 @@ export function readNestedRecoveryDescriptor(asyncDir, runId, agent) {
1165
1184
  throw new Error(`Nested run '${runId}' has a recovery descriptor for a different agent.`);
1166
1185
  return recoveryDescriptor;
1167
1186
  }
1168
- function resolveNestedResumeTarget(match, trustedSessionRoots) {
1187
+ function resolveNestedResumeTarget(match, trustedSessionRoots, resolveSource) {
1169
1188
  const run = match.match.run;
1170
1189
  if (run.state === "running" || run.state === "queued")
1171
1190
  throw new Error(`Nested run '${run.id}' is live; route the follow-up to the owner process instead.`);
@@ -1185,7 +1204,7 @@ function resolveNestedResumeTarget(match, trustedSessionRoots) {
1185
1204
  agent,
1186
1205
  index: 0,
1187
1206
  cwd: asyncDir ? path.dirname(asyncDir) : undefined,
1188
- sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
1207
+ sessionFile: validateNestedSessionFile(run, trustedSessionRoots, resolveSource),
1189
1208
  ...(run.capabilityCeiling ? { capabilityCeiling: run.capabilityCeiling } : {}),
1190
1209
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
1191
1210
  });
@@ -1522,9 +1541,10 @@ async function resumeExternalJobFollowUp(input) {
1522
1541
  }
1523
1542
  function resolveRequestedResumeTarget(params, deps, parentSessionFile) {
1524
1543
  const requestedId = params.id ?? params.runId;
1544
+ const resolutionDeps = omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
1525
1545
  let resolved;
1526
1546
  try {
1527
- resolved = requestedId ? resolveSubagentRunId(requestedId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) })) : undefined;
1547
+ resolved = requestedId ? resolveSubagentRunId(requestedId, resolutionDeps) : undefined;
1528
1548
  }
1529
1549
  catch (error) {
1530
1550
  const message = error instanceof Error ? error.message : "";
@@ -1540,11 +1560,18 @@ function resolveRequestedResumeTarget(params, deps, parentSessionFile) {
1540
1560
  const externalJobTarget = resolveNestedExternalJobResumeTarget(resolved, params, deps);
1541
1561
  if (externalJobTarget)
1542
1562
  return externalJobTarget;
1543
- const trustedSessionRoots = [
1544
- ...(deps.config.defaultSessionDir ? [path.resolve(deps.expandTilde(deps.config.defaultSessionDir))] : []),
1545
- ...(parentSessionFile ? [deps.getSubagentSessionRoot(parentSessionFile)] : []),
1546
- ];
1547
- return resolveNestedResumeTarget(resolved, trustedSessionRoots);
1563
+ const rootRunId = resolved.match.rootRunId;
1564
+ // The source must be a nested run the caller could address itself, in the same registry.
1565
+ const resolveSource = (id) => {
1566
+ try {
1567
+ const source = resolveSubagentRunId(id, resolutionDeps);
1568
+ return source?.kind === "nested" && source.id === id && source.match.rootRunId === rootRunId ? source.match.run : undefined;
1569
+ }
1570
+ catch {
1571
+ return undefined;
1572
+ }
1573
+ };
1574
+ return resolveNestedResumeTarget(resolved, trustedSessionRoots(parentSessionFile, deps), resolveSource);
1548
1575
  }
1549
1576
  return resolveResumeTarget(params, deps.state, { asyncRequireSessionFile: false });
1550
1577
  }
@@ -3012,9 +3039,9 @@ function importWorkflowAwaitedChildResult(completed, options) {
3012
3039
  };
3013
3040
  }
3014
3041
  // A replaced runtime leaves the child running so a relaunch of the workflow can re-attach to it.
3015
- function stopAwaitedAsyncChildOnAbort(signal, state, runId, asyncDir, kill) {
3042
+ function stopAwaitedAsyncChildOnAbort(signal, state, runId, asyncDir, kill, reattachedFromOtherSession = false) {
3016
3043
  const stopOnAbort = () => { if (!workflowStopCause(signal?.reason))
3017
- stopAsyncRun(state, runId, kill, { asyncDir, resolvedId: runId }); };
3044
+ stopAsyncRun(state, runId, kill, { asyncDir, resolvedId: runId, ...(reattachedFromOtherSession ? { reattachedRunId: runId } : {}) }); };
3018
3045
  if (signal?.aborted)
3019
3046
  stopOnAbort();
3020
3047
  else
@@ -4052,13 +4079,16 @@ export function bindMissionWorkflowChildAsyncLaunch(params, binding, asyncByDefa
4052
4079
  }
4053
4080
  function workflowChildResult(key, result, childParams = {}, resumeState, forcedTerminalOutcome) {
4054
4081
  const running = missionWorkflowChildStatus(result) === "running";
4055
- const receiptOutput = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
4082
+ // The receipt opens with the workflow-wide run fan-out line, which says nothing about this child.
4083
+ const fanoutPrefix = result.details.runFanoutBudget ? `${formatRunFanoutBudget(result.details.runFanoutBudget)}\n` : undefined;
4084
+ const fullReceipt = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
4085
+ const receiptOutput = fanoutPrefix && fullReceipt.startsWith(fanoutPrefix) ? fullReceipt.slice(fanoutPrefix.length) : fullReceipt;
4056
4086
  const output = running ? "" : result.details.results.length === 1 && result.details.results[0]?.finalOutput !== undefined
4057
4087
  ? result.details.results[0].finalOutput
4058
4088
  : receiptOutput;
4059
4089
  const childError = result.details.results.map((child) => child.error).find((error) => Boolean(error));
4060
4090
  const stopped = result.details.results.some((child) => child.stopped);
4061
- // A stopped child's receipt text carries the run fan-out annotation; report the structured stop reason instead.
4091
+ // A stopped child reports its structured stop reason alone, without the receipt's output block.
4062
4092
  const failureErrorBase = stopped && childError
4063
4093
  ? childError
4064
4094
  : childError && receiptOutput
@@ -4884,6 +4914,17 @@ export function createSubagentExecutor(deps) {
4884
4914
  && !inheritedNestedRoute(deps)
4885
4915
  && !requestParams.workflowParentRunId;
4886
4916
  const workflowRunId = asyncWorkflow ? randomUUID() : undefined;
4917
+ if (requestParams.reuseFrom !== undefined && !topLevelAsyncWorkflow) {
4918
+ return { content: [{ type: "text", text: "options.reuseFrom requires an async top-level workflow." }], isError: true, details: { mode: "workflow", results: [] } };
4919
+ }
4920
+ let namedWorkflowReuseSource;
4921
+ try {
4922
+ if (requestParams.reuseFrom !== undefined)
4923
+ namedWorkflowReuseSource = readNamedWorkflowReuseSource(DIRS.async, requestParams.reuseFrom, workflowScriptDigest(requestParams.workflowScript), workflowCwd);
4924
+ }
4925
+ catch (error) {
4926
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4927
+ }
4887
4928
  let workflowCapacity;
4888
4929
  if (workflowRunId && topLevelAsyncWorkflow) {
4889
4930
  const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
@@ -4950,7 +4991,7 @@ export function createSubagentExecutor(deps) {
4950
4991
  const toolCallId = _id;
4951
4992
  const workflowSessionRoot = requestParams.sessionDir
4952
4993
  ? path.resolve(deps.expandTilde(requestParams.sessionDir))
4953
- : trustedSessionRootsForStatus(ctx, deps)[0];
4994
+ : trustedSessionRoots(ctx.sessionManager.getSessionFile() ?? null, deps)[0];
4954
4995
  const asyncDir = path.join(DIRS.async, workflowRunId);
4955
4996
  const resultPath = resultFilePath(DIRS.results, workflowRunId);
4956
4997
  const statusPath = path.join(asyncDir, "status.json");
@@ -4961,7 +5002,7 @@ export function createSubagentExecutor(deps) {
4961
5002
  const completionOwnerId = deps.state.completionOwnerId ?? currentCompletionOwnerId();
4962
5003
  deps.state.completionOwnerId = completionOwnerId;
4963
5004
  const workflowScriptSourceDigest = workflowScriptDigest(requestParams.workflowScript);
4964
- const workflowReuseSource = findWorkflowReuseSource(DIRS.async, currentSessionId, workflowScriptSourceDigest, workflowArgsDigest);
5005
+ const workflowReuseSource = namedWorkflowReuseSource ?? findWorkflowReuseSource(DIRS.async, currentSessionId, workflowScriptSourceDigest, workflowArgsDigest);
4965
5006
  const workflowRunEvidence = { ...workflowArgsEvidence, scriptDigest: workflowScriptSourceDigest };
4966
5007
  if (workflowReuseSource)
4967
5008
  workflowRunEvidence.reusedFrom = workflowReuseSource.runId;
@@ -5486,6 +5527,9 @@ export function createSubagentExecutor(deps) {
5486
5527
  if (entry.state === "reused" && existing) {
5487
5528
  continue;
5488
5529
  }
5530
+ // A runtime replacement does not stop awaited async children (stopAwaitedAsyncChildOnAbort): they keep running.
5531
+ if (entry.state === "stopped" && existing?.async === true && existing.runId && workflowStopCause(controller.signal.reason))
5532
+ continue;
5489
5533
  const mapped = entry.state === "started" || entry.state === "reused"
5490
5534
  ? "running"
5491
5535
  : entry.state === "completed"
@@ -5677,7 +5721,7 @@ export function createSubagentExecutor(deps) {
5677
5721
  const priorRunId = priorChild.runId;
5678
5722
  const priorAsyncDir = path.join(DIRS.async, priorRunId);
5679
5723
  appendWorkflowChildJournal(asyncDir, { type: "start", key, fingerprint: journalFingerprint, runId: priorRunId });
5680
- const stopListener = stopAwaitedAsyncChildOnAbort(workflowSignal, deps.state, priorRunId, priorAsyncDir, deps.kill);
5724
+ const stopListener = stopAwaitedAsyncChildOnAbort(workflowSignal, deps.state, priorRunId, priorAsyncDir, deps.kill, requestParams.reuseFrom !== undefined);
5681
5725
  const reattached = await awaitExistingAsyncRun(priorAsyncDir, priorRunId, workflowSignal).finally(stopListener.remove);
5682
5726
  const claimedPath = reattached.status === "settled" ? claimWorkflowAwaitedResult(priorAsyncDir, workflowRunId, { ...reattached.result.importedPublication, runId: priorRunId }) : undefined;
5683
5727
  if (reattached.status === "settled" && claimedPath) {
@@ -5874,7 +5918,7 @@ export function createSubagentExecutor(deps) {
5874
5918
  const errorPreview = status.error && status.error.length > 1_000 ? `${status.error.slice(0, 1_000)}… (truncated; full error: ${statusPath} (error))` : status.error;
5875
5919
  const terminalSummary = `${status.state === "complete"
5876
5920
  ? "Workflow completed after detached child finished."
5877
- : errorPreview ?? (pauseForDetached ? "Workflow paused." : "Workflow failed.")}${stopCause ? ` ${WORKFLOW_RUNTIME_REPLACED_RELAUNCH_NOTICE}` : ""}${workflowOutputPathMappingSummary(partial.children)}${finalPreflightWarnings.length ? ` ${finalPreflightWarnings.join(" ")}` : ""}`;
5921
+ : errorPreview ?? (pauseForDetached ? "Workflow paused." : "Workflow failed.")}${stopCause ? ` ${workflowRuntimeReplacedRelaunchNotice(workflowRunId)}` : ""}${workflowOutputPathMappingSummary(partial.children)}${finalPreflightWarnings.length ? ` ${finalPreflightWarnings.join(" ")}` : ""}`;
5878
5922
  const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, terminalSummary, producedChildOutputPaths);
5879
5923
  const resultSummary = appendWorkflowOutputWarning(terminalSummary, outputWarning);
5880
5924
  const receiptState = status.state === "complete" ? "complete" : status.state === "paused" ? "paused" : status.state === "stopped" ? "stopped" : "failed";
@@ -5947,6 +5991,7 @@ export function createSubagentExecutor(deps) {
5947
5991
  const workflowHostSteps = new Map();
5948
5992
  let liveWorkflow = { trace: [], emits: [], console: [], ...(workflowResource ? { resource: workflowResource.provenance } : {}) };
5949
5993
  const childProgress = new Map();
5994
+ const childLaunches = new Map();
5950
5995
  let activityTimer;
5951
5996
  let lastProgressAt = 0;
5952
5997
  let workflowProgressClosed = false;
@@ -5959,7 +6004,7 @@ export function createSubagentExecutor(deps) {
5959
6004
  clearTimeout(activityTimer);
5960
6005
  activityTimer = undefined;
5961
6006
  lastProgressAt = Date.now();
5962
- const liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: foregroundWorkflowRunId, workflowState: "running", inventoryComplete: false, trace: liveWorkflow.trace, progress: childProgress });
6007
+ const liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: foregroundWorkflowRunId, workflowState: "running", inventoryComplete: false, trace: liveWorkflow.trace, progress: childProgress, launches: childLaunches });
5963
6008
  onUpdate?.(workflowProgressUpdate(foregroundWorkflowRunId, chatProgress, liveWorkflow, liveWorkflowChildren, workflowPreflight));
5964
6009
  };
5965
6010
  try {
@@ -6047,11 +6092,21 @@ export function createSubagentExecutor(deps) {
6047
6092
  throw new Error("Workflow child permit supports native Pi children only.");
6048
6093
  workflowPermitContexts.set(childRequest, { child: { permit: delegatedWorkflowPermit, workflowRunId: foregroundWorkflowRunId, childKey: key } });
6049
6094
  }
6050
- workflowLaunchObservers.set(childRequest, (launch) => recordMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, {
6051
- status: "running",
6052
- agent: launch.agent,
6053
- ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}),
6054
- }));
6095
+ workflowLaunchObservers.set(childRequest, (launch) => {
6096
+ childLaunches.set(key, { agent: launch.agent, ...(launch.runId ? { runId: launch.runId } : {}) });
6097
+ // Telemetry must not decide the child's outcome, as with onTrace in scripted-workflow.
6098
+ try {
6099
+ sendWorkflowProgress();
6100
+ }
6101
+ catch (error) {
6102
+ console.error("Workflow progress update failed:", error);
6103
+ }
6104
+ recordMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, {
6105
+ status: "running",
6106
+ agent: launch.agent,
6107
+ ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}),
6108
+ });
6109
+ });
6055
6110
  return execute(randomUUID(), childRequest, workflowSignal, (update) => {
6056
6111
  const progress = update.details.progress?.[0];
6057
6112
  if (!progress)
@@ -6593,7 +6648,7 @@ export function createSubagentExecutor(deps) {
6593
6648
  };
6594
6649
  };
6595
6650
  const nestedScope = nestedResolutionScopeForExecutor(deps);
6596
- const sessionRoots = trustedSessionRootsForStatus(ctx, deps);
6651
+ const sessionRoots = trustedSessionRoots(ctx.sessionManager.getSessionFile() ?? null, deps);
6597
6652
  if (action === "debug.run") {
6598
6653
  if (!targetRunId && !hasDirectoryTarget) {
6599
6654
  return withBudget({ content: [{ type: "text", text: "action='debug.run' requires id, runId, or dir." }], isError: true, details: { mode: "management", results: [] } });
@@ -7035,12 +7090,20 @@ export function createSubagentExecutor(deps) {
7035
7090
  // Forked children keep their requested thinking level. Signed Anthropic thinking
7036
7091
  // blocks are stripped from the inherited transcript by the resolver (they are bound
7037
7092
  // to the parent session), which is not a reason to disable the child's own reasoning.
7093
+ let sessionRunId;
7038
7094
  try {
7039
7095
  assertLaunchableAgentsAllowedBeforeFork();
7040
7096
  const pruneSession = contextPolicy.usesFork && deps.config.forkContext?.mode === "pruned"
7041
7097
  ? await createPrunedForkSessionWriter(ctx, deps.config.forkContext, signal)
7042
7098
  : undefined;
7043
- const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined, pruneSession ? { pruneSession } : {});
7099
+ const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined, {
7100
+ ...(pruneSession ? { pruneSession } : {}),
7101
+ runDirName: () => {
7102
+ if (!sessionRunId)
7103
+ throw new Error("A fork session was requested before the launch's run id was assigned.");
7104
+ return sessionRunId;
7105
+ },
7106
+ });
7044
7107
  prepareForkSessionForIndex = forkContextResolver.prepareSessionForIndex;
7045
7108
  forkSessionFileForIndex = forkContextResolver.sessionFileForIndex;
7046
7109
  }
@@ -7143,16 +7206,19 @@ export function createSubagentExecutor(deps) {
7143
7206
  console.warn(`[pi-subagents] ${warning}`);
7144
7207
  }
7145
7208
  let sessionRoot;
7209
+ // Key the session directory by the id the run is recorded under: nested resume
7210
+ // accepts a session file only inside a directory named by that run's id.
7211
+ sessionRunId = effectiveAsync ? asyncRunId : runId;
7146
7212
  if (effectiveParams.sessionDir) {
7147
7213
  // An explicit sessionDir is a root keyed by this launch's run id so
7148
7214
  // concurrent children resolve distinct per-child session files.
7149
- sessionRoot = path.join(path.resolve(deps.expandTilde(effectiveParams.sessionDir)), runId);
7215
+ sessionRoot = path.join(path.resolve(deps.expandTilde(effectiveParams.sessionDir)), sessionRunId);
7150
7216
  }
7151
7217
  else {
7152
7218
  const baseSessionRoot = deps.config.defaultSessionDir
7153
7219
  ? path.resolve(deps.expandTilde(deps.config.defaultSessionDir))
7154
7220
  : deps.getSubagentSessionRoot(parentSessionFile);
7155
- sessionRoot = path.join(baseSessionRoot, runId);
7221
+ sessionRoot = path.join(baseSessionRoot, sessionRunId);
7156
7222
  }
7157
7223
  try {
7158
7224
  fs.mkdirSync(sessionRoot, { recursive: true });