@ryan_nookpi/pi-extension-subagent 0.5.4 → 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 -61
- 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: [
|
|
@@ -1505,6 +1589,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1505
1589
|
};
|
|
1506
1590
|
}
|
|
1507
1591
|
|
|
1592
|
+
// No run-start sendMessage here: mid-turn triggerTurn:false messages land between tool call and result (pi #8537).
|
|
1508
1593
|
ctx.ui?.notify?.(
|
|
1509
1594
|
`${continueFromRun ? `Resumed subagent #${runState.id}` : `Started subagent #${runState.id}`}: ${resolvedAgent}`,
|
|
1510
1595
|
"info",
|
|
@@ -1529,6 +1614,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1529
1614
|
return;
|
|
1530
1615
|
}
|
|
1531
1616
|
|
|
1617
|
+
const terminalLabel = getFinalizedRunLabel(finalized);
|
|
1532
1618
|
const completionMessage = buildRunCompletionMessage(finalized);
|
|
1533
1619
|
if (isInOriginSession(ctx, originSessionFile)) {
|
|
1534
1620
|
pi.sendMessage(completionMessage, { deliverAs: "followUp", triggerTurn: true });
|
|
@@ -1539,14 +1625,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1539
1625
|
}
|
|
1540
1626
|
|
|
1541
1627
|
ctx.ui?.notify?.(
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
: `subagent tool run #${runState.id} (${resolvedAgent}) completed`,
|
|
1545
|
-
finalized.isError ? "error" : "info",
|
|
1628
|
+
`subagent tool run #${runState.id} (${resolvedAgent}) ${terminalLabel}`,
|
|
1629
|
+
terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
|
|
1546
1630
|
);
|
|
1547
1631
|
} catch (error: unknown) {
|
|
1548
1632
|
if (runState.removed || store.disposed) return;
|
|
1549
1633
|
const finalized = finalizeRunError(runState, error);
|
|
1634
|
+
const terminalLabel = getFinalizedRunLabel(finalized);
|
|
1550
1635
|
const errorMessage = buildRunCompletionMessage(finalized);
|
|
1551
1636
|
if (isInOriginSession(ctx, originSessionFile)) {
|
|
1552
1637
|
pi.sendMessage(errorMessage, { deliverAs: "followUp", triggerTurn: true });
|
|
@@ -1555,7 +1640,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1555
1640
|
const entry = store.globalLiveRuns.get(runState.id);
|
|
1556
1641
|
if (entry) entry.pendingCompletion = makePendingCompletion(errorMessage, true);
|
|
1557
1642
|
}
|
|
1558
|
-
ctx.ui?.notify?.(
|
|
1643
|
+
ctx.ui?.notify?.(
|
|
1644
|
+
`subagent tool run #${runState.id} ${terminalLabel}: ${runState.lastLine}`,
|
|
1645
|
+
terminalLabel === "aborted" ? "warning" : "error",
|
|
1646
|
+
);
|
|
1559
1647
|
updateCommandRunsWidget(store);
|
|
1560
1648
|
} finally {
|
|
1561
1649
|
clearRunAbortState(runState);
|
|
@@ -1626,6 +1714,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1626
1714
|
runIds: runStates.map(({ runState }) => runState.id),
|
|
1627
1715
|
completedRunIds: new Set(),
|
|
1628
1716
|
failedRunIds: new Set(),
|
|
1717
|
+
abortedRunIds: new Set(),
|
|
1629
1718
|
originSessionFile,
|
|
1630
1719
|
createdAt: Date.now(),
|
|
1631
1720
|
pendingResults: new Map(),
|
|
@@ -1645,11 +1734,18 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1645
1734
|
}),
|
|
1646
1735
|
);
|
|
1647
1736
|
const orderedRuns = runStates.map(({ runState }) => runState);
|
|
1648
|
-
const hasError = finalizedRuns.some((finalized) => finalized.isError);
|
|
1649
|
-
const content = formatBatchSummary(batchId, orderedRuns, hasError ? "error" : "completed");
|
|
1650
1737
|
const batchForSnapshot = store.batchGroups.get(batchId);
|
|
1651
|
-
if (batchForSnapshot)
|
|
1652
|
-
|
|
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
|
+
}
|
|
1653
1749
|
for (const { runState } of runStates) cleanupRunAfterFinalDelivery(runState.id);
|
|
1654
1750
|
clearPendingGroupCompletion("batch", batchId);
|
|
1655
1751
|
store.batchGroups.delete(batchId);
|
|
@@ -1671,7 +1767,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1671
1767
|
.filter((result): result is SingleResult => Boolean(result)),
|
|
1672
1768
|
runStates.map(({ runState }) => toLaunchSummary(runState, "batch")),
|
|
1673
1769
|
),
|
|
1674
|
-
isError:
|
|
1770
|
+
isError: terminalStatus !== "completed",
|
|
1675
1771
|
};
|
|
1676
1772
|
}
|
|
1677
1773
|
|
|
@@ -1682,16 +1778,14 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1682
1778
|
|
|
1683
1779
|
const batch = store.batchGroups.get(batchId);
|
|
1684
1780
|
if (!batch) return;
|
|
1685
|
-
batch
|
|
1686
|
-
if (finalized.isError) batch.failedRunIds.add(runState.id);
|
|
1687
|
-
batch.pendingResults.set(runState.id, finalized.rawOutput);
|
|
1781
|
+
recordBatchResult(batch, finalized);
|
|
1688
1782
|
updateCommandRunsWidget(store);
|
|
1689
1783
|
|
|
1690
1784
|
if (batch.completedRunIds.size === batch.runIds.length) {
|
|
1691
1785
|
const orderedRuns = batch.runIds
|
|
1692
1786
|
.map((runId) => store.commandRuns.get(runId))
|
|
1693
1787
|
.filter((run): run is CommandRunState => Boolean(run));
|
|
1694
|
-
const batchTerminalStatus = batch.failedRunIds.size
|
|
1788
|
+
const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
|
|
1695
1789
|
const content = formatBatchSummary(batchId, orderedRuns, batchTerminalStatus);
|
|
1696
1790
|
const message = {
|
|
1697
1791
|
customType: "subagent-tool" as const,
|
|
@@ -1700,7 +1794,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1700
1794
|
details: {
|
|
1701
1795
|
batchId,
|
|
1702
1796
|
runIds: batch.runIds,
|
|
1703
|
-
status:
|
|
1797
|
+
status: batchTerminalStatus === "completed" ? "done" : batchTerminalStatus,
|
|
1704
1798
|
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1705
1799
|
},
|
|
1706
1800
|
};
|
|
@@ -1728,38 +1822,36 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1728
1822
|
removalReason: "trim",
|
|
1729
1823
|
});
|
|
1730
1824
|
ctx.ui?.notify?.(
|
|
1731
|
-
|
|
1825
|
+
batchTerminalStatus === "error"
|
|
1732
1826
|
? `subagent batch ${batchId} finished with errors`
|
|
1733
|
-
:
|
|
1734
|
-
|
|
1827
|
+
: batchTerminalStatus === "aborted"
|
|
1828
|
+
? `subagent batch ${batchId} aborted`
|
|
1829
|
+
: `subagent batch ${batchId} completed`,
|
|
1830
|
+
batchTerminalStatus === "error" ? "error" : batchTerminalStatus === "aborted" ? "warning" : "info",
|
|
1735
1831
|
);
|
|
1736
1832
|
}
|
|
1737
1833
|
} catch (error: unknown) {
|
|
1738
|
-
|
|
1739
|
-
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
1740
|
-
runState.lastLine = error instanceof Error ? error.message : "Subagent execution failed";
|
|
1741
|
-
runState.lastOutput = runState.lastLine;
|
|
1834
|
+
const finalized = finalizeRunError(runState, error);
|
|
1742
1835
|
const batch = store.batchGroups.get(batchId);
|
|
1743
1836
|
if (!batch) return;
|
|
1744
|
-
batch
|
|
1745
|
-
batch.failedRunIds.add(runState.id);
|
|
1746
|
-
batch.pendingResults.set(runState.id, runState.lastLine);
|
|
1837
|
+
recordBatchResult(batch, finalized);
|
|
1747
1838
|
if (batch.completedRunIds.size === batch.runIds.length) {
|
|
1748
1839
|
const orderedRuns = batch.runIds
|
|
1749
1840
|
.map((runId) => store.commandRuns.get(runId))
|
|
1750
1841
|
.filter((run): run is CommandRunState => Boolean(run));
|
|
1842
|
+
const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
|
|
1751
1843
|
const message = {
|
|
1752
1844
|
customType: "subagent-tool" as const,
|
|
1753
|
-
content: formatBatchSummary(batchId, orderedRuns,
|
|
1845
|
+
content: formatBatchSummary(batchId, orderedRuns, batchTerminalStatus),
|
|
1754
1846
|
display: true,
|
|
1755
1847
|
details: {
|
|
1756
1848
|
batchId,
|
|
1757
1849
|
runIds: batch.runIds,
|
|
1758
|
-
status:
|
|
1850
|
+
status: batchTerminalStatus,
|
|
1759
1851
|
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1760
1852
|
},
|
|
1761
1853
|
};
|
|
1762
|
-
retireFinishedGroup(store, snapshotBatchGroup(store, batch,
|
|
1854
|
+
retireFinishedGroup(store, snapshotBatchGroup(store, batch, batchTerminalStatus));
|
|
1763
1855
|
if (isInOriginSession(ctx, batch.originSessionFile)) {
|
|
1764
1856
|
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
1765
1857
|
clearPendingGroupCompletion("batch", batchId);
|
|
@@ -1777,6 +1869,8 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1777
1869
|
}
|
|
1778
1870
|
}
|
|
1779
1871
|
updateCommandRunsWidget(store);
|
|
1872
|
+
} finally {
|
|
1873
|
+
clearRunAbortState(runState);
|
|
1780
1874
|
}
|
|
1781
1875
|
})();
|
|
1782
1876
|
}
|
|
@@ -1828,7 +1922,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1828
1922
|
|
|
1829
1923
|
if (!shouldRunAsync) {
|
|
1830
1924
|
let previousOutput = "";
|
|
1831
|
-
let terminalStatus: "completed" | "stopped" | "error" = "completed";
|
|
1925
|
+
let terminalStatus: "completed" | "stopped" | "error" | "aborted" = "completed";
|
|
1832
1926
|
const finalizedRuns: FinalizedRun[] = [];
|
|
1833
1927
|
const pipeline = store.pipelines.get(pipelineId);
|
|
1834
1928
|
if (!pipeline) {
|
|
@@ -1895,29 +1989,30 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1895
1989
|
finalizedRuns.push(finalized);
|
|
1896
1990
|
|
|
1897
1991
|
if (runState.removed) {
|
|
1898
|
-
terminalStatus =
|
|
1992
|
+
terminalStatus = "aborted";
|
|
1899
1993
|
pipeline.stepResults.push({
|
|
1900
1994
|
runId: runState.id,
|
|
1901
1995
|
agent: runState.agent,
|
|
1902
1996
|
task: step.task,
|
|
1903
1997
|
output: finalized.rawOutput || "Run removed before pipeline completion.",
|
|
1904
|
-
status: "
|
|
1998
|
+
status: "aborted",
|
|
1905
1999
|
});
|
|
1906
2000
|
break;
|
|
1907
2001
|
}
|
|
1908
2002
|
|
|
2003
|
+
const stepStatus = getFinalizedRunStatus(finalized);
|
|
1909
2004
|
pipeline.stepResults.push({
|
|
1910
2005
|
runId: runState.id,
|
|
1911
2006
|
agent: runState.agent,
|
|
1912
2007
|
task: step.task,
|
|
1913
2008
|
output: finalized.rawOutput,
|
|
1914
|
-
status:
|
|
2009
|
+
status: stepStatus,
|
|
1915
2010
|
});
|
|
1916
2011
|
previousOutput = finalized.rawOutput;
|
|
1917
2012
|
updateCommandRunsWidget(store);
|
|
1918
2013
|
|
|
1919
|
-
if (
|
|
1920
|
-
terminalStatus =
|
|
2014
|
+
if (stepStatus !== "done") {
|
|
2015
|
+
terminalStatus = stepStatus;
|
|
1921
2016
|
break;
|
|
1922
2017
|
}
|
|
1923
2018
|
}
|
|
@@ -1933,7 +2028,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1933
2028
|
}
|
|
1934
2029
|
|
|
1935
2030
|
const hasError = pipeline.stepResults.some((step) => step.status === "error");
|
|
1936
|
-
|
|
2031
|
+
const hasAborted = pipeline.stepResults.some((step) => step.status === "aborted");
|
|
2032
|
+
if (hasError) terminalStatus = "error";
|
|
2033
|
+
else if (terminalStatus === "completed" && hasAborted) terminalStatus = "aborted";
|
|
1937
2034
|
const content = formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus);
|
|
1938
2035
|
retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
|
|
1939
2036
|
for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
|
|
@@ -1963,7 +2060,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1963
2060
|
|
|
1964
2061
|
void (async () => {
|
|
1965
2062
|
let previousOutput = "";
|
|
1966
|
-
let terminalStatus: "completed" | "stopped" | "error" = "completed";
|
|
2063
|
+
let terminalStatus: "completed" | "stopped" | "error" | "aborted" = "completed";
|
|
1967
2064
|
try {
|
|
1968
2065
|
for (let index = 0; index < steps.length; index++) {
|
|
1969
2066
|
const pipeline = store.pipelines.get(pipelineId);
|
|
@@ -2021,13 +2118,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
2021
2118
|
clearRunAbortState(runState);
|
|
2022
2119
|
}
|
|
2023
2120
|
if (runState.removed) {
|
|
2024
|
-
terminalStatus =
|
|
2121
|
+
terminalStatus = "aborted";
|
|
2025
2122
|
pipeline.stepResults.push({
|
|
2026
2123
|
runId: runState.id,
|
|
2027
2124
|
agent: runState.agent,
|
|
2028
2125
|
task: step.task,
|
|
2029
2126
|
output: finalized.rawOutput || "Run removed before pipeline completion.",
|
|
2030
|
-
status: "
|
|
2127
|
+
status: "aborted",
|
|
2031
2128
|
});
|
|
2032
2129
|
break;
|
|
2033
2130
|
}
|
|
@@ -2037,14 +2134,14 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
2037
2134
|
agent: runState.agent,
|
|
2038
2135
|
task: step.task,
|
|
2039
2136
|
output: finalized.rawOutput,
|
|
2040
|
-
status: finalized
|
|
2137
|
+
status: getFinalizedRunStatus(finalized),
|
|
2041
2138
|
};
|
|
2042
2139
|
pipeline.stepResults.push(stepResult);
|
|
2043
2140
|
previousOutput = finalized.rawOutput;
|
|
2044
2141
|
updateCommandRunsWidget(store);
|
|
2045
2142
|
|
|
2046
|
-
if (
|
|
2047
|
-
terminalStatus =
|
|
2143
|
+
if (stepResult.status !== "done") {
|
|
2144
|
+
terminalStatus = stepResult.status;
|
|
2048
2145
|
break;
|
|
2049
2146
|
}
|
|
2050
2147
|
}
|
|
@@ -2064,9 +2161,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
2064
2161
|
const pipeline = store.pipelines.get(pipelineId);
|
|
2065
2162
|
if (pipeline) {
|
|
2066
2163
|
const hasError = pipeline.stepResults.some((step) => step.status === "error");
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2164
|
+
const hasAborted = pipeline.stepResults.some((step) => step.status === "aborted");
|
|
2165
|
+
if (hasError) terminalStatus = "error";
|
|
2166
|
+
else if (terminalStatus === "completed" && hasAborted) terminalStatus = "aborted";
|
|
2070
2167
|
const orderedRuns = pipeline.stepRunIds
|
|
2071
2168
|
.map((runId) => store.commandRuns.get(runId))
|
|
2072
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
|
|