@ferris1225/pi-subagents 0.3.0 → 0.4.0
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-zh.md +13 -0
- package/README.md +16 -0
- package/package.json +1 -1
- package/src/index.ts +43 -43
- package/src/monitor.ts +72 -35
- package/src/spawn.ts +35 -10
package/README-zh.md
CHANGED
|
@@ -117,6 +117,19 @@ agentModels[name] → 当前 session 模型 → agent frontmatter 里的默
|
|
|
117
117
|
{ "tasks": [ { "agent": "explore", "task": "..." }, { "agent": "explore", "task": "..." } ] }
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
## 实时状态与通知
|
|
121
|
+
|
|
122
|
+
子代理运行期间,编辑器上方的挂件为每个运行显示一行状态(图标、agent、模型、
|
|
123
|
+
token 用量、耗时),其下缩进一行显示它正在做什么:`thinking`、`writing`、
|
|
124
|
+
`read src/index.ts`、`bash npm test`……(不会是一坨 JSON 参数)。
|
|
125
|
+
|
|
126
|
+
运行结束(成功**或**失败)时,该行立即从挂件消失,主窗口收到一条通知,
|
|
127
|
+
给出最终摘要(`✓ worker · openai/gpt-5 · ↑12.4k ↓3.1k · 47s`)。工具结果
|
|
128
|
+
本身仍是对话里的持久记录。
|
|
129
|
+
|
|
130
|
+
子代理一律请求**最强思考强度**(`--thinking max`);pi 会按目标模型实际支持
|
|
131
|
+
的级别自适应降级(`max → xhigh → high → … → off`),弱模型也能平稳运行。
|
|
132
|
+
|
|
120
133
|
## 开发
|
|
121
134
|
|
|
122
135
|
```bash
|
package/README.md
CHANGED
|
@@ -125,6 +125,22 @@ Tool shape:
|
|
|
125
125
|
{ "tasks": [ { "agent": "explore", "task": "..." }, { "agent": "explore", "task": "..." } ] }
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
## Live status & notifications
|
|
129
|
+
|
|
130
|
+
While sub-agents run, a widget above the editor shows one line per run — status
|
|
131
|
+
icon, agent, model, token usage, elapsed time — plus a second, indented line
|
|
132
|
+
with what the agent is doing right now: `thinking`, `writing`,
|
|
133
|
+
`read src/index.ts`, `bash npm test`, … (never a raw JSON args blob).
|
|
134
|
+
|
|
135
|
+
When a run finishes (done **or** failed), its row disappears from the widget and
|
|
136
|
+
the main window gets a notification with the final summary
|
|
137
|
+
(`✓ worker · openai/gpt-5 · ↑12.4k ↓3.1k · 47s`). The tool result itself
|
|
138
|
+
remains the durable record in the conversation.
|
|
139
|
+
|
|
140
|
+
Sub-agents always request the **strongest thinking level** (`--thinking max`);
|
|
141
|
+
pi clamps it adaptively to what the resolved model supports
|
|
142
|
+
(`max → xhigh → high → … → off`), so weaker models degrade gracefully.
|
|
143
|
+
|
|
128
144
|
## Development
|
|
129
145
|
|
|
130
146
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Focused sub-agent delegation for pi: explore / plan / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type SubagentLiveEvent,
|
|
39
39
|
type UsageStats,
|
|
40
40
|
} from "./spawn.ts";
|
|
41
|
-
import { monitor, statusColor, statusIcon, statusLabel } from "./monitor.ts";
|
|
41
|
+
import { formatToolActivity, monitor, statusColor, statusIcon, statusLabel } from "./monitor.ts";
|
|
42
42
|
|
|
43
43
|
const TaskItem = Type.Object({
|
|
44
44
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
@@ -124,6 +124,42 @@ export default function (pi: ExtensionAPI): void {
|
|
|
124
124
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
125
125
|
monitor.beginTurn();
|
|
126
126
|
const config = await loadConfig(configPath);
|
|
127
|
+
|
|
128
|
+
// Finished runs leave the widget immediately; the main window gets a
|
|
129
|
+
// notification instead (the tool result remains the durable record).
|
|
130
|
+
const finishRun = (runId: number, status: "done" | "failed"): void => {
|
|
131
|
+
monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
|
|
132
|
+
const run = monitor.removeRun(runId);
|
|
133
|
+
if (!run) return; // already finished — stay idempotent
|
|
134
|
+
const icon = status === "done" ? "✓" : "✗";
|
|
135
|
+
ctx.ui.notify(`${icon} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Live sub-agent activity → concise one-line status ("thinking",
|
|
139
|
+
// "read src/index.ts", ...), never a raw args blob.
|
|
140
|
+
const makeLiveHandler = (runId: number) => (e: SubagentLiveEvent): void => {
|
|
141
|
+
switch (e.kind) {
|
|
142
|
+
case "status":
|
|
143
|
+
if (e.status === "done" || e.status === "failed") finishRun(runId, e.status);
|
|
144
|
+
else monitor.setStatus(runId, e.status);
|
|
145
|
+
break;
|
|
146
|
+
case "usage":
|
|
147
|
+
monitor.setUsage(runId, e.usage, e.model);
|
|
148
|
+
break;
|
|
149
|
+
case "tool_start":
|
|
150
|
+
monitor.setActivity(runId, formatToolActivity(e.toolName, e.args));
|
|
151
|
+
break;
|
|
152
|
+
case "tool_end":
|
|
153
|
+
if (e.isError) monitor.setActivity(runId, `✗ ${e.toolName} failed`);
|
|
154
|
+
break;
|
|
155
|
+
case "thinking":
|
|
156
|
+
monitor.setActivity(runId, "thinking");
|
|
157
|
+
break;
|
|
158
|
+
case "text":
|
|
159
|
+
monitor.setActivity(runId, "writing");
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
127
163
|
const discovery = discoverAgents(ctx.cwd, {
|
|
128
164
|
scope: config.agentScope,
|
|
129
165
|
enabledNames: config.enabledAgents,
|
|
@@ -190,25 +226,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
190
226
|
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
191
227
|
const resolvedModel = agents.find((a) => a.name === t.agent)?.model;
|
|
192
228
|
const runId = monitor.addRun(t.agent, resolvedModel);
|
|
193
|
-
const onLive = (
|
|
194
|
-
switch (e.kind) {
|
|
195
|
-
case "status":
|
|
196
|
-
monitor.setStatus(runId, e.status);
|
|
197
|
-
break;
|
|
198
|
-
case "usage":
|
|
199
|
-
monitor.setUsage(runId, e.usage, e.model);
|
|
200
|
-
break;
|
|
201
|
-
case "tool_start":
|
|
202
|
-
monitor.appendTranscript(runId, { kind: "tool", text: `▸ ${e.toolName}(${typeof e.args === "object" ? JSON.stringify(e.args).slice(0, 120) : String(e.args).slice(0, 120)})` });
|
|
203
|
-
break;
|
|
204
|
-
case "tool_end":
|
|
205
|
-
monitor.appendTranscript(runId, { kind: e.isError ? "error" : "status", text: `${e.isError ? "✗" : "✓"} ${e.toolName}` });
|
|
206
|
-
break;
|
|
207
|
-
case "text_delta":
|
|
208
|
-
monitor.appendTextDelta(runId, e.delta);
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
};
|
|
229
|
+
const onLive = makeLiveHandler(runId);
|
|
212
230
|
const perTaskUpdate: OnUpdateCallback | undefined = onUpdate
|
|
213
231
|
? (partial) => {
|
|
214
232
|
const current = partial.details?.results[0];
|
|
@@ -232,7 +250,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
232
250
|
makeDetails: makeDetails("parallel"),
|
|
233
251
|
});
|
|
234
252
|
} catch (err) {
|
|
235
|
-
|
|
253
|
+
finishRun(runId, "failed");
|
|
236
254
|
throw err;
|
|
237
255
|
}
|
|
238
256
|
allResults[index] = result;
|
|
@@ -261,25 +279,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
261
279
|
// ---- Single mode ----
|
|
262
280
|
const resolvedModel = agents.find((a) => a.name === params.agent)?.model;
|
|
263
281
|
const runId = monitor.addRun(params.agent as string, resolvedModel);
|
|
264
|
-
const onLive = (
|
|
265
|
-
switch (e.kind) {
|
|
266
|
-
case "status":
|
|
267
|
-
monitor.setStatus(runId, e.status);
|
|
268
|
-
break;
|
|
269
|
-
case "usage":
|
|
270
|
-
monitor.setUsage(runId, e.usage, e.model);
|
|
271
|
-
break;
|
|
272
|
-
case "tool_start":
|
|
273
|
-
monitor.appendTranscript(runId, { kind: "tool", text: `▸ ${e.toolName}(${typeof e.args === "object" ? JSON.stringify(e.args).slice(0, 120) : String(e.args).slice(0, 120)})` });
|
|
274
|
-
break;
|
|
275
|
-
case "tool_end":
|
|
276
|
-
monitor.appendTranscript(runId, { kind: e.isError ? "error" : "status", text: `${e.isError ? "✗" : "✓"} ${e.toolName}` });
|
|
277
|
-
break;
|
|
278
|
-
case "text_delta":
|
|
279
|
-
monitor.appendTextDelta(runId, e.delta);
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
};
|
|
282
|
+
const onLive = makeLiveHandler(runId);
|
|
283
283
|
let result: SingleResult;
|
|
284
284
|
try {
|
|
285
285
|
result = await runSingleAgent({
|
|
@@ -294,7 +294,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
294
294
|
makeDetails: makeDetails("single"),
|
|
295
295
|
});
|
|
296
296
|
} catch (err) {
|
|
297
|
-
|
|
297
|
+
finishRun(runId, "failed");
|
|
298
298
|
throw err;
|
|
299
299
|
}
|
|
300
300
|
|
|
@@ -386,8 +386,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
386
386
|
const icon = statusIcon(r.status, theme);
|
|
387
387
|
const label = theme.fg(statusColor(r.status), statusLabel(r.status));
|
|
388
388
|
lines.push(truncateToWidth(` ${icon} ${monitor.summarize(r)} · ${label}`, width, ""));
|
|
389
|
-
|
|
390
|
-
if (activity) lines.push(truncateToWidth(theme.fg("dim", `
|
|
389
|
+
// Activity sits one indent level below the agent name.
|
|
390
|
+
if (r.activity) lines.push(truncateToWidth(theme.fg("dim", ` ${r.activity}`), width, ""));
|
|
391
391
|
}
|
|
392
392
|
return lines;
|
|
393
393
|
},
|
package/src/monitor.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The store notifies subscribers on every mutation so the persistent widget
|
|
6
6
|
* above the editor can re-render. Each run carries timing information
|
|
7
|
-
* (started/ended)
|
|
8
|
-
*
|
|
7
|
+
* (started/ended) plus a concise activity string describing what the run is
|
|
8
|
+
* doing right now ("thinking", "read src/index.ts", ...). Runs are removed
|
|
9
|
+
* as soon as they finish: the tool result is the durable record in the main
|
|
10
|
+
* conversation, so a stale "done" row must not linger in the widget.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
@@ -17,18 +19,14 @@ import type { UsageStats } from "./spawn.ts";
|
|
|
17
19
|
|
|
18
20
|
export type RunStatus = "queued" | "running" | "done" | "failed";
|
|
19
21
|
|
|
20
|
-
export interface TranscriptLine {
|
|
21
|
-
kind: "tool" | "text" | "status" | "error";
|
|
22
|
-
text: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
22
|
export interface RunView {
|
|
26
23
|
id: number;
|
|
27
24
|
agent: string;
|
|
28
25
|
model?: string;
|
|
29
26
|
status: RunStatus;
|
|
30
27
|
usage: UsageStats;
|
|
31
|
-
|
|
28
|
+
/** Concise current activity ("thinking", "read src/index.ts"); last writer wins. */
|
|
29
|
+
activity?: string;
|
|
32
30
|
/** Epoch ms when the run started executing (set on first "running" status). */
|
|
33
31
|
startedAt?: number;
|
|
34
32
|
/** Epoch ms when the run finished (set on "done"/"failed"). */
|
|
@@ -71,6 +69,63 @@ export function formatElapsed(run: RunView, now: number = Date.now()): string {
|
|
|
71
69
|
return formatDuration(end - run.startedAt);
|
|
72
70
|
}
|
|
73
71
|
|
|
72
|
+
/** Max length of the argument target inside a formatted activity line. */
|
|
73
|
+
export const ACTIVITY_TARGET_MAX = 60;
|
|
74
|
+
|
|
75
|
+
function shortTarget(value: unknown): string {
|
|
76
|
+
if (typeof value !== "string") return "";
|
|
77
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
78
|
+
// Slice by code point so emoji / CJK-ext never leave a lone surrogate.
|
|
79
|
+
const chars = [...oneLine];
|
|
80
|
+
return chars.length > ACTIVITY_TARGET_MAX ? `${chars.slice(0, ACTIVITY_TARGET_MAX - 1).join("")}…` : oneLine;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Concise "what is it doing" text for a tool call: the tool name plus its single
|
|
84
|
+
* most telling argument (path, command, pattern, ...) — never a raw JSON blob. */
|
|
85
|
+
export function formatToolActivity(toolName: string, args: unknown): string {
|
|
86
|
+
const a = (typeof args === "object" && args !== null ? args : {}) as Record<string, unknown>;
|
|
87
|
+
const pick = (...keys: string[]): string => {
|
|
88
|
+
for (const key of keys) {
|
|
89
|
+
const s = shortTarget(a[key]);
|
|
90
|
+
if (s) return s;
|
|
91
|
+
}
|
|
92
|
+
return "";
|
|
93
|
+
};
|
|
94
|
+
let target: string;
|
|
95
|
+
switch (toolName) {
|
|
96
|
+
case "bash":
|
|
97
|
+
case "shell":
|
|
98
|
+
target = pick("command");
|
|
99
|
+
break;
|
|
100
|
+
case "read":
|
|
101
|
+
case "edit":
|
|
102
|
+
case "write":
|
|
103
|
+
case "ls":
|
|
104
|
+
target = pick("path", "file", "filePath");
|
|
105
|
+
break;
|
|
106
|
+
case "grep":
|
|
107
|
+
case "find":
|
|
108
|
+
case "glob":
|
|
109
|
+
target = pick("pattern", "query", "path");
|
|
110
|
+
break;
|
|
111
|
+
case "web_search":
|
|
112
|
+
case "search":
|
|
113
|
+
target = pick("query");
|
|
114
|
+
break;
|
|
115
|
+
case "fetch":
|
|
116
|
+
case "web_fetch":
|
|
117
|
+
case "fetch_content":
|
|
118
|
+
target = pick("url");
|
|
119
|
+
break;
|
|
120
|
+
case "subagent":
|
|
121
|
+
target = pick("agent", "task");
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
target = pick("path", "command", "query", "pattern", "url", "file", "task");
|
|
125
|
+
}
|
|
126
|
+
return target ? `${toolName} ${target}` : toolName;
|
|
127
|
+
}
|
|
128
|
+
|
|
74
129
|
// ---------------------------------------------------------------------------
|
|
75
130
|
// MonitorStore
|
|
76
131
|
// ---------------------------------------------------------------------------
|
|
@@ -95,7 +150,6 @@ export class MonitorStore {
|
|
|
95
150
|
model,
|
|
96
151
|
status: "queued",
|
|
97
152
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
98
|
-
transcript: [],
|
|
99
153
|
});
|
|
100
154
|
this.notify();
|
|
101
155
|
return id;
|
|
@@ -120,29 +174,21 @@ export class MonitorStore {
|
|
|
120
174
|
this.notify();
|
|
121
175
|
}
|
|
122
176
|
|
|
123
|
-
|
|
177
|
+
/** Update the run's current one-line activity (what it is doing now). */
|
|
178
|
+
setActivity(id: number, text: string): void {
|
|
124
179
|
const run = this.find(id);
|
|
125
180
|
if (!run) return;
|
|
126
|
-
run.
|
|
181
|
+
run.activity = text;
|
|
127
182
|
this.notify();
|
|
128
183
|
}
|
|
129
184
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const segments = delta.split("\n");
|
|
136
|
-
for (let i = 0; i < segments.length; i++) {
|
|
137
|
-
const seg = segments[i];
|
|
138
|
-
const last = run.transcript[run.transcript.length - 1];
|
|
139
|
-
if (i === 0 && last && last.kind === "text") {
|
|
140
|
-
last.text += seg;
|
|
141
|
-
} else {
|
|
142
|
-
run.transcript.push({ kind: "text", text: seg });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
185
|
+
/** Remove a run (finished runs leave the widget). Returns the removed run. */
|
|
186
|
+
removeRun(id: number): RunView | undefined {
|
|
187
|
+
const index = this.runs.findIndex((r) => r.id === id);
|
|
188
|
+
if (index === -1) return undefined;
|
|
189
|
+
const [run] = this.runs.splice(index, 1);
|
|
145
190
|
this.notify();
|
|
191
|
+
return run;
|
|
146
192
|
}
|
|
147
193
|
|
|
148
194
|
getRuns(): RunView[] {
|
|
@@ -166,15 +212,6 @@ export class MonitorStore {
|
|
|
166
212
|
return parts.join(" · ");
|
|
167
213
|
}
|
|
168
214
|
|
|
169
|
-
/** Text of the most recent transcript entry (what the run is doing now). */
|
|
170
|
-
lastActivity(run: RunView): string | undefined {
|
|
171
|
-
for (let i = run.transcript.length - 1; i >= 0; i--) {
|
|
172
|
-
const text = run.transcript[i].text.trim();
|
|
173
|
-
if (text.length > 0) return text;
|
|
174
|
-
}
|
|
175
|
-
return undefined;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
215
|
private find(id: number): RunView | undefined {
|
|
179
216
|
return this.runs.find((r) => r.id === id);
|
|
180
217
|
}
|
package/src/spawn.ts
CHANGED
|
@@ -20,6 +20,10 @@ import type { AgentConfig, AgentSource } from "./agents.ts";
|
|
|
20
20
|
|
|
21
21
|
export const MAX_PARALLEL_TASKS = 8;
|
|
22
22
|
export const MAX_CONCURRENCY = 4;
|
|
23
|
+
/** Thinking level requested for every sub-agent: the strongest pi offers. pi's
|
|
24
|
+
* session layer clamps it adaptively to what the resolved model supports
|
|
25
|
+
* (max → xhigh → high → … → off), so weaker models degrade gracefully. */
|
|
26
|
+
export const SUBAGENT_THINKING_LEVEL = "max";
|
|
23
27
|
/** Max nesting depth for sub-agent -> sub-agent spawning (recursion guard). */
|
|
24
28
|
export const MAX_SUBAGENT_DEPTH = 2;
|
|
25
29
|
export const DEPTH_ENV_VAR = "PI_SUBAGENT_DEPTH";
|
|
@@ -59,7 +63,8 @@ export type SubagentLiveEvent =
|
|
|
59
63
|
| { kind: "usage"; usage: UsageStats; model?: string }
|
|
60
64
|
| { kind: "tool_start"; toolName: string; args: unknown }
|
|
61
65
|
| { kind: "tool_end"; toolName: string; isError: boolean }
|
|
62
|
-
| { kind: "
|
|
66
|
+
| { kind: "thinking" }
|
|
67
|
+
| { kind: "text" };
|
|
63
68
|
|
|
64
69
|
function emptyUsage(): UsageStats {
|
|
65
70
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
@@ -168,6 +173,8 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
|
|
|
168
173
|
|
|
169
174
|
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
170
175
|
if (agent.model) args.push("--model", agent.model);
|
|
176
|
+
// Strongest thinking by default; clamped adaptively per model by pi.
|
|
177
|
+
args.push("--thinking", SUBAGENT_THINKING_LEVEL);
|
|
171
178
|
if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
|
|
172
179
|
|
|
173
180
|
let tmpPromptDir: string | null = null;
|
|
@@ -237,12 +244,15 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
|
|
|
237
244
|
}
|
|
238
245
|
}
|
|
239
246
|
|
|
240
|
-
// Live event: streamed assistant text
|
|
241
|
-
if (event.type === "message_update"
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
// Live event: streamed assistant reasoning / output text
|
|
248
|
+
if (event.type === "message_update") {
|
|
249
|
+
const t = event.assistantMessageEvent?.type;
|
|
250
|
+
if (t === "thinking_delta" || t === "text_delta") {
|
|
251
|
+
if (onLive) {
|
|
252
|
+
try {
|
|
253
|
+
onLive({ kind: t === "thinking_delta" ? "thinking" : "text" });
|
|
254
|
+
} catch { /* never throw from event handling */ }
|
|
255
|
+
}
|
|
246
256
|
}
|
|
247
257
|
}
|
|
248
258
|
|
|
@@ -309,17 +319,32 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
|
|
|
309
319
|
|
|
310
320
|
proc.on("close", (code) => {
|
|
311
321
|
if (buffer.trim()) processLine(buffer);
|
|
312
|
-
// Live event: final status derived from exit code
|
|
322
|
+
// Live event: final status derived from exit code / abort state.
|
|
323
|
+
// code is null on signal termination (e.g. our own Esc abort), which
|
|
324
|
+
// must read as failure — never as a false "done".
|
|
313
325
|
if (onLive) {
|
|
314
326
|
try {
|
|
315
|
-
const failed =
|
|
327
|
+
const failed =
|
|
328
|
+
code !== 0 ||
|
|
329
|
+
wasAborted ||
|
|
330
|
+
(signal?.aborted ?? false) ||
|
|
331
|
+
currentResult.stopReason === "error" ||
|
|
332
|
+
currentResult.stopReason === "aborted";
|
|
316
333
|
onLive({ kind: "status", status: failed ? "failed" : "done" });
|
|
317
334
|
} catch { /* never throw from event handling */ }
|
|
318
335
|
}
|
|
319
336
|
resolve(code ?? 0);
|
|
320
337
|
});
|
|
321
338
|
|
|
322
|
-
proc.on("error", () =>
|
|
339
|
+
proc.on("error", () => {
|
|
340
|
+
// Spawn itself failed; close may never fire, so finish the run here.
|
|
341
|
+
if (onLive) {
|
|
342
|
+
try {
|
|
343
|
+
onLive({ kind: "status", status: "failed" });
|
|
344
|
+
} catch { /* never throw from event handling */ }
|
|
345
|
+
}
|
|
346
|
+
resolve(1);
|
|
347
|
+
});
|
|
323
348
|
|
|
324
349
|
if (signal) {
|
|
325
350
|
const killProc = (): void => {
|