@ryan_nookpi/pi-extension-subagent 0.5.7 → 0.5.8

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.
package/index.ts CHANGED
@@ -28,6 +28,7 @@
28
28
  */
29
29
 
30
30
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
31
+ import { SubagentAsyncTasks } from "./async-task-lifecycle.js";
31
32
  import { HANG_CHECK_INTERVAL_MS } from "./constants.js";
32
33
  import { SUBAGENT_COMMANDS, SUBAGENT_SHORTCUTS, type SubagentCommandName } from "./registration-manifest.js";
33
34
 
@@ -40,6 +41,7 @@ interface SubagentCore {
40
41
  }
41
42
 
42
43
  export default function (pi: ExtensionAPI) {
44
+ const asyncTasks = new SubagentAsyncTasks(pi);
43
45
  let core: SubagentCore | null = null;
44
46
  let corePromise: Promise<SubagentCore> | null = null;
45
47
  /** Serializes session lifecycle work so events apply in dispatch order. */
@@ -54,6 +56,7 @@ export default function (pi: ExtensionAPI) {
54
56
  import("./store.js"),
55
57
  ]);
56
58
  const store = storeMod.createStore();
59
+ store.asyncTasks = asyncTasks;
57
60
  const registrations = commands.registerAll(pi, store);
58
61
  core = { store, commands, registrations, escalation, lifecycle };
59
62
  return core;
@@ -116,11 +119,24 @@ export default function (pi: ExtensionAPI) {
116
119
  }, 0);
117
120
 
118
121
  let hangCheckTimer: ReturnType<typeof setInterval> | undefined;
122
+ let sessionGeneration = 0;
119
123
 
