pi-subagents 0.80.0 → 0.82.0

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 (184) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/bundle/agents.js +18 -0
  3. package/bundle/background-work.js +16 -0
  4. package/bundle/capability-ceiling.js +22 -0
  5. package/bundle/child-tool-plan.js +11 -0
  6. package/bundle/chunk-43SCHIMD.js +57 -0
  7. package/bundle/chunk-4QFZZV3O.js +57 -0
  8. package/bundle/chunk-4SDU2BZX.js +1190 -0
  9. package/bundle/chunk-52UBALJL.js +753 -0
  10. package/bundle/chunk-5LFLVSTM.js +260 -0
  11. package/bundle/chunk-5TYMSZX3.js +235 -0
  12. package/bundle/chunk-62OCY2S4.js +14 -0
  13. package/bundle/chunk-7F6RKT6W.js +132 -0
  14. package/bundle/chunk-7IVSUMQQ.js +1337 -0
  15. package/bundle/chunk-7J4ZXMY7.js +18 -0
  16. package/bundle/chunk-7PCKZHR5.js +1234 -0
  17. package/bundle/chunk-7YMXOYEU.js +1373 -0
  18. package/bundle/chunk-B5EKXDHE.js +16870 -0
  19. package/bundle/chunk-CIZQXJ5Q.js +8619 -0
  20. package/bundle/chunk-CTPAE36Y.js +94 -0
  21. package/bundle/chunk-D4EHT5SJ.js +14 -0
  22. package/bundle/chunk-GC4DSPMH.js +946 -0
  23. package/bundle/chunk-GJECHSAR.js +179 -0
  24. package/bundle/chunk-M2CTFYT3.js +4164 -0
  25. package/bundle/chunk-MHZ4G23A.js +1296 -0
  26. package/bundle/chunk-N2BN7BF5.js +159 -0
  27. package/bundle/chunk-NF7IEKBH.js +490 -0
  28. package/bundle/chunk-NGGAFDL6.js +742 -0
  29. package/bundle/chunk-NIXWDZZV.js +9 -0
  30. package/bundle/chunk-NRQ75MGH.js +8327 -0
  31. package/bundle/chunk-OYF6AFDY.js +458 -0
  32. package/bundle/chunk-PLCCBLPY.js +526 -0
  33. package/bundle/chunk-RMQFMMAT.js +416 -0
  34. package/bundle/chunk-SIJ4S3EN.js +276 -0
  35. package/bundle/chunk-UKG4V45O.js +228 -0
  36. package/bundle/chunk-VH5OAWVG.js +4096 -0
  37. package/bundle/chunk-W2SZ22EM.js +5462 -0
  38. package/bundle/chunk-WVD4UEL4.js +9367 -0
  39. package/bundle/chunk-XSWOTIA3.js +175 -0
  40. package/bundle/chunk-YFCK7H2S.js +129 -0
  41. package/bundle/chunk-YKEWIB4M.js +380 -0
  42. package/bundle/chunk-YYREO77V.js +137 -0
  43. package/bundle/chunk-Z7KVHQHL.js +173 -0
  44. package/bundle/chunk-ZLINIYTP.js +8613 -0
  45. package/bundle/chunk-ZQGSDHL6.js +128 -0
  46. package/bundle/control-channel.js +11 -0
  47. package/bundle/delegation.js +14 -0
  48. package/bundle/external-job-provider.js +20 -0
  49. package/bundle/external-runs.js +21 -0
  50. package/bundle/index.js +30 -0
  51. package/bundle/inspectors.js +8 -0
  52. package/bundle/intercom-bridge.js +11 -0
  53. package/bundle/preflight.js +425 -0
  54. package/bundle/project-panes.js +28 -0
  55. package/bundle/required-child-extensions.js +6 -0
  56. package/bundle/shared-types.js +8 -0
  57. package/bundle/workflow-resources.js +7 -0
  58. package/docs/agents.md +31 -0
  59. package/docs/configuration.md +9 -1
  60. package/docs/extension-api.md +27 -0
  61. package/docs/observability.md +6 -2
  62. package/docs/tool-reference.md +3 -1
  63. package/docs/workflows.md +16 -0
  64. package/package.json +19 -19
  65. package/skills/pi-subagents/references/execution-controls.md +5 -4
  66. package/src/agents/agent-management.js +3 -3
  67. package/src/agents/agent-memory.js +3 -1
  68. package/src/agents/agents.js +6 -6
  69. package/src/agents/frontmatter.d.ts +4 -0
  70. package/src/agents/frontmatter.js +24 -3
  71. package/src/agents/skills.js +2 -1
  72. package/src/api/delegation.d.ts +20 -0
  73. package/src/extension/config.js +3 -0
  74. package/src/extension/herdr-pi-bridge.js +17 -10
  75. package/src/extension/index.js +109 -9
  76. package/src/extension/schemas.js +3 -3
  77. package/src/intercom/native-supervisor-channel.js +20 -0
  78. package/src/missions/lifecycle.js +7 -1
  79. package/src/runs/background/active-async-capacity.d.ts +1 -0
  80. package/src/runs/background/active-async-capacity.js +9 -3
  81. package/src/runs/background/async-execution.js +20 -10
  82. package/src/runs/background/async-job-tracker.d.ts +6 -1
  83. package/src/runs/background/async-job-tracker.js +9 -4
  84. package/src/runs/background/async-status.d.ts +2 -0
  85. package/src/runs/background/async-status.js +21 -2
  86. package/src/runs/background/await-async-run.js +3 -7
  87. package/src/runs/background/boot-id.d.ts +12 -0
  88. package/src/runs/background/boot-id.js +47 -0
  89. package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
  90. package/src/runs/background/interrupted-elsewhere.js +68 -0
  91. package/src/runs/background/notify.d.ts +2 -0
  92. package/src/runs/background/notify.js +13 -0
  93. package/src/runs/background/result-delivery-ownership.d.ts +6 -0
  94. package/src/runs/background/result-delivery-ownership.js +19 -1
  95. package/src/runs/background/result-watcher.d.ts +3 -1
  96. package/src/runs/background/result-watcher.js +24 -8
  97. package/src/runs/background/run-child-session.d.ts +1 -6
  98. package/src/runs/background/run-child-session.js +19 -1
  99. package/src/runs/background/run-status.js +2 -1
  100. package/src/runs/background/stale-run-reconciler.d.ts +25 -0
  101. package/src/runs/background/stale-run-reconciler.js +54 -13
  102. package/src/runs/background/subagent-runner-bootstrap.js +3 -0
  103. package/src/runs/background/subagent-runner.d.ts +4 -0
  104. package/src/runs/background/subagent-runner.js +13 -5
  105. package/src/runs/background/subagent-wait.d.ts +1 -1
  106. package/src/runs/background/subagent-wait.js +6 -1
  107. package/src/runs/background/terminal-run-index.d.ts +1 -0
  108. package/src/runs/background/terminal-run-index.js +4 -2
  109. package/src/runs/foreground/async-stop-action.js +3 -1
  110. package/src/runs/foreground/execution.js +45 -5
  111. package/src/runs/foreground/foreground-history.d.ts +16 -2
  112. package/src/runs/foreground/foreground-history.js +133 -31
  113. package/src/runs/foreground/subagent-executor.d.ts +11 -0
  114. package/src/runs/foreground/subagent-executor.js +277 -74
  115. package/src/runs/shared/child-launch.d.ts +2 -2
  116. package/src/runs/shared/child-session.d.ts +3 -2
  117. package/src/runs/shared/child-session.js +51 -8
  118. package/src/runs/shared/child-tool-plan.d.ts +7 -0
  119. package/src/runs/shared/child-tool-plan.js +21 -6
  120. package/src/runs/shared/herdr-connection.d.ts +17 -8
  121. package/src/runs/shared/herdr-connection.js +19 -7
  122. package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
  123. package/src/runs/shared/herdr-external-adapters.js +1 -1
  124. package/src/runs/shared/herdr-machine.d.ts +12 -2
  125. package/src/runs/shared/herdr-machine.js +55 -9
  126. package/src/runs/shared/herdr-pane-host.d.ts +88 -0
  127. package/src/runs/shared/herdr-pane-host.js +155 -0
  128. package/src/runs/shared/herdr-placed-run.d.ts +27 -150
  129. package/src/runs/shared/herdr-placed-run.js +48 -395
  130. package/src/runs/shared/local-machine-access.d.ts +5 -0
  131. package/src/runs/shared/local-machine-access.js +28 -0
  132. package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
  133. package/src/runs/shared/orca-progress-tabs.js +1 -24
  134. package/src/runs/shared/parallel-utils.d.ts +1 -1
  135. package/src/runs/shared/placed-run-owner.d.ts +49 -0
  136. package/src/runs/shared/placed-run-owner.js +93 -0
  137. package/src/runs/shared/placement-host.d.ts +99 -0
  138. package/src/runs/shared/placement-host.js +157 -0
  139. package/src/runs/shared/rex-machine.d.ts +25 -0
  140. package/src/runs/shared/rex-machine.js +83 -0
  141. package/src/runs/shared/rex-pane-host.d.ts +62 -0
  142. package/src/runs/shared/rex-pane-host.js +327 -0
  143. package/src/runs/shared/structured-output.d.ts +1 -0
  144. package/src/runs/shared/structured-output.js +1 -0
  145. package/src/shared/accessible-dir.js +4 -1
  146. package/src/shared/completion-owner.d.ts +5 -0
  147. package/src/shared/completion-owner.js +5 -0
  148. package/src/shared/provider-diagnostics.d.ts +9 -0
  149. package/src/shared/provider-diagnostics.js +75 -0
  150. package/src/shared/session-identity.d.ts +2 -0
  151. package/src/shared/session-identity.js +26 -0
  152. package/src/shared/types.d.ts +48 -6
  153. package/src/slash/delegation-adapters.d.ts +3 -1
  154. package/src/slash/delegation-adapters.js +7 -0
  155. package/src/slash/prompt-template-bridge.js +3 -1
  156. package/src/slash/subagents-admin.d.ts +1 -0
  157. package/src/slash/subagents-admin.js +1 -1
  158. package/src/tui/fleet-layout.d.ts +20 -0
  159. package/src/tui/fleet-layout.js +17 -0
  160. package/src/tui/fleet-viewport.d.ts +12 -0
  161. package/src/tui/fleet-viewport.js +35 -0
  162. package/src/tui/fleet.d.ts +6 -6
  163. package/src/tui/fleet.js +93 -66
  164. package/src/tui/render.d.ts +6 -0
  165. package/src/tui/render.js +3 -3
  166. package/src/tui/subagent-messages.js +1 -0
  167. package/src/workflows/scripted-workflow.d.ts +1 -1
  168. package/src/workflows/structured-workflow-scripts.d.ts +1 -0
  169. package/src/workflows/structured-workflow-scripts.js +5 -4
  170. package/src/workflows/workflow-auto-resume.d.ts +32 -0
  171. package/src/workflows/workflow-auto-resume.js +102 -0
  172. package/src/workflows/workflow-launch-record.d.ts +27 -0
  173. package/src/workflows/workflow-launch-record.js +48 -0
  174. package/src/workflows/workflow-resources.js +2 -2
  175. package/src/workflows/workflow-resume-claim.d.ts +82 -0
  176. package/src/workflows/workflow-resume-claim.js +210 -0
  177. package/src/workflows/workflow-resume.d.ts +56 -0
  178. package/src/workflows/workflow-resume.js +106 -0
  179. package/src/workflows/workflow-reuse.d.ts +15 -9
  180. package/src/workflows/workflow-reuse.js +77 -14
  181. package/src/workflows/workflow-revival.d.ts +2 -0
  182. package/src/workflows/workflow-revival.js +2 -1
  183. package/src/workflows/workflow-stop-cause.d.ts +6 -0
  184. package/src/workflows/workflow-stop-cause.js +8 -0
