@ryan_nookpi/pi-extension-subagent 0.5.5 → 0.5.7

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/README.md CHANGED
@@ -68,7 +68,7 @@ Run `/subagents`, `subagent agents`, or the `list-agents` tool to inspect discov
68
68
 
69
69
  ### Optional starter pack
70
70
 
71
- If no agents are found, `/subagents` can offer an optional, opinionated starter pack. It copies nine agent templates, two example workflow skills, and missing global `subagent` settings. Existing files and configured values are not overwritten.
71
+ If no agents are found, `/subagents` can offer an optional, opinionated starter pack. It copies eight agent templates, two example workflow skills, and missing global `subagent` settings. Existing files and configured values are not overwritten.
72
72
 
73
73
  The starter pack is not required. Decline it if you prefer to define agents manually. It fills a missing `claudeRuntime` setting with `cli`; without that setting, the extension default is `sdk`.
74
74
 
@@ -98,9 +98,9 @@ subagent run <agent> [--main|--isolated] -- <task>
98
98
  subagent continue <runId> [--agent <agent>] [--main|--isolated] -- <task>
99
99
  subagent batch [--main|--isolated] --agent <agent> --task <task> ...
100
100
  subagent chain [--main|--isolated] --agent <agent> --task <task> ...
101
- subagent status <runId>
102
- subagent detail <runId>
103
- subagent abort <runId|runId,runId|all>
101
+ subagent status <runId|groupId>
102
+ subagent detail <runId|groupId>
103
+ subagent abort <runId|groupId|runId,runId|all>
104
104
  subagent remove <runId|runId,runId|all>
