@ryan_nookpi/pi-extension-subagent 0.5.4 → 0.5.6

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
 
package/commands.ts CHANGED
@@ -35,6 +35,12 @@ import {
35
35
  shouldSummarizeSubagentTask,
36
36
  summarizeSubagentDisplayTask,
37
37
  } from "./display-task.js";
38
+ import {
39
+ classifySubagentFailure,
40
+ getSubagentRunStatusLabel,
41
+ getSubagentTerminalLabel,
42
+ isSubagentAbort,
43
+ } from "./failure-telemetry.js";
38
44
  import {
39
45
  AGENT_NAME_PALETTE,
40
46
  agentBgIndex,
@@ -215,10 +221,11 @@ class SubagentHistoryOverlay {
215
221
  const marker = isSelected ? "▸" : " ";
216
222
 
217
223
  // Status color
224
+ const statusLabel = getSubagentRunStatusLabel(run.status, run.errorClass);
218
225
  let statusColor: "success" | "error" | "warning" | "dim" = "dim";
219
- if (run.status === "done") statusColor = "success";
220
- else if (run.status === "error") statusColor = "error";
221
- else if (run.status === "running") statusColor = "warning";
226
+ if (statusLabel === "done") statusColor = "success";
227
+ else if (statusLabel === "error") statusColor = "error";
228
+ else if (statusLabel === "running" || statusLabel === "aborted") statusColor = "warning";
222
229
 
223
230
  const timeLabel = new Date(run.startedAt).toLocaleTimeString([], {
224
231
  hour: "2-digit",
@@ -227,7 +234,7 @@ class SubagentHistoryOverlay {
227
234
  });
228
235
 
229
236
  const removedBadge = run.removed ? theme.fg("dim", " [removed]") : "";
230
- const statusStr = theme.fg(statusColor, `[${run.status}]`);
237
+ const statusStr = theme.fg(statusColor, `[${statusLabel}]`);
231
238
  const agentStr = theme.fg("accent", run.agent);
232
239
  const taskPreview = run.task
233
240
  .replace(/\s*\n+\s*/g, " ")
@@ -395,7 +402,7 @@ function attachRunResponseToEditor(ctx: ExtensionContext, run: CommandRunState,
395
402
  async function showRunLatestResponseOverlay(ctx: ExtensionContext, run: CommandRunState): Promise<void> {
396
403
  const response = getRunLatestResponse(run);
397
404
  const contextLabel = run.contextMode === "main" ? "main" : "sub";
398
- const subtitle = `#${run.id} · ${run.agent} · ${run.status} · ctx:${contextLabel} · turn:${run.turnCount ?? DEFAULT_TURN_COUNT}`;
405
+ const subtitle = `#${run.id} · ${run.agent} · ${getSubagentRunStatusLabel(run.status, run.errorClass)} · ctx:${contextLabel} · turn:${run.turnCount ?? DEFAULT_TURN_COUNT}`;
399
406
 
400
407
  await ctx.ui.custom(
401
408
  (tui, theme, _kb, done) => {
@@ -520,7 +527,11 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
520
527
  try {
521
528
  const entries = ctx.sessionManager.getEntries();
522
529
  const restoredRuns = new Map<number, CommandRunState>();
523
- const removedRunIds = new Set<number>();
530
+ const removedRuns = new Map<
531
+ number,
532
+ { entryIndex: number; startedAt?: number; aborted: boolean; message?: string }
533
+ >();
534
+ const latestRunEntryIndexes = new Map<number, number>();
524
535
  const displayTaskUpdates = new Map<number, { task?: string; displayTask?: string; startedAt?: number }>();
525
536
  let maxRunId = 0;
526
537
 
@@ -541,13 +552,19 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
541
552
  store.currentParentSessionFile = latestParentSessionFile;
542
553
 
543
554
  // First pass: collect removed run IDs and persisted displayTask updates
544
- for (const entry of entries) {
555
+ for (const [entryIndex, entry] of entries.entries()) {
545
556
  if (entry.type === "custom") {
546
557
  const ce = entry as any;
547
558
  if (ce.customType === "subagent-removed") {
548
559
  sawSubagentMarkers = true;
549
560
  if (ce.data?.runId != null) {
550
- removedRunIds.add(ce.data.runId);
561
+ removedRuns.set(ce.data.runId, {
562
+ entryIndex,
563
+ startedAt: toValidTimestampMs(ce.data?.startedAt),
564
+ aborted:
565
+ ce.data?.status === "aborted" || ce.data?.errorClass === "aborted" || ce.data?.stopReason === "aborted",
566
+ message: typeof ce.data?.message === "string" ? ce.data.message : undefined,
567
+ });
551
568
  }
552
569
  continue;
553
570
  }
@@ -562,7 +579,7 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
562
579
  }
563
580
  }
564
581
 
565
- for (const entry of entries) {
582
+ for (const [entryIndex, entry] of entries.entries()) {
566
583
  if (entry.type !== "custom_message") continue;
567
584
  const cm = entry as any;
568
585
  if (cm.customType !== "subagent-command" && cm.customType !== "subagent-tool") continue;
@@ -571,6 +588,7 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
571
588
  if (!d || typeof d.runId !== "number") continue;
572
589
 
573
590
  const runId = d.runId;
591
+ latestRunEntryIndexes.set(runId, entryIndex);
574
592
  if (runId > maxRunId) maxRunId = runId;
575
593
 
576
594
  const existing = restoredRuns.get(runId);
@@ -584,11 +602,12 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
584
602
 
585
603
  // Determine final status primarily from structured metadata.
586
604
  const content = typeof cm.content === "string" ? cm.content : "";
605
+ const firstContentLine = content.split("\n", 1)[0]?.trim() ?? "";
587
606
  const statusRaw = typeof d.status === "string" ? d.status.trim().toLowerCase() : "";
588
607
  const statusFromDetails: "done" | "error" | null =
589
608
  statusRaw === "done" || statusRaw === "completed"
590
609
  ? "done"
591
- : statusRaw === "error" || statusRaw === "failed"
610
+ : statusRaw === "error" || statusRaw === "failed" || statusRaw === "aborted"
592
611
  ? "error"
593
612
  : null;
594
613
  const statusFromExitCode: "done" | "error" | null =
@@ -597,13 +616,25 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
597
616
  typeof d.error === "string" && d.error.trim() ? "error" : null;
598
617
 
599
618
  // Legacy fallback for old sessions where structured fields are missing.
600
- const legacyStatusFromContent: "done" | "error" | null = content.includes("] completed")
619
+ const legacyStatusFromContent: "done" | "error" | null = firstContentLine.includes("] completed")
601
620
  ? "done"
602
- : content.includes("] failed") || content.includes("] error")
621
+ : firstContentLine.includes("] failed") ||
622
+ firstContentLine.includes("] error") ||
623
+ firstContentLine.includes("] aborted")
603
624
  ? "error"
604
625
  : null;
605
626
 
606
627
  const finalStatus = statusFromDetails ?? statusFromExitCode ?? statusFromErrorField ?? legacyStatusFromContent;
628
+ const hasStructuredTerminalOutcome =
629
+ statusFromDetails !== null ||
630
+ statusFromExitCode !== null ||
631
+ typeof d.stopReason === "string" ||
632
+ typeof d.errorClass === "string";
633
+ const isAbortedCompletion =
634
+ d.stopReason === "aborted" ||
635
+ d.errorClass === "aborted" ||
636
+ statusRaw === "aborted" ||
637
+ (!hasStructuredTerminalOutcome && firstContentLine.includes("] aborted"));
607
638
 
608
639
  // Derive source from customType for backward-compatible restored run metadata.
609
640
  const restoredSource: "tool" | "command" = cm.customType === "subagent-tool" ? "tool" : "command";
@@ -648,7 +679,7 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
648
679
  contextMode: d.contextMode ?? existing?.contextMode,
649
680
  usage: d.usage ?? existing?.usage,
650
681
  model: d.model ?? existing?.model,
651
- errorClass: d.errorClass ?? existing?.errorClass,
682
+ errorClass: d.errorClass ?? (isAbortedCompletion ? "aborted" : existing?.errorClass),
652
683
  peakContextTokens: d.peakContextTokens ?? existing?.peakContextTokens,
653
684
  lastToolName: d.lastToolName ?? existing?.lastToolName,
654
685
  lastToolOutputChars: d.lastToolOutputChars ?? existing?.lastToolOutputChars,
@@ -723,8 +754,26 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
723
754
  }
724
755
 
725
756
  for (const [id, run] of restoredRuns) {
726
- if (removedRunIds.has(id)) {
727
- store.commandRuns.set(id, { ...run, removed: true }); // restore removed runs while preserving removed=true
757
+ const removal = removedRuns.get(id);
758
+ const latestRunEntryIndex = latestRunEntryIndexes.get(id) ?? -1;
759
+ const removalApplies = removal
760
+ ? removal.startedAt !== undefined
761
+ ? removal.startedAt === run.startedAt
762
+ : removal.entryIndex > latestRunEntryIndex
763
+ : false;
764
+ if (removal && removalApplies) {
765
+ store.commandRuns.set(id, {
766
+ ...run,
767
+ ...(removal.aborted
768
+ ? {
769
+ status: "error" as const,
770
+ errorClass: "aborted" as const,
771
+ lastLine: removal.message ?? run.lastLine,
772
+ lastOutput: removal.message ?? run.lastOutput,
773
+ }
774
+ : {}),
775
+ removed: true,
776
+ });
728
777
  continue;
729
778
  }
730
779
  store.commandRuns.set(id, run);
@@ -1107,6 +1156,13 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1107
1156
  const targetRunId = Number(firstToken);
1108
1157
  const targetRun = continuationRun;
1109
1158
 
1159
+ if (targetRun.abortPending) {
1160
+ ctx.ui.notify(
1161
+ `Subagent #${targetRunId} is still aborting. Wait for cancellation to settle before continuing it.`,
1162
+ "warning",
1163
+ );
1164
+ return;
1165
+ }
1110
1166
  if (targetRun.status === "running") {
1111
1167
  ctx.ui.notify(`Subagent #${targetRunId} is already running.`, "warning");
1112
1168
  return;
@@ -1274,6 +1330,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1274
1330
  runState.lastRetryReason = undefined;
1275
1331
  runState.errorClass = undefined;
1276
1332
  runState.autoAbortReason = undefined;
1333
+ runState.abortPending = false;
1277
1334
  runState.removed = false;
1278
1335
  runState.deliveryMode = deliveryMode;
1279
1336
  runState.turnCount = Math.max(DEFAULT_TURN_COUNT, runState.turnCount || DEFAULT_TURN_COUNT) + 1;
@@ -1509,6 +1566,22 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1509
1566
  updateRunFromResult(runState, result);
1510
1567
  const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
1511
1568
  runState.status = isError ? "error" : "done";
1569
+ if (isError) {
1570
+ runState.errorClass =
1571
+ result.errorClass ??
1572
+ classifySubagentFailure({
1573
+ failed: true,
1574
+ stopReason: result.stopReason,
1575
+ exitCode: result.exitCode,
1576
+ errorMessage: result.errorMessage,
1577
+ stderr: result.stderr,
1578
+ output: getFinalOutput(result.messages),
1579
+ });
1580
+ }
1581
+ const terminalLabel = getSubagentTerminalLabel(isError, {
1582
+ stopReason: result.stopReason,
1583
+ errorClass: runState.errorClass,
1584
+ });
1512
1585
  runState.elapsedMs = Date.now() - runState.startedAt;
1513
1586
  updateCommandRunsWidget(store);
1514
1587
 
@@ -1527,7 +1600,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1527
1600
  const completionMessage = {
1528
1601
  customType: "subagent-command" as const,
1529
1602
  content:
1530
- `[subagent:${selectedAgent}#${runId}] ${isError ? "failed" : "completed"}` +
1603
+ `[subagent:${selectedAgent}#${runId}] ${terminalLabel}` +
1531
1604
  `\nPrompt: ${truncateLines(taskForDisplay, 2)}` +
1532
1605
  (usage ? `\nUsage: ${usage}` : "") +
1533
1606
  (runState.retryCount ? `\nRetries: ${runState.retryCount}/${MAX_SUBAGENT_AUTO_RETRIES}` : "") +
@@ -1548,6 +1621,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1548
1621
  elapsedMs: runState.elapsedMs,
1549
1622
  lastActivityAt: runState.lastActivityAt,
1550
1623
  exitCode: result.exitCode,
1624
+ stopReason: result.stopReason,
1551
1625
  usage: result.usage,
1552
1626
  model: result.model,
1553
1627
  source: result.agentSource,
@@ -1570,24 +1644,27 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1570
1644
  runId,
1571
1645
  `Hidden subagent #${runId} · ${selectedAgent}`,
1572
1646
  completionMessage.content,
1573
- isError
1574
- ? `hidden subagent #${runId} (${selectedAgent}) failed`
1575
- : `hidden subagent #${runId} (${selectedAgent}) completed`,
1576
- isError ? "error" : "info",
1647
+ `hidden subagent #${runId} (${selectedAgent}) ${terminalLabel}`,
1648
+ terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
1577
1649
  );
1578
1650
  } else {
1579
1651
  deliverOrQueueCompletion(store, pi, ctx, runId, runState, completionMessage);
1580
1652
  ctx.ui.notify(
1581
- isError
1582
- ? `subagent #${runId} (${selectedAgent}) failed`
1583
- : `subagent #${runId} (${selectedAgent}) completed`,
1584
- isError ? "error" : "info",
1653
+ `subagent #${runId} (${selectedAgent}) ${terminalLabel}`,
1654
+ terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
1585
1655
  );
1586
1656
  }
1587
1657
  } catch (error: any) {
1588
1658
  if (runState.removed || store.disposed) return;
1589
1659
  runState.status = "error";
1590
- runState.errorClass = "process_error";
1660
+ runState.errorClass = isSubagentAbort({
1661
+ signal: runState.abortController?.signal,
1662
+ autoAbortReason: runState.autoAbortReason,
1663
+ error,
1664
+ })
1665
+ ? "aborted"
1666
+ : "process_error";
1667
+ const terminalLabel = getSubagentTerminalLabel(true, { errorClass: runState.errorClass });
1591
1668
  runState.elapsedMs = Date.now() - runState.startedAt;
1592
1669
  runState.lastLine =
1593
1670
  runState.autoAbortReason ?? (error?.message ? String(error.message) : "Subagent execution failed");
@@ -1596,7 +1673,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1596
1673
  const cmdErrorMessage = {
1597
1674
  customType: "subagent-command" as const,
1598
1675
  content:
1599
- `[subagent:${selectedAgent}#${runId}] failed` +
1676
+ `[subagent:${selectedAgent}#${runId}] ${terminalLabel}` +
1600
1677
  `\nPrompt: ${truncateLines(taskForDisplay, 2)}` +
1601
1678
  `\n\n${runState.lastLine}`,
1602
1679
  display: true,
@@ -1615,6 +1692,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1615
1692
  lastActivityAt: runState.lastActivityAt,
1616
1693
  error: runState.lastLine,
1617
1694
  errorClass: runState.errorClass,
1695
+ stopReason: runState.errorClass === "aborted" ? "aborted" : undefined,
1618
1696
  peakContextTokens: runState.peakContextTokens,
1619
1697
  lastToolName: runState.lastToolName,
1620
1698
  lastToolOutputChars: runState.lastToolOutputChars,
@@ -1632,16 +1710,20 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1632
1710
  runId,
1633
1711
  `Hidden subagent #${runId} · ${selectedAgent}`,
1634
1712
  cmdErrorMessage.content,
1635
- `hidden subagent #${runId} failed: ${runState.lastLine}`,
1636
- "error",
1713
+ `hidden subagent #${runId} ${terminalLabel}: ${runState.lastLine}`,
1714
+ terminalLabel === "aborted" ? "warning" : "error",
1637
1715
  );
1638
1716
  } else {
1639
1717
  deliverOrQueueCompletion(store, pi, ctx, runId, runState, cmdErrorMessage);
1640
- ctx.ui.notify(`subagent #${runId} failed: ${runState.lastLine}`, "error");
1718
+ ctx.ui.notify(
1719
+ `subagent #${runId} ${terminalLabel}: ${runState.lastLine}`,
1720
+ terminalLabel === "aborted" ? "warning" : "error",
1721
+ );
1641
1722
  }
1642
1723
  } finally {
1643
1724
  clearInterval(tick);
1644
1725
  runState.abortController = undefined;
1726
+ runState.abortPending = false;
1645
1727
  if (!store.disposed) {
1646
1728
  trimCommandRunHistory(store, {
1647
1729
  maxRuns: 10,
@@ -1783,7 +1865,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1783
1865
  run.status === "running" ? "(still running; no final output yet)" : run.lastLine || "(no output captured)";
1784
1866
  const contextLabel = run.contextMode === "main" ? "main" : "isolated";
1785
1867
  const content =
1786
- `Subagent #${run.id} [${run.status}] ${run.agent} ctx:${contextLabel} turn:${run.turnCount ?? DEFAULT_TURN_COUNT} ${elapsedSec}s tools:${run.toolCalls}` +
1868
+ `Subagent #${run.id} [${getSubagentRunStatusLabel(run.status, run.errorClass)}] ${run.agent} ctx:${contextLabel} turn:${run.turnCount ?? DEFAULT_TURN_COUNT} ${elapsedSec}s tools:${run.toolCalls}` +
1787
1869
  `\n${run.task}` +
1788
1870
  usageLine +
1789
1871
  `\n\n${output || fallback}`;
@@ -1840,7 +1922,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1840
1922
  .replace(/\s*\n+\s*/g, " ")
1841
1923
  .trim()
1842
1924
  .slice(0, COMMAND_TASK_PREVIEW_CHARS);
1843
- return `#${r.id} [${r.status}]${removed} ${r.agent}: ${task}`;
1925
+ return `#${r.id} [${getSubagentRunStatusLabel(r.status, r.errorClass)}]${removed} ${r.agent}: ${task}`;
1844
1926
  });
1845
1927
  ctx.ui.notify(lines.join("\n"), "info");
1846
1928
  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";
@@ -18,6 +19,36 @@ export interface FailureTelemetryInput {
18
19
  output?: string;
19
20
  }
20
21
 
22
+ export interface AbortTelemetryInput {
23
+ stopReason?: string;
24
+ errorClass?: SubagentErrorClass;
25
+ signal?: AbortSignal;
26
+ autoAbortReason?: string;
27
+ error?: unknown;
28
+ }
29
+
30
+ export function isSubagentAbort(input: AbortTelemetryInput): boolean {
31
+ return (
32
+ input.stopReason === "aborted" ||
33
+ input.errorClass === "aborted" ||
34
+ input.signal?.aborted === true ||
35
+ Boolean(input.autoAbortReason) ||
36
+ (input.error instanceof Error && input.error.message === "Subagent was aborted")
37
+ );
38
+ }
39
+
40
+ export function getSubagentTerminalLabel(
41
+ failed: boolean,
42
+ input: AbortTelemetryInput,
43
+ ): "completed" | "aborted" | "failed" {
44
+ if (!failed) return "completed";
45
+ return isSubagentAbort(input) ? "aborted" : "failed";
46
+ }
47
+
48
+ export function getSubagentRunStatusLabel(status: string, errorClass?: SubagentErrorClass): string {
49
+ return errorClass === "aborted" ? "aborted" : status;
50
+ }
51
+
21
52
  const OVERLOADED_PATTERNS = [
22
53
  /servers? (?:are|is) currently overloaded/i,
23
54
  /server overloaded/i,
@@ -34,15 +65,26 @@ const TOOL_ERROR_PATTERNS = [
34
65
  /invalid tool/i,
35
66
  ];
36
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
+
37
71
  export function classifySubagentFailure(input: FailureTelemetryInput): SubagentErrorClass | undefined {
38
72
  if (!input.failed) return undefined;
39
73
  const text = [input.errorMessage, input.stderr, input.output].filter(Boolean).join("\n");
40
74
 
75
+ if (input.stopReason === "aborted") return "aborted";
41
76
  if (isContextOverflowText(text)) return "context_overflow";
42
77
  if (OVERLOADED_PATTERNS.some((pattern) => pattern.test(text))) return "overloaded";
43
78
  if (RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(text))) return "rate_limit";
44
- if (input.stopReason === "aborted") return "aborted";
45
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
+ }
46
88
  if ((input.exitCode ?? 0) !== 0) return "process_error";
47
89
  return "unknown";
48
90
  }
package/lifecycle.ts CHANGED
@@ -36,6 +36,7 @@ function reconcileRunWithPersistedSession(run: CommandRunState): void {
36
36
  snapshot.completionMarker?.exitCode ??
37
37
  (snapshot.terminalStopReason === "error" || snapshot.terminalStopReason === "aborted" ? 1 : 0);
38
38
  run.status = exitCode === 0 ? "done" : "error";
39
+ if (snapshot.terminalStopReason === "aborted") run.errorClass = "aborted";
39
40
  if (snapshot.finalOutput) {
40
41
  run.lastOutput = snapshot.finalOutput;
41
42
  run.lastLine = getLastNonEmptyLine(snapshot.finalOutput) || run.lastLine;
@@ -90,7 +91,7 @@ export function shutdownSubagentRuns(store: SubagentStore, pi: ExtensionAPI, rea
90
91
  pi.sendMessage(
91
92
  {
92
93
  customType: run.source === "tool" ? "subagent-tool" : "subagent-command",
93
- content: `[subagent:${run.agent}#${runId}] failed\n\n${message}`,
94
+ content: `[subagent:${run.agent}#${runId}] aborted\n\n${message}`,
94
95
  display: false,
95
96
  details: {
96
97
  runId,
@@ -100,6 +101,7 @@ export function shutdownSubagentRuns(store: SubagentStore, pi: ExtensionAPI, rea
100
101
  status: "error",
101
102
  error: message,
102
103
  errorClass: run.errorClass,
104
+ stopReason: "aborted",
103
105
  peakContextTokens: run.peakContextTokens,
104
106
  lastToolName: run.lastToolName,
105
107
  lastToolOutputChars: run.lastToolOutputChars,
@@ -155,7 +157,9 @@ export function checkForHungRuns(store: SubagentStore, _pi: ExtensionAPI): void
155
157
  run.lastLine = reason;
156
158
  run.lastOutput = reason;
157
159
  run.status = "error";
160
+ run.errorClass = "aborted";
158
161
  run.autoAbortReason = reason;
162
+ run.abortPending = Boolean(controller);
159
163
 
160
164
  if (controller) {
161
165
  controller.abort({ source: "hang_timeout", runId, idleMs, reason });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/replay.ts CHANGED
@@ -44,6 +44,7 @@ import {
44
44
  TOOL_RESULT_DETAILS_SUMMARY_MAX_CHARS,
45
45
  USAGE_EXTRA_ROWS,
46
46
  } from "./constants.js";
47
+ import { getSubagentRunStatusLabel } from "./failure-telemetry.js";
47
48
  import { formatUsageStats, truncatePlainToWidth } from "./format.js";
48
49
  import type { CommandRunState, SessionReplayItem } from "./types.js";
49
50
  import { formatDuration, formatDurationBetween } from "./utils/time-utils.js";
@@ -351,7 +352,7 @@ export class SubagentSessionReplayOverlay {
351
352
  theme.fg("toolTitle", theme.bold(`#${this.run.id} ${this.run.agent}`)) +
352
353
  theme.fg(
353
354
  "dim",
354
- ` [${this.run.status}] ctx:${contextLabel} turn:${this.run.turnCount ?? DEFAULT_TURN_COUNT} ${elapsedLabel} tools:${this.run.toolCalls}`,
355
+ ` [${getSubagentRunStatusLabel(this.run.status, this.run.errorClass)}] ctx:${contextLabel} turn:${this.run.turnCount ?? DEFAULT_TURN_COUNT} ${elapsedLabel} tools:${this.run.toolCalls}`,
355
356
  ),
356
357
  0,
357
358
  0,
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
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
10
  import { FINISHED_GROUP_TTL_MS, MAX_FINISHED_GROUPS, STATUS_OUTPUT_PREVIEW_MAX_CHARS } from "./constants.js";
11
+ import { getSubagentRunStatusLabel } from "./failure-telemetry.js";
11
12
  import type { SubagentStore } from "./store.js";
12
13
  import type {
13
14
  BatchGroupState,
@@ -57,7 +58,7 @@ export interface ClearFinishedRunsOptions {
57
58
  export function formatCommandRunSummary(run: CommandRunState): string {
58
59
  const elapsedSec = Math.max(0, Math.round(run.elapsedMs / 1000));
59
60
  const contextLabel = run.contextMode === "main" ? "main" : "isolated";
60
- return `#${run.id} [${run.status}] ${run.agent} ctx:${contextLabel} turn:${run.turnCount ?? 1} ${elapsedSec}s tools:${run.toolCalls}`;
61
+ return `#${run.id} [${getSubagentRunStatusLabel(run.status, run.errorClass)}] ${run.agent} ctx:${contextLabel} turn:${run.turnCount ?? 1} ${elapsedSec}s tools:${run.toolCalls}`;
61
62
  }
62
63
 
63
64
  /**
@@ -95,12 +96,19 @@ export function removeRun(store: SubagentStore, runId: number, options: RemoveRu
95
96
  const globalEntry = store.globalLiveRuns.get(runId);
96
97
  const controller = run.abortController ?? globalEntry?.abortController;
97
98
 
98
- if (abortIfRunning && run.status === "running" && controller) {
99
+ if (abortIfRunning && run.status === "running") {
99
100
  const reason = options.reason ?? "Aborting by remove...";
101
+ run.status = "error";
102
+ run.errorClass = "aborted";
103
+ run.elapsedMs = Date.now() - run.startedAt;
104
+ run.lastActivityAt = Date.now();
100
105
  run.lastLine = reason;
101
106
  run.lastOutput = reason;
102
- controller.abort({ source: "remove_run", runId, reason, removalReason: options.removalReason });
103
- aborted = true;
107
+ run.abortPending = Boolean(controller);
108
+ if (controller) {
109
+ controller.abort({ source: "remove_run", runId, reason, removalReason: options.removalReason });
110
+ aborted = true;
111
+ }
104
112
  }
105
113
 
106
114
  run.abortController = undefined;
@@ -112,6 +120,13 @@ export function removeRun(store: SubagentStore, runId: number, options: RemoveRu
112
120
  if (persistRemovedEntry && options.pi && run.deliveryMode !== "humanOnly") {
113
121
  const payload: Record<string, unknown> = { runId };
114
122
  if (options.removalReason) payload.reason = options.removalReason;
123
+ if (run.errorClass === "aborted") {
124
+ payload.startedAt = run.startedAt;
125
+ payload.status = "aborted";
126
+ payload.errorClass = "aborted";
127
+ payload.stopReason = "aborted";
128
+ payload.message = run.lastLine;
129
+ }
115
130
  try {
116
131
  options.pi.appendEntry("subagent-removed", payload);
117
132
  } catch {
@@ -225,16 +240,19 @@ export function snapshotBatchGroup(
225
240
  terminalStatus: FinishedGroupSnapshot["terminalStatus"],
226
241
  ): FinishedGroupSnapshot {
227
242
  let failed = 0;
243
+ let aborted = 0;
228
244
  const members: FinishedGroupMember[] = batch.runIds.map((runId) => {
229
245
  const run = store.commandRuns.get(runId);
230
246
  if (!run) {
231
- if (batch.failedRunIds.has(runId)) failed++;
247
+ if (batch.abortedRunIds.has(runId)) aborted++;
248
+ else if (batch.failedRunIds.has(runId)) failed++;
232
249
  return {
233
250
  summaryLine: `#${runId} [gone] (run no longer available)`,
234
251
  output: batch.pendingResults.get(runId)?.trim() || "(no output)",
235
252
  };
236
253
  }
237
- if (run.status === "error" || batch.failedRunIds.has(runId)) failed++;
254
+ if (run.errorClass === "aborted" || batch.abortedRunIds.has(runId)) aborted++;
255
+ else if (run.status === "error" || batch.failedRunIds.has(runId)) failed++;
238
256
  return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run, batch.pendingResults.get(runId)) };
239
257
  });
240
258
  return {
@@ -244,6 +262,7 @@ export function snapshotBatchGroup(
244
262
  finishedAt: Date.now(),
245
263
  total: batch.runIds.length,
246
264
  failed,
265
+ aborted,
247
266
  members,
248
267
  };
249
268
  }
@@ -265,6 +284,7 @@ export function snapshotPipeline(
265
284
  finishedAt: Date.now(),
266
285
  total: pipeline.stepResults.length,
267
286
  failed: pipeline.stepResults.filter((step) => step.status === "error").length,
287
+ aborted: pipeline.stepResults.filter((step) => step.status === "aborted").length,
268
288
  members,
269
289
  };
270
290
  }
@@ -304,9 +324,10 @@ function formatFinishedAge(finishedAt: number, now = Date.now()): string {
304
324
  export function formatFinishedGroupStatus(snapshot: FinishedGroupSnapshot, detailed: boolean): string {
305
325
  const label = snapshot.kind === "batch" ? "subagent-batch" : "subagent-chain";
306
326
  const failedSuffix = snapshot.failed > 0 ? `, ${snapshot.failed} failed` : "";
327
+ const abortedSuffix = snapshot.aborted > 0 ? `, ${snapshot.aborted} aborted` : "";
307
328
  const header = `[${label}#${snapshot.groupId}] ${snapshot.terminalStatus} · ${snapshot.total} ${
308
329
  snapshot.kind === "batch" ? "runs" : "steps"
309
- }${failedSuffix} · finished ${formatFinishedAge(snapshot.finishedAt)}`;
330
+ }${failedSuffix}${abortedSuffix} · finished ${formatFinishedAge(snapshot.finishedAt)}`;
310
331
  const body = snapshot.members
311
332
  .map((member) => {
312
333
  if (!detailed) return member.summaryLine;
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