@@ -10,6 +10,7 @@ import { buildSkillInjection, resolveSkills } from "../agents/skills.js";
10
10
  import { buildAgentMemoryInjection } from "../agents/agent-memory.js";
11
11
  import { appendAgentRefinementOverlay } from "../agents/agent-refinements.js";
12
12
  import { rewriteSubagentPrompt } from "../runs/shared/subagent-prompt-runtime.js";
13
+ import { assertNoToolModifierEntries } from "../runs/shared/child-tool-plan.js";
13
14
  import { resolveExistingReadPaths } from "../shared/settings.js";
14
15
  import { MODEL_ONLY_TOOL } from "../shared/extension-context.js";
15
16
  function gitEvidence(cwd) {
@@ -37,6 +38,7 @@ export function resolveRemoteHerdrResources(cwd, resources, remoteDefaultTools =
37
38
  const reads = resources.reads === undefined ? agent.defaultReads ?? false : resources.reads;
38
39
  const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, cwd) : [];
39
40
  const systemPrompt = appendAgentRefinementOverlay(`${agent.systemPrompt}${buildAgentMemoryInjection(agent, cwd)}${skillPrompt ? `\n\n${skillPrompt}` : ""}${readPaths.length ? `\n\n[Read from: ${readPaths.join(", ")}]` : ""}`, { cwd, agentName: agent.name });
41
+ assertNoToolModifierEntries(agent.name, agent.tools ?? []);
40
42
  const denied = new Set(agent.excludeTools ?? []);
41
43
  const tools = (agent.tools === undefined ? remoteDefaultTools : agent.tools).filter((tool) => !denied.has(tool) && (!resources.toolCeiling || resources.toolCeiling.includes(tool)));
42
44
  if (agent.tools === undefined && !tools.length)
@@ -91,7 +93,6 @@ export default function registerHerdrPiBridge(pi) {
91
93
  client.write(encodeHerdrPiFrame(record));
92
94
  };
93
95
  const sessionId = (ctx) => ctx.sessionManager?.getSessionId?.();
94
- const activeTools = (ctx) => ctx.getActiveTools?.() ?? [];
95
96
  pi.registerTool({
96
97
  name: "contact_supervisor",
97
98
  ...MODEL_ONLY_TOOL,
@@ -104,11 +105,11 @@ export default function registerHerdrPiBridge(pi) {
104
105
  const id = `sup_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
105
106
  send({ type: "supervisor-request", requestId: id, reason: input.reason, message: input.message?.slice(0, 65_536) ?? "", ...(input.interview !== undefined ? { interview: input.interview } : {}), expectsReply: input.reason !== "progress_update", nativeSessionId });
106
107
  if (input.reason === "progress_update") {
107
- await new Promise((resolve) => supervisor.set(id, { reason: input.reason, resolve }));
108
+ await new Promise((resolve, reject) => supervisor.set(id, { reason: input.reason, resolve, reject }));
108
109
  return { content: [{ type: "text", text: "Supervisor progress update queued." }], details: { delivered: true, requestId: id, reason: input.reason } };
109
110
  }
110
111
  const reason = input.reason;
111
- const answer = await new Promise((resolve) => supervisor.set(id, { reason, resolve }));
112
+ const answer = await new Promise((resolve, reject) => supervisor.set(id, { reason, resolve, reject }));
112
113
  const details = { requestId: id, reason };
113
114
  if (reason === "interview_request")
114
115
  try {
@@ -145,7 +146,10 @@ export default function registerHerdrPiBridge(pi) {
145
146
  if (!entry)
146
147
  throw new Error("Unknown supervisor request.");
147
148
  supervisor.delete(request.supervisorId);
148
- entry.resolve(request.text ?? "");
149
+ if (request.error !== undefined)
150
+ entry.reject(new Error(request.error));
151
+ else
152
+ entry.resolve(request.text ?? "");
149
153
  }
150
154
  send({ type: "control-ack", requestId, operation: request.operation, nativeSessionId });
151
155
  };
@@ -165,7 +169,7 @@ export default function registerHerdrPiBridge(pi) {
165
169
  if (!nativeSessionId)
166
170
  throw new Error("Pane-native Pi bridge requires a persisted native session identity.");
167
171
  fs.writeFileSync(manifestPath, JSON.stringify({ protocol: HERDR_PI_PROTOCOL, packageVersion: packageJson.version, runId, socketPath, nativeSessionId, pid: process.pid }), { mode: 0o600, flag: "wx" });
168
- send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: ctx.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: activeTools(ctx), model: ctx.model?.provider && ctx.model.id ? `${ctx.model.provider}/${ctx.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
172
+ send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: ctx.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: pi.getActiveTools(), model: ctx.model?.provider && ctx.model.id ? `${ctx.model.provider}/${ctx.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
169
173
  });
170
174
  on("message_end", (event) => {
171
175
  const message = event.message;
@@ -215,11 +219,11 @@ export default function registerHerdrPiBridge(pi) {
215
219
  const invalidReads = resources.reads !== undefined && resources.reads !== false && (!Array.isArray(resources.reads) || resources.reads.some((name) => typeof name !== "string"));
216
220
  if (typeof resources.agent !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(resources.agent) || invalidSkills || invalidReads)
217
221
  throw new Error("Remote logical resource names are invalid.");
218
- const resolved = resolveRemoteHerdrResources(process.cwd(), { agent: resources.agent, ...(resources.skills ? { skills: resources.skills } : {}), ...(resources.toolCeiling ? { toolCeiling: resources.toolCeiling } : {}), ...(resources.reads !== undefined ? { reads: resources.reads } : {}) }, context ? activeTools(context) : []);
222
+ const resolved = resolveRemoteHerdrResources(process.cwd(), { agent: resources.agent, ...(resources.skills ? { skills: resources.skills } : {}), ...(resources.toolCeiling ? { toolCeiling: resources.toolCeiling } : {}), ...(resources.reads !== undefined ? { reads: resources.reads } : {}) }, context ? pi.getActiveTools() : []);
219
223
  resolvedSystemPrompt = resolved.systemPrompt;
220
224
  resolvedContextPolicy = resolved;
221
- pi.setActiveTools?.(resolved.tools);
222
- const acknowledged = context ? activeTools(context) : [];
225
+ pi.setActiveTools(resolved.tools);
226
+ const acknowledged = context ? pi.getActiveTools() : [];
223
227
  if (acknowledged.some((tool) => !resolved.tools.includes(tool)) || resolved.tools.some((tool) => !acknowledged.includes(tool)))
224
228
  throw new Error("Remote Pi did not apply the resolved active-tool set exactly.");
225
229
  configured = true;
@@ -234,7 +238,10 @@ export default function registerHerdrPiBridge(pi) {
234
238
  const text = frame.text;
235
239
  if (text !== undefined && (typeof text !== "string" || Buffer.byteLength(text) > 256 * 1024))
236
240
  throw new Error("Bridge request text is invalid or too large.");
237
- pending.set(frame.requestId, { operation: operation, ...(text !== undefined ? { text } : {}), ...(typeof frame.supervisorId === "string" ? { supervisorId: frame.supervisorId } : {}) });
241
+ const error = frame.error;
242
+ if (error !== undefined && (operation !== "supervisor-reply" || typeof error !== "string" || Buffer.byteLength(error) > 256 * 1024))
243
+ throw new Error("Bridge supervisor error is invalid or too large.");
244
+ pending.set(frame.requestId, { operation: operation, ...(text !== undefined ? { text } : {}), ...(typeof frame.supervisorId === "string" ? { supervisorId: frame.supervisorId } : {}), ...(error !== undefined ? { error } : {}) });
238
245
  send({ type: "prepared", requestId: frame.requestId, nativeSessionId });
239
246
  }
240
247
  }
@@ -245,7 +252,7 @@ export default function registerHerdrPiBridge(pi) {
245
252
  socket.on("close", () => { if (client === socket)
246
253
  client = undefined; });
247
254
  if (context && nativeSessionId)
248
- send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: context.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: activeTools(context), model: context.model?.provider && context.model.id ? `${context.model.provider}/${context.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
255
+ send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: context.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: pi.getActiveTools(), model: context.model?.provider && context.model.id ? `${context.model.provider}/${context.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
249
256
  });
250
257
  server.listen(socketPath, () => { fs.chmodSync(socketPath, 0o600); });
251
258
  }
@@ -43,6 +43,7 @@ import { createResultWatcher } from "../runs/background/result-watcher.js";
43
43
  import { createResultDeliveryOwnership } from "../runs/background/result-delivery-ownership.js";
44
44
  import { createScheduledRunManager } from "../runs/background/scheduled-runs.js";
45
45
  import { registerSlashCommands } from "../slash/slash-commands.js";
46
+ import { ADMIN_MESSAGE_TYPE } from "../slash/subagents-admin.js";
46
47
  import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.js";
47
48
  import { registerMainWatchdog } from "../watchdog/register-main.js";
48
49
  import { registerSlashSubagentBridge } from "../slash/slash-bridge.js";
@@ -72,10 +73,14 @@ import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./
72
73
  import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.js";
73
74
  import { formatWorkflowPreflightSummary, normalizeWorkflowPreflight } from "../workflows/workflow-preflight.js";
74
75
  import { runtimeReplacedAbortReason } from "../workflows/workflow-reuse.js";
76
+ import { autoResumeFailureText, cancelPendingWorkflowAutoResumes, finishWorkflowAutoResume, planWorkflowAutoResumes, WORKFLOW_AUTO_RESUME_MESSAGE_TYPE } from "../workflows/workflow-auto-resume.js";
77
+ import { recordWorkflowResumeRefusal, releaseWorkflowResumeClaim, resumedRunStarted } from "../workflows/workflow-resume-claim.js";
78
+ import { workflowResumeRefusal } from "../workflows/workflow-resume.js";
75
79
  import { finalizeToolResult } from "./tool-result.js";
76
80
  import { flattenSubagentToolOptions } from "./subagent-options.js";
77
81
  import { collectGoalContinuationNotices } from "../missions/goal-driver.js";
78
82
  import { restoreForegroundRunHistory } from "../runs/foreground/foreground-history.js";
83
+ import { createInterruptedElsewhereCollector } from "../runs/background/interrupted-elsewhere.js";
79
84
  import { resolveMissionStoreLocation } from "../missions/store.js";
80
85
  import { listRetainedChildren } from "../runs/background/retained-children.js";
81
86
  import { DIRS, DEFAULT_ARTIFACT_CONFIG, SLASH_RESULT_TYPE, SLASH_TEXT_RESULT_TYPE, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_PROCESS_TERMINAL_EVENT, SUBAGENT_CONTROL_EVENT, SUBAGENT_STEERING_NOTICE_EVENT, WIDGET_KEY, resolveMaxSubagentSpawnsPerSession, } from "../shared/types.js";
@@ -433,6 +438,7 @@ export default function registerSubagentExtension(pi) {
433
438
  refreshResultDelivery();
434
439
  },
435
440
  onJobCleanup: (asyncId) => deliveredRunIds.delete(asyncId),
441
+ deliveryOwnerId: resultDeliveryOwnership.deliveryOwnerId,
436
442
  supervisorRequestState: supervisorChannel.getSupervisorRequestState,
437
443
  onJobsChanged: programStatus.sync,
438
444
  });
@@ -557,13 +563,32 @@ export default function registerSubagentExtension(pi) {
557
563
  return undefined;
558
564
  return createSlashResultComponent(details, options, theme, config.mainWindowRenderer, foregroundDetachShortcut, () => state.lastUiContext?.ui.theme ?? theme);
559
565
  });
560
- // Keep view-only reports in the transcript/RPC stream, but out of model context.
561
- // SLASH_RESULT_TYPE carries delegated work and must remain model-visible.
562
- pi.on("context", (event) => ({
563
- messages: event.messages.filter((message) => message.role !== "custom"
564
- || message.customType !== SLASH_TEXT_RESULT_TYPE
565
- || message.details?.viewOnly !== true),
566
- }));
566
+ // Pi sends custom messages to the model as plain user messages, so every model-visible
567
+ // pi-subagents message is wrapped in a marker naming its source. The marker depends only on
568
+ // the message, so history renders the same bytes every turn and cached prefixes stay valid.
569
+ const markedMessageTypes = new Set([...SUBAGENT_MESSAGE_TYPES, "subagent-compaction-resume", SLASH_RESULT_TYPE, SLASH_TEXT_RESULT_TYPE, ADMIN_MESSAGE_TYPE]);
570
+ pi.on("context", (event) => {
571
+ const messages = [];
572
+ for (const message of event.messages) {
573
+ if (message.role !== "custom" || !markedMessageTypes.has(message.customType)) {
574
+ messages.push(message);
575
+ continue;
576
+ }
577
+ // Keep view-only reports in the transcript/RPC stream, but out of model context.
578
+ // SLASH_RESULT_TYPE carries delegated work and must remain model-visible.
579
+ if (message.customType === SLASH_TEXT_RESULT_TYPE && message.details?.viewOnly === true)
580
+ continue;
581
+ const open = `<pi_subagents_notice type="${message.customType}" origin="automated message from the pi-subagents extension, not typed by the user">`;
582
+ const close = "</pi_subagents_notice>";
583
+ messages.push({
584
+ ...message,
585
+ content: typeof message.content === "string"
586
+ ? `${open}\n${message.content}\n${close}`
587
+ : [{ type: "text", text: `${open}\n` }, ...message.content, { type: "text", text: `\n${close}` }],
588
+ });
589
+ }
590
+ return { messages };
591
+ });
567
592
  pi.registerMessageRenderer(SLASH_TEXT_RESULT_TYPE, (message, _options, _theme) => {
568
593
  const content = typeof message.content === "string"
569
594
  ? message.content
@@ -577,6 +602,60 @@ export default function registerSubagentExtension(pi) {
577
602
  await waitForAdvertisement();
578
603
  return (await getExecutor()).executePublic(id, params, signal, onUpdate, ctx);
579
604
  };
605
+ // Names this runtime's planned auto-resumes in the process-global registry.
606
+ const autoResumeOwner = {};
607
+ const postAutoResumeFailure = (sourceRunId, refusal) => {
608
+ pi.sendMessage({ customType: WORKFLOW_AUTO_RESUME_MESSAGE_TYPE, content: autoResumeFailureText(sourceRunId, refusal), display: true }, { triggerTurn: false });
609
+ };
610
+ // Each session start claims its interrupted workflows synchronously, then launches them here, off
611
+ // session start. Cleanup and the next session start cancel the launches that have not started.
612
+ const launchWorkflowAutoResume = async (entry, ctx) => {
613
+ const { prepared } = entry;
614
+ try {
615
+ // Let the rest of session start (agent advertisement, other handlers) run first.
616
+ await new Promise((resolve) => setImmediate(resolve));
617
+ await waitForAdvertisement();
618
+ const executor = await getExecutor();
619
+ if (entry.state !== "pending")
620
+ return;
621
+ entry.state = "started";
622
+ const outcome = await executor.executeWorkflowResume(randomUUID(), prepared, new AbortController().signal, ctx);
623
+ // The executor has given back the claim and recorded any refusal; nothing below may replace them.
624
+ // A run launched by a runtime that was cleaned up meanwhile would keep running through its stale context.
625
+ if (outcome.launched && runtimeCleaned)
626
+ state.workflowControllers?.get(prepared.newRunId)?.abort(runtimeReplacedAbortReason());
627
+ // A cleaned-up runtime's pi can no longer post; the refusal stays in the run for its notice.
628
+ if (outcome.launched || runtimeCleaned)
629
+ return;
630
+ try {
631
+ postAutoResumeFailure(prepared.sourceRunId, outcome.refusal);
632
+ }
633
+ catch (error) {
634
+ console.error(`Failed to post the auto-resume failure of workflow ${prepared.sourceRunId}:`, error);
635
+ }
636
+ }
637
+ catch (error) {
638
+ // A cancelled launch already gave its claim back.
639
+ if (entry.state === "cancelled")
640
+ return;
641
+ const message = error instanceof Error ? error.message : String(error);
642
+ console.error(`Failed to auto-resume workflow ${prepared.sourceRunId}:`, error);
643
+ try {
644
+ if (resumedRunStarted(prepared.sourceAsyncDir, prepared.newRunId))
645
+ return;
646
+ releaseWorkflowResumeClaim(prepared.sourceAsyncDir, prepared.newRunId);
647
+ const refusal = workflowResumeRefusal("launch-failed", `launch failed: ${message}`, prepared.sourceRunId);
648
+ recordWorkflowResumeRefusal(prepared.sourceAsyncDir, { reason: refusal.reason, byHand: refusal.byHand, newRunId: prepared.newRunId });
649
+ postAutoResumeFailure(prepared.sourceRunId, refusal);
650
+ }
651
+ catch (cleanupError) {
652
+ console.error(`Failed to give back the auto-resume claim of workflow ${prepared.sourceRunId}:`, cleanupError);
653
+ }
654
+ }
655
+ finally {
656
+ finishWorkflowAutoResume(entry);
657
+ }
658
+ };
580
659
  const executeSubagentCollapsed = (id, params, signal, onUpdate, ctx) => {
581
660
  if (ctx.hasUI)
582
661
  ctx.ui.setToolsExpanded(false);
@@ -853,12 +932,31 @@ export default function registerSubagentExtension(pi) {
853
932
  resetJobs(ctx);
854
933
  logSlowPhase("reset-jobs", phaseStartedAt);
855
934
  phaseStartedAt = Date.now();
856
- restoreForegroundRunHistory(state, { resultsDir: DIRS.results });
935
+ // Runs other sessions in this folder left when their Pi died, from what this restore reads anyway.
936
+ const interruptedElsewhere = ctx.hasUI ? createInterruptedElsewhereCollector({ currentSessionId: state.currentSessionId, cwd: ctx.cwd, now: Date.now() }) : undefined;
937
+ restoreForegroundRunHistory(state, { resultsDir: DIRS.results, observeForeign: interruptedElsewhere?.observeForegroundRun });
857
938
  logSlowPhase("foreground-history", phaseStartedAt);
858
939
  phaseStartedAt = Date.now();
859
- restoreActiveJobs(ctx);
940
+ restoreActiveJobs(ctx, interruptedElsewhere?.observeStatus);
860
941
  logSlowPhase("active-job-restore", phaseStartedAt);
861
942
  phaseStartedAt = Date.now();
943
+ // Read-only and UI-only: never sent to the model, so prompts and history are unchanged.
944
+ if (interruptedElsewhere)
945
+ for (const line of interruptedElsewhere.lines())
946
+ ctx.ui.notify(line, "info");
947
+ logSlowPhase("cross-session-notice", phaseStartedAt);
948
+ // Before any stored result is delivered, so the interrupted run's notice can name its resume.
949
+ phaseStartedAt = Date.now();
950
+ // Launches an earlier session start of this runtime planned and has not started belong to that session.
951
+ cancelPendingWorkflowAutoResumes((entry) => entry.owner === autoResumeOwner);
952
+ // Not in a one-shot print run: its turn would then wait on the resumed workflow at settle.
953
+ if (recovering && ctx.hasUI && config.autoResume !== false) {
954
+ for (const entry of planWorkflowAutoResumes({ asyncDirRoot: DIRS.async, currentSessionId: state.currentSessionId, now: Date.now(), owner: autoResumeOwner })) {
955
+ void launchWorkflowAutoResume(entry, ctx);
956
+ }
957
+ }
958
+ logSlowPhase("workflow-auto-resume", phaseStartedAt);
959
+ phaseStartedAt = Date.now();
862
960
  scheduledRunManager.bindSession(ctx);
863
961
  logSlowPhase("scheduled-runs", phaseStartedAt);
864
962
  phaseStartedAt = Date.now();
@@ -883,6 +981,8 @@ export default function registerSubagentExtension(pi) {
883
981
  if (runtimeCleaned)
884
982
  return;
885
983
  runtimeCleaned = true;
984
+ // Synchronously, so a reload's new runtime finds these runs unclaimed when it scans.
985
+ cancelPendingWorkflowAutoResumes((entry) => entry.owner === autoResumeOwner);
886
986
  programStatus.dispose(shutdownReason);
887
987
  releaseHostSessionLiveness();
888
988
  releaseHostSessionLiveness = () => { };
@@ -205,7 +205,7 @@ const SubagentParamProperties = {
205
205
  description: "true: run the one ```js workflow block written in this same reply (false invalid). String with '/': script file read from request cwd. Other string: named workflow resource. Raw scripts (true or path) have no runs.host.",
206
206
  })),
207
207
  args: Type.Optional(Type.Unsafe({ type: "object", maxProperties: MAX_ARGS_FIELDS, additionalProperties: true, description: `Plain-JSON args for workflow; ${MAX_ARGS_FIELDS} fields/object, ${MAX_ARGS_ITEMS} items/array, depth ${MAX_ARGS_DEPTH}, ${MAX_ARGS_BYTES / 1024} KiB total; raw-script args are exposed deeply frozen and persisted, so do not include secrets.` })),
208
- reuseFrom: Type.Optional(Type.String({ minLength: 1, description: "Exact id of a workflow run stopped by a session handoff or reload; this async workflow reuses its finished children and re-attaches to its running ones." })),
208
+ reuseFrom: Type.Optional(Type.String({ minLength: 1, description: "Exact id of a workflow run ended by a reload, handoff or Pi exit; this async workflow reuses its finished children and re-attaches to its running ones." })),
209
209
  globalConcurrencyLimit: Type.Optional(Type.Integer({ minimum: 1 })),
210
210
  maxSubagentSpawnsPerRun: Type.Optional(Type.Integer({ minimum: 1 })),
211
211
  preflight: Type.Optional(WorkflowPreflightOverride),
@@ -227,7 +227,7 @@ const SubagentParamProperties = {
227
227
  usageBudget: Type.Optional(UsageBudgetOverride),
228
228
  agentScope: Type.Optional(Type.String({ description: "user/project/both (default); project wins collisions." })),
229
229
  cwd: Type.Optional(Type.String({ description: "Execution/project-pane directory." })),
230
- machine: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Herdr saved machine id or label; runs an external CLI agent there. cwd then means the directory on that machine." })),
230
+ machine: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Herdr saved machine id or label (runs an external CLI agent), a Rex host named in settings (runs a Pi child), or \"local\" (a Pi child in a Rex pane here). On another machine, cwd is a directory there." })),
231
231
  artifacts: Type.Optional(Type.Boolean({ description: "Debug artifacts; default true." })),
232
232
  includeProgress: Type.Optional(Type.Boolean({ description: "Full result progress; default false." })),
233
233
  share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist; default false." })),
@@ -306,6 +306,6 @@ const SubagentWaitParamsSchema = Type.Object({
306
306
  nonBlocking: Type.Optional(Type.Boolean({ description: "Subscribe to id's wake and return immediately; requires id, not with all." })),
307
307
  all: Type.Optional(Type.Boolean({ description: "Wait for all work active at call time." })),
308
308
  timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Max wait; default waitTool.defaultTimeoutMs, then 30 min." })),
309
- stopOnAttention: Type.Optional(Type.Boolean({ description: "false: keep waiting through idle or long-thinking attention." })),
309
+ stopOnAttention: Type.Optional(Type.Boolean({ description: "false: keep waiting through all attention except supervisor requests." })),
310
310
  });
311
311
  export const SubagentWaitParams = keepTopLevelParameterDescriptions(SubagentWaitParamsSchema);
@@ -113,6 +113,8 @@ async function waitForReply(channelDir, requestId, initialDeadline, signal) {
113
113
  if (fs.existsSync(file)) {
114
114
  const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
115
115
  if (parsed.type === "subagent.supervisor.reply" && parsed.requestId === requestId && typeof parsed.message === "string") {
116
+ if (typeof parsed.error === "string")
117
+ throw new Error(parsed.error);
116
118
  return parsed;
117
119
  }
118
120
  }
@@ -761,6 +763,24 @@ export function createNativeSupervisorChannel(pi, state, deps = {}) {
761
763
  cleanupRequestLifecycle(request, lifecycle);
762
764
  continue;
763
765
  }
766
+ if (request.expectsReply && runState(request).blockingWorkflowChildren?.has(request.runId)) {
767
+ // Pi cannot deliver an ask behind the workflow tool call that awaits this child.
768
+ // Return a tool error instead of renewing an unreachable ask until the run deadline.
769
+ try {
770
+ writePrivateAtomicJson(replyPath(channelDir, request.id), {
771
+ type: "subagent.supervisor.reply", requestId: request.id, createdAt: now, message: "",
772
+ error: "Supervisor unavailable: the parent is blocked in an async:false workflow and cannot answer this request. Report the blocker and any partial results, then stop. Relaunch the workflow with async:true to allow supervisor replies.",
773
+ });
774
+ }
775
+ catch (error) {
776
+ console.error(`Failed to report unreachable supervisor request ${request.id}; will retry:`, error);
777
+ continue;
778
+ }
779
+ seenFiles.add(file);
780
+ observeRequestLifecycle(request, "resolved");
781
+ cleanupRequestLifecycle(request, "resolved");
782
+ continue;
783
+ }
764
784
  seenFiles.add(file);
765
785
  if (!request.expectsReply) {
766
786
  // Progress is already visible through child activity; do not inject a
@@ -253,7 +253,13 @@ export function readMissionBinding(asyncDir) {
253
253
  const bindingPath = path.join(asyncDir, MISSION_BINDING_FILE);
254
254
  if (!fs.existsSync(bindingPath))
255
255
  return undefined;
256
- return parsePersistedBinding(JSON.parse(fs.readFileSync(bindingPath, "utf-8")), bindingPath);
256
+ const raw = fs.readFileSync(bindingPath, "utf-8");
257
+ try {
258
+ return parsePersistedBinding(JSON.parse(raw), bindingPath);
259
+ }
260
+ catch (error) {
261
+ throw new Error(`Invalid mission binding file '${bindingPath}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
262
+ }
257
263
  }
258
264
  export function syncMissionFromAsyncCompletion(value) {
259
265
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -34,6 +34,7 @@ interface CapacityOptions {
34
34
  abandonedSlotReleaseAfterMs?: number | false;
35
35
  pidLiveness?: (pid: number) => PidLiveness;
36
36
  pidNamespaceScope?: () => string | undefined;
37
+ bootId?: () => string | undefined;
37
38
  afterSlotRename?: (releasedDir: string) => void;
38
39
  writeOwner?: (filePath: string, owner: ActiveAsyncCapacityOwner) => void;
39
40
  }
@@ -4,7 +4,8 @@ import * as path from "node:path";
4
4
  import { writePrivateAtomicJson } from "../../shared/atomic-json.js";
5
5
  import { TEMP_ROOT_DIR } from "../../shared/types.js";
6
6
  import { readStatus } from "../../shared/utils.js";
7
- import { checkPidLiveness } from "./stale-run-reconciler.js";
7
+ import { checkPidLiveness, checkRecordedPidLiveness } from "./stale-run-reconciler.js";
8
+ import { currentBootId } from "./boot-id.js";
8
9
  import { currentPidNamespaceScope } from "./pid-namespace.js";
9
10
  import { readProcessTerminal } from "./process-terminal.js";
10
11
  import { isTerminalAsyncState as terminalState, readWorkflowChildEvidence, readWorkflowChildProcessEvidence } from "./workflow-terminal-proof.js";
@@ -201,9 +202,14 @@ function abandonedRunnerReleaseVerdict(status, proofState, options, anyTerminalS
201
202
  if (typeof status.pid !== "number" || !Number.isInteger(status.pid) || status.pid < 1)
202
203
  return { state: "retained", reason: `${proofReason}; runner PID is missing or invalid` };
203
204
  // A PID from another namespace can look dead here while the runner is alive.
204
- if (status.pidNamespaceScope !== undefined && status.pidNamespaceScope !== (options.pidNamespaceScope ?? currentPidNamespaceScope)())
205
+ const observedScope = (options.pidNamespaceScope ?? currentPidNamespaceScope)();
206
+ if (status.pidNamespaceScope !== undefined && status.pidNamespaceScope !== observedScope)
205
207
  return { state: "retained", reason: `${proofReason}; runner PID namespace differs from this process, so liveness is unknown` };
206
- const liveness = (options.pidLiveness ?? checkPidLiveness)(status.pid);
208
+ const liveness = checkRecordedPidLiveness({ pid: status.pid, pidNamespaceScope: status.pidNamespaceScope, bootId: status.bootId }, {
209
+ observedScope,
210
+ observedBootId: (options.bootId ?? currentBootId)(),
211
+ probe: options.pidLiveness ?? ((pid) => checkPidLiveness(pid)),
212
+ });
207
213
  if (liveness !== "dead")
208
214
  return { state: "retained", reason: `${proofReason}; runner PID liveness is ${liveness}` };
209
215
  const lastActivityAt = status.lastActivityAt ?? status.lastUpdate ?? status.endedAt;
@@ -13,7 +13,7 @@ import { childCacheRetentionEnv } from "../../shared/child-cache-retention.js";
13
13
  import { buildEffectiveSystemPrompt } from "../shared/effective-system-prompt.js";
14
14
  import { currentCompletionOwnerId } from "../../shared/completion-owner.js";
15
15
  import { planChildLaunch, projectChainOutputSchemas, resolveStepBehavior, suppressProgressForReadOnlyTask } from "../shared/child-launch-plan.js";
16
- import { formatHerdrMachineRunnerUnsupported, resolveHerdrMachinePlacement } from "../shared/herdr-machine.js";
16
+ import { formatHerdrMachineRunnerUnsupported, resolveMachinePlacement } from "../shared/herdr-machine.js";
17
17
  import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.js";
18
18
  import { assertClaudeCodeModelScope, isClaudeCodeAdapterId, resolveClaudeCodeOverride } from "../shared/claude-code-adapter.js";
19
19
  import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.js";
@@ -51,8 +51,9 @@ import { usageBudgetState } from "../shared/usage-budget.js";
51
51
  import { finalizeProcessTerminal, initializeProcessTerminal, readProcessTerminal } from "./process-terminal.js";
52
52
  import { persistRunnerStartupFailure } from "./runner-startup-failure.js";
53
53
  import { statusStepDescription } from "./chain-append.js";
54
+ import { currentBootId, currentRecordedPid } from "./boot-id.js";
54
55
  import { currentPidNamespaceScope } from "./pid-namespace.js";
55
- import { checkPidLiveness } from "./stale-run-reconciler.js";
56
+ import { checkRecordedPidLiveness } from "./stale-run-reconciler.js";
56
57
  import { lookupRunnerLauncher, resolveAgentRunnerLauncher, runnerLauncherPlacementError } from "../shared/runner-launcher.js";
57
58
  import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.js";
58
59
  import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling } from "../shared/capability-ceiling.js";
@@ -432,7 +433,12 @@ function spawnRunner(cfg, suffix, cwd, initialStatus, initialStatusPath, launchP
432
433
  const runnerProcessInstanceId = randomUUID();
433
434
  const hasRevivalLease = typeof cfg.revivalLease === "object";
434
435
  const launchBarrierToken = hasRevivalLease ? undefined : runnerProcessInstanceId;
435
- const launchConfig = { ...cfg, runnerProcessInstanceId, ...(launchBarrierToken ? { launchBarrierToken } : {}) };
436
+ // A broker launcher may start the runner outside its own process, even under another kernel, so the
437
+ // spawned pid is only the wrapper's and only a direct runner shares Pi's boot id. The runner reports
438
+ // its own identity before it is allowed to proceed.
439
+ const launcher = cfg.launcher;
440
+ const bootId = launcher ? undefined : currentBootId();
441
+ const launchConfig = { ...cfg, runnerProcessInstanceId, ...(launchBarrierToken ? { launchBarrierToken } : {}), ...(bootId ? { bootId } : {}) };
436
442
  writePrivateAtomicJson(cfgPath, launchConfig);
437
443
  const command = binaryHost ?? nodeExecutable;
438
444
  const launchForStartup = launchConfig;
@@ -456,9 +462,6 @@ function spawnRunner(cfg, suffix, cwd, initialStatus, initialStatusPath, launchP
456
462
  fs.rmSync(startupConfirmPath, { force: true });
457
463
  if (startupProceedPath)
458
464
  fs.rmSync(startupProceedPath, { force: true });
459
- // A broker launcher may start the runner outside its own process, so the spawned pid is only the
460
- // wrapper's. The runner reports its own identity before it is allowed to proceed.
461
- const launcher = launchConfig.launcher;
462
465
  const identityPath = launcher && launchBarrierToken && launchAsyncDir ? path.join(launchAsyncDir, "runner-startup.json") : undefined;
463
466
  if (identityPath)
464
467
  fs.rmSync(identityPath, { force: true });
@@ -550,7 +553,8 @@ function spawnRunner(cfg, suffix, cwd, initialStatus, initialStatusPath, launchP
550
553
  }
551
554
  // The runner may outlive its wrapper; only a confirmed-dead runner in this pid namespace finalizes here,
552
555
  // otherwise status-pid reconciliation does it later.
553
- if (!runnerIdentity || runnerIdentity.pidNamespaceScope !== currentPidNamespaceScope() || checkPidLiveness(runnerIdentity.pid, process.kill, runnerIdentity.pidNamespaceScope !== undefined) !== "dead")
556
+ if (!runnerIdentity || runnerIdentity.pidNamespaceScope !== currentPidNamespaceScope()
557
+ || checkRecordedPidLiveness(runnerIdentity) !== "dead")
554
558
  return;
555
559
  }
556
560
  finalizeProcessTerminal(asyncDir, runId, {
@@ -612,7 +616,7 @@ function spawnRunner(cfg, suffix, cwd, initialStatus, initialStatusPath, launchP
612
616
  try {
613
617
  writePrivateAtomicJson(initialStatusPath, {
614
618
  ...initialStatus,
615
- ...(launcher ? {} : { pid: proc.pid, pidNamespaceScope: currentPidNamespaceScope() }),
619
+ ...(launcher ? {} : { pid: proc.pid, pidNamespaceScope: currentPidNamespaceScope(), ...(bootId ? { bootId } : {}) }),
616
620
  processTerminal: { version: 1, state: "pending", runId: initialStatus.runId, runnerProcessInstanceId },
617
621
  });
618
622
  // Aggregate waits must see the launch before the runner's first status update.
@@ -834,7 +838,7 @@ export function buildAsyncRunnerSteps(id, params) {
834
838
  let machineEnv;
835
839
  if (requestedMachine) {
836
840
  try {
837
- const placement = resolveHerdrMachinePlacement({ machine: requestedMachine, cwd: runnerCwd, stepCwd: s.cwd ?? params.machineCwd });
841
+ const placement = resolveMachinePlacement({ machine: requestedMachine, cwd: runnerCwd, stepCwd: s.cwd ?? params.machineCwd });
838
842
  machine = placement.machine;
839
843
  machineEnv = placement.env;
840
844
  }
@@ -1331,6 +1335,7 @@ export function executeAsyncChain(id, params) {
1331
1335
  }
1332
1336
  const initialStatusAt = Date.now();
1333
1337
  const initialCompletionOwnerId = ctx.completionOwnerId ?? currentCompletionOwnerId();
1338
+ const completionOwnerProcess = currentRecordedPid();
1334
1339
  const launchParentSessionId = ctx.parentSessionId ?? ctx.currentSessionId;
1335
1340
  try {
1336
1341
  writeRetainedRequiredChildExtensions(asyncDir, params.requiredExtensions ?? ctx.childRuntime?.requiredExtensions ?? resolveRequiredChildExtensions(launchParentSessionId ?? undefined));
@@ -1355,6 +1360,7 @@ export function executeAsyncChain(id, params) {
1355
1360
  asyncDir,
1356
1361
  sessionId: ctx.currentSessionId,
1357
1362
  completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
1363
+ completionOwnerProcess,
1358
1364
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1359
1365
  piPackageRoot,
1360
1366
  childSessionFactoryModule: childSessionFactoryModule(),
@@ -1393,6 +1399,7 @@ export function executeAsyncChain(id, params) {
1393
1399
  runId: id,
1394
1400
  ...(ctx.currentSessionId ? { sessionId: ctx.currentSessionId } : {}),
1395
1401
  ...(initialCompletionOwnerId ? { completionOwnerId: initialCompletionOwnerId } : {}),
1402
+ completionOwnerProcess,
1396
1403
  mode: resultMode,
1397
1404
  state: "running",
1398
1405
  startedAt: initialStatusAt,
@@ -1620,7 +1627,7 @@ export function executeAsyncSingle(id, params) {
1620
1627
  let machineEnv;
1621
1628
  if (requestedMachine) {
1622
1629
  try {
1623
- const placement = resolveHerdrMachinePlacement({ machine: requestedMachine, cwd: runnerCwd, stepCwd: params.machineCwd });
1630
+ const placement = resolveMachinePlacement({ machine: requestedMachine, cwd: runnerCwd, stepCwd: params.machineCwd });
1624
1631
  machine = placement.machine;
1625
1632
  machineEnv = placement.env;
1626
1633
  }
@@ -1895,6 +1902,7 @@ export function executeAsyncSingle(id, params) {
1895
1902
  let spawnResultOrPromise = {};
1896
1903
  const initialStatusAt = Date.now();
1897
1904
  const initialCompletionOwnerId = ctx.completionOwnerId ?? currentCompletionOwnerId();
1905
+ const completionOwnerProcess = currentRecordedPid();
1898
1906
  const launchParentSessionId = ctx.parentSessionId ?? ctx.currentSessionId;
1899
1907
  try {
1900
1908
  spawnResultOrPromise = spawnRunner({
@@ -1977,6 +1985,7 @@ export function executeAsyncSingle(id, params) {
1977
1985
  asyncDir,
1978
1986
  sessionId: ctx.currentSessionId,
1979
1987
  completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
1988
+ completionOwnerProcess,
1980
1989
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1981
1990
  piPackageRoot,
1982
1991
  childSessionFactoryModule: childSessionFactoryModule(),
@@ -2018,6 +2027,7 @@ export function executeAsyncSingle(id, params) {
2018
2027
  runId: id,
2019
2028
  ...(ctx.currentSessionId ? { sessionId: ctx.currentSessionId } : {}),
2020
2029
  ...(initialCompletionOwnerId ? { completionOwnerId: initialCompletionOwnerId } : {}),
2030
+ completionOwnerProcess,
2021
2031
  mode: "single",
2022
2032
  ...(params.toolCallId ? { toolCallId: params.toolCallId } : {}),
2023
2033
  state: "running",
@@ -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,8 @@ 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;
145
147
  }
146
148
  type TargetedAsyncRunResolution = {
147
149
  kind: "exact";