@ryan_nookpi/pi-extension-subagent 0.5.6 → 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/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,
@@ -502,14 +511,21 @@ function recordBatchResult(batch: BatchGroupState, finalized: FinalizedRun): voi
502
511
  batch.completedRunIds.add(runId);
503
512
  if (status === "aborted") batch.abortedRunIds.add(runId);
504
513
  else if (status === "error") batch.failedRunIds.add(runId);
505
- batch.pendingResults.set(runId, finalized.rawOutput);
514
+ batch.completedRuns.set(runId, { ...finalized.runState, abortController: undefined });
515
+ }
516
+
517
+ function getCompletedBatchRuns(batch: BatchGroupState): CommandRunState[] {
518
+ return batch.runIds
519
+ .map((runId) => batch.completedRuns.get(runId))
520
+ .filter((run): run is CommandRunState => Boolean(run));
506
521
  }
507
522
 
508
523
  function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?: boolean }) {
509
524
  const { runState, result, rawOutput } = finalized;
510
525
  const usage = result ? formatUsageStats(result.usage, result.model) : "";
511
526
  const terminalLabel = getFinalizedRunLabel(finalized);
512
- return {
527
+ completeAsyncInvocation(terminalLabel);
528
+ return retainAsyncCompletion({
513
529
  customType: "subagent-tool" as const,
514
530
  content:
515
531
  `[subagent:${runState.agent}#${runState.id}] ${terminalLabel}` +
@@ -519,6 +535,7 @@ function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?
519
535
  `\n\n${rawOutput}`,
520
536
  display: options?.display ?? true,
521
537
  details: {
538
+ ...asyncInvocationDetails(),
522
539
  runId: runState.id,
523
540
  agent: runState.agent,
524
541
  task: runState.task,
@@ -549,12 +566,13 @@ function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?
549
566
  claudeSessionId: runState.claudeSessionId,
550
567
  claudeProjectDir: runState.claudeProjectDir,
551
568
  },
552
- };
569
+ });
553
570
  }
554
571
 
