pi-subagents 0.17.1 → 0.17.2
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/CHANGELOG.md +11 -0
- package/README.md +27 -0
- package/async-execution.ts +38 -13
- package/async-job-tracker.ts +36 -18
- package/execution.ts +58 -2
- package/index.ts +1 -1
- package/notify.ts +2 -1
- package/package.json +1 -1
- package/post-exit-stdio-guard.ts +85 -0
- package/session-tokens.ts +48 -0
- package/subagent-executor.ts +35 -29
- package/subagent-runner.ts +72 -42
- package/top-level-async.ts +13 -0
- package/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.17.2] - 2026-04-21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added `forceTopLevelAsync` so depth-0 delegated runs can be forced into background mode with `clarify: false`, while nested runs keep their existing behavior.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Background completion notifications now render `(no output)` instead of a blank body when a completion summary is empty or whitespace-only.
|
|
12
|
+
- Async status and token reporting now rerender more reliably when cleanup state changes, read token usage from `message.usage`, and prefer the newest session file when multiple async session files exist.
|
|
13
|
+
- Async/background startup now fails fast for invalid resolved `cwd` values and spawn failures instead of reporting false launch success.
|
|
14
|
+
- Sync and async runner paths now drain stuck child processes in bounded time, covering both post-exit stdio holders and children that emit a final message but never exit.
|
|
15
|
+
|
|
5
16
|
## [0.17.1] - 2026-04-20
|
|
6
17
|
|
|
7
18
|
### Added
|
package/README.md
CHANGED
|
@@ -855,6 +855,33 @@ This aggregated output becomes `{previous}` for the next step.
|
|
|
855
855
|
|
|
856
856
|
`pi-subagents` reads optional JSON config from `~/.pi/agent/extensions/subagent/config.json`.
|
|
857
857
|
|
|
858
|
+
### `asyncByDefault`
|
|
859
|
+
|
|
860
|
+
`asyncByDefault` makes top-level subagent calls use background execution when the request does not explicitly set `async`.
|
|
861
|
+
|
|
862
|
+
```json
|
|
863
|
+
{
|
|
864
|
+
"asyncByDefault": true
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
This only changes the default. Callers can still force foreground execution by setting `async: false` unless `forceTopLevelAsync` is also enabled.
|
|
869
|
+
|
|
870
|
+
### `forceTopLevelAsync`
|
|
871
|
+
|
|
872
|
+
`forceTopLevelAsync` forces depth-0 subagent execution into background mode. This is useful for automation setups that never want the top-level orchestrator to block on child runs.
|
|
873
|
+
|
|
874
|
+
```json
|
|
875
|
+
{
|
|
876
|
+
"forceTopLevelAsync": true
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
When enabled:
|
|
881
|
+
- top-level single, parallel, and chain runs are forced to `async: true`
|
|
882
|
+
- top-level clarify UI is bypassed by forcing `clarify: false`
|
|
883
|
+
- nested subagent calls still follow their own inherited depth and async settings
|
|
884
|
+
|
|
858
885
|
### `parallel`
|
|
859
886
|
|
|
860
887
|
`parallel` controls top-level `tasks` mode defaults and limits.
|
package/async-execution.ts
CHANGED
|
@@ -114,22 +114,39 @@ export function isAsyncAvailable(): boolean {
|
|
|
114
114
|
/**
|
|
115
115
|
* Spawn the async runner process
|
|
116
116
|
*/
|
|
117
|
-
function spawnRunner(cfg: object, suffix: string, cwd: string): number
|
|
118
|
-
if (!jitiCliPath)
|
|
119
|
-
|
|
117
|
+
function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number; error?: string } {
|
|
118
|
+
if (!jitiCliPath) {
|
|
119
|
+
return { error: "jiti for TypeScript execution could not be found" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const cwdStats = fs.statSync(cwd);
|
|
124
|
+
if (!cwdStats.isDirectory()) {
|
|
125
|
+
return { error: `cwd is not a directory: ${cwd}` };
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
return { error: `cwd does not exist: ${cwd}` };
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
|
|
121
132
|
const cfgPath = getAsyncConfigPath(suffix);
|
|
122
133
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg));
|
|
123
134
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
124
|
-
|
|
135
|
+
|
|
125
136
|
const proc = spawn(process.execPath, [jitiCliPath, runner, cfgPath], {
|
|
126
137
|
cwd,
|
|
127
138
|
detached: true,
|
|
128
139
|
stdio: "ignore",
|
|
129
140
|
windowsHide: true,
|
|
130
141
|
});
|
|
142
|
+
proc.on("error", (error) => {
|
|
143
|
+
console.error(`[pi-subagents] async spawn failed: ${error.message}`);
|
|
144
|
+
});
|
|
145
|
+
if (typeof proc.pid !== "number") {
|
|
146
|
+
return { error: `async runner did not produce a pid for cwd: ${cwd}` };
|
|
147
|
+
}
|
|
131
148
|
proc.unref();
|
|
132
|
-
return proc.pid;
|
|
149
|
+
return { pid: proc.pid };
|
|
133
150
|
}
|
|
134
151
|
|
|
135
152
|
function formatAsyncStartError(mode: "single" | "chain", message: string): AsyncExecutionResult {
|
|
@@ -260,9 +277,9 @@ export function executeAsyncChain(
|
|
|
260
277
|
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
261
278
|
});
|
|
262
279
|
|
|
263
|
-
let
|
|
280
|
+
let spawnResult: { pid?: number; error?: string } = {};
|
|
264
281
|
try {
|
|
265
|
-
|
|
282
|
+
spawnResult = spawnRunner(
|
|
266
283
|
{
|
|
267
284
|
id,
|
|
268
285
|
steps,
|
|
@@ -289,14 +306,18 @@ export function executeAsyncChain(
|
|
|
289
306
|
return formatAsyncStartError("chain", `Failed to start async chain '${id}': ${message}`);
|
|
290
307
|
}
|
|
291
308
|
|
|
292
|
-
if (
|
|
309
|
+
if (spawnResult.error) {
|
|
310
|
+
return formatAsyncStartError("chain", `Failed to start async chain '${id}': ${spawnResult.error}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (spawnResult.pid) {
|
|
293
314
|
const firstStep = chain[0];
|
|
294
315
|
const firstAgents = isParallelStep(firstStep)
|
|
295
316
|
? firstStep.parallel.map((t) => t.agent)
|
|
296
317
|
: [(firstStep as SequentialStep).agent];
|
|
297
318
|
ctx.pi.events.emit("subagent:started", {
|
|
298
319
|
id,
|
|
299
|
-
pid,
|
|
320
|
+
pid: spawnResult.pid,
|
|
300
321
|
agent: firstAgents[0],
|
|
301
322
|
task: isParallelStep(firstStep)
|
|
302
323
|
? firstStep.parallel[0]?.task?.slice(0, 50)
|
|
@@ -368,9 +389,9 @@ export function executeAsyncSingle(
|
|
|
368
389
|
|
|
369
390
|
const outputPath = resolveSingleOutputPath(params.output, ctx.cwd, runnerCwd);
|
|
370
391
|
const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath);
|
|
371
|
-
let
|
|
392
|
+
let spawnResult: { pid?: number; error?: string } = {};
|
|
372
393
|
try {
|
|
373
|
-
|
|
394
|
+
spawnResult = spawnRunner(
|
|
374
395
|
{
|
|
375
396
|
id,
|
|
376
397
|
steps: [
|
|
@@ -418,10 +439,14 @@ export function executeAsyncSingle(
|
|
|
418
439
|
return formatAsyncStartError("single", `Failed to start async run '${id}': ${message}`);
|
|
419
440
|
}
|
|
420
441
|
|
|
421
|
-
if (
|
|
442
|
+
if (spawnResult.error) {
|
|
443
|
+
return formatAsyncStartError("single", `Failed to start async run '${id}': ${spawnResult.error}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (spawnResult.pid) {
|
|
422
447
|
ctx.pi.events.emit("subagent:started", {
|
|
423
448
|
id,
|
|
424
|
-
pid,
|
|
449
|
+
pid: spawnResult.pid,
|
|
425
450
|
agent,
|
|
426
451
|
task: task?.slice(0, 50),
|
|
427
452
|
cwd: runnerCwd,
|
package/async-job-tracker.ts
CHANGED
|
@@ -7,18 +7,42 @@ import {
|
|
|
7
7
|
} from "./types.ts";
|
|
8
8
|
import { readStatus } from "./utils.ts";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
interface AsyncJobTrackerOptions {
|
|
11
|
+
completionRetentionMs?: number;
|
|
12
|
+
pollIntervalMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string, options: AsyncJobTrackerOptions = {}): {
|
|
11
16
|
ensurePoller: () => void;
|
|
12
17
|
handleStarted: (data: unknown) => void;
|
|
13
18
|
handleComplete: (data: unknown) => void;
|
|
14
19
|
resetJobs: (ctx?: ExtensionContext) => void;
|
|
15
20
|
} {
|
|
21
|
+
const completionRetentionMs = options.completionRetentionMs ?? 10000;
|
|
22
|
+
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
23
|
+
const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
|
|
24
|
+
renderWidget(ctx, jobs);
|
|
25
|
+
ctx.ui.requestRender?.();
|
|
26
|
+
};
|
|
27
|
+
const scheduleCleanup = (asyncId: string) => {
|
|
28
|
+
const existingTimer = state.cleanupTimers.get(asyncId);
|
|
29
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
state.cleanupTimers.delete(asyncId);
|
|
32
|
+
state.asyncJobs.delete(asyncId);
|
|
33
|
+
if (state.lastUiContext) {
|
|
34
|
+
rerenderWidget(state.lastUiContext);
|
|
35
|
+
}
|
|
36
|
+
}, completionRetentionMs);
|
|
37
|
+
state.cleanupTimers.set(asyncId, timer);
|
|
38
|
+
};
|
|
39
|
+
|
|
16
40
|
const ensurePoller = () => {
|
|
17
41
|
if (state.poller) return;
|
|
18
42
|
state.poller = setInterval(() => {
|
|
19
43
|
if (!state.lastUiContext || !state.lastUiContext.hasUI) return;
|
|
20
44
|
if (state.asyncJobs.size === 0) {
|
|
21
|
-
|
|
45
|
+
rerenderWidget(state.lastUiContext, []);
|
|
22
46
|
if (state.poller) {
|
|
23
47
|
clearInterval(state.poller);
|
|
24
48
|
state.poller = null;
|
|
@@ -27,12 +51,10 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
27
51
|
}
|
|
28
52
|
|
|
29
53
|
for (const job of state.asyncJobs.values()) {
|
|
30
|
-
if (job.status === "complete" || job.status === "failed") {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
54
|
try {
|
|
34
55
|
const status = readStatus(job.asyncDir);
|
|
35
56
|
if (status) {
|
|
57
|
+
const previousStatus = job.status;
|
|
36
58
|
job.status = status.state;
|
|
37
59
|
job.mode = status.mode;
|
|
38
60
|
job.currentStep = status.currentStep ?? job.currentStep;
|
|
@@ -46,6 +68,9 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
46
68
|
job.outputFile = status.outputFile ?? job.outputFile;
|
|
47
69
|
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
48
70
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
71
|
+
if ((job.status === "complete" || job.status === "failed") && previousStatus !== job.status) {
|
|
72
|
+
scheduleCleanup(job.asyncId);
|
|
73
|
+
}
|
|
49
74
|
continue;
|
|
50
75
|
}
|
|
51
76
|
job.status = job.status === "queued" ? "running" : job.status;
|
|
@@ -57,8 +82,8 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
57
82
|
}
|
|
58
83
|
}
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
},
|
|
85
|
+
rerenderWidget(state.lastUiContext);
|
|
86
|
+
}, pollIntervalMs);
|
|
62
87
|
state.poller.unref?.();
|
|
63
88
|
};
|
|
64
89
|
|
|
@@ -84,7 +109,7 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
84
109
|
updatedAt: now,
|
|
85
110
|
});
|
|
86
111
|
if (state.lastUiContext) {
|
|
87
|
-
|
|
112
|
+
rerenderWidget(state.lastUiContext);
|
|
88
113
|
ensurePoller();
|
|
89
114
|
}
|
|
90
115
|
};
|
|
@@ -100,16 +125,9 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
100
125
|
if (result.asyncDir) job.asyncDir = result.asyncDir;
|
|
101
126
|
}
|
|
102
127
|
if (state.lastUiContext) {
|
|
103
|
-
|
|
128
|
+
rerenderWidget(state.lastUiContext);
|
|
104
129
|
}
|
|
105
|
-
|
|
106
|
-
state.cleanupTimers.delete(asyncId);
|
|
107
|
-
state.asyncJobs.delete(asyncId);
|
|
108
|
-
if (state.lastUiContext) {
|
|
109
|
-
renderWidget(state.lastUiContext, Array.from(state.asyncJobs.values()));
|
|
110
|
-
}
|
|
111
|
-
}, 10000);
|
|
112
|
-
state.cleanupTimers.set(asyncId, timer);
|
|
130
|
+
scheduleCleanup(asyncId);
|
|
113
131
|
};
|
|
114
132
|
|
|
115
133
|
const resetJobs = (ctx?: ExtensionContext) => {
|
|
@@ -121,7 +139,7 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
121
139
|
state.resultFileCoalescer.clear();
|
|
122
140
|
if (ctx?.hasUI) {
|
|
123
141
|
state.lastUiContext = ctx;
|
|
124
|
-
|
|
142
|
+
rerenderWidget(ctx, []);
|
|
125
143
|
}
|
|
126
144
|
};
|
|
127
145
|
|
package/execution.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { buildSkillInjection, resolveSkillsWithFallback } from "./skills.ts";
|
|
35
35
|
import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
36
36
|
import { createJsonlWriter } from "./jsonl-writer.ts";
|
|
37
|
+
import { attachPostExitStdioGuard, trySignalChild } from "./post-exit-stdio-guard.ts";
|
|
37
38
|
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "./pi-args.ts";
|
|
38
39
|
import { captureSingleOutputSnapshot, resolveSingleOutput, type SingleOutputSnapshot } from "./single-output.ts";
|
|
39
40
|
import {
|
|
@@ -188,6 +189,42 @@ async function runSingleAttempt(
|
|
|
188
189
|
finish(-2);
|
|
189
190
|
};
|
|
190
191
|
|
|
192
|
+
// If the child emits its final assistant message but never exits,
|
|
193
|
+
// start a bounded drain window and force termination if needed.
|
|
194
|
+
const FINAL_DRAIN_MS = 5000;
|
|
195
|
+
const HARD_KILL_MS = 3000;
|
|
196
|
+
let childExited = false;
|
|
197
|
+
let forcedTerminationSignal = false;
|
|
198
|
+
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
199
|
+
let finalHardKillTimer: NodeJS.Timeout | undefined;
|
|
200
|
+
const clearFinalDrainTimers = () => {
|
|
201
|
+
if (finalDrainTimer) {
|
|
202
|
+
clearTimeout(finalDrainTimer);
|
|
203
|
+
finalDrainTimer = undefined;
|
|
204
|
+
}
|
|
205
|
+
if (finalHardKillTimer) {
|
|
206
|
+
clearTimeout(finalHardKillTimer);
|
|
207
|
+
finalHardKillTimer = undefined;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const startFinalDrain = () => {
|
|
211
|
+
if (childExited || finalDrainTimer || settled || processClosed || detached) return;
|
|
212
|
+
finalDrainTimer = setTimeout(() => {
|
|
213
|
+
if (settled || processClosed || detached) return;
|
|
214
|
+
const termSent = trySignalChild(proc, "SIGTERM");
|
|
215
|
+
if (!termSent) return;
|
|
216
|
+
forcedTerminationSignal = true;
|
|
217
|
+
result.error = result.error
|
|
218
|
+
?? `Subagent process did not exit within ${FINAL_DRAIN_MS}ms after its final message. Forcing termination.`;
|
|
219
|
+
finalHardKillTimer = setTimeout(() => {
|
|
220
|
+
if (settled || processClosed || detached) return;
|
|
221
|
+
forcedTerminationSignal = trySignalChild(proc, "SIGKILL") || forcedTerminationSignal;
|
|
222
|
+
}, HARD_KILL_MS);
|
|
223
|
+
finalHardKillTimer.unref?.();
|
|
224
|
+
}, FINAL_DRAIN_MS);
|
|
225
|
+
finalDrainTimer.unref?.();
|
|
226
|
+
};
|
|
227
|
+
|
|
191
228
|
const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
|
|
192
229
|
if (!options.allowIntercomDetach || detached || processClosed) return;
|
|
193
230
|
if (!payload || typeof payload !== "object") return;
|
|
@@ -202,6 +239,8 @@ async function runSingleAttempt(
|
|
|
202
239
|
const finish = (code: number) => {
|
|
203
240
|
if (settled) return;
|
|
204
241
|
settled = true;
|
|
242
|
+
clearFinalDrainTimers();
|
|
243
|
+
clearStdioGuard();
|
|
205
244
|
unsubscribeIntercomDetach?.();
|
|
206
245
|
removeAbortListener?.();
|
|
207
246
|
resolve(code);
|
|
@@ -279,6 +318,13 @@ async function runSingleAttempt(
|
|
|
279
318
|
if (!result.model && evt.message.model) result.model = evt.message.model;
|
|
280
319
|
if (evt.message.errorMessage) result.error = evt.message.errorMessage;
|
|
281
320
|
appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
|
|
321
|
+
// Final assistant message: start the exit drain window.
|
|
322
|
+
const stopReason = (evt.message as { stopReason?: string }).stopReason;
|
|
323
|
+
const hasToolCall = Array.isArray(evt.message.content)
|
|
324
|
+
&& evt.message.content.some((part) => (part as { type?: string }).type === "toolCall");
|
|
325
|
+
if (stopReason === "stop" && !hasToolCall) {
|
|
326
|
+
startFinalDrain();
|
|
327
|
+
}
|
|
282
328
|
}
|
|
283
329
|
fireUpdate();
|
|
284
330
|
}
|
|
@@ -292,6 +338,7 @@ async function runSingleAttempt(
|
|
|
292
338
|
|
|
293
339
|
let stderrBuf = "";
|
|
294
340
|
|
|
341
|
+
const clearStdioGuard = attachPostExitStdioGuard(proc, { idleMs: 2000, hardMs: 8000 });
|
|
295
342
|
proc.stdout.on("data", (d) => {
|
|
296
343
|
buf += d.toString();
|
|
297
344
|
const lines = buf.split("\n");
|
|
@@ -301,7 +348,13 @@ async function runSingleAttempt(
|
|
|
301
348
|
proc.stderr.on("data", (d) => {
|
|
302
349
|
stderrBuf += d.toString();
|
|
303
350
|
});
|
|
304
|
-
proc.on("
|
|
351
|
+
proc.on("exit", () => {
|
|
352
|
+
childExited = true;
|
|
353
|
+
clearFinalDrainTimers();
|
|
354
|
+
});
|
|
355
|
+
proc.on("close", (code, signal) => {
|
|
356
|
+
clearFinalDrainTimers();
|
|
357
|
+
clearStdioGuard();
|
|
305
358
|
void jsonlWriter.close().catch(() => {
|
|
306
359
|
// JSONL artifact flush is best effort.
|
|
307
360
|
});
|
|
@@ -315,9 +368,12 @@ async function runSingleAttempt(
|
|
|
315
368
|
if (code !== 0 && stderrBuf.trim() && !result.error) {
|
|
316
369
|
result.error = stderrBuf.trim();
|
|
317
370
|
}
|
|
318
|
-
|
|
371
|
+
const finalCode = forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
|
|
372
|
+
finish(finalCode);
|
|
319
373
|
});
|
|
320
374
|
proc.on("error", (error) => {
|
|
375
|
+
clearFinalDrainTimers();
|
|
376
|
+
clearStdioGuard();
|
|
321
377
|
void jsonlWriter.close().catch(() => {
|
|
322
378
|
// JSONL artifact flush is best effort.
|
|
323
379
|
});
|
package/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Toggle: async parameter (default: false, configurable via config.json)
|
|
10
10
|
*
|
|
11
11
|
* Config file: ~/.pi/agent/extensions/subagent/config.json
|
|
12
|
-
* { "asyncByDefault": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
12
|
+
* { "asyncByDefault": true, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as fs from "node:fs";
|
package/notify.ts
CHANGED
|
@@ -54,10 +54,11 @@ export default function registerSubagentNotify(pi: ExtensionAPI): void {
|
|
|
54
54
|
extra.push(`Session file: ${result.sessionFile}`);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const summary = result.summary.trim() ? result.summary : "(no output)";
|
|
57
58
|
const content = [
|
|
58
59
|
`Background task ${status}: **${agent}**${taskInfo}`,
|
|
59
60
|
"",
|
|
60
|
-
|
|
61
|
+
summary,
|
|
61
62
|
extra.length ? "" : undefined,
|
|
62
63
|
extra.length ? extra.join("\n") : undefined,
|
|
63
64
|
]
|
package/package.json
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
interface PostExitStdioGuardOptions {
|
|
4
|
+
idleMs: number;
|
|
5
|
+
hardMs: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ChildWithPipedStdio {
|
|
9
|
+
stdout: ChildProcess["stdout"];
|
|
10
|
+
stderr: ChildProcess["stderr"];
|
|
11
|
+
on: ChildProcess["on"];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ChildWithKill {
|
|
15
|
+
kill(signal?: NodeJS.Signals | number): boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function trySignalChild(child: ChildWithKill, signal: NodeJS.Signals): boolean {
|
|
19
|
+
try {
|
|
20
|
+
return child.kill(signal);
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function attachPostExitStdioGuard(
|
|
27
|
+
child: ChildWithPipedStdio,
|
|
28
|
+
options: PostExitStdioGuardOptions,
|
|
29
|
+
): () => void {
|
|
30
|
+
const { idleMs, hardMs } = options;
|
|
31
|
+
let exited = false;
|
|
32
|
+
let stdoutEnded = false;
|
|
33
|
+
let stderrEnded = false;
|
|
34
|
+
let idleTimer: NodeJS.Timeout | undefined;
|
|
35
|
+
let hardTimer: NodeJS.Timeout | undefined;
|
|
36
|
+
|
|
37
|
+
const destroyUnendedStdio = () => {
|
|
38
|
+
if (!stdoutEnded) {
|
|
39
|
+
try { child.stdout?.destroy(); } catch {}
|
|
40
|
+
}
|
|
41
|
+
if (!stderrEnded) {
|
|
42
|
+
try { child.stderr?.destroy(); } catch {}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const clearTimers = () => {
|
|
47
|
+
if (idleTimer) {
|
|
48
|
+
clearTimeout(idleTimer);
|
|
49
|
+
idleTimer = undefined;
|
|
50
|
+
}
|
|
51
|
+
if (hardTimer) {
|
|
52
|
+
clearTimeout(hardTimer);
|
|
53
|
+
hardTimer = undefined;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const armIdleTimer = () => {
|
|
58
|
+
if (!exited) return;
|
|
59
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
60
|
+
idleTimer = setTimeout(destroyUnendedStdio, idleMs);
|
|
61
|
+
idleTimer.unref?.();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
child.stdout?.on("data", armIdleTimer);
|
|
65
|
+
child.stderr?.on("data", armIdleTimer);
|
|
66
|
+
child.stdout?.on("end", () => {
|
|
67
|
+
stdoutEnded = true;
|
|
68
|
+
if (stdoutEnded && stderrEnded) clearTimers();
|
|
69
|
+
});
|
|
70
|
+
child.stderr?.on("end", () => {
|
|
71
|
+
stderrEnded = true;
|
|
72
|
+
if (stdoutEnded && stderrEnded) clearTimers();
|
|
73
|
+
});
|
|
74
|
+
child.on("exit", () => {
|
|
75
|
+
exited = true;
|
|
76
|
+
armIdleTimer();
|
|
77
|
+
if (hardTimer) return;
|
|
78
|
+
hardTimer = setTimeout(destroyUnendedStdio, hardMs);
|
|
79
|
+
hardTimer.unref?.();
|
|
80
|
+
});
|
|
81
|
+
child.on("close", clearTimers);
|
|
82
|
+
child.on("error", clearTimers);
|
|
83
|
+
|
|
84
|
+
return clearTimers;
|
|
85
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface TokenUsage {
|
|
5
|
+
input: number;
|
|
6
|
+
output: number;
|
|
7
|
+
total: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function findLatestSessionFile(sessionDir: string): string | null {
|
|
11
|
+
try {
|
|
12
|
+
const files = fs.readdirSync(sessionDir)
|
|
13
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
14
|
+
.map((f) => path.join(sessionDir, f));
|
|
15
|
+
if (files.length === 0) return null;
|
|
16
|
+
files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
17
|
+
return files[0] ?? null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseSessionTokens(sessionDir: string): TokenUsage | null {
|
|
24
|
+
const sessionFile = findLatestSessionFile(sessionDir);
|
|
25
|
+
if (!sessionFile) return null;
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(sessionFile, "utf-8");
|
|
28
|
+
let input = 0;
|
|
29
|
+
let output = 0;
|
|
30
|
+
for (const line of content.split("\n")) {
|
|
31
|
+
if (!line.trim()) continue;
|
|
32
|
+
try {
|
|
33
|
+
const entry = JSON.parse(line);
|
|
34
|
+
const usage = entry.usage ?? entry.message?.usage;
|
|
35
|
+
if (usage) {
|
|
36
|
+
input += usage.inputTokens ?? usage.input ?? 0;
|
|
37
|
+
output += usage.outputTokens ?? usage.output ?? 0;
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore malformed lines while scanning usage entries.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { input, output, total: input + output };
|
|
44
|
+
} catch {
|
|
45
|
+
// Usage extraction should not fail the run.
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/subagent-executor.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { createForkContextResolver } from "./fork-context.ts";
|
|
|
26
26
|
import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget } from "./intercom-bridge.ts";
|
|
27
27
|
import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
|
|
28
28
|
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "./utils.ts";
|
|
29
|
+
import { applyForceTopLevelAsyncOverride } from "./top-level-async.ts";
|
|
29
30
|
import {
|
|
30
31
|
cleanupWorktrees,
|
|
31
32
|
createWorktrees,
|
|
@@ -1209,32 +1210,38 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1209
1210
|
if (normalized.error) return normalized.error;
|
|
1210
1211
|
const normalizedParams = normalized.params!;
|
|
1211
1212
|
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1213
|
+
const effectiveParams = applyForceTopLevelAsyncOverride(
|
|
1214
|
+
normalizedParams,
|
|
1215
|
+
depth,
|
|
1216
|
+
deps.config.forceTopLevelAsync === true,
|
|
1217
|
+
);
|
|
1218
|
+
|
|
1219
|
+
const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope);
|
|
1220
|
+
const effectiveCwd = effectiveParams.cwd ?? ctx.cwd;
|
|
1214
1221
|
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
1215
1222
|
deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1216
1223
|
const discoveredAgents = deps.discoverAgents(effectiveCwd, scope).agents;
|
|
1217
1224
|
const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
|
|
1218
1225
|
const intercomBridge = resolveIntercomBridge({
|
|
1219
1226
|
config: deps.config.intercomBridge,
|
|
1220
|
-
context:
|
|
1227
|
+
context: effectiveParams.context,
|
|
1221
1228
|
orchestratorTarget: sessionName,
|
|
1222
1229
|
});
|
|
1223
1230
|
const agents = intercomBridge.active
|
|
1224
1231
|
? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
|
|
1225
1232
|
: discoveredAgents;
|
|
1226
1233
|
const runId = randomUUID().slice(0, 8);
|
|
1227
|
-
const shareEnabled =
|
|
1228
|
-
const hasChain = (
|
|
1229
|
-
const hasTasks = (
|
|
1230
|
-
const hasSingle = Boolean(
|
|
1234
|
+
const shareEnabled = effectiveParams.share === true;
|
|
1235
|
+
const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
|
|
1236
|
+
const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
|
|
1237
|
+
const hasSingle = Boolean(effectiveParams.agent && effectiveParams.task);
|
|
1231
1238
|
const allowClarifyTaskPrompt = hasChain
|
|
1232
|
-
&&
|
|
1239
|
+
&& effectiveParams.clarify === true
|
|
1233
1240
|
&& ctx.hasUI
|
|
1234
|
-
&& !(
|
|
1241
|
+
&& !(effectiveParams.chain?.some(isParallelStep) ?? false);
|
|
1235
1242
|
|
|
1236
1243
|
const validationError = validateExecutionInput(
|
|
1237
|
-
|
|
1244
|
+
effectiveParams,
|
|
1238
1245
|
agents,
|
|
1239
1246
|
hasChain,
|
|
1240
1247
|
hasTasks,
|
|
@@ -1245,25 +1252,24 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1245
1252
|
|
|
1246
1253
|
let sessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
|
|
1247
1254
|
try {
|
|
1248
|
-
sessionFileForIndex = createForkContextResolver(ctx.sessionManager,
|
|
1255
|
+
sessionFileForIndex = createForkContextResolver(ctx.sessionManager, effectiveParams.context).sessionFileForIndex;
|
|
1249
1256
|
} catch (error) {
|
|
1250
|
-
return toExecutionErrorResult(
|
|
1257
|
+
return toExecutionErrorResult(effectiveParams, error);
|
|
1251
1258
|
}
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
const backgroundRequestedWhileClarifying = hasTasks && requestedAsync && normalizedParams.clarify === true;
|
|
1259
|
+
const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
|
|
1260
|
+
const backgroundRequestedWhileClarifying = hasTasks && requestedAsync && effectiveParams.clarify === true;
|
|
1255
1261
|
const effectiveAsync = requestedAsync
|
|
1256
|
-
&& (hasChain ?
|
|
1262
|
+
&& (hasChain ? effectiveParams.clarify === false : effectiveParams.clarify !== true);
|
|
1257
1263
|
|
|
1258
1264
|
const artifactConfig: ArtifactConfig = {
|
|
1259
1265
|
...DEFAULT_ARTIFACT_CONFIG,
|
|
1260
|
-
enabled:
|
|
1266
|
+
enabled: effectiveParams.artifacts !== false,
|
|
1261
1267
|
};
|
|
1262
1268
|
const artifactsDir = effectiveAsync ? deps.tempArtifactsDir : getArtifactsDir(parentSessionFile);
|
|
1263
1269
|
|
|
1264
1270
|
let sessionRoot: string;
|
|
1265
|
-
if (
|
|
1266
|
-
sessionRoot = path.resolve(deps.expandTilde(
|
|
1271
|
+
if (effectiveParams.sessionDir) {
|
|
1272
|
+
sessionRoot = path.resolve(deps.expandTilde(effectiveParams.sessionDir));
|
|
1267
1273
|
} else {
|
|
1268
1274
|
const baseSessionRoot = deps.config.defaultSessionDir
|
|
1269
1275
|
? path.resolve(deps.expandTilde(deps.config.defaultSessionDir))
|
|
@@ -1275,7 +1281,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1275
1281
|
} catch (error) {
|
|
1276
1282
|
const message = error instanceof Error ? error.message : String(error);
|
|
1277
1283
|
return toExecutionErrorResult(
|
|
1278
|
-
|
|
1284
|
+
effectiveParams,
|
|
1279
1285
|
new Error(`Failed to create session directory '${sessionRoot}': ${message}`),
|
|
1280
1286
|
);
|
|
1281
1287
|
}
|
|
@@ -1283,11 +1289,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1283
1289
|
path.join(sessionRoot, `run-${idx ?? 0}`);
|
|
1284
1290
|
|
|
1285
1291
|
const onUpdateWithContext = onUpdate
|
|
1286
|
-
? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r,
|
|
1292
|
+
? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
|
|
1287
1293
|
: undefined;
|
|
1288
1294
|
|
|
1289
1295
|
const execData: ExecutionContextData = {
|
|
1290
|
-
params:
|
|
1296
|
+
params: effectiveParams,
|
|
1291
1297
|
effectiveCwd,
|
|
1292
1298
|
ctx,
|
|
1293
1299
|
signal,
|
|
@@ -1306,18 +1312,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1306
1312
|
|
|
1307
1313
|
try {
|
|
1308
1314
|
const asyncResult = runAsyncPath(execData, deps);
|
|
1309
|
-
if (asyncResult) return withForkContext(asyncResult,
|
|
1315
|
+
if (asyncResult) return withForkContext(asyncResult, effectiveParams.context);
|
|
1310
1316
|
|
|
1311
|
-
if (hasChain &&
|
|
1312
|
-
return withForkContext(await runChainPath(execData, deps),
|
|
1317
|
+
if (hasChain && effectiveParams.chain) {
|
|
1318
|
+
return withForkContext(await runChainPath(execData, deps), effectiveParams.context);
|
|
1313
1319
|
}
|
|
1314
1320
|
|
|
1315
|
-
if (hasTasks &&
|
|
1316
|
-
return withForkContext(await runParallelPath(execData, deps),
|
|
1321
|
+
if (hasTasks && effectiveParams.tasks) {
|
|
1322
|
+
return withForkContext(await runParallelPath(execData, deps), effectiveParams.context);
|
|
1317
1323
|
}
|
|
1318
1324
|
|
|
1319
1325
|
if (hasSingle) {
|
|
1320
|
-
return withForkContext(await runSinglePath(execData, deps),
|
|
1326
|
+
return withForkContext(await runSinglePath(execData, deps), effectiveParams.context);
|
|
1321
1327
|
}
|
|
1322
1328
|
} catch (error) {
|
|
1323
1329
|
return toExecutionErrorResult(normalizedParams, error);
|
|
@@ -1327,7 +1333,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1327
1333
|
content: [{ type: "text", text: "Invalid params" }],
|
|
1328
1334
|
isError: true,
|
|
1329
1335
|
details: { mode: "single" as const, results: [] },
|
|
1330
|
-
},
|
|
1336
|
+
}, effectiveParams.context);
|
|
1331
1337
|
};
|
|
1332
1338
|
|
|
1333
1339
|
return { execute };
|
package/subagent-runner.ts
CHANGED
|
@@ -28,7 +28,9 @@ import {
|
|
|
28
28
|
} from "./parallel-utils.ts";
|
|
29
29
|
import { buildPiArgs, cleanupTempDir } from "./pi-args.ts";
|
|
30
30
|
import { formatModelAttemptNote, isRetryableModelFailure } from "./model-fallback.ts";
|
|
31
|
+
import { attachPostExitStdioGuard, trySignalChild } from "./post-exit-stdio-guard.ts";
|
|
31
32
|
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "./utils.ts";
|
|
33
|
+
import { parseSessionTokens, type TokenUsage } from "./session-tokens.ts";
|
|
32
34
|
import {
|
|
33
35
|
cleanupWorktrees,
|
|
34
36
|
createWorktrees,
|
|
@@ -89,38 +91,6 @@ function findLatestSessionFile(sessionDir: string): string | null {
|
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
interface TokenUsage {
|
|
93
|
-
input: number;
|
|
94
|
-
output: number;
|
|
95
|
-
total: number;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function parseSessionTokens(sessionDir: string): TokenUsage | null {
|
|
99
|
-
const sessionFile = findLatestSessionFile(sessionDir);
|
|
100
|
-
if (!sessionFile) return null;
|
|
101
|
-
try {
|
|
102
|
-
const content = fs.readFileSync(sessionFile, "utf-8");
|
|
103
|
-
let input = 0;
|
|
104
|
-
let output = 0;
|
|
105
|
-
for (const line of content.split("\n")) {
|
|
106
|
-
if (!line.trim()) continue;
|
|
107
|
-
try {
|
|
108
|
-
const entry = JSON.parse(line);
|
|
109
|
-
if (entry.usage) {
|
|
110
|
-
input += entry.usage.inputTokens ?? entry.usage.input ?? 0;
|
|
111
|
-
output += entry.usage.outputTokens ?? entry.usage.output ?? 0;
|
|
112
|
-
}
|
|
113
|
-
} catch {
|
|
114
|
-
// Ignore malformed lines while scanning usage entries.
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return { input, output, total: input + output };
|
|
118
|
-
} catch {
|
|
119
|
-
// Usage extraction should not fail the run.
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
94
|
function emptyUsage(): Usage {
|
|
125
95
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
126
96
|
}
|
|
@@ -248,13 +218,18 @@ function runPiStreaming(
|
|
|
248
218
|
if (event.message.model) model = event.message.model;
|
|
249
219
|
if (event.message.errorMessage) error = event.message.errorMessage;
|
|
250
220
|
const eventUsage = event.message.usage;
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
221
|
+
if (eventUsage) {
|
|
222
|
+
usage.turns++;
|
|
223
|
+
usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
|
|
224
|
+
usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
|
|
225
|
+
usage.cacheRead += eventUsage.cacheRead ?? 0;
|
|
226
|
+
usage.cacheWrite += eventUsage.cacheWrite ?? 0;
|
|
227
|
+
usage.cost += eventUsage.cost?.total ?? 0;
|
|
228
|
+
}
|
|
229
|
+
const stopReason = (event.message as { stopReason?: string }).stopReason;
|
|
230
|
+
const hasToolCall = Array.isArray(event.message.content)
|
|
231
|
+
&& event.message.content.some((part) => (part as { type?: string }).type === "toolCall");
|
|
232
|
+
if (stopReason === "stop" && !hasToolCall) startFinalDrain();
|
|
258
233
|
}
|
|
259
234
|
};
|
|
260
235
|
|
|
@@ -271,6 +246,16 @@ function runPiStreaming(
|
|
|
271
246
|
}
|
|
272
247
|
};
|
|
273
248
|
|
|
249
|
+
// Guard both cases that can leave the parent waiting on `close` forever:
|
|
250
|
+
// a lingering stdio holder after `exit`, or a child that never exits.
|
|
251
|
+
const FINAL_DRAIN_MS = 5000;
|
|
252
|
+
const HARD_KILL_MS = 3000;
|
|
253
|
+
let childExited = false;
|
|
254
|
+
let forcedTerminationSignal = false;
|
|
255
|
+
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
256
|
+
let finalHardKillTimer: NodeJS.Timeout | undefined;
|
|
257
|
+
let settled = false;
|
|
258
|
+
const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
|
|
274
259
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
275
260
|
const text = chunk.toString();
|
|
276
261
|
stdoutBuf += text;
|
|
@@ -282,16 +267,61 @@ function runPiStreaming(
|
|
|
282
267
|
child.stderr.on("data", (chunk: Buffer) => {
|
|
283
268
|
processStderrText(chunk.toString());
|
|
284
269
|
});
|
|
285
|
-
|
|
286
|
-
|
|
270
|
+
const clearDrainTimers = () => {
|
|
271
|
+
if (finalDrainTimer) {
|
|
272
|
+
clearTimeout(finalDrainTimer);
|
|
273
|
+
finalDrainTimer = undefined;
|
|
274
|
+
}
|
|
275
|
+
if (finalHardKillTimer) {
|
|
276
|
+
clearTimeout(finalHardKillTimer);
|
|
277
|
+
finalHardKillTimer = undefined;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
function startFinalDrain(): void {
|
|
281
|
+
if (childExited || finalDrainTimer || settled) return;
|
|
282
|
+
finalDrainTimer = setTimeout(() => {
|
|
283
|
+
if (settled) return;
|
|
284
|
+
const termSent = trySignalChild(child, "SIGTERM");
|
|
285
|
+
if (!termSent) return;
|
|
286
|
+
forcedTerminationSignal = true;
|
|
287
|
+
if (!error) {
|
|
288
|
+
error = `Subagent process did not exit within ${FINAL_DRAIN_MS}ms after its final message. Forcing termination.`;
|
|
289
|
+
}
|
|
290
|
+
finalHardKillTimer = setTimeout(() => {
|
|
291
|
+
if (settled) return;
|
|
292
|
+
forcedTerminationSignal = trySignalChild(child, "SIGKILL") || forcedTerminationSignal;
|
|
293
|
+
}, HARD_KILL_MS);
|
|
294
|
+
finalHardKillTimer.unref?.();
|
|
295
|
+
}, FINAL_DRAIN_MS);
|
|
296
|
+
finalDrainTimer.unref?.();
|
|
297
|
+
}
|
|
298
|
+
child.on("exit", () => {
|
|
299
|
+
childExited = true;
|
|
300
|
+
clearDrainTimers();
|
|
301
|
+
});
|
|
302
|
+
child.on("close", (exitCode, signal) => {
|
|
303
|
+
settled = true;
|
|
304
|
+
clearDrainTimers();
|
|
305
|
+
clearStdioGuard();
|
|
287
306
|
if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
|
|
288
307
|
if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
|
|
289
308
|
outputStream.end();
|
|
290
309
|
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
291
|
-
resolve({
|
|
310
|
+
resolve({
|
|
311
|
+
stderr,
|
|
312
|
+
exitCode: forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
313
|
+
messages,
|
|
314
|
+
usage,
|
|
315
|
+
model,
|
|
316
|
+
error,
|
|
317
|
+
finalOutput,
|
|
318
|
+
});
|
|
292
319
|
});
|
|
293
320
|
|
|
294
321
|
child.on("error", (spawnError) => {
|
|
322
|
+
settled = true;
|
|
323
|
+
clearDrainTimers();
|
|
324
|
+
clearStdioGuard();
|
|
295
325
|
outputStream.end();
|
|
296
326
|
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
297
327
|
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AsyncOverrideParams {
|
|
2
|
+
async?: boolean;
|
|
3
|
+
clarify?: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function applyForceTopLevelAsyncOverride<T extends AsyncOverrideParams>(
|
|
7
|
+
params: T,
|
|
8
|
+
depth: number,
|
|
9
|
+
forceTopLevelAsync: boolean,
|
|
10
|
+
): T {
|
|
11
|
+
if (!(depth === 0 && forceTopLevelAsync)) return params;
|
|
12
|
+
return { ...params, async: true, clarify: false };
|
|
13
|
+
}
|
package/types.ts
CHANGED