105
105
  ```
106
106
 
package/cli.ts CHANGED
@@ -64,7 +64,7 @@ export const SUBAGENT_CLI_HELP_TEXT = [
64
64
  " subagent chain [--main|--isolated] --agent <agent> --task <task> --agent <agent> --task <task> ...",
65
65
  "",
66
66
  " Cleanup:",
67
- " subagent abort <runId|runId,runId|all>",
67
+ " subagent abort <runId|groupId|runId,runId|all>",
68
68
  " subagent remove <runId|runId,runId|all>",
69
69
  "",
70
70
  "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
@@ -88,6 +88,7 @@ export const SUBAGENT_CLI_HELP_TEXT = [
88
88
  " subagent status 22",
89
89
  " subagent detail 22",
90
90
  " subagent abort 22",
91
+ " subagent abort p_1712...",
91
92
  " subagent remove all",
92
93
  "",
93
94
  "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
@@ -508,8 +509,14 @@ export function parseSubagentToolCommand(
508
509
  if (!target)
509
510
  return {
510
511
  type: "error",
511
- message: `❌ ${verb} requires <runId|runId,runId|all>\n\n✓ Examples:\n subagent ${verb} 22\n subagent ${verb} 22,23,24\n subagent ${verb} all`,
512
+ message:
513
+ verb === "abort"
514
+ ? "❌ abort requires <runId|groupId|runId,runId|all>\n\n✓ Examples:\n subagent abort 22\n subagent abort p_1712_abcd\n subagent abort 22,23,24\n subagent abort all"
515
+ : "❌ remove requires <runId|runId,runId|all>\n\n✓ Examples:\n subagent remove 22\n subagent remove 22,23,24\n subagent remove all",
512
516
  };
517
+ if (verb === "abort" && isGroupId(target)) {
518
+ return { type: "params", params: { asyncAction: "abort", groupId: target } };
519
+ }
513
520
  const parsedTarget = parseRunTarget(target, options.knownRunIds);
514
521
  if ("error" in parsedTarget)
515
522
  return {
@@ -5,6 +5,7 @@ export type SubagentErrorClass =
5
5
  | "overloaded"
6
6
  | "rate_limit"
7
7
  | "tool_error"
8
+ | "network_error"
8
9
  | "aborted"
9
10
  | "process_error"
10
11
  | "unknown";
@@ -64,6 +65,9 @@ const TOOL_ERROR_PATTERNS = [
64
65
  /invalid tool/i,
65
66
  ];
66
67
 
68
+ const NETWORK_PATTERNS = [/\bwebsocket\b/i, /\bconnection error\b/i];
69
+ const PROCESS_SIGNAL_TERMINATION_PATTERN = /\bprocess terminated by signal\b/i;
70
+
67
71
  export function classifySubagentFailure(input: FailureTelemetryInput): SubagentErrorClass | undefined {
68
72
  if (!input.failed) return undefined;
69
73
  const text = [input.errorMessage, input.stderr, input.output].filter(Boolean).join("\n");
@@ -73,6 +77,14 @@ export function classifySubagentFailure(input: FailureTelemetryInput): SubagentE
73
77
  if (OVERLOADED_PATTERNS.some((pattern) => pattern.test(text))) return "overloaded";
74
78
  if (RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(text))) return "rate_limit";
75
79
  if (TOOL_ERROR_PATTERNS.some((pattern) => pattern.test(text))) return "tool_error";
80
+ const signalTerminated = PROCESS_SIGNAL_TERMINATION_PATTERN.test(input.stderr ?? "");
81
+ if (
82
+ !signalTerminated &&
83
+ (NETWORK_PATTERNS.some((pattern) => pattern.test(text)) ||
84
+ /^terminated\.?$/i.test(input.errorMessage?.trim() ?? ""))
85
+ ) {
86
+ return "network_error";
87
+ }
76
88
  if ((input.exitCode ?? 0) !== 0) return "process_error";
77
89
  return "unknown";
78
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/retry.ts CHANGED
@@ -34,9 +34,13 @@ function buildFailureText(result: SingleResult): string {
34
34
  .join("\n");
35
35
  }
36
36
 
37
+ const PROCESS_SIGNAL_TERMINATION_PATTERN = /\bprocess terminated by signal\b/i;
38
+
37
39
  const TRANSIENT_FAILURE_PATTERNS: RegExp[] = [
38
40
  /\bnetwork\b/i,
41
+ /\bwebsocket\b/i,
39
42
  /\bfetch failed\b/i,
43
+ /\bconnection error\b/i,
40
44
  /\bconnection reset\b/i,
41
45
  /\bsocket hang up\b/i,
42
46
  /\btemporar(?:y|ily) unavailable\b/i,
@@ -70,12 +74,18 @@ function matchesTransientPattern(text: string): boolean {
70
74
 
71
75
  export function diagnoseRetryableResult(result: SingleResult): RetryDecision {
72
76
  if (result.stopReason === "aborted") return { retryable: false };
77
+ if (PROCESS_SIGNAL_TERMINATION_PATTERN.test(result.stderr)) return { retryable: false };
73
78
 
74
79
  const finalOutput = getFinalOutput(result.messages).trim();
75
80
  if (result.exitCode === 0 && result.stopReason !== "error" && finalOutput) {
76
81
  return { retryable: false };
77
82
  }
78
83
 
84
+ const errorMessage = result.errorMessage?.trim();
85
+ if (errorMessage && /^terminated\.?$/i.test(errorMessage)) {
86
+ return { retryable: true, reason: errorMessage };
87
+ }
88
+
79
89
  const failureText = buildFailureText(result);
80
90
  if (!failureText) return { retryable: false };
81
91
  if (!matchesTransientPattern(failureText)) return { retryable: false };
package/run-utils.ts CHANGED
@@ -242,18 +242,18 @@ export function snapshotBatchGroup(
242
242
  let failed = 0;
243
243
  let aborted = 0;
244
244
  const members: FinishedGroupMember[] = batch.runIds.map((runId) => {
245
- const run = store.commandRuns.get(runId);
245
+ const run = batch.completedRuns.get(runId) ?? store.commandRuns.get(runId);
246
246
  if (!run) {
247
247
  if (batch.abortedRunIds.has(runId)) aborted++;
248
248
  else if (batch.failedRunIds.has(runId)) failed++;
249
249
  return {
250
250
  summaryLine: `#${runId} [gone] (run no longer available)`,
251
- output: batch.pendingResults.get(runId)?.trim() || "(no output)",
251
+ output: "(no output)",
252
252
  };
253
253
  }