555
572
  function buildEscalationMessage(runState: CommandRunState, escalationMessage: string, result: SingleResult) {
573
+ completeAsyncInvocation("completed");
556
574
  const usage = formatUsageStats(result.usage, result.model);
557
- return {
575
+ return retainAsyncCompletion({
558
576
  customType: "subagent-tool" as const,
559
577
  content:
560
578
  `[subagent:${runState.agent}#${runState.id}] escalated` +
@@ -563,6 +581,7 @@ function buildEscalationMessage(runState: CommandRunState, escalationMessage: st
563
581
  `\n\n[ESCALATION] ${escalationMessage}`,
564
582
  display: true,
565
583
  details: {
584
+ ...asyncInvocationDetails(),
566
585
  runId: runState.id,
567
586
  agent: runState.agent,
568
587
  task: runState.task,
@@ -586,7 +605,7 @@ function buildEscalationMessage(runState: CommandRunState, escalationMessage: st
586
605
  claudeSessionId: runState.claudeSessionId,
587
606
  claudeProjectDir: runState.claudeProjectDir,
588
607
  },
589
- };
608
+ });
590
609
  }
591
610
 
592
611
  function buildStrongWaitMessage(runId: number): string {
@@ -654,6 +673,7 @@ function formatBatchSummary(
654
673
  runs: CommandRunState[],
655
674
  terminalStatus: "completed" | "error" | "aborted",
656
675
  ): string {
676
+ completeAsyncInvocation(terminalStatus);
657
677
  const statuses = runs.map((run) => getSubagentRunStatusLabel(run.status, run.errorClass));
658
678
  const headerStatus = runs.map((run, index) => `#${run.id} ${statuses[index]}`).join(", ");
659
679
  const outcomeCounts = formatOutcomeCounts(
@@ -674,6 +694,7 @@ function formatPipelineSummary(
674
694
  stepResults: PipelineStepResult[],
675
695
  terminalStatus: "completed" | "stopped" | "error" | "aborted",
676
696
  ): string {
697
+ completeAsyncInvocation(terminalStatus);
677
698
  const outcomeCounts = formatOutcomeCounts(
678
699
  stepResults.filter((step) => step.status === "error").length,
679
700
  stepResults.filter((step) => step.status === "aborted").length,
@@ -811,6 +832,7 @@ function finalizeRunError(runState: CommandRunState, error: unknown): FinalizedR
811
832
  }
812
833
 
813
834
  export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore) {
835
+ pi = store.asyncTasks?.wrap(pi) ?? pi;
814
836
  const execute = async (
815
837
  _toolCallId: string,
816
838
  params: Record<string, any>,
@@ -925,7 +947,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
925
947
  const hasChain = asyncAction === "chain";
926
948
  const hasSingle = asyncAction === "run" || asyncAction === "continue";
927
949
  const mode: LaunchMode = hasBatch ? "batch" : hasChain ? "chain" : "single";
928
- const shouldRunAsync = isInteractiveTuiContext(ctx);
950
+ const shouldRunAsync = isInteractiveTuiContext(ctx) || store.asyncTasks?.provider.accepting === true;
929
951
  const makeDetails = (
930
952
  modeOverride: LaunchMode = mode,
931
953
  results: SingleResult[] = [],
@@ -1040,6 +1062,45 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1040
1062
  };
1041
1063
  }
1042
1064
 
1065
+ let abortGroupTarget: { groupId: string; kind: "batch" | "chain" } | undefined;
1066
+ if (asyncAction === "abort" && typeof cmdParams.groupId === "string") {
1067
+ evictStaleFinishedGroups(store);
1068
+ const groupId = cmdParams.groupId;
1069
+ const finished = store.finishedGroups.get(groupId);
1070
+ if (finished) {
1071
+ return {
1072
+ content: [{ type: "text", text: `Subagent ${finished.kind} ${groupId} has already finished.` }],
1073
+ details: makeDetails("single"),
1074
+ };
1075
+ }
1076
+
1077
+ const batch = store.batchGroups.get(groupId);
1078
+ const pipeline = store.pipelines.get(groupId);
1079
+ if (!batch && !pipeline) {
1080
+ return {
1081
+ content: [{ type: "text", text: `Unknown subagent group "${groupId}".` }],
1082
+ details: makeDetails("single"),
1083
+ isError: true,
1084
+ };
1085
+ }
1086
+
1087
+ abortGroupTarget = { groupId, kind: batch ? "batch" : "chain" };
1088
+ if (pipeline) pipeline.abortRequested = true;
1089
+ const groupRunIds = (batch?.runIds ?? pipeline?.stepRunIds ?? []).filter((runId) => {
1090
+ const run = store.globalLiveRuns.get(runId)?.runState ?? store.commandRuns.get(runId);
1091
+ return batch ? run?.batchId === groupId : run?.pipelineId === groupId;
1092
+ });
1093
+ if (groupRunIds.length === 0) {
1094
+ return {
1095
+ content: [
1096
+ { type: "text", text: `Subagent ${abortGroupTarget.kind} ${groupId} has no started runs to abort.` },
1097
+ ],
1098
+ details: makeDetails("single"),
1099
+ };
1100
+ }
1101
+ cmdParams.runIds = groupRunIds;
1102
+ }
1103
+
1043
1104
  if ((asyncAction === "status" || asyncAction === "detail") && typeof cmdParams.groupId === "string") {
1044
1105
  evictStaleFinishedGroups(store);
1045
1106
  const groupId = cmdParams.groupId;
@@ -1178,7 +1239,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1178
1239
  const unknown: number[] = [];
1179
1240
 
1180
1241
  for (const runId of targetRunIds) {
1181
- const run = store.commandRuns.get(runId);
1242
+ const run = abortGroupTarget
1243
+ ? (store.globalLiveRuns.get(runId)?.runState ?? store.commandRuns.get(runId))
1244
+ : store.commandRuns.get(runId);
1182
1245
  if (!run) {
1183
1246
  unknown.push(runId);
1184
1247
  continue;
@@ -1200,7 +1263,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1200
1263
  updateCommandRunsWidget(store, ctx as WidgetRenderCtx);
1201
1264
  }
1202
1265
 
1203
- if (targetRunIds.length === 1 && aborting.length === 1 && notRunning.length === 0 && unknown.length === 0) {
1266
+ if (
1267
+ !abortGroupTarget &&
1268
+ targetRunIds.length === 1 &&
1269
+ aborting.length === 1 &&
1270
+ notRunning.length === 0 &&
1271
+ unknown.length === 0
1272
+ ) {
1204
1273
  return {
1205
1274
  content: [{ type: "text", text: `Aborting subagent run #${aborting[0]}...` }],
1206
1275
  details: makeDetails("single"),
@@ -1208,6 +1277,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1208
1277
  }
1209
1278
 
1210
1279
  const lines: string[] = [];
1280
+ if (abortGroupTarget) {
1281
+ lines.push(`Subagent ${abortGroupTarget.kind} ${abortGroupTarget.groupId}:`);
1282
+ }
1211
1283
  if (aborting.length > 0) lines.push(`Aborting: ${aborting.map((id) => `#${id}`).join(", ")}.`);
1212
1284
  if (notRunning.length > 0) lines.push(`Not running: ${notRunning.map((id) => `#${id}`).join(", ")}.`);
1213
1285
  if (unknown.length > 0) lines.push(`Unknown: ${unknown.map((id) => `#${id}`).join(", ")}.`);
@@ -1333,6 +1405,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1333
1405
  }
1334
1406
 
1335
1407
  function registerRunLaunch(config: RunLaunchConfig): CommandRunState {
1408
+ assertAsyncAdmission();
1336
1409
  const initialDisplayTask = buildSubagentDisplayTaskFallback(config.taskForDisplay);
1337
1410
  let runState: CommandRunState;
1338
1411
  if (config.existingRunState) {
@@ -1413,6 +1486,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1413
1486
  store.recentLaunchTimestamps.delete(runId);
1414
1487
  }
1415
1488
 
1489
+ function cleanupGroupRunAfterFinalDelivery(runId: number, groupId: string, kind: "batch" | "chain") {
1490
+ const run = store.globalLiveRuns.get(runId)?.runState ?? store.commandRuns.get(runId);
1491
+ const ownsGroup = kind === "batch" ? run?.batchId === groupId : run?.pipelineId === groupId;
1492
+ if (run && !ownsGroup) return;
1493
+ cleanupRunAfterFinalDelivery(runId);
1494
+ }
1495
+
1416
1496
  function launchRunInBackground(runState: CommandRunState, taskForAgent: string): Promise<FinalizedRun> {
1417
1497
  let claudeCheckpointSent = !!runState.claudeSessionId;
1418
1498
  const abortSignal = runState.abortController?.signal;
@@ -1460,6 +1540,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1460
1540
  resumeSessionId: runState.claudeSessionId,
1461
1541
  sidecarSessionFile: runState.sessionFile,
1462
1542
  persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1543
+ ...(store.asyncTasks?.provider.supported ? { asyncRunId: runState.id } : {}),
1463
1544
  onDiagnostic: createRunDiagnosticSink(pi, runState),
1464
1545
  },
1465
1546
  );
@@ -1615,7 +1696,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1615
1696
  void (async () => {
1616
1697
  try {
1617
1698
  const finalized = await launchRunInBackground(runState, taskForAgent);
1618
- if (runState.removed || store.disposed) return;
1699
+ if (runState.removed || store.disposed) {
1700
+ discardRemovedAsyncInvocation(finalized.rawOutput);
1701
+ return;
1702
+ }
1619
1703
  updateCommandRunsWidget(store);
1620
1704
 
1621
1705
  if (finalized.result?.exitCode === ESCALATION_EXIT_CODE) {
@@ -1641,12 +1725,16 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1641
1725
  if (entry) entry.pendingCompletion = makePendingCompletion(completionMessage, true);
1642
1726
  }
1643
1727
 
1644
- ctx.ui?.notify?.(
1645
- `subagent tool run #${runState.id} (${resolvedAgent}) ${terminalLabel}`,
1646
- terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
1647
- );
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
+ );
1648
1733
  } catch (error: unknown) {
1649
- if (runState.removed || store.disposed) return;
1734
+ if (runState.removed || store.disposed) {
1735
+ discardRemovedAsyncInvocation(String(error));
1736
+ return;
1737
+ }
1650
1738
  const finalized = finalizeRunError(runState, error);
1651
1739
  const terminalLabel = getFinalizedRunLabel(finalized);
1652
1740
  const errorMessage = buildRunCompletionMessage(finalized);
@@ -1657,10 +1745,11 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1657
1745
  const entry = store.globalLiveRuns.get(runState.id);
1658
1746
  if (entry) entry.pendingCompletion = makePendingCompletion(errorMessage, true);
1659
1747
  }
1660
- ctx.ui?.notify?.(
1661
- `subagent tool run #${runState.id} ${terminalLabel}: ${runState.lastLine}`,
1662
- terminalLabel === "aborted" ? "warning" : "error",
1663
- );
1748
+ if (terminalLabel !== "aborted" || !isExpectedTrackedCancellation())
1749
+ ctx.ui?.notify?.(
1750
+ `subagent tool run #${runState.id} ${terminalLabel}: ${runState.lastLine}`,
1751
+ terminalLabel === "aborted" ? "warning" : "error",
1752
+ );
1664
1753
  updateCommandRunsWidget(store);
1665
1754
  } finally {
1666
1755
  clearRunAbortState(runState);
@@ -1734,7 +1823,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1734
1823
  abortedRunIds: new Set(),
1735
1824
  originSessionFile,
1736
1825
  createdAt: Date.now(),
1737
- pendingResults: new Map(),
1826
+ completedRuns: new Map(),
1738
1827
  });
1739
1828
  updateCommandRunsWidget(store, ctx as WidgetRenderCtx);
1740
1829
 
@@ -1750,11 +1839,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1750
1839
  }
1751
1840
  }),
1752
1841
  );
1753
- const orderedRuns = runStates.map(({ runState }) => runState);
1754
1842
  const batchForSnapshot = store.batchGroups.get(batchId);
1755
1843
  if (batchForSnapshot) {
1756
1844
  for (const finalized of finalizedRuns) recordBatchResult(batchForSnapshot, finalized);
1757
1845
  }
1846
+ const orderedRuns = batchForSnapshot
1847
+ ? getCompletedBatchRuns(batchForSnapshot)
1848
+ : runStates.map(({ runState }) => runState);
1758
1849
  const terminalStatus = resolveGroupTerminalStatus(
1759
1850
  batchForSnapshot?.failedRunIds.size ?? 0,
1760
1851
  batchForSnapshot?.abortedRunIds.size ?? 0,
@@ -1763,7 +1854,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1763
1854
  if (batchForSnapshot) {
1764
1855
  retireFinishedGroup(store, snapshotBatchGroup(store, batchForSnapshot, terminalStatus));
1765
1856
  }
1766
- for (const { runState } of runStates) cleanupRunAfterFinalDelivery(runState.id);
1857
+ for (const { runState } of runStates) cleanupGroupRunAfterFinalDelivery(runState.id, batchId, "batch");
1767
1858
  clearPendingGroupCompletion("batch", batchId);
1768
1859
  store.batchGroups.delete(batchId);
1769
1860
  trimCommandRunHistory(store, {
@@ -1799,9 +1890,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1799
1890
  updateCommandRunsWidget(store);
1800
1891
 
1801
1892
  if (batch.completedRunIds.size === batch.runIds.length) {
1802
- const orderedRuns = batch.runIds
1803
- .map((runId) => store.commandRuns.get(runId))
1804
- .filter((run): run is CommandRunState => Boolean(run));
1893
+ const orderedRuns = getCompletedBatchRuns(batch);
1805
1894
  const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
1806
1895
  const content = formatBatchSummary(batchId, orderedRuns, batchTerminalStatus);
1807
1896
  const message = {
@@ -1809,17 +1898,19 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1809
1898
  content,
1810
1899
  display: true,
1811
1900
  details: {
1901
+ ...asyncInvocationDetails(),
1812
1902
  batchId,
1813
1903
  runIds: batch.runIds,
1814
1904
  status: batchTerminalStatus === "completed" ? "done" : batchTerminalStatus,
1815
1905
  runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
1816
1906
  },
1817
1907
  };
1908
+ retainAsyncCompletion(message);
1818
1909
  retireFinishedGroup(store, snapshotBatchGroup(store, batch, batchTerminalStatus));
1819
1910
  if (isInOriginSession(ctx, batch.originSessionFile)) {
1820
1911
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
1821
1912
  clearPendingGroupCompletion("batch", batchId);
1822
- for (const runId of batch.runIds) cleanupRunAfterFinalDelivery(runId);
1913
+ for (const runId of batch.runIds) cleanupGroupRunAfterFinalDelivery(runId, batchId, "batch");
1823
1914
  store.batchGroups.delete(batchId);
1824
1915
  } else {
1825
1916
  batch.pendingCompletion = makePendingCompletion(message, true);
@@ -1838,14 +1929,15 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1838
1929
  updateWidget: false,
1839
1930
  removalReason: "trim",
1840
1931
  });
1841
- ctx.ui?.notify?.(
1842
- batchTerminalStatus === "error"
1843
- ? `subagent batch ${batchId} finished with errors`
1844
- : batchTerminalStatus === "aborted"
1845
- ? `subagent batch ${batchId} aborted`
1846
- : `subagent batch ${batchId} completed`,
1847
- batchTerminalStatus === "error" ? "error" : batchTerminalStatus === "aborted" ? "warning" : "info",
1848
- );
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
+ );
1849
1941
  }
1850
1942
  } catch (error: unknown) {
1851
1943
  const finalized = finalizeRunError(runState, error);
@@ -1853,26 +1945,26 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1853
1945
  if (!batch) return;
1854
1946
  recordBatchResult(batch, finalized);
1855
1947
  if (batch.completedRunIds.size === batch.runIds.length) {
1856
- const orderedRuns = batch.runIds
1857
- .map((runId) => store.commandRuns.get(runId))
1858
- .filter((run): run is CommandRunState => Boolean(run));
1948
+ const orderedRuns = getCompletedBatchRuns(batch);
1859
1949
  const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
1860
1950
  const message = {
1861
1951
  customType: "subagent-tool" as const,
1862
1952
  content: formatBatchSummary(batchId, orderedRuns, batchTerminalStatus),
1863
1953
  display: true,
1864
1954
  details: {
1955
+ ...asyncInvocationDetails(),
1865
1956
  batchId,
1866
1957
  runIds: batch.runIds,
1867
1958
  status: batchTerminalStatus,
1868
1959
  runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
1869
1960
  },
1870
1961
  };
1962
+ retainAsyncCompletion(message);
1871
1963
  retireFinishedGroup(store, snapshotBatchGroup(store, batch, batchTerminalStatus));
1872
1964
  if (isInOriginSession(ctx, batch.originSessionFile)) {
1873
1965
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
1874
1966
  clearPendingGroupCompletion("batch", batchId);
1875
- for (const runId of batch.runIds) cleanupRunAfterFinalDelivery(runId);
1967
+ for (const runId of batch.runIds) cleanupGroupRunAfterFinalDelivery(runId, batchId, "batch");
1876
1968
  store.batchGroups.delete(batchId);
1877
1969
  } else {
1878
1970
  batch.pendingCompletion = makePendingCompletion(message, true);
@@ -1931,6 +2023,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1931
2023
  store.pipelines.set(pipelineId, {
1932
2024
  pipelineId,
1933
2025
  currentIndex: 0,
2026
+ abortRequested: false,
1934
2027
  stepRunIds: [],
1935
2028
  stepResults: [],
1936
2029
  originSessionFile,
@@ -1952,6 +2045,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1952
2045
 
1953
2046
  try {
1954
2047
  for (let index = 0; index < steps.length; index++) {
2048
+ if (pipeline.abortRequested) {
2049
+ terminalStatus = "aborted";
2050
+ break;
2051
+ }
1955
2052
  pipeline.currentIndex = index;
1956
2053
  const step = steps[index];
1957
2054
  const pipelineReferenceSection =
@@ -2005,26 +2102,20 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2005
2102
  }
2006
2103
  finalizedRuns.push(finalized);
2007
2104
 
2008
- if (runState.removed) {
2009
- terminalStatus = "aborted";
2010
- pipeline.stepResults.push({
2011
- runId: runState.id,
2012
- agent: runState.agent,
2013
- task: step.task,
2014
- output: finalized.rawOutput || "Run removed before pipeline completion.",
2015
- status: "aborted",
2016
- });
2017
- break;
2018
- }
2019
-
2020
- const stepStatus = getFinalizedRunStatus(finalized);
2105
+ const stepStatus = runState.removed ? "aborted" : getFinalizedRunStatus(finalized);
2021
2106
  pipeline.stepResults.push({
2022
2107
  runId: runState.id,
2023
2108
  agent: runState.agent,
2024
2109
  task: step.task,
2025
- output: finalized.rawOutput,
2110
+ output: runState.removed
2111
+ ? finalized.rawOutput || "Run removed before pipeline completion."
2112
+ : finalized.rawOutput,
2026
2113
  status: stepStatus,
2027
2114
  });
2115
+ if (runState.removed || pipeline.abortRequested) {
2116
+ terminalStatus = "aborted";
2117
+ break;
2118
+ }
2028
2119
  previousOutput = finalized.rawOutput;
2029
2120
  updateCommandRunsWidget(store);
2030
2121
 
@@ -2050,7 +2141,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2050
2141
  else if (terminalStatus === "completed" && hasAborted) terminalStatus = "aborted";
2051
2142
  const content = formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus);
2052
2143
  retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
2053
- for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
2144
+ for (const runId of pipeline.stepRunIds) cleanupGroupRunAfterFinalDelivery(runId, pipelineId, "chain");
2054
2145
  clearPendingGroupCompletion("chain", pipelineId);
2055
2146
  store.pipelines.delete(pipelineId);
2056
2147
  trimCommandRunHistory(store, {
@@ -2082,6 +2173,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2082
2173
  for (let index = 0; index < steps.length; index++) {
2083
2174
  const pipeline = store.pipelines.get(pipelineId);
2084
2175
  if (!pipeline) return;
2176
+ if (pipeline.abortRequested) {
2177
+ terminalStatus = "aborted";
2178
+ break;
2179
+ }
2085
2180
  pipeline.currentIndex = index;
2086
2181
 
2087
2182
  const step = steps[index];
@@ -2134,26 +2229,20 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2134
2229
  } finally {
2135
2230
  clearRunAbortState(runState);
2136
2231
  }
2137
- if (runState.removed) {
2138
- terminalStatus = "aborted";
2139
- pipeline.stepResults.push({
2140
- runId: runState.id,
2141
- agent: runState.agent,
2142
- task: step.task,
2143
- output: finalized.rawOutput || "Run removed before pipeline completion.",
2144
- status: "aborted",
2145
- });
2146
- break;
2147
- }
2148
-
2149
2232
  const stepResult: PipelineStepResult = {
2150
2233
  runId: runState.id,
2151
2234
  agent: runState.agent,
2152
2235
  task: step.task,
2153
- output: finalized.rawOutput,
2154
- status: getFinalizedRunStatus(finalized),
2236
+ output: runState.removed
2237
+ ? finalized.rawOutput || "Run removed before pipeline completion."
2238
+ : finalized.rawOutput,
2239
+ status: runState.removed ? "aborted" : getFinalizedRunStatus(finalized),
2155
2240
  };
2156
2241
  pipeline.stepResults.push(stepResult);
2242
+ if (runState.removed || pipeline.abortRequested) {
2243
+ terminalStatus = "aborted";
2244
+ break;
2245
+ }
2157
2246
  previousOutput = finalized.rawOutput;
2158
2247
  updateCommandRunsWidget(store);
2159
2248
 
@@ -2189,17 +2278,19 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2189
2278
  content: formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus),
2190
2279
  display: true,
2191
2280
  details: {
2281
+ ...asyncInvocationDetails(),
2192
2282
  pipelineId,
2193
2283
  stepRunIds: pipeline.stepRunIds,
2194
2284
  status: terminalStatus === "completed" ? "done" : terminalStatus,
2195
2285
  runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
2196
2286
  },
2197
2287
  };
2288
+ retainAsyncCompletion(message);
2198
2289
  retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
2199
2290
  if (isInOriginSession(ctx, pipeline.originSessionFile)) {
2200
2291
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
2201
2292
  clearPendingGroupCompletion("chain", pipelineId);
2202
- for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
2293
+ for (const runId of pipeline.stepRunIds) cleanupGroupRunAfterFinalDelivery(runId, pipelineId, "chain");
2203
2294
  store.pipelines.delete(pipelineId);
2204
2295
  } else {
2205
2296
  pipeline.pendingCompletion = makePendingCompletion(message, true);
@@ -2243,5 +2334,20 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2243
2334
  };
2244
2335
  };
2245
2336
 
2246
- 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
+ };
2247
2353
  }
package/types.ts CHANGED
@@ -177,7 +177,8 @@ export interface BatchGroupState {
177
177
  abortedRunIds: Set<number>;
178
178
  originSessionFile: string;
179
179
  createdAt: number;
180
- pendingResults: Map<number, string>;
180
+ /** Immutable member states captured as each batch run completes. */
181
+ completedRuns: Map<number, CommandRunState>;
181
182
  pendingCompletion?: PendingCompletion;
182
183
  }
183
184
 
@@ -192,6 +193,8 @@ export interface PipelineStepResult {
192
193
  export interface PipelineState {
193
194
  pipelineId: string;
194
195
  currentIndex: number;
196
+ /** Set by `abort <pipelineId>` to prevent any subsequent step from launching. */
197
+ abortRequested: boolean;
195
198
  stepRunIds: number[];
196
199
  stepResults: PipelineStepResult[];
197
200
  originSessionFile: string;