120
124
  pi.on("session_start", (_event, ctx) => {
121
- enqueue((c) => {
125
+ const generation = ++sessionGeneration;
126
+ const sessionId = ctx.sessionManager.getSessionId();
127
+ // Fence tool admission synchronously, before lazy lifecycle work can yield.
128
+ try {
129
+ if (pi.events) asyncTasks.bind(sessionId);
130
+ } catch (error) {
131
+ process.stderr.write(`[subagent] session binding failed: ${error instanceof Error ? error.message : error}\n`);
132
+ return;
133
+ }
134
+ enqueue(async (c) => {
135
+ if (generation !== sessionGeneration) return;
136
+ if (pi.events) await asyncTasks.provider.whenDiscovered();
137
+ if (generation !== sessionGeneration || ctx.sessionManager.getSessionId() !== sessionId) return;
122
138
  c.store.disposed = false;
123
- c.commands.handleSessionStart(pi, c.store, ctx as unknown as ExtensionContext);
139
+ c.commands.handleSessionStart(asyncTasks.wrap(pi), c.store, ctx as unknown as ExtensionContext);
124
140
  c.escalation.maybeRegisterAskMaster(pi, ctx);
125
141
  });
126
142
  if (!hangCheckTimer) {
@@ -131,6 +147,8 @@ export default function (pi: ExtensionAPI) {
131
147
  });
132
148
 
133
149
  pi.on("session_shutdown", async (event) => {
150
+ ++sessionGeneration;
151
+ asyncTasks.shutdown();
134
152
  if (hangCheckTimer) {
135
153
  clearInterval(hangCheckTimer);
136
154
  hangCheckTimer = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,6 +39,8 @@
39
39
  "format.ts",
40
40
  "group-pending.ts",
41
41
  "index.ts",
42
+ "async-task-provider.ts",
43
+ "async-task-lifecycle.ts",
42
44
  "lifecycle.ts",
43
45
  "invocation-queue.ts",
44
46
  "live-preview.ts",
package/runner.ts CHANGED
@@ -7,6 +7,7 @@ import { spawn } from "node:child_process";
7
7
  import * as fs from "node:fs";
8
8
  import type { Message } from "@earendil-works/pi-ai";
9
9
  import type { AgentConfig } from "./agents.js";
10
+ import { currentResourceObserver, trackSubagentRunner } from "./async-task-lifecycle.js";
10
11
  import { buildClaudeArgs } from "./claude-args.js";
11
12
  import { createSidecarWriter } from "./claude-sidecar-writer.js";
12
13
  import {
@@ -43,6 +44,7 @@ import {
43
44
  } from "./utils/agent-utils.js";
44
45
 
45
46
  export interface RunSingleAgentSessionConfig {
47
+ asyncRunId?: number;
46
48
  sessionFile?: string;
47
49
  resumeSessionId?: string;
48
50
  sidecarSessionFile?: string;
@@ -198,7 +200,21 @@ export function computeAgentAliasHints(agents: AgentConfig[]): string {
198
200
 
199
201
  // ─── Single Agent Execution ──────────────────────────────────────────────────
200
202
 
201
- export async function runSingleAgent(
203
+ export async function runSingleAgent(...args: Parameters<typeof runSingleAgentUntracked>): Promise<SingleResult> {
204
+ const config = typeof args[8] === "object" ? args[8] : undefined;
205
+ return trackSubagentRunner(
206
+ args[2],
207
+ args[5],
208
+ (signal) => {
209
+ const invocation: Parameters<typeof runSingleAgentUntracked> = [...args];
210
+ invocation[5] = signal;
211
+ return runSingleAgentUntracked(...invocation);
212
+ },
213
+ config?.asyncRunId,
214
+ );
215
+ }
216
+
217
+ async function runSingleAgentUntracked(
202
218
  defaultCwd: string,
203
219
  agents: AgentConfig[],
204
220
  agentName: string,
@@ -209,6 +225,7 @@ export async function runSingleAgent(
209
225
  makeDetails: (results: SingleResult[]) => SubagentDetails,
210
226
  sessionConfig?: string | RunSingleAgentSessionConfig,
211
227
  ): Promise<SingleResult> {
228
+ currentResourceObserver()("settled");
212
229
  const normalizedSessionConfig = normalizeRunSessionConfig(sessionConfig);
213
230
  const sessionFile = normalizedSessionConfig.sessionFile;
214
231
  const resumeSessionId = normalizedSessionConfig.resumeSessionId;
@@ -246,7 +263,10 @@ export async function runSingleAgent(
246
263
 
247
264
  if (agent.runtime === "claude") {
248
265
  if (resolveClaudeRuntimeMode(defaultCwd) === "sdk") {
266
+ // SDK query.close() has no process-exit acknowledgement.
267
+ currentResourceObserver()("unknown");
249
268
  const { runClaudeAgentViaSdk } = await import("./claude-sdk-runner.js");
269
+ if (signal?.aborted) throw new Error("Subagent was aborted");
250
270
  return runClaudeAgentViaSdk(
251
271
  defaultCwd,
252
272
  agent,
@@ -300,6 +320,7 @@ async function runClaudeAgent(
300
320
  sidecarSessionFile?: string,
301
321
  onDiagnostic?: RunnerDiagnosticSink,
302
322
  ): Promise<SingleResult> {
323
+ const observeResource = currentResourceObserver();
303
324
  try {
304
325
  validateClaudeRuntimeModel(agent.model);
305
326
  } catch (err: any) {
@@ -392,7 +413,9 @@ async function runClaudeAgent(
392
413
  });
393
414
 
394
415
  const exitCode = await new Promise<number>((resolve) => {
416
+ if (signal?.aborted) throw new Error("Subagent was aborted");
395
417
  const proc = spawn("claude", args, { cwd: defaultCwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
418
+ observeResource("active");
396
419
  let buffer = "";
397
420
  let procExited = false;
398
421
  let settled = false;
@@ -564,6 +587,7 @@ async function runClaudeAgent(
564
587
  });
565
588
 
566
589
  proc.on("exit", (code, terminationSignal) => {
590
+ observeResource("settled");
567
591
  procExited = true;
568
592
  lastExitCode = normalizeChildExitCode(code, terminationSignal, lastExitCode);
569
593
  lastProcessSignal = terminationSignal;
@@ -583,6 +607,7 @@ async function runClaudeAgent(
583
607
  });
584
608
 
585
609
  proc.on("close", (code, terminationSignal) => {
610
+ observeResource("settled");
586
611
  procExited = true;
587
612
  lastProcessSignal = terminationSignal ?? lastProcessSignal;
588
613
  diagnose({ event: "close", childPid: proc.pid, code, signal: terminationSignal });
@@ -593,6 +618,7 @@ async function runClaudeAgent(
593
618
  });
594
619
 
595
620
  proc.on("error", (error) => {
621
+ observeResource(proc.pid ? "unknown" : "settled");
596
622
  procExited = true;
597
623
  stderrBuf += `[runner] process error: ${error?.message || String(error)}\n`;
598
624
  diagnose({
@@ -658,6 +684,7 @@ async function runPiAgent(
658
684
  persistedSessionBaseOffset = 0,
659
685
  onDiagnostic?: RunnerDiagnosticSink,
660
686
  ): Promise<SingleResult> {
687
+ const observeResource = currentResourceObserver();
661
688
  const args: string[] = ["--mode", "json", "-p"];
662
689
  if (sessionFile) args.push("--session", sessionFile);
663
690
  else args.push("--no-session");
@@ -721,7 +748,9 @@ async function runPiAgent(
721
748
  args.push(normalizeTaskForSubagentPrompt(task));
722
749
 
723
750
  const exitCode = await new Promise<number>((resolve) => {
751
+ if (signal?.aborted) throw new Error("Subagent was aborted");
724
752
  const proc = spawn("pi", args, { cwd: defaultCwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
753
+ observeResource("active");
725
754
  let buffer = "";
726
755
  let procExited = false;
727
756
  let settled = false;
@@ -1089,6 +1118,7 @@ async function runPiAgent(
1089
1118
  });
1090
1119
 
1091
1120
  proc.on("exit", (code, terminationSignal) => {
1121
+ observeResource("settled");
1092
1122
  procExited = true;
1093
1123
  lastExitCode = normalizeChildExitCode(code, terminationSignal, lastExitCode);
1094
1124
  lastProcessSignal = terminationSignal;
@@ -1105,6 +1135,7 @@ async function runPiAgent(
1105
1135
  });
1106
1136
 
1107
1137
  proc.on("close", (code, terminationSignal) => {
1138
+ observeResource("settled");
1108
1139
  procExited = true;
1109
1140
  lastProcessSignal = terminationSignal ?? lastProcessSignal;
1110
1141
  diagnose({ event: "close", childPid: proc.pid, code, signal: terminationSignal });
@@ -1113,6 +1144,7 @@ async function runPiAgent(
1113
1144
  });
1114
1145
 
1115
1146
  proc.on("error", (error) => {
1147
+ observeResource(proc.pid ? "unknown" : "settled");
1116
1148
  procExited = true;
1117
1149
  appendStderrDiagnostic(currentResult, `process error: ${error?.message || String(error)}`);
1118
1150
  diagnose({
package/store.ts CHANGED
@@ -18,6 +18,7 @@ import type { WidgetRenderCtx } from "./widget.js";
18
18
  export const COLLAPSED_ITEM_COUNT = 10;
19
19
 
20
20
  export interface SubagentStore {
21
+ asyncTasks?: import("./async-task-lifecycle.js").SubagentAsyncTasks;
21
22
  /** True after session_shutdown; async callbacks must not use captured runtime APIs. */
22
23
  disposed: boolean;
23
24
  commandRuns: Map<number, CommandRunState>;
package/tool-execute.ts CHANGED
@@ -10,6 +10,14 @@ import * as fs from "node:fs";
10
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
11
  import { createRunActivityRecorder } from "./activity.js";
12
12
  import { type AgentConfig, discoverAgents } from "./agents.js";
13
+ import {
14
+ assertAsyncAdmission,
15
+ asyncInvocationDetails,
16
+ completeAsyncInvocation,
17
+ discardRemovedAsyncInvocation,
18
+ isExpectedTrackedCancellation,
19
+ retainAsyncCompletion,
20
+ } from "./async-task-lifecycle.js";
13
21
  import { parseSubagentToolCommand, SUBAGENT_CLI_HELP_TEXT } from "./cli.js";
14
22
  import {
15
23
  DEFAULT_TURN_COUNT,
@@ -453,6 +461,7 @@ function buildRunStartMessage(runState: CommandRunState, status: "started" | "re
453
461
  `\nContext: ${contextLabel} · turn ${runState.turnCount}`,
454
462
  display: false,
455
463
  details: {
464
+ ...asyncInvocationDetails(),
456
465
  runId: runState.id,
457
466
  agent: runState.agent,
458
467
  task: runState.task,
@@ -515,7 +524,8 @@ function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?
515
524
  const { runState, result, rawOutput } = finalized;
516
525
  const usage = result ? formatUsageStats(result.usage, result.model) : "";
517
526
  const terminalLabel = getFinalizedRunLabel(finalized);
518
- return {
527
+ completeAsyncInvocation(terminalLabel);
528
+ return retainAsyncCompletion({
519
529
  customType: "subagent-tool" as const,
520
530
  content:
521
531
  `[subagent:${runState.agent}#${runState.id}] ${terminalLabel}` +
@@ -525,6 +535,7 @@ function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?
525
535
  `\n\n${rawOutput}`,
526
536
  display: options?.display ?? true,
527
537
  details: {
538
+ ...asyncInvocationDetails(),
528
539
  runId: runState.id,
529
540
  agent: runState.agent,
530
541
  task: runState.task,
@@ -555,12 +566,13 @@ function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?
555
566
  claudeSessionId: runState.claudeSessionId,
556
567
  claudeProjectDir: runState.claudeProjectDir,
557
568
  },
558
- };
569
+ });
559
570
  }
560
571
 
561
572
  function buildEscalationMessage(runState: CommandRunState, escalationMessage: string, result: SingleResult) {
573
+ completeAsyncInvocation("completed");
562
574
  const usage = formatUsageStats(result.usage, result.model);
563
- return {
575
+ return retainAsyncCompletion({
564
576
  customType: "subagent-tool" as const,
565
577
  content:
566
578
  `[subagent:${runState.agent}#${runState.id}] escalated` +
@@ -569,6 +581,7 @@ function buildEscalationMessage(runState: CommandRunState, escalationMessage: st
569
581
  `\n\n[ESCALATION] ${escalationMessage}`,
570
582
  display: true,
571
583
  details: {
584
+ ...asyncInvocationDetails(),
572
585
  runId: runState.id,
573
586
  agent: runState.agent,
574
587
  task: runState.task,
@@ -592,7 +605,7 @@ function buildEscalationMessage(runState: CommandRunState, escalationMessage: st
592
605
  claudeSessionId: runState.claudeSessionId,
593
606
  claudeProjectDir: runState.claudeProjectDir,
594
607
  },
595
- };
608
+ });
596
609
  }
597
610
 
598
611
  function buildStrongWaitMessage(runId: number): string {
@@ -660,6 +673,7 @@ function formatBatchSummary(
660
673
  runs: CommandRunState[],
661
674
  terminalStatus: "completed" | "error" | "aborted",
662
675
  ): string {
676
+ completeAsyncInvocation(terminalStatus);
663
677
  const statuses = runs.map((run) => getSubagentRunStatusLabel(run.status, run.errorClass));
664
678
  const headerStatus = runs.map((run, index) => `#${run.id} ${statuses[index]}`).join(", ");
665
679
  const outcomeCounts = formatOutcomeCounts(
@@ -680,6 +694,7 @@ function formatPipelineSummary(
680
694
  stepResults: PipelineStepResult[],
681
695
  terminalStatus: "completed" | "stopped" | "error" | "aborted",
682
696
  ): string {
697
+ completeAsyncInvocation(terminalStatus);
683
698
  const outcomeCounts = formatOutcomeCounts(
684
699
  stepResults.filter((step) => step.status === "error").length,
685
700
  stepResults.filter((step) => step.status === "aborted").length,
@@ -817,6 +832,7 @@ function finalizeRunError(runState: CommandRunState, error: unknown): FinalizedR
817
832
  }
818
833
 
819
834
  export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore) {
835
+ pi = store.asyncTasks?.wrap(pi) ?? pi;
820
836
  const execute = async (
821
837
  _toolCallId: string,
822
838
  params: Record<string, any>,
@@ -931,7 +947,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
931
947
  const hasChain = asyncAction === "chain";
932
948
  const hasSingle = asyncAction === "run" || asyncAction === "continue";
933
949
  const mode: LaunchMode = hasBatch ? "batch" : hasChain ? "chain" : "single";
934
- const shouldRunAsync = isInteractiveTuiContext(ctx);
950
+ const shouldRunAsync = isInteractiveTuiContext(ctx) || store.asyncTasks?.provider.accepting === true;
935
951
  const makeDetails = (
936
952
  modeOverride: LaunchMode = mode,
937
953
  results: SingleResult[] = [],
@@ -1389,6 +1405,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1389
1405
  }
1390
1406
 
1391
1407
  function registerRunLaunch(config: RunLaunchConfig): CommandRunState {
1408
+ assertAsyncAdmission();
1392
1409
  const initialDisplayTask = buildSubagentDisplayTaskFallback(config.taskForDisplay);
1393
1410
  let runState: CommandRunState;
1394
1411
  if (config.existingRunState) {
@@ -1523,6 +1540,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1523
1540
  resumeSessionId: runState.claudeSessionId,
1524
1541
  sidecarSessionFile: runState.sessionFile,
1525
1542
  persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1543
+ ...(store.asyncTasks?.provider.supported ? { asyncRunId: runState.id } : {}),
1526
1544
  onDiagnostic: createRunDiagnosticSink(pi, runState),
1527
1545
  },
1528
1546
  );
@@ -1678,7 +1696,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1678
1696
  void (async () => {
1679
1697
  try {
1680
1698
  const finalized = await launchRunInBackground(runState, taskForAgent);
1681
- if (runState.removed || store.disposed) return;
1699
+ if (runState.removed || store.disposed) {
1700
+ discardRemovedAsyncInvocation(finalized.rawOutput);
1701
+ return;
1702
+ }
1682
1703
  updateCommandRunsWidget(store);
1683
1704
 
1684
1705
  if (finalized.result?.exitCode === ESCALATION_EXIT_CODE) {
@@ -1704,12 +1725,16 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1704
1725
  if (entry) entry.pendingCompletion = makePendingCompletion(completionMessage, true);
1705
1726
  }
1706
1727
 
1707
- ctx.ui?.notify?.(
1708
- `subagent tool run #${runState.id} (${resolvedAgent}) ${terminalLabel}`,
1709
- terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
1710
- );
1728
+ if (terminalLabel !== "aborted" || !isExpectedTrackedCancellation())
1729
+ ctx.ui?.notify?.(
1730
+ `subagent tool run #${runState.id} (${resolvedAgent}) ${terminalLabel}`,
1731
+ terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
1732
+ );
1711
1733
  } catch (error: unknown) {
1712
- if (runState.removed || store.disposed) return;
1734
+ if (runState.removed || store.disposed) {
1735
+ discardRemovedAsyncInvocation(String(error));
1736
+ return;
1737
+ }
1713
1738
  const finalized = finalizeRunError(runState, error);
1714
1739
  const terminalLabel = getFinalizedRunLabel(finalized);
1715
1740
  const errorMessage = buildRunCompletionMessage(finalized);
@@ -1720,10 +1745,11 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1720
1745
  const entry = store.globalLiveRuns.get(runState.id);
1721
1746
  if (entry) entry.pendingCompletion = makePendingCompletion(errorMessage, true);
1722
1747
  }
1723
- ctx.ui?.notify?.(
1724
- `subagent tool run #${runState.id} ${terminalLabel}: ${runState.lastLine}`,
1725
- terminalLabel === "aborted" ? "warning" : "error",
1726
- );
1748
+ if (terminalLabel !== "aborted" || !isExpectedTrackedCancellation())
1749
+ ctx.ui?.notify?.(
1750
+ `subagent tool run #${runState.id} ${terminalLabel}: ${runState.lastLine}`,
1751
+ terminalLabel === "aborted" ? "warning" : "error",
1752
+ );
1727
1753
  updateCommandRunsWidget(store);
1728
1754
  } finally {
1729
1755
  clearRunAbortState(runState);
@@ -1872,12 +1898,14 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1872
1898
  content,
1873
1899
  display: true,
1874
1900
  details: {
1901
+ ...asyncInvocationDetails(),
1875
1902
  batchId,
1876
1903
  runIds: batch.runIds,
1877
1904
  status: batchTerminalStatus === "completed" ? "done" : batchTerminalStatus,
1878
1905
  runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
1879
1906
  },
1880
1907
  };
1908
+ retainAsyncCompletion(message);
1881
1909
  retireFinishedGroup(store, snapshotBatchGroup(store, batch, batchTerminalStatus));
1882
1910
  if (isInOriginSession(ctx, batch.originSessionFile)) {
1883
1911
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
@@ -1901,14 +1929,15 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1901
1929
  updateWidget: false,
1902
1930
  removalReason: "trim",
1903
1931
  });
1904
- ctx.ui?.notify?.(
1905
- batchTerminalStatus === "error"
1906
- ? `subagent batch ${batchId} finished with errors`
1907
- : batchTerminalStatus === "aborted"
1908
- ? `subagent batch ${batchId} aborted`
1909
- : `subagent batch ${batchId} completed`,
1910
- batchTerminalStatus === "error" ? "error" : batchTerminalStatus === "aborted" ? "warning" : "info",
1911
- );
1932
+ if (batchTerminalStatus !== "aborted" || !isExpectedTrackedCancellation())
1933
+ ctx.ui?.notify?.(
1934
+ batchTerminalStatus === "error"
1935
+ ? `subagent batch ${batchId} finished with errors`
1936
+ : batchTerminalStatus === "aborted"
1937
+ ? `subagent batch ${batchId} aborted`
1938
+ : `subagent batch ${batchId} completed`,
1939
+ batchTerminalStatus === "error" ? "error" : batchTerminalStatus === "aborted" ? "warning" : "info",
1940
+ );
1912
1941
  }
1913
1942
  } catch (error: unknown) {
1914
1943
  const finalized = finalizeRunError(runState, error);
@@ -1923,12 +1952,14 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1923
1952
  content: formatBatchSummary(batchId, orderedRuns, batchTerminalStatus),
1924
1953
  display: true,
1925
1954
  details: {
1955
+ ...asyncInvocationDetails(),
1926
1956
  batchId,
1927
1957
  runIds: batch.runIds,
1928
1958
  status: batchTerminalStatus,
1929
1959
  runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
1930
1960
  },
1931
1961
  };
1962
+ retainAsyncCompletion(message);
1932
1963
  retireFinishedGroup(store, snapshotBatchGroup(store, batch, batchTerminalStatus));
1933
1964
  if (isInOriginSession(ctx, batch.originSessionFile)) {
1934
1965
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
@@ -2247,12 +2278,14 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2247
2278
  content: formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus),
2248
2279
  display: true,
2249
2280
  details: {
2281
+ ...asyncInvocationDetails(),
2250
2282
  pipelineId,
2251
2283
  stepRunIds: pipeline.stepRunIds,
2252
2284
  status: terminalStatus === "completed" ? "done" : terminalStatus,
2253
2285
  runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
2254
2286
  },
2255
2287
  };
2288
+ retainAsyncCompletion(message);
2256
2289
  retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
2257
2290
  if (isInOriginSession(ctx, pipeline.originSessionFile)) {
2258
2291
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
@@ -2301,5 +2334,20 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2301
2334
  };
2302
2335
  };
2303
2336
 
2304
- return execute;
2337
+ return (...args: Parameters<typeof execute>) => {
2338
+ const parsed = parseSubagentToolCommand(args[1].command, { knownRunIds: [...store.commandRuns.keys()] });
2339
+ if (
2340
+ parsed.type !== "params" ||
2341
+ !["run", "continue", "batch", "chain"].includes(String(parsed.params.asyncAction ?? "run")) ||
2342
+ !store.asyncTasks
2343
+ )
2344
+ return execute(...args);
2345
+ return store.asyncTasks.invoke(
2346
+ String(args[1].command).slice(0, 500),
2347
+ args[0],
2348
+ () => execute(...args),
2349
+ false,
2350
+ args[2],
2351
+ );
2352
+ };
2305
2353
  }