@ryan_nookpi/pi-extension-subagent 0.5.3 → 0.5.5
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/commands.ts +113 -31
- package/failure-telemetry.ts +31 -1
- package/lifecycle.ts +5 -1
- package/package.json +1 -1
- package/replay.ts +2 -1
- package/run-utils.ts +28 -7
- package/tool-execute.ts +158 -63
- package/types.ts +6 -2
- package/widget.ts +6 -2
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 (
|
|
220
|
-
else if (
|
|
221
|
-
else if (
|
|
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, `[${
|
|
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
|
|
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
|
-
|
|
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 =
|
|
619
|
+
const legacyStatusFromContent: "done" | "error" | null = firstContentLine.includes("] completed")
|
|
601
620
|
? "done"
|
|
602
|
-
:
|
|
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
|
-
|
|
727
|
-
|
|
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}] ${
|
|
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
|
-
|
|
1574
|
-
|
|
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
|
-
|
|
1582
|
-
|
|
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 =
|
|
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}]
|
|
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}
|
|
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(
|
|
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;
|
package/failure-telemetry.ts
CHANGED
|
@@ -18,6 +18,36 @@ export interface FailureTelemetryInput {
|
|
|
18
18
|
output?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export interface AbortTelemetryInput {
|
|
22
|
+
stopReason?: string;
|
|
23
|
+
errorClass?: SubagentErrorClass;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
autoAbortReason?: string;
|
|
26
|
+
error?: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isSubagentAbort(input: AbortTelemetryInput): boolean {
|
|
30
|
+
return (
|
|
31
|
+
input.stopReason === "aborted" ||
|
|
32
|
+
input.errorClass === "aborted" ||
|
|
33
|
+
input.signal?.aborted === true ||
|
|
34
|
+
Boolean(input.autoAbortReason) ||
|
|
35
|
+
(input.error instanceof Error && input.error.message === "Subagent was aborted")
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getSubagentTerminalLabel(
|
|
40
|
+
failed: boolean,
|
|
41
|
+
input: AbortTelemetryInput,
|
|
42
|
+
): "completed" | "aborted" | "failed" {
|
|
43
|
+
if (!failed) return "completed";
|
|
44
|
+
return isSubagentAbort(input) ? "aborted" : "failed";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getSubagentRunStatusLabel(status: string, errorClass?: SubagentErrorClass): string {
|
|
48
|
+
return errorClass === "aborted" ? "aborted" : status;
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
const OVERLOADED_PATTERNS = [
|
|
22
52
|
/servers? (?:are|is) currently overloaded/i,
|
|
23
53
|
/server overloaded/i,
|
|
@@ -38,10 +68,10 @@ export function classifySubagentFailure(input: FailureTelemetryInput): SubagentE
|
|
|
38
68
|
if (!input.failed) return undefined;
|
|
39
69
|
const text = [input.errorMessage, input.stderr, input.output].filter(Boolean).join("\n");
|
|
40
70
|
|
|
71
|
+
if (input.stopReason === "aborted") return "aborted";
|
|
41
72
|
if (isContextOverflowText(text)) return "context_overflow";
|
|
42
73
|
if (OVERLOADED_PATTERNS.some((pattern) => pattern.test(text))) return "overloaded";
|
|
43
74
|
if (RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(text))) return "rate_limit";
|
|
44
|
-
if (input.stopReason === "aborted") return "aborted";
|
|
45
75
|
if (TOOL_ERROR_PATTERNS.some((pattern) => pattern.test(text))) return "tool_error";
|
|
46
76
|
if ((input.exitCode ?? 0) !== 0) return "process_error";
|
|
47
77
|
return "unknown";
|
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}]
|
|
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
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/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"
|
|
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
|
-
|
|
103
|
-
|
|
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.
|
|
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.
|
|
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/tool-execute.ts
CHANGED
|
@@ -32,7 +32,13 @@ import {
|
|
|
32
32
|
summarizeSubagentDisplayTask,
|
|
33
33
|
} from "./display-task.js";
|
|
34
34
|
import { ESCALATION_EXIT_CODE, readAndConsumeEscalation } from "./escalation.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
classifySubagentFailure,
|
|
37
|
+
getSubagentRunStatusLabel,
|
|
38
|
+
getSubagentTerminalLabel,
|
|
39
|
+
isSubagentAbort,
|
|
40
|
+
type SubagentErrorClass,
|
|
41
|
+
} from "./failure-telemetry.js";
|
|
36
42
|
import {
|
|
37
43
|
formatContextUsageBar,
|
|
38
44
|
formatUsageStats,
|
|
@@ -299,6 +305,9 @@ export function diagnoseResultFailure(result: SingleResult): ResultFailureDiagno
|
|
|
299
305
|
output: finalOutput,
|
|
300
306
|
});
|
|
301
307
|
|
|
308
|
+
if (result.stopReason === "aborted") {
|
|
309
|
+
return { failed: true, errorClass: "aborted", reason: "Subagent execution was aborted." };
|
|
310
|
+
}
|
|
302
311
|
if (result.exitCode !== 0 || result.stopReason === "error") {
|
|
303
312
|
const overflowText = result.errorMessage || result.stderr || finalOutput;
|
|
304
313
|
if (isContextOverflowText(overflowText)) {
|
|
@@ -324,8 +333,6 @@ export function diagnoseResultFailure(result: SingleResult): ResultFailureDiagno
|
|
|
324
333
|
}
|
|
325
334
|
if (result.stopReason === "error")
|
|
326
335
|
return { failed: true, errorClass, reason: result.errorMessage || "Subagent reported stopReason=error." };
|
|
327
|
-
if (result.stopReason === "aborted")
|
|
328
|
-
return { failed: true, errorClass: errorClass ?? "aborted", reason: "Subagent execution was aborted." };
|
|
329
336
|
|
|
330
337
|
const hasAssistantText = finalOutput.length > 0;
|
|
331
338
|
if (hasAssistantText) return { failed: false };
|
|
@@ -469,13 +476,42 @@ function buildRunStartMessage(runState: CommandRunState, status: "started" | "re
|
|
|
469
476
|
};
|
|
470
477
|
}
|
|
471
478
|
|
|
479
|
+
function getFinalizedRunLabel(finalized: FinalizedRun): "completed" | "aborted" | "failed" {
|
|
480
|
+
return getSubagentTerminalLabel(finalized.isError, {
|
|
481
|
+
stopReason: finalized.result?.stopReason,
|
|
482
|
+
errorClass: finalized.runState.errorClass,
|
|
483
|
+
autoAbortReason: finalized.runState.autoAbortReason,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function getFinalizedRunStatus(finalized: FinalizedRun): PipelineStepResult["status"] {
|
|
488
|
+
const label = getFinalizedRunLabel(finalized);
|
|
489
|
+
return label === "completed" ? "done" : label === "aborted" ? "aborted" : "error";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function resolveGroupTerminalStatus(failed: number, aborted: number): "completed" | "error" | "aborted" {
|
|
493
|
+
if (failed > 0) return "error";
|
|
494
|
+
if (aborted > 0) return "aborted";
|
|
495
|
+
return "completed";
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function recordBatchResult(batch: BatchGroupState, finalized: FinalizedRun): void {
|
|
499
|
+
const runId = finalized.runState.id;
|
|
500
|
+
const status = finalized.runState.removed ? "aborted" : getFinalizedRunStatus(finalized);
|
|
501
|
+
batch.completedRunIds.add(runId);
|
|
502
|
+
if (status === "aborted") batch.abortedRunIds.add(runId);
|
|
503
|
+
else if (status === "error") batch.failedRunIds.add(runId);
|
|
504
|
+
batch.pendingResults.set(runId, finalized.rawOutput);
|
|
505
|
+
}
|
|
506
|
+
|
|
472
507
|
function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?: boolean }) {
|
|
473
|
-
const { runState, result,
|
|
508
|
+
const { runState, result, rawOutput } = finalized;
|
|
474
509
|
const usage = result ? formatUsageStats(result.usage, result.model) : "";
|
|
510
|
+
const terminalLabel = getFinalizedRunLabel(finalized);
|
|
475
511
|
return {
|
|
476
512
|
customType: "subagent-tool" as const,
|
|
477
513
|
content:
|
|
478
|
-
`[subagent:${runState.agent}#${runState.id}] ${
|
|
514
|
+
`[subagent:${runState.agent}#${runState.id}] ${terminalLabel}` +
|
|
479
515
|
`\nPrompt: ${truncateLines(runState.task, 2)}` +
|
|
480
516
|
(usage ? `\nUsage: ${usage}` : "") +
|
|
481
517
|
(runState.thoughtText ? `\nThought: ${runState.thoughtText}` : "") +
|
|
@@ -495,6 +531,7 @@ function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?
|
|
|
495
531
|
elapsedMs: runState.elapsedMs,
|
|
496
532
|
lastActivityAt: runState.lastActivityAt,
|
|
497
533
|
exitCode: result?.exitCode,
|
|
534
|
+
stopReason: result?.stopReason ?? (runState.errorClass === "aborted" ? "aborted" : undefined),
|
|
498
535
|
usage: result?.usage,
|
|
499
536
|
model: result?.model,
|
|
500
537
|
source: result?.agentSource,
|
|
@@ -560,6 +597,7 @@ function finalizeRunState(runState: CommandRunState, result: SingleResult): Fina
|
|
|
560
597
|
|
|
561
598
|
if (runState.autoAbortReason) {
|
|
562
599
|
runState.status = "error";
|
|
600
|
+
runState.errorClass = "aborted";
|
|
563
601
|
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
564
602
|
runState.lastOutput = runState.autoAbortReason;
|
|
565
603
|
runState.lastLine = runState.autoAbortReason;
|
|
@@ -606,29 +644,46 @@ function finalizeRunState(runState: CommandRunState, result: SingleResult): Fina
|
|
|
606
644
|
return { runState, result, isError, rawOutput };
|
|
607
645
|
}
|
|
608
646
|
|
|
609
|
-
function
|
|
610
|
-
|
|
647
|
+
function formatOutcomeCounts(failed: number, aborted: number): string {
|
|
648
|
+
return [failed > 0 ? `${failed} failed` : "", aborted > 0 ? `${aborted} aborted` : ""].filter(Boolean).join(", ");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function formatBatchSummary(
|
|
652
|
+
batchId: string,
|
|
653
|
+
runs: CommandRunState[],
|
|
654
|
+
terminalStatus: "completed" | "error" | "aborted",
|
|
655
|
+
): string {
|
|
656
|
+
const statuses = runs.map((run) => getSubagentRunStatusLabel(run.status, run.errorClass));
|
|
657
|
+
const headerStatus = runs.map((run, index) => `#${run.id} ${statuses[index]}`).join(", ");
|
|
658
|
+
const outcomeCounts = formatOutcomeCounts(
|
|
659
|
+
statuses.filter((status) => status === "error").length,
|
|
660
|
+
statuses.filter((status) => status === "aborted").length,
|
|
661
|
+
);
|
|
611
662
|
const body = runs
|
|
612
663
|
.map((run) => {
|
|
613
664
|
const summary = run.lastOutput?.trim() || run.lastLine?.trim() || "(no output)";
|
|
614
665
|
return `#${run.id} ${run.agent}\n- ${summary}`;
|
|
615
666
|
})
|
|
616
667
|
.join("\n\n");
|
|
617
|
-
return `[subagent-batch#${batchId}] ${terminalStatus}\nRuns: ${headerStatus}\n\n${body}`;
|
|
668
|
+
return `[subagent-batch#${batchId}] ${terminalStatus}\nRuns: ${headerStatus}${outcomeCounts ? `\nOutcomes: ${outcomeCounts}` : ""}\n\n${body}`;
|
|
618
669
|
}
|
|
619
670
|
|
|
620
671
|
function formatPipelineSummary(
|
|
621
672
|
pipelineId: string,
|
|
622
673
|
stepResults: PipelineStepResult[],
|
|
623
|
-
terminalStatus: "completed" | "stopped" | "error",
|
|
674
|
+
terminalStatus: "completed" | "stopped" | "error" | "aborted",
|
|
624
675
|
): string {
|
|
676
|
+
const outcomeCounts = formatOutcomeCounts(
|
|
677
|
+
stepResults.filter((step) => step.status === "error").length,
|
|
678
|
+
stepResults.filter((step) => step.status === "aborted").length,
|
|
679
|
+
);
|
|
625
680
|
const steps = stepResults
|
|
626
681
|
.map(
|
|
627
682
|
(step, index) =>
|
|
628
683
|
`Step ${index + 1} · #${step.runId} ${step.agent} · ${step.status}\nTask: ${step.task}\n${step.output}`,
|
|
629
684
|
)
|
|
630
685
|
.join("\n\n");
|
|
631
|
-
return `[subagent-chain#${pipelineId}] ${terminalStatus}\n\n${steps}`;
|
|
686
|
+
return `[subagent-chain#${pipelineId}] ${terminalStatus}${outcomeCounts ? `\nOutcomes: ${outcomeCounts}` : ""}\n\n${steps}`;
|
|
632
687
|
}
|
|
633
688
|
|
|
634
689
|
function previewOutput(output: string): string {
|
|
@@ -642,9 +697,10 @@ function formatBatchGroupStatus(store: SubagentStore, batch: BatchGroupState, de
|
|
|
642
697
|
const total = batch.runIds.length;
|
|
643
698
|
const done = batch.completedRunIds.size;
|
|
644
699
|
const failed = batch.failedRunIds.size;
|
|
700
|
+
const aborted = batch.abortedRunIds.size;
|
|
645
701
|
const header = `[subagent-batch#${batch.batchId}] running · ${done}/${total} finished${
|
|
646
702
|
failed > 0 ? `, ${failed} failed` : ""
|
|
647
|
-
}`;
|
|
703
|
+
}${aborted > 0 ? `, ${aborted} aborted` : ""}`;
|
|
648
704
|
const body = batch.runIds
|
|
649
705
|
.map((runId) => {
|
|
650
706
|
const run = store.commandRuns.get(runId);
|
|
@@ -735,7 +791,13 @@ function isInteractiveTuiContext(ctx: SubagentToolExecuteContext): boolean {
|
|
|
735
791
|
|
|
736
792
|
function finalizeRunError(runState: CommandRunState, error: unknown): FinalizedRun {
|
|
737
793
|
runState.status = "error";
|
|
738
|
-
runState.errorClass =
|
|
794
|
+
runState.errorClass = isSubagentAbort({
|
|
795
|
+
signal: runState.abortController?.signal,
|
|
796
|
+
autoAbortReason: runState.autoAbortReason,
|
|
797
|
+
error,
|
|
798
|
+
})
|
|
799
|
+
? "aborted"
|
|
800
|
+
: "process_error";
|
|
739
801
|
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
740
802
|
runState.lastLine =
|
|
741
803
|
runState.autoAbortReason ?? (error instanceof Error ? error.message : "Subagent execution failed");
|
|
@@ -1266,6 +1328,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1266
1328
|
function clearRunAbortState(runState: CommandRunState): void {
|
|
1267
1329
|
clearParentAbortListener(runState.id);
|
|
1268
1330
|
runState.abortController = undefined;
|
|
1331
|
+
runState.abortPending = false;
|
|
1269
1332
|
}
|
|
1270
1333
|
|
|
1271
1334
|
function registerRunLaunch(config: RunLaunchConfig): CommandRunState {
|
|
@@ -1289,6 +1352,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1289
1352
|
runState.lastRetryReason = undefined;
|
|
1290
1353
|
runState.errorClass = undefined;
|
|
1291
1354
|
runState.autoAbortReason = undefined;
|
|
1355
|
+
runState.abortPending = false;
|
|
1292
1356
|
runState.removed = false;
|
|
1293
1357
|
runState.turnCount = Math.max(DEFAULT_TURN_COUNT, runState.turnCount || DEFAULT_TURN_COUNT) + 1;
|
|
1294
1358
|
runState.contextMode = runState.contextMode ?? (config.inheritMainContext ? "main" : "sub");
|
|
@@ -1350,15 +1414,17 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1350
1414
|
|
|
1351
1415
|
function launchRunInBackground(runState: CommandRunState, taskForAgent: string): Promise<FinalizedRun> {
|
|
1352
1416
|
let claudeCheckpointSent = !!runState.claudeSessionId;
|
|
1417
|
+
const abortSignal = runState.abortController?.signal;
|
|
1353
1418
|
const recordActivity = createRunActivityRecorder(pi, runState, (model) => resolveContextWindow(ctx, model));
|
|
1354
|
-
return enqueueSubagentInvocation(() =>
|
|
1355
|
-
|
|
1419
|
+
return enqueueSubagentInvocation(async () => {
|
|
1420
|
+
if (runState.removed || abortSignal?.aborted) throw new Error("Subagent was aborted");
|
|
1421
|
+
return runSingleAgent(
|
|
1356
1422
|
ctx.cwd,
|
|
1357
1423
|
agents,
|
|
1358
1424
|
runState.agent,
|
|
1359
1425
|
taskForAgent,
|
|
1360
1426
|
runState.pipelineStepIndex,
|
|
1361
|
-
|
|
1427
|
+
abortSignal,
|
|
1362
1428
|
(partial) => {
|
|
1363
1429
|
if (runState.removed || store.disposed) return;
|
|
1364
1430
|
const current = partial.details?.results?.[0];
|
|
@@ -1382,8 +1448,12 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1382
1448
|
persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
|
|
1383
1449
|
onDiagnostic: createRunDiagnosticSink(pi, runState),
|
|
1384
1450
|
},
|
|
1385
|
-
)
|
|
1386
|
-
).then((result) =>
|
|
1451
|
+
);
|
|
1452
|
+
}).then((result) =>
|
|
1453
|
+
runState.removed || abortSignal?.aborted
|
|
1454
|
+
? finalizeRunError(runState, new Error("Subagent was aborted"))
|
|
1455
|
+
: finalizeRunState(runState, result),
|
|
1456
|
+
);
|
|
1387
1457
|
}
|
|
1388
1458
|
|
|
1389
1459
|
if (hasSingle) {
|
|
@@ -1405,6 +1475,20 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1405
1475
|
isError: true,
|
|
1406
1476
|
};
|
|
1407
1477
|
}
|
|
1478
|
+
if (continueFromRun.abortPending) {
|
|
1479
|
+
return {
|
|
1480
|
+
content: [
|
|
1481
|
+
{
|
|
1482
|
+
type: "text",
|
|
1483
|
+
text: withIdleRunWarning(
|
|
1484
|
+
`Subagent #${continuationRunId} is still aborting. Wait for cancellation to settle before continuing it.`,
|
|
1485
|
+
),
|
|
1486
|
+
},
|
|
1487
|
+
],
|
|
1488
|
+
details: makeDetails("single"),
|
|
1489
|
+
isError: true,
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1408
1492
|
if (continueFromRun.status === "running") {
|
|
1409
1493
|
return {
|
|
1410
1494
|
content: [
|
|
@@ -1473,7 +1557,6 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1473
1557
|
existingRunState: continueFromRun,
|
|
1474
1558
|
});
|
|
1475
1559
|
const launchSummary = toLaunchSummary(runState, continueFromRun ? "continue" : "run");
|
|
1476
|
-
const startedState = continueFromRun ? "resumed" : "started";
|
|
1477
1560
|
|
|
1478
1561
|
if (!shouldRunAsync) {
|
|
1479
1562
|
let finalized: FinalizedRun;
|
|
@@ -1506,7 +1589,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1506
1589
|
};
|
|
1507
1590
|
}
|
|
1508
1591
|
|
|
1509
|
-
|
|
1592
|
+
// No run-start sendMessage here: mid-turn triggerTurn:false messages land between tool call and result (pi #8537).
|
|
1510
1593
|
ctx.ui?.notify?.(
|
|
1511
1594
|
`${continueFromRun ? `Resumed subagent #${runState.id}` : `Started subagent #${runState.id}`}: ${resolvedAgent}`,
|
|
1512
1595
|
"info",
|
|
@@ -1531,6 +1614,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1531
1614
|
return;
|
|
1532
1615
|
}
|
|
1533
1616
|
|
|
1617
|
+
const terminalLabel = getFinalizedRunLabel(finalized);
|
|
1534
1618
|
const completionMessage = buildRunCompletionMessage(finalized);
|
|
1535
1619
|
if (isInOriginSession(ctx, originSessionFile)) {
|
|
1536
1620
|
pi.sendMessage(completionMessage, { deliverAs: "followUp", triggerTurn: true });
|
|
@@ -1541,14 +1625,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1541
1625
|
}
|
|
1542
1626
|
|
|
1543
1627
|
ctx.ui?.notify?.(
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
: `subagent tool run #${runState.id} (${resolvedAgent}) completed`,
|
|
1547
|
-
finalized.isError ? "error" : "info",
|
|
1628
|
+
`subagent tool run #${runState.id} (${resolvedAgent}) ${terminalLabel}`,
|
|
1629
|
+
terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
|
|
1548
1630
|
);
|
|
1549
1631
|
} catch (error: unknown) {
|
|
1550
1632
|
if (runState.removed || store.disposed) return;
|
|
1551
1633
|
const finalized = finalizeRunError(runState, error);
|
|
1634
|
+
const terminalLabel = getFinalizedRunLabel(finalized);
|
|
1552
1635
|
const errorMessage = buildRunCompletionMessage(finalized);
|
|
1553
1636
|
if (isInOriginSession(ctx, originSessionFile)) {
|
|
1554
1637
|
pi.sendMessage(errorMessage, { deliverAs: "followUp", triggerTurn: true });
|
|
@@ -1557,7 +1640,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1557
1640
|
const entry = store.globalLiveRuns.get(runState.id);
|
|
1558
1641
|
if (entry) entry.pendingCompletion = makePendingCompletion(errorMessage, true);
|
|
1559
1642
|
}
|
|
1560
|
-
ctx.ui?.notify?.(
|
|
1643
|
+
ctx.ui?.notify?.(
|
|
1644
|
+
`subagent tool run #${runState.id} ${terminalLabel}: ${runState.lastLine}`,
|
|
1645
|
+
terminalLabel === "aborted" ? "warning" : "error",
|
|
1646
|
+
);
|
|
1561
1647
|
updateCommandRunsWidget(store);
|
|
1562
1648
|
} finally {
|
|
1563
1649
|
clearRunAbortState(runState);
|
|
@@ -1628,6 +1714,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1628
1714
|
runIds: runStates.map(({ runState }) => runState.id),
|
|
1629
1715
|
completedRunIds: new Set(),
|
|
1630
1716
|
failedRunIds: new Set(),
|
|
1717
|
+
abortedRunIds: new Set(),
|
|
1631
1718
|
originSessionFile,
|
|
1632
1719
|
createdAt: Date.now(),
|
|
1633
1720
|
pendingResults: new Map(),
|
|
@@ -1647,11 +1734,18 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1647
1734
|
}),
|
|
1648
1735
|
);
|
|
1649
1736
|
const orderedRuns = runStates.map(({ runState }) => runState);
|
|
1650
|
-
const hasError = finalizedRuns.some((finalized) => finalized.isError);
|
|
1651
|
-
const content = formatBatchSummary(batchId, orderedRuns, hasError ? "error" : "completed");
|
|
1652
1737
|
const batchForSnapshot = store.batchGroups.get(batchId);
|
|
1653
|
-
if (batchForSnapshot)
|
|
1654
|
-
|
|
1738
|
+
if (batchForSnapshot) {
|
|
1739
|
+
for (const finalized of finalizedRuns) recordBatchResult(batchForSnapshot, finalized);
|
|
1740
|
+
}
|
|
1741
|
+
const terminalStatus = resolveGroupTerminalStatus(
|
|
1742
|
+
batchForSnapshot?.failedRunIds.size ?? 0,
|
|
1743
|
+
batchForSnapshot?.abortedRunIds.size ?? 0,
|
|
1744
|
+
);
|
|
1745
|
+
const content = formatBatchSummary(batchId, orderedRuns, terminalStatus);
|
|
1746
|
+
if (batchForSnapshot) {
|
|
1747
|
+
retireFinishedGroup(store, snapshotBatchGroup(store, batchForSnapshot, terminalStatus));
|
|
1748
|
+
}
|
|
1655
1749
|
for (const { runState } of runStates) cleanupRunAfterFinalDelivery(runState.id);
|
|
1656
1750
|
clearPendingGroupCompletion("batch", batchId);
|
|
1657
1751
|
store.batchGroups.delete(batchId);
|
|
@@ -1673,7 +1767,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1673
1767
|
.filter((result): result is SingleResult => Boolean(result)),
|
|
1674
1768
|
runStates.map(({ runState }) => toLaunchSummary(runState, "batch")),
|
|
1675
1769
|
),
|
|
1676
|
-
isError:
|
|
1770
|
+
isError: terminalStatus !== "completed",
|
|
1677
1771
|
};
|
|
1678
1772
|
}
|
|
1679
1773
|
|
|
@@ -1684,16 +1778,14 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1684
1778
|
|
|
1685
1779
|
const batch = store.batchGroups.get(batchId);
|
|
1686
1780
|
if (!batch) return;
|
|
1687
|
-
batch
|
|
1688
|
-
if (finalized.isError) batch.failedRunIds.add(runState.id);
|
|
1689
|
-
batch.pendingResults.set(runState.id, finalized.rawOutput);
|
|
1781
|
+
recordBatchResult(batch, finalized);
|
|
1690
1782
|
updateCommandRunsWidget(store);
|
|
1691
1783
|
|
|
1692
1784
|
if (batch.completedRunIds.size === batch.runIds.length) {
|
|
1693
1785
|
const orderedRuns = batch.runIds
|
|
1694
1786
|
.map((runId) => store.commandRuns.get(runId))
|
|
1695
1787
|
.filter((run): run is CommandRunState => Boolean(run));
|
|
1696
|
-
const batchTerminalStatus = batch.failedRunIds.size
|
|
1788
|
+
const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
|
|
1697
1789
|
const content = formatBatchSummary(batchId, orderedRuns, batchTerminalStatus);
|
|
1698
1790
|
const message = {
|
|
1699
1791
|
customType: "subagent-tool" as const,
|
|
@@ -1702,7 +1794,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1702
1794
|
details: {
|
|
1703
1795
|
batchId,
|
|
1704
1796
|
runIds: batch.runIds,
|
|
1705
|
-
status:
|
|
1797
|
+
status: batchTerminalStatus === "completed" ? "done" : batchTerminalStatus,
|
|
1706
1798
|
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1707
1799
|
},
|
|
1708
1800
|
};
|
|
@@ -1730,38 +1822,36 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1730
1822
|
removalReason: "trim",
|
|
1731
1823
|
});
|
|
1732
1824
|
ctx.ui?.notify?.(
|
|
1733
|
-
|
|
1825
|
+
batchTerminalStatus === "error"
|
|
1734
1826
|
? `subagent batch ${batchId} finished with errors`
|
|
1735
|
-
:
|
|
1736
|
-
|
|
1827
|
+
: batchTerminalStatus === "aborted"
|
|
1828
|
+
? `subagent batch ${batchId} aborted`
|
|
1829
|
+
: `subagent batch ${batchId} completed`,
|
|
1830
|
+
batchTerminalStatus === "error" ? "error" : batchTerminalStatus === "aborted" ? "warning" : "info",
|
|
1737
1831
|
);
|
|
1738
1832
|
}
|
|
1739
1833
|
} catch (error: unknown) {
|
|
1740
|
-
|
|
1741
|
-
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
1742
|
-
runState.lastLine = error instanceof Error ? error.message : "Subagent execution failed";
|
|
1743
|
-
runState.lastOutput = runState.lastLine;
|
|
1834
|
+
const finalized = finalizeRunError(runState, error);
|
|
1744
1835
|
const batch = store.batchGroups.get(batchId);
|
|
1745
1836
|
if (!batch) return;
|
|
1746
|
-
batch
|
|
1747
|
-
batch.failedRunIds.add(runState.id);
|
|
1748
|
-
batch.pendingResults.set(runState.id, runState.lastLine);
|
|
1837
|
+
recordBatchResult(batch, finalized);
|
|
1749
1838
|
if (batch.completedRunIds.size === batch.runIds.length) {
|
|
1750
1839
|
const orderedRuns = batch.runIds
|
|
1751
1840
|
.map((runId) => store.commandRuns.get(runId))
|
|
1752
1841
|
.filter((run): run is CommandRunState => Boolean(run));
|
|
1842
|
+
const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
|
|
1753
1843
|
const message = {
|
|
1754
1844
|
customType: "subagent-tool" as const,
|
|
1755
|
-
content: formatBatchSummary(batchId, orderedRuns,
|
|
1845
|
+
content: formatBatchSummary(batchId, orderedRuns, batchTerminalStatus),
|
|
1756
1846
|
display: true,
|
|
1757
1847
|
details: {
|
|
1758
1848
|
batchId,
|
|
1759
1849
|
runIds: batch.runIds,
|
|
1760
|
-
status:
|
|
1850
|
+
status: batchTerminalStatus,
|
|
1761
1851
|
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1762
1852
|
},
|
|
1763
1853
|
};
|
|
1764
|
-
retireFinishedGroup(store, snapshotBatchGroup(store, batch,
|
|
1854
|
+
retireFinishedGroup(store, snapshotBatchGroup(store, batch, batchTerminalStatus));
|
|
1765
1855
|
if (isInOriginSession(ctx, batch.originSessionFile)) {
|
|
1766
1856
|
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
1767
1857
|
clearPendingGroupCompletion("batch", batchId);
|
|
@@ -1779,6 +1869,8 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1779
1869
|
}
|
|
1780
1870
|
}
|
|
1781
1871
|
updateCommandRunsWidget(store);
|
|
1872
|
+
} finally {
|
|
1873
|
+
clearRunAbortState(runState);
|
|
1782
1874
|
}
|
|
1783
1875
|
})();
|
|
1784
1876
|
}
|
|
@@ -1830,7 +1922,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1830
1922
|
|
|
1831
1923
|
if (!shouldRunAsync) {
|
|
1832
1924
|
let previousOutput = "";
|
|
1833
|
-
let terminalStatus: "completed" | "stopped" | "error" = "completed";
|
|
1925
|
+
let terminalStatus: "completed" | "stopped" | "error" | "aborted" = "completed";
|
|
1834
1926
|
const finalizedRuns: FinalizedRun[] = [];
|
|
1835
1927
|
const pipeline = store.pipelines.get(pipelineId);
|
|
1836
1928
|
if (!pipeline) {
|
|
@@ -1897,29 +1989,30 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1897
1989
|
finalizedRuns.push(finalized);
|
|
1898
1990
|
|
|
1899
1991
|
if (runState.removed) {
|
|
1900
|
-
terminalStatus =
|
|
1992
|
+
terminalStatus = "aborted";
|
|
1901
1993
|
pipeline.stepResults.push({
|
|
1902
1994
|
runId: runState.id,
|
|
1903
1995
|
agent: runState.agent,
|
|
1904
1996
|
task: step.task,
|
|
1905
1997
|
output: finalized.rawOutput || "Run removed before pipeline completion.",
|
|
1906
|
-
status: "
|
|
1998
|
+
status: "aborted",
|
|
1907
1999
|
});
|
|
1908
2000
|
break;
|
|
1909
2001
|
}
|
|
1910
2002
|
|
|
2003
|
+
const stepStatus = getFinalizedRunStatus(finalized);
|
|
1911
2004
|
pipeline.stepResults.push({
|
|
1912
2005
|
runId: runState.id,
|
|
1913
2006
|
agent: runState.agent,
|
|
1914
2007
|
task: step.task,
|
|
1915
2008
|
output: finalized.rawOutput,
|
|
1916
|
-
status:
|
|
2009
|
+
status: stepStatus,
|
|
1917
2010
|
});
|
|
1918
2011
|
previousOutput = finalized.rawOutput;
|
|
1919
2012
|
updateCommandRunsWidget(store);
|
|
1920
2013
|
|
|
1921
|
-
if (
|
|
1922
|
-
terminalStatus =
|
|
2014
|
+
if (stepStatus !== "done") {
|
|
2015
|
+
terminalStatus = stepStatus;
|
|
1923
2016
|
break;
|
|
1924
2017
|
}
|
|
1925
2018
|
}
|
|
@@ -1935,7 +2028,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1935
2028
|
}
|
|
1936
2029
|
|
|
1937
2030
|
const hasError = pipeline.stepResults.some((step) => step.status === "error");
|
|
1938
|
-
|
|
2031
|
+
const hasAborted = pipeline.stepResults.some((step) => step.status === "aborted");
|
|
2032
|
+
if (hasError) terminalStatus = "error";
|
|
2033
|
+
else if (terminalStatus === "completed" && hasAborted) terminalStatus = "aborted";
|
|
1939
2034
|
const content = formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus);
|
|
1940
2035
|
retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
|
|
1941
2036
|
for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
|
|
@@ -1965,7 +2060,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1965
2060
|
|
|
1966
2061
|
void (async () => {
|
|
1967
2062
|
let previousOutput = "";
|
|
1968
|
-
let terminalStatus: "completed" | "stopped" | "error" = "completed";
|
|
2063
|
+
let terminalStatus: "completed" | "stopped" | "error" | "aborted" = "completed";
|
|
1969
2064
|
try {
|
|
1970
2065
|
for (let index = 0; index < steps.length; index++) {
|
|
1971
2066
|
const pipeline = store.pipelines.get(pipelineId);
|
|
@@ -2023,13 +2118,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
2023
2118
|
clearRunAbortState(runState);
|
|
2024
2119
|
}
|
|
2025
2120
|
if (runState.removed) {
|
|
2026
|
-
terminalStatus =
|
|
2121
|
+
terminalStatus = "aborted";
|
|
2027
2122
|
pipeline.stepResults.push({
|
|
2028
2123
|
runId: runState.id,
|
|
2029
2124
|
agent: runState.agent,
|
|
2030
2125
|
task: step.task,
|
|
2031
2126
|
output: finalized.rawOutput || "Run removed before pipeline completion.",
|
|
2032
|
-
status: "
|
|
2127
|
+
status: "aborted",
|
|
2033
2128
|
});
|
|
2034
2129
|
break;
|
|
2035
2130
|
}
|
|
@@ -2039,14 +2134,14 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
2039
2134
|
agent: runState.agent,
|
|
2040
2135
|
task: step.task,
|
|
2041
2136
|
output: finalized.rawOutput,
|
|
2042
|
-
status: finalized
|
|
2137
|
+
status: getFinalizedRunStatus(finalized),
|
|
2043
2138
|
};
|
|
2044
2139
|
pipeline.stepResults.push(stepResult);
|
|
2045
2140
|
previousOutput = finalized.rawOutput;
|
|
2046
2141
|
updateCommandRunsWidget(store);
|
|
2047
2142
|
|
|
2048
|
-
if (
|
|
2049
|
-
terminalStatus =
|
|
2143
|
+
if (stepResult.status !== "done") {
|
|
2144
|
+
terminalStatus = stepResult.status;
|
|
2050
2145
|
break;
|
|
2051
2146
|
}
|
|
2052
2147
|
}
|
|
@@ -2066,9 +2161,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
2066
2161
|
const pipeline = store.pipelines.get(pipelineId);
|
|
2067
2162
|
if (pipeline) {
|
|
2068
2163
|
const hasError = pipeline.stepResults.some((step) => step.status === "error");
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2164
|
+
const hasAborted = pipeline.stepResults.some((step) => step.status === "aborted");
|
|
2165
|
+
if (hasError) terminalStatus = "error";
|
|
2166
|
+
else if (terminalStatus === "completed" && hasAborted) terminalStatus = "aborted";
|
|
2072
2167
|
const orderedRuns = pipeline.stepRunIds
|
|
2073
2168
|
.map((runId) => store.commandRuns.get(runId))
|
|
2074
2169
|
.filter((run): run is CommandRunState => Boolean(run));
|
package/types.ts
CHANGED
|
@@ -106,6 +106,8 @@ export interface CommandRunState {
|
|
|
106
106
|
lastToolOutputChars?: number;
|
|
107
107
|
/** Hang detector reason preserved until the normal finalizer emits the sole completion. */
|
|
108
108
|
autoAbortReason?: string;
|
|
109
|
+
/** True while a removed invocation is still settling; blocks same-ID continuation races. */
|
|
110
|
+
abortPending?: boolean;
|
|
109
111
|
runtime?: AgentRuntime;
|
|
110
112
|
claudeSessionId?: string;
|
|
111
113
|
claudeProjectDir?: string;
|
|
@@ -172,6 +174,7 @@ export interface BatchGroupState {
|
|
|
172
174
|
runIds: number[];
|
|
173
175
|
completedRunIds: Set<number>;
|
|
174
176
|
failedRunIds: Set<number>;
|
|
177
|
+
abortedRunIds: Set<number>;
|
|
175
178
|
originSessionFile: string;
|
|
176
179
|
createdAt: number;
|
|
177
180
|
pendingResults: Map<number, string>;
|
|
@@ -183,7 +186,7 @@ export interface PipelineStepResult {
|
|
|
183
186
|
agent: string;
|
|
184
187
|
task: string;
|
|
185
188
|
output: string;
|
|
186
|
-
status: "done" | "error";
|
|
189
|
+
status: "done" | "error" | "aborted";
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
export interface PipelineState {
|
|
@@ -213,10 +216,11 @@ export interface FinishedGroupMember {
|
|
|
213
216
|
export interface FinishedGroupSnapshot {
|
|
214
217
|
groupId: string;
|
|
215
218
|
kind: "batch" | "chain";
|
|
216
|
-
terminalStatus: "completed" | "error" | "stopped";
|
|
219
|
+
terminalStatus: "completed" | "error" | "aborted" | "stopped";
|
|
217
220
|
finishedAt: number;
|
|
218
221
|
total: number;
|
|
219
222
|
failed: number;
|
|
223
|
+
aborted: number;
|
|
220
224
|
members: FinishedGroupMember[];
|
|
221
225
|
}
|
|
222
226
|
|
package/widget.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Box, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
7
|
import { HANG_WARNING_IDLE_MS, PARENT_HINT } from "./constants.js";
|
|
8
|
+
import { getSubagentRunStatusLabel } from "./failure-telemetry.js";
|
|
8
9
|
import {
|
|
9
10
|
AGENT_NAME_PALETTE,
|
|
10
11
|
agentBgIndex,
|
|
@@ -79,9 +80,12 @@ export function cleanupCommandRunsWidgetTimer(): void {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function getStatusVisual(run: CommandRunState): { statusColor: ThemeColor; statusIcon: string } {
|
|
82
|
-
const
|
|
83
|
+
const statusLabel = getSubagentRunStatusLabel(run.status, run.errorClass);
|
|
84
|
+
const statusColor: ThemeColor =
|
|
85
|
+
statusLabel === "running" || statusLabel === "aborted" ? "warning" : statusLabel === "done" ? "success" : "error";
|
|
83
86
|
const spinnerFrame = SPINNER_FRAMES[Math.floor(Date.now() / SPINNER_INTERVAL_MS) % SPINNER_FRAMES.length];
|
|
84
|
-
const statusIcon =
|
|
87
|
+
const statusIcon =
|
|
88
|
+
statusLabel === "running" ? spinnerFrame : statusLabel === "done" ? "✓" : statusLabel === "aborted" ? "■" : "✗";
|
|
85
89
|
return { statusColor, statusIcon };
|
|
86
90
|
}
|
|
87
91
|
|