254
254
  if (run.errorClass === "aborted" || batch.abortedRunIds.has(runId)) aborted++;
255
255
  else if (run.status === "error" || batch.failedRunIds.has(runId)) failed++;
256
- return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run, batch.pendingResults.get(runId)) };
256
+ return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run) };
257
257
  });
258
258
  return {
259
259
  groupId: batch.batchId,
package/starter-pack.ts CHANGED
@@ -11,7 +11,6 @@ export const STARTER_AGENT_NAMES = [
11
11
  "reviewer",
12
12
  "searcher",
13
13
  "security-auditor",
14
- "simplifier",
15
14
  "verifier",
16
15
  "worker",
17
16
  ] as const;
@@ -220,7 +219,7 @@ export async function offerStarterPackIfEmpty(
220
219
 
221
220
  const accepted = await ctx.ui.confirm(
222
221
  "Install starter subagents?",
223
- "No subagent definitions were found. Install 9 portable English agents, the stress-interview and self-healing skills, and missing subagent settings? Existing files and configured values will not be overwritten.",
222
+ "No subagent definitions were found. Install 8 portable English agents, the stress-interview and self-healing skills, and missing subagent settings? Existing files and configured values will not be overwritten.",
224
223
  );
225
224
  if (!accepted) return { status: "declined", discovery };
226
225
 
package/tool-execute.ts CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  import { clearPendingGroupCompletion, upsertPendingGroupCompletion } from "./group-pending.js";
50
50
  import { enqueueSubagentInvocation } from "./invocation-queue.js";
51
51
  import { appendDisplayTaskUpdate, getSessionFileSize } from "./persisted-session.js";
52
+ import { invokeWithAutoRetry, MAX_SUBAGENT_AUTO_RETRIES } from "./retry.js";
52
53
  import {
53
54
  clearFinishedRuns,
54
55
  evictStaleFinishedGroups,
@@ -501,7 +502,13 @@ function recordBatchResult(batch: BatchGroupState, finalized: FinalizedRun): voi
501
502
  batch.completedRunIds.add(runId);
502
503
  if (status === "aborted") batch.abortedRunIds.add(runId);
503
504
  else if (status === "error") batch.failedRunIds.add(runId);
504
- batch.pendingResults.set(runId, finalized.rawOutput);
505
+ batch.completedRuns.set(runId, { ...finalized.runState, abortController: undefined });
506
+ }
507
+
508
+ function getCompletedBatchRuns(batch: BatchGroupState): CommandRunState[] {
509
+ return batch.runIds
510
+ .map((runId) => batch.completedRuns.get(runId))
511
+ .filter((run): run is CommandRunState => Boolean(run));
505
512
  }
506
513
 
507
514
  function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?: boolean }) {
@@ -1039,6 +1046,45 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1039
1046
  };
1040
1047
  }
1041
1048
 
