pi-agent-squad 0.8.3 → 0.8.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/README.md +42 -0
- package/index.ts +572 -107
- package/package.json +18 -1
- package/session.ts +2 -0
- package/spawn.ts +86 -39
- package/task-delivery.ts +201 -0
- package/task-recovery.ts +41 -0
- package/task-state.ts +286 -0
- package/task-status.ts +107 -0
package/index.ts
CHANGED
|
@@ -36,6 +36,27 @@ import { openSubagentSessionOverlay } from "./session-ui.ts";
|
|
|
36
36
|
import { getFinalOutput, spawnInteractiveSubagent } from "./spawn.ts";
|
|
37
37
|
import { deadlockMessage, MessageWaitGraph } from "./wait-graph.ts";
|
|
38
38
|
import { ActiveRunRegistry, type ActiveRun } from "./active-runs.ts";
|
|
39
|
+
import {
|
|
40
|
+
boundTaskText,
|
|
41
|
+
isTerminalTaskStatus,
|
|
42
|
+
isRetryableTaskStatus,
|
|
43
|
+
reconstructTaskStates,
|
|
44
|
+
TASK_STATE_ENTRY,
|
|
45
|
+
TASK_STATE_LIMITS,
|
|
46
|
+
TASK_STATE_VERSION,
|
|
47
|
+
validateLaunchText,
|
|
48
|
+
type PersistedTaskState,
|
|
49
|
+
type PersistedTaskStatus,
|
|
50
|
+
} from "./task-state.ts";
|
|
51
|
+
import { planTaskRecovery } from "./task-recovery.ts";
|
|
52
|
+
import {
|
|
53
|
+
BACKGROUND_EVENT_TYPE,
|
|
54
|
+
branchHasResultDelivery,
|
|
55
|
+
RecoveryOutbox,
|
|
56
|
+
resultDeliveryId,
|
|
57
|
+
type BackgroundEventDetails,
|
|
58
|
+
} from "./task-delivery.ts";
|
|
59
|
+
import { formatTaskStatus, type LiveTaskView } from "./task-status.ts";
|
|
39
60
|
|
|
40
61
|
const MESSAGE_ROOT_BASE = path.join(
|
|
41
62
|
os.tmpdir(),
|
|
@@ -51,7 +72,6 @@ const RUNNING_WIDGET_TICK_MS = 1000;
|
|
|
51
72
|
const RUNNING_WIDGET_NAV_DEBOUNCE_MS = 150;
|
|
52
73
|
const RUNNING_WIDGET_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
53
74
|
const INCOMING_MESSAGE_TYPE = "pi_message_request";
|
|
54
|
-
const BACKGROUND_EVENT_TYPE = "pi_subagent_background_event";
|
|
55
75
|
|
|
56
76
|
interface IncomingMessageDetails {
|
|
57
77
|
id?: string;
|
|
@@ -61,15 +81,6 @@ interface IncomingMessageDetails {
|
|
|
61
81
|
createdAt?: number;
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
interface BackgroundEventDetails {
|
|
65
|
-
agent?: string;
|
|
66
|
-
address?: string;
|
|
67
|
-
status?: "done" | "error";
|
|
68
|
-
body?: string;
|
|
69
|
-
elapsedMs?: number;
|
|
70
|
-
runId?: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
84
|
type TranscriptEventKind = "message" | "done" | "error";
|
|
74
85
|
|
|
75
86
|
function customMessageText(content: unknown): string {
|
|
@@ -101,6 +112,8 @@ function formatEventDuration(elapsedMs: number): string {
|
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
class SubagentTranscriptEventComponent implements Component {
|
|
115
|
+
private static readonly COLLAPSED_MAX_BODY_LINES = 5;
|
|
116
|
+
|
|
104
117
|
private readonly markdown: Markdown | undefined;
|
|
105
118
|
|
|
106
119
|
constructor(
|
|
@@ -110,6 +123,7 @@ class SubagentTranscriptEventComponent implements Component {
|
|
|
110
123
|
private readonly theme: any,
|
|
111
124
|
private readonly replyRequested = false,
|
|
112
125
|
private readonly elapsedMs?: number,
|
|
126
|
+
private readonly expanded = false,
|
|
113
127
|
) {
|
|
114
128
|
const content = body.trim();
|
|
115
129
|
if (content) {
|
|
@@ -153,6 +167,25 @@ class SubagentTranscriptEventComponent implements Component {
|
|
|
153
167
|
return truncateToWidth(header, Math.max(1, width), "…");
|
|
154
168
|
}
|
|
155
169
|
|
|
170
|
+
/**
|
|
171
|
+
* When collapsed, keep at most COLLAPSED_MAX_BODY_LINES body lines: the first
|
|
172
|
+
* two, a dim "hidden" marker, and the last two. ctrl+o (tool output expand)
|
|
173
|
+
* toggles the full body via the renderer's expanded option.
|
|
174
|
+
*/
|
|
175
|
+
private clampBodyLines(bodyLines: string[]): string[] {
|
|
176
|
+
const shown = Math.max(2, SubagentTranscriptEventComponent.COLLAPSED_MAX_BODY_LINES);
|
|
177
|
+
if (this.expanded || bodyLines.length <= shown) return bodyLines;
|
|
178
|
+
const kept = Math.max(1, Math.floor((shown - 1) / 2));
|
|
179
|
+
const head = bodyLines.slice(0, kept);
|
|
180
|
+
const tail = bodyLines.slice(-kept);
|
|
181
|
+
const hidden = bodyLines.length - head.length - tail.length;
|
|
182
|
+
const marker = this.theme?.fg?.(
|
|
183
|
+
"dim",
|
|
184
|
+
`⋯ ${hidden} hidden line${hidden === 1 ? "" : "s"} · ctrl+o expand`,
|
|
185
|
+
) ?? `⋯ ${hidden} hidden lines · ctrl+o expand`;
|
|
186
|
+
return [...head, marker, ...tail];
|
|
187
|
+
}
|
|
188
|
+
|
|
156
189
|
render(width: number): string[] {
|
|
157
190
|
const padding = " ".repeat(Math.min(1, Math.max(0, width - 1)));
|
|
158
191
|
const contentWidth = Math.max(1, width - padding.length);
|
|
@@ -161,10 +194,12 @@ class SubagentTranscriptEventComponent implements Component {
|
|
|
161
194
|
const railWidth = contentWidth >= 2 ? 1 : 0;
|
|
162
195
|
const railGap = contentWidth >= 3 ? 1 : 0;
|
|
163
196
|
const bodyWidth = Math.max(1, contentWidth - railWidth - railGap);
|
|
164
|
-
const bodyLines =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
197
|
+
const bodyLines = this.clampBodyLines(
|
|
198
|
+
normalizeCompactCodeBlockLines(
|
|
199
|
+
this.markdown.render(bodyWidth),
|
|
200
|
+
bodyWidth,
|
|
201
|
+
0,
|
|
202
|
+
),
|
|
168
203
|
);
|
|
169
204
|
for (let index = 0; index < bodyLines.length; index++) {
|
|
170
205
|
const connector = index === bodyLines.length - 1 ? "└" : "│";
|
|
@@ -327,7 +362,12 @@ interface AsyncTask {
|
|
|
327
362
|
runId: string;
|
|
328
363
|
agent: string;
|
|
329
364
|
address: string;
|
|
365
|
+
task: string;
|
|
366
|
+
cwd: string;
|
|
367
|
+
readOnly: boolean;
|
|
330
368
|
startedAt: number;
|
|
369
|
+
updatedAt: number;
|
|
370
|
+
timeoutAt: number;
|
|
331
371
|
sessionGeneration: number;
|
|
332
372
|
}
|
|
333
373
|
|
|
@@ -555,7 +595,14 @@ export class RunningSubagentWidgetController {
|
|
|
555
595
|
}
|
|
556
596
|
}
|
|
557
597
|
|
|
558
|
-
export
|
|
598
|
+
export interface AgentSquadRuntimeDependencies {
|
|
599
|
+
spawnInteractiveSubagent: typeof spawnInteractiveSubagent;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function createAgentSquadExtension(
|
|
603
|
+
pi: ExtensionAPI,
|
|
604
|
+
dependencies: AgentSquadRuntimeDependencies = { spawnInteractiveSubagent },
|
|
605
|
+
) {
|
|
559
606
|
const isChild = process.env[ENV_ROLE] === "child";
|
|
560
607
|
|
|
561
608
|
if (isChild) {
|
|
@@ -565,9 +612,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
565
612
|
}
|
|
566
613
|
|
|
567
614
|
// ===== main-agent mode =====
|
|
615
|
+
const ownerRuntimeId = randomUUID();
|
|
568
616
|
pi.registerMessageRenderer<IncomingMessageDetails>(
|
|
569
617
|
INCOMING_MESSAGE_TYPE,
|
|
570
|
-
(message,
|
|
618
|
+
(message, options, theme) => {
|
|
571
619
|
const details = message.details ?? {};
|
|
572
620
|
const agent = String(details.from || "subagent");
|
|
573
621
|
const body =
|
|
@@ -580,12 +628,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
580
628
|
body,
|
|
581
629
|
theme,
|
|
582
630
|
Boolean(details.expectsReply),
|
|
631
|
+
undefined,
|
|
632
|
+
options.expanded,
|
|
583
633
|
);
|
|
584
634
|
},
|
|
585
635
|
);
|
|
586
636
|
pi.registerMessageRenderer<BackgroundEventDetails>(
|
|
587
637
|
BACKGROUND_EVENT_TYPE,
|
|
588
|
-
(message,
|
|
638
|
+
(message, options, theme) => {
|
|
589
639
|
const details = message.details ?? {};
|
|
590
640
|
const kind: TranscriptEventKind = details.status === "error" ? "error" : "done";
|
|
591
641
|
const agent = String(details.agent || "subagent");
|
|
@@ -604,6 +654,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
604
654
|
theme,
|
|
605
655
|
false,
|
|
606
656
|
elapsedMs,
|
|
657
|
+
options.expanded,
|
|
607
658
|
);
|
|
608
659
|
},
|
|
609
660
|
);
|
|
@@ -623,13 +674,142 @@ export default function (pi: ExtensionAPI) {
|
|
|
623
674
|
string,
|
|
624
675
|
{ release: () => void; timer?: ReturnType<typeof setTimeout> }
|
|
625
676
|
>();
|
|
677
|
+
const persistedTasks = new Map<string, PersistedTaskState>();
|
|
626
678
|
let sessionContext: any;
|
|
679
|
+
let runtimeContext: any;
|
|
680
|
+
let currentSessionToken: string | undefined;
|
|
681
|
+
let acceptingTasks = false;
|
|
682
|
+
let shutdownStarted = false;
|
|
683
|
+
let router: ReturnType<typeof createMessageRouter> | undefined;
|
|
627
684
|
let terminalInputUnsubscribe: (() => void) | undefined;
|
|
628
685
|
let sessionOverlayOpen = false;
|
|
629
686
|
let closeSessionOverlay: (() => void) | undefined;
|
|
630
687
|
let pendingOpenActivityId: string | undefined;
|
|
631
688
|
let lastNavigation: { direction: -1 | 1; at: number } | undefined;
|
|
632
689
|
|
|
690
|
+
function appendTaskSnapshot(state: PersistedTaskState): PersistedTaskState {
|
|
691
|
+
pi.appendEntry(TASK_STATE_ENTRY, state);
|
|
692
|
+
persistedTasks.set(state.runId, state);
|
|
693
|
+
return state;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function transitionTask(
|
|
697
|
+
runId: string,
|
|
698
|
+
expected: readonly PersistedTaskStatus[],
|
|
699
|
+
patch: Partial<Omit<PersistedTaskState, "version" | "runId" | "updatedAt">>,
|
|
700
|
+
now = Date.now(),
|
|
701
|
+
): PersistedTaskState | undefined {
|
|
702
|
+
const current = persistedTasks.get(runId);
|
|
703
|
+
if (!current || !expected.includes(current.status)) return undefined;
|
|
704
|
+
const next: PersistedTaskState = {
|
|
705
|
+
...current,
|
|
706
|
+
...patch,
|
|
707
|
+
version: TASK_STATE_VERSION,
|
|
708
|
+
runId,
|
|
709
|
+
updatedAt: now,
|
|
710
|
+
};
|
|
711
|
+
return appendTaskSnapshot(next);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function reportPersistenceFailure(error: unknown, prefix: string): void {
|
|
715
|
+
const message = `${prefix}: ${error instanceof Error ? error.message : String(error)}`;
|
|
716
|
+
try {
|
|
717
|
+
runtimeContext?.ui?.notify?.(message, "warning");
|
|
718
|
+
} catch {
|
|
719
|
+
/* UI may already be closing */
|
|
720
|
+
}
|
|
721
|
+
console.error(`[pi-agent-squad] ${message}`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const recoveryOutbox = new RecoveryOutbox({
|
|
725
|
+
getSessionToken: () => currentSessionToken,
|
|
726
|
+
getBranch: () => runtimeContext?.sessionManager?.getBranch?.() ?? [],
|
|
727
|
+
getTaskState: (runId) => persistedTasks.get(runId),
|
|
728
|
+
send: (message, options) => pi.sendMessage(message, options),
|
|
729
|
+
markInjected: (state) => {
|
|
730
|
+
transitionTask(
|
|
731
|
+
state.runId,
|
|
732
|
+
["completed"],
|
|
733
|
+
{ status: "completed", resultInjected: true },
|
|
734
|
+
);
|
|
735
|
+
},
|
|
736
|
+
onError: (error, state) =>
|
|
737
|
+
reportPersistenceFailure(
|
|
738
|
+
error,
|
|
739
|
+
`Could not finish result delivery bookkeeping for run ${state.runId.slice(0, 8)}`,
|
|
740
|
+
),
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
function refreshPersistedTasks(entries: readonly unknown[]): void {
|
|
744
|
+
persistedTasks.clear();
|
|
745
|
+
for (const [runId, state] of reconstructTaskStates(entries)) {
|
|
746
|
+
persistedTasks.set(runId, state);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function liveTaskRunIds(): Set<string> {
|
|
751
|
+
return new Set([
|
|
752
|
+
...tasks.keys(),
|
|
753
|
+
...activeRuns.list().map((run) => run.runId),
|
|
754
|
+
]);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function reconcileCurrentBranch(): void {
|
|
758
|
+
const branch = runtimeContext?.sessionManager?.getBranch?.() ?? [];
|
|
759
|
+
const currentLiveIds = liveTaskRunIds();
|
|
760
|
+
const liveSnapshots = [...persistedTasks.values()].filter(
|
|
761
|
+
(state) =>
|
|
762
|
+
state.ownerRuntimeId === ownerRuntimeId &&
|
|
763
|
+
currentLiveIds.has(state.runId) &&
|
|
764
|
+
(state.status === "starting" || state.status === "running"),
|
|
765
|
+
);
|
|
766
|
+
refreshPersistedTasks(branch);
|
|
767
|
+
for (const state of liveSnapshots) {
|
|
768
|
+
if (!persistedTasks.has(state.runId)) persistedTasks.set(state.runId, state);
|
|
769
|
+
}
|
|
770
|
+
const plan = planTaskRecovery(
|
|
771
|
+
persistedTasks,
|
|
772
|
+
ownerRuntimeId,
|
|
773
|
+
currentLiveIds,
|
|
774
|
+
(deliveryId) => branchHasResultDelivery(branch, deliveryId),
|
|
775
|
+
);
|
|
776
|
+
for (const stale of plan.interrupt) {
|
|
777
|
+
const now = Date.now();
|
|
778
|
+
try {
|
|
779
|
+
transitionTask(
|
|
780
|
+
stale.runId,
|
|
781
|
+
["starting", "running"],
|
|
782
|
+
{
|
|
783
|
+
status: "interrupted",
|
|
784
|
+
error: "Owning Pi runtime ended before the task reached a terminal state.",
|
|
785
|
+
endedAt: now,
|
|
786
|
+
},
|
|
787
|
+
now,
|
|
788
|
+
);
|
|
789
|
+
} catch (error) {
|
|
790
|
+
reportPersistenceFailure(
|
|
791
|
+
error,
|
|
792
|
+
`Could not mark stale run ${stale.runId.slice(0, 8)} interrupted`,
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const delivered of plan.markInjected) {
|
|
797
|
+
try {
|
|
798
|
+
transitionTask(
|
|
799
|
+
delivered.runId,
|
|
800
|
+
["completed"],
|
|
801
|
+
{ status: "completed", resultInjected: true },
|
|
802
|
+
);
|
|
803
|
+
} catch (error) {
|
|
804
|
+
reportPersistenceFailure(
|
|
805
|
+
error,
|
|
806
|
+
`Could not repair delivery state for run ${delivered.runId.slice(0, 8)}`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
for (const pending of plan.deliver) recoveryOutbox.enqueue(pending);
|
|
811
|
+
}
|
|
812
|
+
|
|
633
813
|
const bindPoolLifecycle = (): void => {
|
|
634
814
|
pool.setProcessExitHandler((agentName, runId) => {
|
|
635
815
|
const run = activeRuns.resolveExact(runId);
|
|
@@ -688,6 +868,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
688
868
|
|
|
689
869
|
pi.on("session_start", (event, ctx) => {
|
|
690
870
|
sessionGeneration++;
|
|
871
|
+
shutdownStarted = false;
|
|
872
|
+
acceptingTasks = false;
|
|
873
|
+
recoveryOutbox.cancelAll();
|
|
691
874
|
if (poolDisposed) {
|
|
692
875
|
pool = new SubagentPool(messageRoot);
|
|
693
876
|
bindPoolLifecycle();
|
|
@@ -698,6 +881,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
698
881
|
ctx.sessionManager?.getSessionId?.() ??
|
|
699
882
|
(event as any).sessionId ??
|
|
700
883
|
"ephemeral";
|
|
884
|
+
currentSessionToken = `${sessionId}:${sessionGeneration}`;
|
|
885
|
+
runtimeContext = ctx;
|
|
701
886
|
messageRoot = sessionRoot(sessionId);
|
|
702
887
|
ensureChannelRoot(MESSAGE_ROOT_BASE);
|
|
703
888
|
sweepMessageRoots(MESSAGE_ROOT_BASE);
|
|
@@ -706,6 +891,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
706
891
|
pool.setWorkingDirectory(cwd);
|
|
707
892
|
runningWidget.attach(ctx);
|
|
708
893
|
sessionContext = ctx.mode === "tui" ? ctx : undefined;
|
|
894
|
+
reconcileCurrentBranch();
|
|
895
|
+
acceptingTasks = true;
|
|
709
896
|
lastNavigation = undefined;
|
|
710
897
|
terminalInputUnsubscribe?.();
|
|
711
898
|
terminalInputUnsubscribe =
|
|
@@ -733,7 +920,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
733
920
|
: undefined;
|
|
734
921
|
});
|
|
735
922
|
|
|
736
|
-
pi.on("session_shutdown", () => {
|
|
923
|
+
pi.on("session_shutdown", (event, ctx) => {
|
|
924
|
+
if (shutdownStarted) return;
|
|
925
|
+
shutdownStarted = true;
|
|
926
|
+
acceptingTasks = false;
|
|
927
|
+
recoveryOutbox.cancelAll();
|
|
928
|
+
const now = Date.now();
|
|
929
|
+
for (const state of [...persistedTasks.values()]) {
|
|
930
|
+
if (
|
|
931
|
+
state.ownerRuntimeId !== ownerRuntimeId ||
|
|
932
|
+
(state.status !== "starting" && state.status !== "running")
|
|
933
|
+
) {
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const reason = boundTaskText(
|
|
937
|
+
[
|
|
938
|
+
"Owning Pi runtime shut down before the task reached a terminal state.",
|
|
939
|
+
event.reason ? `Reason: ${event.reason}.` : "",
|
|
940
|
+
]
|
|
941
|
+
.filter(Boolean)
|
|
942
|
+
.join(" "),
|
|
943
|
+
TASK_STATE_LIMITS.error,
|
|
944
|
+
);
|
|
945
|
+
try {
|
|
946
|
+
transitionTask(
|
|
947
|
+
state.runId,
|
|
948
|
+
["starting", "running"],
|
|
949
|
+
{ status: "interrupted", error: reason, endedAt: now },
|
|
950
|
+
now,
|
|
951
|
+
);
|
|
952
|
+
} catch (error) {
|
|
953
|
+
reportPersistenceFailure(
|
|
954
|
+
error,
|
|
955
|
+
`Could not persist shutdown interruption for run ${state.runId.slice(0, 8)}`,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
737
959
|
sessionGeneration++;
|
|
738
960
|
for (const controller of runControllers.values()) controller.abort();
|
|
739
961
|
runControllers.clear();
|
|
@@ -745,16 +967,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
745
967
|
sessionContext = undefined;
|
|
746
968
|
pendingOpenActivityId = undefined;
|
|
747
969
|
sessionHandles.clear();
|
|
748
|
-
activeRuns.clear();
|
|
749
970
|
for (const pending of pendingMainReplyEdges.values()) {
|
|
750
971
|
if (pending.timer) clearTimeout(pending.timer);
|
|
751
972
|
pending.release();
|
|
752
973
|
}
|
|
753
974
|
pendingMainReplyEdges.clear();
|
|
754
975
|
waitGraph.clear();
|
|
755
|
-
runningWidget.shutdown();
|
|
756
976
|
pool.dispose();
|
|
757
977
|
poolDisposed = true;
|
|
978
|
+
router?.dispose();
|
|
979
|
+
router = undefined;
|
|
980
|
+
activeRuns.clear();
|
|
981
|
+
tasks.clear();
|
|
982
|
+
runningWidget.shutdown();
|
|
983
|
+
currentSessionToken = undefined;
|
|
984
|
+
runtimeContext = undefined;
|
|
758
985
|
sweepMessageRoots(MESSAGE_ROOT_BASE);
|
|
759
986
|
});
|
|
760
987
|
|
|
@@ -782,7 +1009,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
782
1009
|
// ---- message router: subagent -> main injects into main session; child
|
|
783
1010
|
// messages resolve through the active-run registry before being delivered
|
|
784
1011
|
// to a direct/background session or the resident pool. ----
|
|
785
|
-
let router: ReturnType<typeof createMessageRouter> | undefined;
|
|
786
1012
|
const routedTimeout = (msg: MessageRequest): number =>
|
|
787
1013
|
Math.min(
|
|
788
1014
|
MAX_SUBAGENT_TIMEOUT_SECONDS * 1000,
|
|
@@ -982,11 +1208,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
982
1208
|
router.start();
|
|
983
1209
|
});
|
|
984
1210
|
|
|
985
|
-
pi.on("session_shutdown", () => {
|
|
986
|
-
router?.dispose();
|
|
987
|
-
router = undefined;
|
|
988
|
-
});
|
|
989
|
-
|
|
990
1211
|
/** Write a reply back to the message sender */
|
|
991
1212
|
function writeReplyTo(msg: MessageRequest, content: string, root = messageRoot): boolean {
|
|
992
1213
|
try {
|
|
@@ -1018,20 +1239,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1018
1239
|
return `${agent.name}#${runId.slice(0, 8)}`;
|
|
1019
1240
|
}
|
|
1020
1241
|
|
|
1021
|
-
function
|
|
1242
|
+
function resolveDirectRunAddress(
|
|
1022
1243
|
agent: AgentConfig,
|
|
1023
1244
|
runId: string,
|
|
1024
|
-
mode: "background" | "task",
|
|
1025
1245
|
requestedAddress?: string,
|
|
1026
|
-
|
|
1027
|
-
runCwd = cwd,
|
|
1028
|
-
): {
|
|
1029
|
-
address: string;
|
|
1030
|
-
session: Promise<SubagentSessionHandle>;
|
|
1031
|
-
resolveSession: (session: SubagentSessionHandle) => void;
|
|
1032
|
-
rejectSession: (error: Error) => void;
|
|
1033
|
-
} {
|
|
1246
|
+
): string {
|
|
1034
1247
|
const address = requestedAddress?.trim() || directRunAddress(agent, runId);
|
|
1248
|
+
validateLaunchText("address", address);
|
|
1035
1249
|
if (
|
|
1036
1250
|
(requestedAddress &&
|
|
1037
1251
|
(!/^[A-Za-z0-9_.-]+$/.test(address) ||
|
|
@@ -1046,6 +1260,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
1046
1260
|
`Invalid or unavailable runtime address "${address}". Use a unique name matching [A-Za-z0-9_.-] that is not a logical agent name or "main".`,
|
|
1047
1261
|
);
|
|
1048
1262
|
}
|
|
1263
|
+
return address;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function createDirectRun(
|
|
1267
|
+
agent: AgentConfig,
|
|
1268
|
+
runId: string,
|
|
1269
|
+
mode: "background" | "task",
|
|
1270
|
+
address: string,
|
|
1271
|
+
readOnly = agent.readOnly === true,
|
|
1272
|
+
runCwd = cwd,
|
|
1273
|
+
): {
|
|
1274
|
+
address: string;
|
|
1275
|
+
session: Promise<SubagentSessionHandle>;
|
|
1276
|
+
resolveSession: (session: SubagentSessionHandle) => void;
|
|
1277
|
+
rejectSession: (error: Error) => void;
|
|
1278
|
+
} {
|
|
1279
|
+
if (activeRuns.hasAddress(address)) {
|
|
1280
|
+
throw new Error(`Runtime address "${address}" became unavailable before launch.`);
|
|
1281
|
+
}
|
|
1049
1282
|
let resolveSession!: (session: SubagentSessionHandle) => void;
|
|
1050
1283
|
let rejectSession!: (error: Error) => void;
|
|
1051
1284
|
const session = new Promise<SubagentSessionHandle>((resolve, reject) => {
|
|
@@ -1095,29 +1328,77 @@ export default function (pi: ExtensionAPI) {
|
|
|
1095
1328
|
timeoutMs?: number,
|
|
1096
1329
|
requestedAddress?: string,
|
|
1097
1330
|
readOnly = agent.readOnly === true,
|
|
1331
|
+
retryOf?: string,
|
|
1098
1332
|
): { runId: string; address: string } {
|
|
1333
|
+
if (!acceptingTasks) throw new Error("This Pi session is shutting down and cannot accept new subagent tasks.");
|
|
1334
|
+
validateLaunchText("agent", agent.name);
|
|
1335
|
+
validateLaunchText("task", taskText, { allowEmpty: true });
|
|
1336
|
+
const runCwd = normalizeCwd(cwdOverride ?? cwd);
|
|
1337
|
+
validateLaunchText("cwd", runCwd);
|
|
1099
1338
|
const runId = randomUUID();
|
|
1100
|
-
const
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1339
|
+
const address = resolveDirectRunAddress(agent, runId, requestedAddress);
|
|
1340
|
+
const startedAt = Date.now();
|
|
1341
|
+
const effectiveTimeoutMs = Math.max(
|
|
1342
|
+
1000,
|
|
1343
|
+
timeoutMs ?? DEFAULT_SUBAGENT_TIMEOUT_SECONDS * 1000,
|
|
1344
|
+
);
|
|
1345
|
+
const timeoutAt = Math.min(Number.MAX_SAFE_INTEGER, startedAt + effectiveTimeoutMs);
|
|
1346
|
+
const starting: PersistedTaskState = {
|
|
1347
|
+
version: TASK_STATE_VERSION,
|
|
1103
1348
|
runId,
|
|
1104
|
-
|
|
1105
|
-
|
|
1349
|
+
address,
|
|
1350
|
+
agent: agent.name,
|
|
1351
|
+
task: taskText,
|
|
1352
|
+
cwd: runCwd,
|
|
1106
1353
|
readOnly,
|
|
1107
|
-
|
|
1108
|
-
|
|
1354
|
+
mode: "async",
|
|
1355
|
+
status: "starting",
|
|
1356
|
+
startedAt,
|
|
1357
|
+
updatedAt: startedAt,
|
|
1358
|
+
timeoutAt,
|
|
1359
|
+
...(retryOf ? { retryOf } : {}),
|
|
1360
|
+
ownerRuntimeId,
|
|
1361
|
+
};
|
|
1362
|
+
appendTaskSnapshot(starting);
|
|
1363
|
+
|
|
1364
|
+
const controller = new AbortController();
|
|
1365
|
+
let direct: ReturnType<typeof createDirectRun>;
|
|
1366
|
+
try {
|
|
1367
|
+
direct = createDirectRun(agent, runId, "background", address, readOnly, runCwd);
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
const now = Date.now();
|
|
1370
|
+
transitionTask(
|
|
1371
|
+
runId,
|
|
1372
|
+
["starting"],
|
|
1373
|
+
{
|
|
1374
|
+
status: "failed",
|
|
1375
|
+
error: boundTaskText(
|
|
1376
|
+
error instanceof Error ? error.message : String(error),
|
|
1377
|
+
TASK_STATE_LIMITS.error,
|
|
1378
|
+
),
|
|
1379
|
+
endedAt: now,
|
|
1380
|
+
},
|
|
1381
|
+
now,
|
|
1382
|
+
);
|
|
1383
|
+
throw error;
|
|
1384
|
+
}
|
|
1109
1385
|
runControllers.set(runId, controller);
|
|
1110
1386
|
tasks.set(runId, {
|
|
1111
1387
|
runId,
|
|
1112
1388
|
agent: agent.name,
|
|
1113
|
-
address
|
|
1114
|
-
|
|
1389
|
+
address,
|
|
1390
|
+
task: taskText,
|
|
1391
|
+
cwd: runCwd,
|
|
1392
|
+
readOnly,
|
|
1393
|
+
startedAt,
|
|
1394
|
+
updatedAt: startedAt,
|
|
1395
|
+
timeoutAt,
|
|
1115
1396
|
sessionGeneration,
|
|
1116
1397
|
});
|
|
1117
1398
|
runningWidget.start(runId, agent.name, taskText, "background");
|
|
1118
1399
|
|
|
1119
1400
|
const start = async () => {
|
|
1120
|
-
return await spawnInteractiveSubagent({
|
|
1401
|
+
return await dependencies.spawnInteractiveSubagent({
|
|
1121
1402
|
agent,
|
|
1122
1403
|
task: taskText,
|
|
1123
1404
|
address: direct.address,
|
|
@@ -1126,7 +1407,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
1126
1407
|
runId,
|
|
1127
1408
|
childIndex: 0,
|
|
1128
1409
|
signal: controller.signal,
|
|
1129
|
-
timeoutMs,
|
|
1410
|
+
timeoutMs: effectiveTimeoutMs,
|
|
1411
|
+
persistSession: true,
|
|
1412
|
+
onStarted: (childSessionFile) => {
|
|
1413
|
+
const now = Date.now();
|
|
1414
|
+
const running = transitionTask(
|
|
1415
|
+
runId,
|
|
1416
|
+
["starting"],
|
|
1417
|
+
{
|
|
1418
|
+
status: "running",
|
|
1419
|
+
...(childSessionFile
|
|
1420
|
+
? {
|
|
1421
|
+
childSessionFile: boundTaskText(
|
|
1422
|
+
childSessionFile,
|
|
1423
|
+
TASK_STATE_LIMITS.childSessionFile,
|
|
1424
|
+
),
|
|
1425
|
+
}
|
|
1426
|
+
: {}),
|
|
1427
|
+
},
|
|
1428
|
+
now,
|
|
1429
|
+
);
|
|
1430
|
+
const task = tasks.get(runId);
|
|
1431
|
+
if (task && running) task.updatedAt = running.updatedAt;
|
|
1432
|
+
},
|
|
1130
1433
|
onSession: (session) => {
|
|
1131
1434
|
direct.resolveSession(session);
|
|
1132
1435
|
registerSessionHandle(runId, session);
|
|
@@ -1141,47 +1444,82 @@ export default function (pi: ExtensionAPI) {
|
|
|
1141
1444
|
activeRuns.remove(runId);
|
|
1142
1445
|
const task = tasks.get(runId);
|
|
1143
1446
|
if (!task || task.sessionGeneration !== sessionGeneration) return;
|
|
1144
|
-
const finalStatus: "done" | "error" =
|
|
1145
|
-
result.exitCode === 0 && result.stopReason !== "error" ? "done" : "error";
|
|
1146
1447
|
const output = getFinalOutput(result.messages) || "(no text output)";
|
|
1147
1448
|
const failureReason =
|
|
1148
1449
|
result.exitCode !== 0
|
|
1149
|
-
? result.
|
|
1450
|
+
? result.errorMessage || result.stderr || `process exited with code ${result.exitCode}`
|
|
1150
1451
|
: result.stopReason === "error"
|
|
1151
1452
|
? result.errorMessage || "subagent reported an error"
|
|
1152
1453
|
: "";
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
?
|
|
1160
|
-
:
|
|
1161
|
-
|
|
1454
|
+
const cancelled =
|
|
1455
|
+
result.exitCode === 124 ||
|
|
1456
|
+
result.stopReason === "aborted" ||
|
|
1457
|
+
/\b(?:timed out|cancelled|canceled|aborted)\b/i.test(result.errorMessage ?? "");
|
|
1458
|
+
const finalStatus: PersistedTaskStatus =
|
|
1459
|
+
result.exitCode === 0 && result.stopReason !== "error" && result.stopReason !== "aborted"
|
|
1460
|
+
? "completed"
|
|
1461
|
+
: cancelled
|
|
1462
|
+
? "cancelled"
|
|
1463
|
+
: "failed";
|
|
1464
|
+
const now = Date.now();
|
|
1465
|
+
try {
|
|
1466
|
+
if (finalStatus === "completed") {
|
|
1467
|
+
const completed = transitionTask(
|
|
1468
|
+
runId,
|
|
1469
|
+
["starting", "running"],
|
|
1470
|
+
{
|
|
1471
|
+
status: "completed",
|
|
1472
|
+
resultSummary: boundTaskText(output, TASK_STATE_LIMITS.resultSummary),
|
|
1473
|
+
deliveryId: resultDeliveryId(runId),
|
|
1474
|
+
resultInjected: false,
|
|
1475
|
+
endedAt: now,
|
|
1476
|
+
},
|
|
1477
|
+
now,
|
|
1478
|
+
);
|
|
1479
|
+
if (completed) {
|
|
1480
|
+
recoveryOutbox.enqueue(completed, { body: output, content: output });
|
|
1481
|
+
}
|
|
1482
|
+
} else {
|
|
1483
|
+
const diagnostic = boundTaskText(
|
|
1484
|
+
failureReason || result.errorMessage || `Subagent ${finalStatus}.`,
|
|
1485
|
+
TASK_STATE_LIMITS.error,
|
|
1486
|
+
);
|
|
1487
|
+
const terminal = transitionTask(
|
|
1488
|
+
runId,
|
|
1489
|
+
["starting", "running"],
|
|
1490
|
+
{ status: finalStatus, error: diagnostic, endedAt: now },
|
|
1491
|
+
now,
|
|
1492
|
+
);
|
|
1493
|
+
if (terminal) {
|
|
1494
|
+
const displayBody = [
|
|
1495
|
+
diagnostic,
|
|
1162
1496
|
output === "(no text output)" ? "" : `**Partial result**\n\n${output}`,
|
|
1163
1497
|
]
|
|
1164
1498
|
.filter(Boolean)
|
|
1165
1499
|
.join("\n\n");
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1500
|
+
pi.sendMessage(
|
|
1501
|
+
{
|
|
1502
|
+
customType: BACKGROUND_EVENT_TYPE,
|
|
1503
|
+
content: `[background subagent error] ${agent.name} (run ${runId.slice(0, 8)}): ${diagnostic}`,
|
|
1504
|
+
display: true,
|
|
1505
|
+
details: {
|
|
1506
|
+
agent: agent.name,
|
|
1507
|
+
address,
|
|
1508
|
+
status: "error",
|
|
1509
|
+
body: displayBody,
|
|
1510
|
+
elapsedMs: now - task.startedAt,
|
|
1511
|
+
runId,
|
|
1512
|
+
},
|
|
1513
|
+
},
|
|
1514
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
reportPersistenceFailure(
|
|
1520
|
+
error,
|
|
1521
|
+
`Could not finalize background run ${runId.slice(0, 8)}`,
|
|
1182
1522
|
);
|
|
1183
|
-
} catch {
|
|
1184
|
-
/* session is closed */
|
|
1185
1523
|
}
|
|
1186
1524
|
})
|
|
1187
1525
|
.catch((err) => {
|
|
@@ -1189,26 +1527,47 @@ export default function (pi: ExtensionAPI) {
|
|
|
1189
1527
|
direct.rejectSession(err instanceof Error ? err : new Error(String(err)));
|
|
1190
1528
|
const task = tasks.get(runId);
|
|
1191
1529
|
const errorText = err instanceof Error ? err.message : String(err);
|
|
1530
|
+
const current = persistedTasks.get(runId);
|
|
1531
|
+
if (!current || isTerminalTaskStatus(current.status)) return;
|
|
1532
|
+
const cancelled =
|
|
1533
|
+
controller.signal.aborted ||
|
|
1534
|
+
/\b(?:timed out|cancelled|canceled|aborted)\b/i.test(errorText);
|
|
1535
|
+
const now = Date.now();
|
|
1192
1536
|
try {
|
|
1193
1537
|
if (task && task.sessionGeneration !== sessionGeneration) return;
|
|
1194
|
-
|
|
1538
|
+
const terminal = transitionTask(
|
|
1539
|
+
runId,
|
|
1540
|
+
["starting", "running"],
|
|
1195
1541
|
{
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
details: {
|
|
1200
|
-
agent: agent.name,
|
|
1201
|
-
address: direct.address,
|
|
1202
|
-
status: "error",
|
|
1203
|
-
body: errorText,
|
|
1204
|
-
elapsedMs: task ? Date.now() - task.startedAt : undefined,
|
|
1205
|
-
runId,
|
|
1206
|
-
},
|
|
1542
|
+
status: cancelled ? "cancelled" : "failed",
|
|
1543
|
+
error: boundTaskText(errorText, TASK_STATE_LIMITS.error),
|
|
1544
|
+
endedAt: now,
|
|
1207
1545
|
},
|
|
1208
|
-
|
|
1546
|
+
now,
|
|
1547
|
+
);
|
|
1548
|
+
if (terminal) {
|
|
1549
|
+
pi.sendMessage(
|
|
1550
|
+
{
|
|
1551
|
+
customType: BACKGROUND_EVENT_TYPE,
|
|
1552
|
+
content: `[background subagent error] ${agent.name} (run ${runId.slice(0, 8)}): ${errorText}`,
|
|
1553
|
+
display: true,
|
|
1554
|
+
details: {
|
|
1555
|
+
agent: agent.name,
|
|
1556
|
+
address,
|
|
1557
|
+
status: "error",
|
|
1558
|
+
body: errorText,
|
|
1559
|
+
elapsedMs: task ? now - task.startedAt : undefined,
|
|
1560
|
+
runId,
|
|
1561
|
+
},
|
|
1562
|
+
},
|
|
1563
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
reportPersistenceFailure(
|
|
1568
|
+
error,
|
|
1569
|
+
`Could not record background failure for run ${runId.slice(0, 8)}`,
|
|
1209
1570
|
);
|
|
1210
|
-
} catch {
|
|
1211
|
-
/* ignore */
|
|
1212
1571
|
}
|
|
1213
1572
|
})
|
|
1214
1573
|
.finally(() => {
|
|
@@ -1220,7 +1579,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1220
1579
|
runningWidget.finish(runId);
|
|
1221
1580
|
});
|
|
1222
1581
|
|
|
1223
|
-
return { runId, address
|
|
1582
|
+
return { runId, address };
|
|
1224
1583
|
}
|
|
1225
1584
|
|
|
1226
1585
|
// ---- adaptive orchestration prompt; `/orchestrate on` installs the opt-in prompt ----
|
|
@@ -1250,11 +1609,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1250
1609
|
"While running, a subagent may send_message (to=main) to reach you, or contact other subagents — reply promptly with reply_message. Synchronous tasks must use wait=false when contacting main.",
|
|
1251
1610
|
].join(" "),
|
|
1252
1611
|
parameters: Type.Object({
|
|
1253
|
-
agent: Type.String({ description: "Subagent name" }),
|
|
1254
|
-
task: Type.String({ description: "Task description for the subagent" }),
|
|
1255
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the subagent" })),
|
|
1612
|
+
agent: Type.String({ maxLength: TASK_STATE_LIMITS.agent, description: "Subagent name" }),
|
|
1613
|
+
task: Type.String({ maxLength: TASK_STATE_LIMITS.task, description: "Task description for the subagent" }),
|
|
1614
|
+
cwd: Type.Optional(Type.String({ maxLength: TASK_STATE_LIMITS.cwd, description: "Working directory for the subagent" })),
|
|
1256
1615
|
as: Type.Optional(
|
|
1257
1616
|
Type.String({
|
|
1617
|
+
maxLength: TASK_STATE_LIMITS.address,
|
|
1258
1618
|
description: "Optional unique runtime address for this run (for example actor-a); identity still comes from agent",
|
|
1259
1619
|
}),
|
|
1260
1620
|
),
|
|
@@ -1274,6 +1634,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1274
1634
|
}),
|
|
1275
1635
|
|
|
1276
1636
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1637
|
+
if (!acceptingTasks) {
|
|
1638
|
+
return {
|
|
1639
|
+
content: [{ type: "text", text: "This Pi session is shutting down and cannot accept new subagent tasks." }],
|
|
1640
|
+
details: undefined,
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1277
1643
|
const agents = discoverAgents(ctx.cwd);
|
|
1278
1644
|
const agent = agents.find((a) => a.name === params.agent);
|
|
1279
1645
|
if (!agent) {
|
|
@@ -1324,7 +1690,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1324
1690
|
else signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
1325
1691
|
let direct: ReturnType<typeof createDirectRun>;
|
|
1326
1692
|
try {
|
|
1327
|
-
|
|
1693
|
+
validateLaunchText("task", params.task, { allowEmpty: true });
|
|
1694
|
+
const address = resolveDirectRunAddress(agent, runId, params.as);
|
|
1695
|
+
direct = createDirectRun(agent, runId, "task", address, readOnly, params.cwd ?? cwd);
|
|
1328
1696
|
} catch (error) {
|
|
1329
1697
|
signal?.removeEventListener("abort", forwardAbort);
|
|
1330
1698
|
runControllers.delete(runId);
|
|
@@ -1346,7 +1714,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1346
1714
|
runningWidget.start(runId, agent.name, params.task, "task");
|
|
1347
1715
|
let result: Awaited<ReturnType<typeof spawnInteractiveSubagent>>;
|
|
1348
1716
|
try {
|
|
1349
|
-
result = await spawnInteractiveSubagent({
|
|
1717
|
+
result = await dependencies.spawnInteractiveSubagent({
|
|
1350
1718
|
agent,
|
|
1351
1719
|
task: params.task,
|
|
1352
1720
|
address: direct.address,
|
|
@@ -1482,25 +1850,122 @@ export default function (pi: ExtensionAPI) {
|
|
|
1482
1850
|
},
|
|
1483
1851
|
});
|
|
1484
1852
|
|
|
1853
|
+
pi.registerCommand("subagent-retry", {
|
|
1854
|
+
description: "Retry an interrupted, failed, or cancelled background task",
|
|
1855
|
+
getArgumentCompletions: (prefix) => {
|
|
1856
|
+
const items = [...persistedTasks.values()]
|
|
1857
|
+
.filter((state) => isRetryableTaskStatus(state.status))
|
|
1858
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
1859
|
+
.map((state) => ({
|
|
1860
|
+
value: state.runId,
|
|
1861
|
+
label: state.runId,
|
|
1862
|
+
description: `${state.address} · ${state.status}`,
|
|
1863
|
+
}))
|
|
1864
|
+
.filter((item) => item.value.startsWith(prefix.trim()));
|
|
1865
|
+
return items.length > 0 ? items : null;
|
|
1866
|
+
},
|
|
1867
|
+
handler: async (args, ctx) => {
|
|
1868
|
+
runtimeContext = ctx;
|
|
1869
|
+
reconcileCurrentBranch();
|
|
1870
|
+
const raw = Array.isArray(args) ? String(args[0] ?? "") : String(args ?? "");
|
|
1871
|
+
const oldRunId = raw.trim().split(/\s+/)[0] ?? "";
|
|
1872
|
+
if (!oldRunId) {
|
|
1873
|
+
ctx.ui.notify("Usage: /subagent-retry <runId>", "warning");
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
const previous = persistedTasks.get(oldRunId);
|
|
1877
|
+
if (!previous) {
|
|
1878
|
+
ctx.ui.notify(`No task "${oldRunId}" exists on the current branch.`, "error");
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (!isRetryableTaskStatus(previous.status)) {
|
|
1882
|
+
ctx.ui.notify(
|
|
1883
|
+
`Task ${oldRunId.slice(0, 8)} is ${previous.status} and cannot be retried. Only interrupted, failed, or cancelled tasks are retryable.`,
|
|
1884
|
+
"warning",
|
|
1885
|
+
);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const agent = discoverAgents(ctx.cwd).find((candidate) => candidate.name === previous.agent);
|
|
1889
|
+
if (!agent) {
|
|
1890
|
+
ctx.ui.notify(
|
|
1891
|
+
`Cannot retry run ${oldRunId.slice(0, 8)} because subagent "${previous.agent}" no longer exists.`,
|
|
1892
|
+
"error",
|
|
1893
|
+
);
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
const originalTimeoutMs =
|
|
1897
|
+
previous.timeoutAt !== undefined
|
|
1898
|
+
? Math.max(
|
|
1899
|
+
MIN_SUBAGENT_TIMEOUT_SECONDS * 1000,
|
|
1900
|
+
Math.min(
|
|
1901
|
+
MAX_SUBAGENT_TIMEOUT_SECONDS * 1000,
|
|
1902
|
+
previous.timeoutAt - previous.startedAt,
|
|
1903
|
+
),
|
|
1904
|
+
)
|
|
1905
|
+
: DEFAULT_SUBAGENT_TIMEOUT_SECONDS * 1000;
|
|
1906
|
+
try {
|
|
1907
|
+
const launched = launchBackground(
|
|
1908
|
+
agent,
|
|
1909
|
+
previous.task,
|
|
1910
|
+
previous.cwd,
|
|
1911
|
+
originalTimeoutMs,
|
|
1912
|
+
undefined,
|
|
1913
|
+
previous.readOnly,
|
|
1914
|
+
previous.runId,
|
|
1915
|
+
);
|
|
1916
|
+
ctx.ui.notify(
|
|
1917
|
+
`Retry started at ${launched.address} (new run ${launched.runId.slice(0, 8)}, retry of ${previous.runId.slice(0, 8)}).`,
|
|
1918
|
+
"info",
|
|
1919
|
+
);
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
ctx.ui.notify(
|
|
1922
|
+
`Failed to retry run ${oldRunId.slice(0, 8)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1923
|
+
"error",
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
},
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1485
1929
|
pi.registerCommand("subagent-status", {
|
|
1486
|
-
description: "Show
|
|
1930
|
+
description: "Show live subagent tasks and current-branch task history",
|
|
1487
1931
|
handler: async (_args, ctx) => {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1932
|
+
runtimeContext = ctx;
|
|
1933
|
+
reconcileCurrentBranch();
|
|
1934
|
+
const live: LiveTaskView[] = [...tasks.values()].map((task) => {
|
|
1935
|
+
const persisted = persistedTasks.get(task.runId);
|
|
1936
|
+
return {
|
|
1937
|
+
runId: task.runId,
|
|
1938
|
+
address: task.address,
|
|
1939
|
+
agent: task.agent,
|
|
1940
|
+
task: task.task,
|
|
1941
|
+
status:
|
|
1942
|
+
persisted?.status === "starting" || persisted?.status === "running"
|
|
1943
|
+
? persisted.status
|
|
1944
|
+
: "running",
|
|
1945
|
+
startedAt: task.startedAt,
|
|
1946
|
+
updatedAt: persisted?.updatedAt ?? task.updatedAt,
|
|
1947
|
+
};
|
|
1492
1948
|
});
|
|
1493
|
-
const
|
|
1494
|
-
const
|
|
1949
|
+
const taskRunIds = new Set(live.map((task) => task.runId));
|
|
1950
|
+
const otherActive = activeRuns
|
|
1951
|
+
.list()
|
|
1952
|
+
.filter((run) => !taskRunIds.has(run.runId))
|
|
1953
|
+
.slice(0, 10);
|
|
1954
|
+
const activeLines = otherActive.map(
|
|
1495
1955
|
(run) => `- ${run.address} -> ${run.agent} [${run.mode}] (run ${run.runId.slice(0, 8)})`,
|
|
1496
1956
|
);
|
|
1957
|
+
const status = formatTaskStatus(live, persistedTasks);
|
|
1497
1958
|
ctx.ui.notify(
|
|
1498
1959
|
[
|
|
1499
|
-
|
|
1500
|
-
activeLines.length
|
|
1960
|
+
status,
|
|
1961
|
+
activeLines.length
|
|
1962
|
+
? `Other live processes:\n${activeLines.join("\n")}${activeRuns.list().length - taskRunIds.size > 10 ? "\n- … more live processes omitted" : ""}`
|
|
1963
|
+
: "",
|
|
1501
1964
|
].join("\n"),
|
|
1502
1965
|
"info",
|
|
1503
1966
|
);
|
|
1504
1967
|
},
|
|
1505
1968
|
});
|
|
1506
1969
|
}
|
|
1970
|
+
|
|
1971
|
+
export default createAgentSquadExtension;
|