1049
+ let abortGroupTarget: { groupId: string; kind: "batch" | "chain" } | undefined;
1050
+ if (asyncAction === "abort" && typeof cmdParams.groupId === "string") {
1051
+ evictStaleFinishedGroups(store);
1052
+ const groupId = cmdParams.groupId;
1053
+ const finished = store.finishedGroups.get(groupId);
1054
+ if (finished) {
1055
+ return {
1056
+ content: [{ type: "text", text: `Subagent ${finished.kind} ${groupId} has already finished.` }],
1057
+ details: makeDetails("single"),
1058
+ };
1059
+ }
1060
+
1061
+ const batch = store.batchGroups.get(groupId);
1062
+ const pipeline = store.pipelines.get(groupId);
1063
+ if (!batch && !pipeline) {
1064
+ return {
1065
+ content: [{ type: "text", text: `Unknown subagent group "${groupId}".` }],
1066
+ details: makeDetails("single"),
1067
+ isError: true,
1068
+ };
1069
+ }
1070
+
1071
+ abortGroupTarget = { groupId, kind: batch ? "batch" : "chain" };
1072
+ if (pipeline) pipeline.abortRequested = true;
1073
+ const groupRunIds = (batch?.runIds ?? pipeline?.stepRunIds ?? []).filter((runId) => {
1074
+ const run = store.globalLiveRuns.get(runId)?.runState ?? store.commandRuns.get(runId);
1075
+ return batch ? run?.batchId === groupId : run?.pipelineId === groupId;
1076
+ });
1077
+ if (groupRunIds.length === 0) {
1078
+ return {
1079
+ content: [
1080
+ { type: "text", text: `Subagent ${abortGroupTarget.kind} ${groupId} has no started runs to abort.` },
1081
+ ],
1082
+ details: makeDetails("single"),
1083
+ };
1084
+ }
1085
+ cmdParams.runIds = groupRunIds;
1086
+ }
1087
+
1042
1088
  if ((asyncAction === "status" || asyncAction === "detail") && typeof cmdParams.groupId === "string") {
1043
1089
  evictStaleFinishedGroups(store);
1044
1090
  const groupId = cmdParams.groupId;
@@ -1177,7 +1223,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1177
1223
  const unknown: number[] = [];
1178
1224
 
1179
1225
  for (const runId of targetRunIds) {
1180
- const run = store.commandRuns.get(runId);
1226
+ const run = abortGroupTarget
1227
+ ? (store.globalLiveRuns.get(runId)?.runState ?? store.commandRuns.get(runId))
1228
+ : store.commandRuns.get(runId);
1181
1229
  if (!run) {
1182
1230
  unknown.push(runId);
1183
1231
  continue;
@@ -1199,7 +1247,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1199
1247
  updateCommandRunsWidget(store, ctx as WidgetRenderCtx);
1200
1248
  }
1201
1249
 
1202
- if (targetRunIds.length === 1 && aborting.length === 1 && notRunning.length === 0 && unknown.length === 0) {
1250
+ if (
1251
+ !abortGroupTarget &&
1252
+ targetRunIds.length === 1 &&
1253
+ aborting.length === 1 &&
1254
+ notRunning.length === 0 &&
1255
+ unknown.length === 0
1256
+ ) {
1203
1257
  return {
1204
1258
  content: [{ type: "text", text: `Aborting subagent run #${aborting[0]}...` }],
1205
1259
  details: makeDetails("single"),
@@ -1207,6 +1261,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1207
1261
  }
1208
1262
 
1209
1263
  const lines: string[] = [];
1264
+ if (abortGroupTarget) {
1265
+ lines.push(`Subagent ${abortGroupTarget.kind} ${abortGroupTarget.groupId}:`);
1266
+ }
1210
1267
  if (aborting.length > 0) lines.push(`Aborting: ${aborting.map((id) => `#${id}`).join(", ")}.`);
1211
1268
  if (notRunning.length > 0) lines.push(`Not running: ${notRunning.map((id) => `#${id}`).join(", ")}.`);
1212
1269
  if (unknown.length > 0) lines.push(`Unknown: ${unknown.map((id) => `#${id}`).join(", ")}.`);
@@ -1412,48 +1469,71 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1412
1469
  store.recentLaunchTimestamps.delete(runId);
1413
1470
  }
1414
1471
 
1472
+ function cleanupGroupRunAfterFinalDelivery(runId: number, groupId: string, kind: "batch" | "chain") {
1473
+ const run = store.globalLiveRuns.get(runId)?.runState ?? store.commandRuns.get(runId);
1474
+ const ownsGroup = kind === "batch" ? run?.batchId === groupId : run?.pipelineId === groupId;
1475
+ if (run && !ownsGroup) return;
1476
+ cleanupRunAfterFinalDelivery(runId);
1477
+ }
1478
+
1415
1479
  function launchRunInBackground(runState: CommandRunState, taskForAgent: string): Promise<FinalizedRun> {
1416
1480
  let claudeCheckpointSent = !!runState.claudeSessionId;
1417
1481
  const abortSignal = runState.abortController?.signal;
1418
1482
  const recordActivity = createRunActivityRecorder(pi, runState, (model) => resolveContextWindow(ctx, model));
1419
- return enqueueSubagentInvocation(async () => {
1420
- if (runState.removed || abortSignal?.aborted) throw new Error("Subagent was aborted");
1421
- return runSingleAgent(
1422
- ctx.cwd,
1423
- agents,
1424
- runState.agent,
1425
- taskForAgent,
1426
- runState.pipelineStepIndex,
1427
- abortSignal,
1428
- (partial) => {
1429
- if (runState.removed || store.disposed) return;
1430
- const current = partial.details?.results?.[0];
1431
- if (!current) return;
1432
- updateRunFromResult(runState, current);
1433
- recordActivity();
1434
- if (shouldRunAsync && !claudeCheckpointSent && runState.claudeSessionId) {
1435
- claudeCheckpointSent = true;
1436
- pi.sendMessage(buildRunStartMessage(runState, "started"), {
1437
- deliverAs: "followUp",
1438
- triggerTurn: false,
1439
- });
1440
- }
1441
- updateCommandRunsWidget(store);
1442
- },
1443
- (results) => makeDetails(mode, results),
1444
- {
1445
- sessionFile: runState.sessionFile,
1446
- resumeSessionId: runState.claudeSessionId,
1447
- sidecarSessionFile: runState.sessionFile,
1448
- persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1449
- onDiagnostic: createRunDiagnosticSink(pi, runState),
1450
- },
1451
- );
1452
- }).then((result) =>
1453
- runState.removed || abortSignal?.aborted
1483
+ return invokeWithAutoRetry({
1484
+ maxRetries: MAX_SUBAGENT_AUTO_RETRIES,
1485
+ signal: abortSignal,
1486
+ onRetryScheduled: ({ retryIndex, maxRetries, delayMs, reason }) => {
1487
+ runState.retryCount = retryIndex;
1488
+ runState.lastRetryReason = reason;
1489
+ runState.lastActivityAt = Date.now();
1490
+ runState.lastLine = `Auto-retrying ${retryIndex}/${maxRetries} in ${Math.ceil(delayMs / 1000)}s: ${reason}`;
1491
+ runState.lastOutput = runState.lastLine;
1492
+ updateCommandRunsWidget(store);
1493
+ },
1494
+ invoke: () => {
1495
+ runState.persistedSessionBaseOffset = getSessionFileSize(runState.sessionFile);
1496
+ return enqueueSubagentInvocation(async () => {
1497
+ if (runState.removed || abortSignal?.aborted) throw new Error("Subagent was aborted");
1498
+ return runSingleAgent(
1499
+ ctx.cwd,
1500
+ agents,
1501
+ runState.agent,
1502
+ taskForAgent,
1503
+ runState.pipelineStepIndex,
1504
+ abortSignal,
1505
+ (partial) => {
1506
+ if (runState.removed || store.disposed) return;
1507
+ const current = partial.details?.results?.[0];
1508
+ if (!current) return;
1509
+ updateRunFromResult(runState, current);
1510
+ recordActivity();
1511
+ if (shouldRunAsync && !claudeCheckpointSent && runState.claudeSessionId) {
1512
+ claudeCheckpointSent = true;
1513
+ pi.sendMessage(buildRunStartMessage(runState, "started"), {
1514
+ deliverAs: "followUp",
1515
+ triggerTurn: false,
1516
+ });
1517
+ }
1518
+ updateCommandRunsWidget(store);
1519
+ },
1520
+ (results) => makeDetails(mode, results),
1521
+ {
1522
+ sessionFile: runState.sessionFile,
1523
+ resumeSessionId: runState.claudeSessionId,
1524
+ sidecarSessionFile: runState.sessionFile,
1525
+ persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1526
+ onDiagnostic: createRunDiagnosticSink(pi, runState),
1527
+ },
1528
+ );
1529
+ });
1530
+ },
1531
+ }).then(({ result, retryCount }) => {
1532
+ runState.retryCount = retryCount;
1533
+ return runState.removed || abortSignal?.aborted
1454
1534
  ? finalizeRunError(runState, new Error("Subagent was aborted"))
1455
- : finalizeRunState(runState, result),
1456
- );
1535
+ : finalizeRunState(runState, result);
1536
+ });
1457
1537
  }
1458
1538
 
1459
1539
  if (hasSingle) {
@@ -1717,7 +1797,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1717
1797
  abortedRunIds: new Set(),
1718
1798
  originSessionFile,
1719
1799
  createdAt: Date.now(),
1720
- pendingResults: new Map(),
1800
+ completedRuns: new Map(),
1721
1801
  });
1722
1802
  updateCommandRunsWidget(store, ctx as WidgetRenderCtx);
1723
1803
 
@@ -1733,11 +1813,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1733
1813
  }
1734
1814
  }),
1735
1815
  );
1736
- const orderedRuns = runStates.map(({ runState }) => runState);
1737
1816
  const batchForSnapshot = store.batchGroups.get(batchId);
1738
1817
  if (batchForSnapshot) {
1739
1818
  for (const finalized of finalizedRuns) recordBatchResult(batchForSnapshot, finalized);
1740
1819
  }
1820
+ const orderedRuns = batchForSnapshot
1821
+ ? getCompletedBatchRuns(batchForSnapshot)
1822
+ : runStates.map(({ runState }) => runState);
1741
1823
  const terminalStatus = resolveGroupTerminalStatus(
1742
1824
  batchForSnapshot?.failedRunIds.size ?? 0,
1743
1825
  batchForSnapshot?.abortedRunIds.size ?? 0,
@@ -1746,7 +1828,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1746
1828
  if (batchForSnapshot) {
1747
1829
  retireFinishedGroup(store, snapshotBatchGroup(store, batchForSnapshot, terminalStatus));
1748
1830
  }
1749
- for (const { runState } of runStates) cleanupRunAfterFinalDelivery(runState.id);
1831
+ for (const { runState } of runStates) cleanupGroupRunAfterFinalDelivery(runState.id, batchId, "batch");
1750
1832
  clearPendingGroupCompletion("batch", batchId);
1751
1833
  store.batchGroups.delete(batchId);
1752
1834
  trimCommandRunHistory(store, {
@@ -1782,9 +1864,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1782
1864
  updateCommandRunsWidget(store);
1783
1865
 
1784
1866
  if (batch.completedRunIds.size === batch.runIds.length) {
1785
- const orderedRuns = batch.runIds
1786
- .map((runId) => store.commandRuns.get(runId))
1787
- .filter((run): run is CommandRunState => Boolean(run));
1867
+ const orderedRuns = getCompletedBatchRuns(batch);
1788
1868
  const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
1789
1869
  const content = formatBatchSummary(batchId, orderedRuns, batchTerminalStatus);
1790
1870
  const message = {
@@ -1802,7 +1882,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1802
1882
  if (isInOriginSession(ctx, batch.originSessionFile)) {
1803
1883
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
1804
1884
  clearPendingGroupCompletion("batch", batchId);
1805
- for (const runId of batch.runIds) cleanupRunAfterFinalDelivery(runId);
1885
+ for (const runId of batch.runIds) cleanupGroupRunAfterFinalDelivery(runId, batchId, "batch");
1806
1886
  store.batchGroups.delete(batchId);
1807
1887
  } else {
1808
1888
  batch.pendingCompletion = makePendingCompletion(message, true);
@@ -1836,9 +1916,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1836
1916
  if (!batch) return;
1837
1917
  recordBatchResult(batch, finalized);
1838
1918
  if (batch.completedRunIds.size === batch.runIds.length) {
1839
- const orderedRuns = batch.runIds
1840
- .map((runId) => store.commandRuns.get(runId))
1841
- .filter((run): run is CommandRunState => Boolean(run));
1919
+ const orderedRuns = getCompletedBatchRuns(batch);
1842
1920
  const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
1843
1921
  const message = {
1844
1922
  customType: "subagent-tool" as const,
@@ -1855,7 +1933,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1855
1933
  if (isInOriginSession(ctx, batch.originSessionFile)) {
1856
1934
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
1857
1935
  clearPendingGroupCompletion("batch", batchId);
1858
- for (const runId of batch.runIds) cleanupRunAfterFinalDelivery(runId);
1936
+ for (const runId of batch.runIds) cleanupGroupRunAfterFinalDelivery(runId, batchId, "batch");
1859
1937
  store.batchGroups.delete(batchId);
1860
1938
  } else {
1861
1939
  batch.pendingCompletion = makePendingCompletion(message, true);
@@ -1914,6 +1992,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1914
1992
  store.pipelines.set(pipelineId, {
1915
1993
  pipelineId,
1916
1994
  currentIndex: 0,
1995
+ abortRequested: false,
1917
1996
  stepRunIds: [],
1918
1997
  stepResults: [],
1919
1998
  originSessionFile,
@@ -1935,6 +2014,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1935
2014
 
1936
2015
  try {
1937
2016
  for (let index = 0; index < steps.length; index++) {
2017
+ if (pipeline.abortRequested) {
2018
+ terminalStatus = "aborted";
2019
+ break;
2020
+ }
1938
2021
  pipeline.currentIndex = index;
1939
2022
  const step = steps[index];
1940
2023
  const pipelineReferenceSection =
@@ -1988,26 +2071,20 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1988
2071
  }
1989
2072
  finalizedRuns.push(finalized);
1990
2073
 
1991
- if (runState.removed) {
1992
- terminalStatus = "aborted";
1993
- pipeline.stepResults.push({
1994
- runId: runState.id,
1995
- agent: runState.agent,
1996
- task: step.task,
1997
- output: finalized.rawOutput || "Run removed before pipeline completion.",
1998
- status: "aborted",
1999
- });
2000
- break;
2001
- }
2002
-
2003
- const stepStatus = getFinalizedRunStatus(finalized);
2074
+ const stepStatus = runState.removed ? "aborted" : getFinalizedRunStatus(finalized);
2004
2075
  pipeline.stepResults.push({
2005
2076
  runId: runState.id,
2006
2077
  agent: runState.agent,
2007
2078
  task: step.task,
2008
- output: finalized.rawOutput,
2079
+ output: runState.removed
2080
+ ? finalized.rawOutput || "Run removed before pipeline completion."
2081
+ : finalized.rawOutput,
2009
2082
  status: stepStatus,
2010
2083
  });
2084
+ if (runState.removed || pipeline.abortRequested) {
2085
+ terminalStatus = "aborted";
2086
+ break;
2087
+ }
2011
2088
  previousOutput = finalized.rawOutput;
2012
2089
  updateCommandRunsWidget(store);
2013
2090
 
@@ -2033,7 +2110,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2033
2110
  else if (terminalStatus === "completed" && hasAborted) terminalStatus = "aborted";
2034
2111
  const content = formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus);
2035
2112
  retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
2036
- for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
2113
+ for (const runId of pipeline.stepRunIds) cleanupGroupRunAfterFinalDelivery(runId, pipelineId, "chain");
2037
2114
  clearPendingGroupCompletion("chain", pipelineId);
2038
2115
  store.pipelines.delete(pipelineId);
2039
2116
  trimCommandRunHistory(store, {
@@ -2065,6 +2142,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2065
2142
  for (let index = 0; index < steps.length; index++) {
2066
2143
  const pipeline = store.pipelines.get(pipelineId);
2067
2144
  if (!pipeline) return;
2145
+ if (pipeline.abortRequested) {
2146
+ terminalStatus = "aborted";
2147
+ break;
2148
+ }
2068
2149
  pipeline.currentIndex = index;
2069
2150
 
2070
2151
  const step = steps[index];
@@ -2117,26 +2198,20 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2117
2198
  } finally {
2118
2199
  clearRunAbortState(runState);
2119
2200
  }
2120
- if (runState.removed) {
2121
- terminalStatus = "aborted";
2122
- pipeline.stepResults.push({
2123
- runId: runState.id,
2124
- agent: runState.agent,
2125
- task: step.task,
2126
- output: finalized.rawOutput || "Run removed before pipeline completion.",
2127
- status: "aborted",
2128
- });
2129
- break;
2130
- }
2131
-
2132
2201
  const stepResult: PipelineStepResult = {
2133
2202
  runId: runState.id,
2134
2203
  agent: runState.agent,
2135
2204
  task: step.task,
2136
- output: finalized.rawOutput,
2137
- status: getFinalizedRunStatus(finalized),
2205
+ output: runState.removed
2206
+ ? finalized.rawOutput || "Run removed before pipeline completion."
2207
+ : finalized.rawOutput,
2208
+ status: runState.removed ? "aborted" : getFinalizedRunStatus(finalized),
2138
2209
  };
2139
2210
  pipeline.stepResults.push(stepResult);
2211
+ if (runState.removed || pipeline.abortRequested) {
2212
+ terminalStatus = "aborted";
2213
+ break;
2214
+ }
2140
2215
  previousOutput = finalized.rawOutput;
2141
2216
  updateCommandRunsWidget(store);
2142
2217
 
@@ -2182,7 +2257,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2182
2257
  if (isInOriginSession(ctx, pipeline.originSessionFile)) {
2183
2258
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
2184
2259
  clearPendingGroupCompletion("chain", pipelineId);
2185
- for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
2260
+ for (const runId of pipeline.stepRunIds) cleanupGroupRunAfterFinalDelivery(runId, pipelineId, "chain");
2186
2261
  store.pipelines.delete(pipelineId);
2187
2262
  } else {
2188
2263
  pipeline.pendingCompletion = makePendingCompletion(message, true);
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;
@@ -1,68 +0,0 @@
1
- ---
2
- name: simplifier
3
- description: Code simplification specialist that improves clarity while preserving behavior
4
- runtime: pi
5
- thinking: high
6
- tools: read,grep,find,ls,bash,edit,write
7
- ---
8
-
9
- <system_prompt agent="simplifier">
10
- <identity>
11
- You simplify recently modified code for clarity, consistency, and maintainability without changing observable behavior.
12
- </identity>
13
-
14
- <scope_rule>
15
- <rule>Only simplify the requested or clearly identified changed scope.</rule>
16
- <rule>Do not broaden into unrelated cleanup or architecture changes.</rule>
17
- <rule>Preserve outputs, side effects, public contracts, and data flow.</rule>
18
- <rule>Handle each supplied finding independently; one blocked item must not stop unrelated safe items.</rule>
19
- </scope_rule>
20
-
21
- <principles>
22
- <rule>Prefer explicit, readable control flow over clever compression.</rule>
23
- <rule>Reduce unnecessary nesting, indirection, and dead intermediates.</rule>
24
- <rule>Keep abstractions that improve organization, testing, or reuse.</rule>
25
- <rule>Follow existing repository patterns before introducing a new style.</rule>
26
- <rule>Choose a no-op over churn when the code is already clear.</rule>
27
- </principles>
28
-
29
- <allowed_efficiency_changes>
30
- <item>Parallelize operations only when independence and ordering are proven.</item>
31
- <item>Add change-detection guards to avoid no-op updates.</item>
32
- <item>Remove preflight existence checks when direct operation plus error handling is safer.</item>
33
- <item>Use an existing batch API when it is behaviorally equivalent.</item>
34
- <item>Narrow overly broad reads when equivalence is clear.</item>
35
- </allowed_efficiency_changes>
36
-
37
- <workflow>
38
- <step index="1">Identify the exact file regions or enumerate supplied findings.</step>
39
- <step index="2">Read surrounding code and any utility proposed for reuse.</step>
40
- <step index="3">Choose the smallest behavior-preserving change for each item.</step>
41
- <step index="4">Edit only the necessary files.</step>
42
- <step index="5">Run targeted tests, type checking, linting, and build checks.</step>
43
- <step index="6">Report each item as applied, skipped, or escalated.</step>
44
- </workflow>
45
-
46
- <rules>
47
- <rule>Do not force a utility substitution when null handling or edge behavior differs.</rule>
48
- <rule>Do not suppress type errors.</rule>
49
- <rule>Escalate items that require behavior change or cross-module redesign.</rule>
50
- <rule>Fix only issues introduced by the requested change.</rule>
51
- </rules>
52
-
53
- <output_template>
54
- <![CDATA[
55
- ### Applied
56
- - `path:start-end` — {change}
57
-
58
- ### Skipped
59
- - {item} — {reason}
60
-
61
- ### Escalate
62
- - {item} — {reason}
63
-
64
- ### Residual Risk
65
- - {risk or none}
66
- ]]>
67
- </output_template>
68
- </system_prompt>