killeros 1.4.7 → 1.4.8
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 +8 -0
- package/README.md +5 -5
- package/package.json +1 -1
- package/subagent-process.ts +19 -1
- package/subagents.ts +89 -22
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to KillerOS are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.4.8] - 2026-08-02
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Parallel batches with write-capable roles now run through a shared pool by default; `writerConcurrency` caps the entire batch and setting it to `1` serializes the writer-containing batch. Reader-only batches reject `writerConcurrency` because it does not apply.
|
|
10
|
+
|
|
5
11
|
## [1.4.7] - 2026-08-01
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
8
14
|
|
|
9
15
|
- Serialized every write-capable task in a parallel batch in input order instead of rejecting batches with multiple writers.
|
|
16
|
+
- Added opt-in `writerConcurrency` scheduling for independent batches while keeping serialization as the safe default and documenting shared-worktree conflict responsibility.
|
|
17
|
+
- Parent tool-call aborts now settle only queued tasks; active children finish naturally, and session directories remain until child exit is confirmed.
|
|
10
18
|
- Settled queued tasks on interrupted parallel batches and documented the shared-worktree execution model.
|
|
11
19
|
- Restricted the `message` parameter to `action: "steer"` and added focused regression coverage.
|
|
12
20
|
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
|
33
33
|
Pin an install to a release:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.4.
|
|
36
|
+
pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.4.8
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -97,13 +97,13 @@ KillerOS ships `planner`, `reviewer`, `scout`, and `security` as read-only roles
|
|
|
97
97
|
|
|
98
98
|
The default `agentScope: "user"` uses bundled and personal roles. Use `"project"` or `"both"` to opt into trusted project roles; a selected project override requires interactive confirmation. Role frontmatter requires `name`, `description`, `access`, and an explicit `tools` list. Optional fields are `model`, `thinking`, and `timeoutMs`. Every bundled role shows `model: inherit` and `thinking: inherit` as editable placeholders. Replace them with an available `provider/model` and a separate thinking level when you want to pin a role; `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max` are checked against that model’s supported capabilities.
|
|
99
99
|
|
|
100
|
-
The tool supports a single `agent` + `task`, parallel `tasks`, or a sequential `chain` whose task text may include `{previous}`.
|
|
100
|
+
The tool supports a single `agent` + `task`, parallel `tasks`, or a sequential `chain` whose task text may include `{previous}`. Read-only-only batches run concurrently, up to four at a time. Batches with write-capable roles use one shared pool by default, up to ten tasks at once; set `writerConcurrency` to choose a smaller cap, or set it to `1` to serialize the entire batch. Reader-only batches reject `writerConcurrency` because it does not apply. All children share the parent worktree, so concurrent writers must avoid file conflicts. A call can also set `model` and `thinking` for every task, overriding role settings; use `inherit` to fall back to each role and then the active parent model. The `message` field is only valid with `action: "steer"`. For example:
|
|
101
101
|
|
|
102
102
|
```json
|
|
103
103
|
{"agent":"reviewer","task":"Review the change","model":"provider/model","thinking":"high"}
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
Use the separate `model` and `thinking` fields for new configuration. The older `provider/model:thinking` model form remains accepted. Children run as isolated `pi --mode json -p` processes with a private `--session-dir` and `--session-id`, plus explicit local tools and `web_search`, `source_check`, `fetch_content`, and `get_search_content`. Steering restarts the same child session, so the child keeps its prior conversation. Each child explicitly loads `npm:pi-web-access`, discovers available skills, and keeps arbitrary extensions and prompt templates disabled; project-local skills load only when the parent project is trusted. Every bundled role is instructed to load the most relevant `SKILL.md` and report useful evidence. Children have no default token, dollar, turn, tool-call, research, wall-time, JSONL-line, trace, stderr, or returned-output execution quota. KillerOS bounds retained trace, stderr, and returned text and spills a large JSONL line to temporary storage; retention never stops a child or marks it `limited`. The parent limits each request to ten tasks
|
|
106
|
+
Use the separate `model` and `thinking` fields for new configuration. The older `provider/model:thinking` model form remains accepted. Children run as isolated `pi --mode json -p` processes with a private `--session-dir` and `--session-id`, plus explicit local tools and `web_search`, `source_check`, `fetch_content`, and `get_search_content`. Steering restarts the same child session, so the child keeps its prior conversation. Each child explicitly loads `npm:pi-web-access`, discovers available skills, and keeps arbitrary extensions and prompt templates disabled; project-local skills load only when the parent project is trusted. Every bundled role is instructed to load the most relevant `SKILL.md` and report useful evidence. Children have no default token, dollar, turn, tool-call, research, wall-time, JSONL-line, trace, stderr, or returned-output execution quota. KillerOS bounds retained trace, stderr, and returned text and spills a large JSONL line to temporary storage; retention never stops a child or marks it `limited`. The parent limits each request to ten tasks, read-only-only batches to four concurrent readers, and bounds role files, task input, and combined parent output. An embedding caller may opt into named child resource guards. A parent tool-call abort cancels queued tasks but lets already-running children finish; explicit `interrupt` actions and session shutdown terminate active children and escalate after five seconds.
|
|
107
107
|
|
|
108
108
|
### Thread lifecycle
|
|
109
109
|
|
|
@@ -113,7 +113,7 @@ Threads move through `queued`, `active`, `done`, `failed`, `stopped`, and `close
|
|
|
113
113
|
|
|
114
114
|
The parent can inspect a thread’s prompt, role, model, tools, trace, usage, and handoff; steer an active thread with one bounded follow-up; interrupt one child or all active children; collect a concise handoff into parent context; and close a finished or stopped thread. An interrupt preserves the partial trace, states the reason, and reports the handoff as partial rather than successful.
|
|
115
115
|
|
|
116
|
-
A child completes naturally when it returns a final answer. The default path has no per-child execution quota. Explicit embedding options can add wall-time, output, trace, stderr, JSONL, token, or cost guards; those guards report their cause and return partial work clearly. The parent still bounds task count, reader concurrency, role files, task input, and combined parent output.
|
|
116
|
+
A child completes naturally when it returns a final answer. The default path has no per-child execution quota. Explicit embedding options can add wall-time, output, trace, stderr, JSONL, token, or cost guards; those guards report their cause and return partial work clearly. The parent still bounds task count, reader concurrency, role files, task input, and combined parent output. Parent tool-call aborts leave active children running while queued work is settled as cancelled; explicit `interrupt` actions and real child-process failures remain visible. Session shutdown still terminates active children and escalates after five seconds.
|
|
117
117
|
|
|
118
118
|
The replacement lifecycle has nine phases:
|
|
119
119
|
|
|
@@ -163,7 +163,7 @@ The package manifest lists Pi’s built-in modules as peer dependencies, so npm
|
|
|
163
163
|
|
|
164
164
|
The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog.
|
|
165
165
|
|
|
166
|
-
For release `1.4.
|
|
166
|
+
For release `1.4.8`, publish after the validation checks pass:
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
169
|
npm login
|
package/package.json
CHANGED
package/subagent-process.ts
CHANGED
|
@@ -96,6 +96,10 @@ export interface SubagentProcessRetention {
|
|
|
96
96
|
|
|
97
97
|
export interface SubagentProcessHandle {
|
|
98
98
|
readonly pid: number | undefined;
|
|
99
|
+
/** True after the child close event, or when no child was spawned. */
|
|
100
|
+
readonly hasExited: boolean;
|
|
101
|
+
/** Resolves after the child close event, or when no child was spawned. */
|
|
102
|
+
readonly exited: Promise<void>;
|
|
99
103
|
readonly result: Promise<SubagentProcessResult>;
|
|
100
104
|
/** Stop this child and retain any work received before it exits. */
|
|
101
105
|
stop(reason?: string): void;
|
|
@@ -324,6 +328,7 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
324
328
|
durationMs: 0,
|
|
325
329
|
};
|
|
326
330
|
let child: SubagentProcessChild | undefined;
|
|
331
|
+
let processExited = false;
|
|
327
332
|
let closed = false;
|
|
328
333
|
let finishing = false;
|
|
329
334
|
let requestedStatus: Exclude<SubagentProcessStatus, "running" | "complete"> | undefined;
|
|
@@ -338,7 +343,14 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
338
343
|
let settleTimer: NodeJS.Timeout | undefined;
|
|
339
344
|
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
340
345
|
let resolveResult!: (result: SubagentProcessResult) => void;
|
|
346
|
+
let resolveExited!: () => void;
|
|
341
347
|
const result = new Promise<SubagentProcessResult>((resolve) => { resolveResult = resolve; });
|
|
348
|
+
const exited = new Promise<void>((resolve) => { resolveExited = resolve; });
|
|
349
|
+
const markExited = (): void => {
|
|
350
|
+
if (processExited) return;
|
|
351
|
+
processExited = true;
|
|
352
|
+
resolveExited();
|
|
353
|
+
};
|
|
342
354
|
|
|
343
355
|
const clearStdoutLine = (): void => {
|
|
344
356
|
if (stdoutLineSpoolDescriptor !== undefined) {
|
|
@@ -385,6 +397,7 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
385
397
|
const finish = (code: number | null): void => {
|
|
386
398
|
if (closed || finishing) return;
|
|
387
399
|
finishing = true;
|
|
400
|
+
if (!child || processExited) markExited();
|
|
388
401
|
if (stdoutLineBytes && !requestedStatus) processLine(readStdoutLine());
|
|
389
402
|
else clearStdoutLine();
|
|
390
403
|
closed = true;
|
|
@@ -559,7 +572,10 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
559
572
|
}
|
|
560
573
|
});
|
|
561
574
|
child.on("error", (error) => requestTermination("failed", "spawn_error", error.message));
|
|
562
|
-
child.once("close",
|
|
575
|
+
child.once("close", (code) => {
|
|
576
|
+
markExited();
|
|
577
|
+
finish(code);
|
|
578
|
+
});
|
|
563
579
|
if (limits.wallTimeMs !== undefined) timeoutTimer = setTimeout(() => requestTermination("limited", "wall_time_limit"), limits.wallTimeMs);
|
|
564
580
|
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
565
581
|
if (options.signal?.aborted) abortHandler();
|
|
@@ -570,6 +586,8 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
570
586
|
|
|
571
587
|
return {
|
|
572
588
|
get pid() { return child?.pid; },
|
|
589
|
+
get hasExited() { return processExited; },
|
|
590
|
+
exited,
|
|
573
591
|
result,
|
|
574
592
|
stop(reason = "stopped") { requestTermination("cancelled", reason); },
|
|
575
593
|
snapshot: () => cloneResult(state),
|
package/subagents.ts
CHANGED
|
@@ -713,6 +713,22 @@ async function mapReadTasks<T>(items: T[], concurrency: number, run: (item: T, i
|
|
|
713
713
|
await Promise.all(workers);
|
|
714
714
|
}
|
|
715
715
|
|
|
716
|
+
function waitForConfirmedProcessExit(handle: SubagentProcessHandle, timeoutMs = 1_000): Promise<boolean> {
|
|
717
|
+
if (handle.hasExited) return Promise.resolve(true);
|
|
718
|
+
return new Promise((resolve) => {
|
|
719
|
+
let settled = false;
|
|
720
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
721
|
+
const finish = (exited: boolean): void => {
|
|
722
|
+
if (settled) return;
|
|
723
|
+
settled = true;
|
|
724
|
+
if (timeout) clearTimeout(timeout);
|
|
725
|
+
resolve(exited);
|
|
726
|
+
};
|
|
727
|
+
timeout = setTimeout(() => finish(handle.hasExited), timeoutMs);
|
|
728
|
+
void handle.exited.then(() => finish(true));
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
716
732
|
function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxReadConcurrency" | "taskCharacters">) {
|
|
717
733
|
const taskSchema = Type.Object({
|
|
718
734
|
agent: Type.String({ minLength: 1, maxLength: 64, description: "Agent role name" }),
|
|
@@ -732,7 +748,8 @@ function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxRead
|
|
|
732
748
|
all: Type.Optional(Type.Boolean({ description: "Interrupt every active child thread" })),
|
|
733
749
|
agent: Type.Optional(Type.String({ minLength: 1, maxLength: 64, description: "Agent role for single mode" })),
|
|
734
750
|
task: Type.Optional(Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task for single mode" })),
|
|
735
|
-
tasks: Type.Optional(Type.Array(taskSchema, { minItems: 1, maxItems: limits.maxTasks, description: `Parallel role tasks: read-only
|
|
751
|
+
tasks: Type.Optional(Type.Array(taskSchema, { minItems: 1, maxItems: limits.maxTasks, description: `Parallel role tasks: read-only batches run concurrently up to ${limits.maxReadConcurrency}; batches with writers use a shared pool by default up to ${limits.maxTasks}. Set writerConcurrency to cap that pool, or set it to 1 to serialize the entire writer-containing batch; concurrent writers share the parent worktree, so callers must avoid file conflicts` })),
|
|
752
|
+
writerConcurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: limits.maxTasks, description: `Optional shared-pool cap for parallel tasks that include writers. Defaults to ${limits.maxTasks}; set 1 to serialize the entire writer-containing batch. Concurrent writers share the parent worktree, so callers must avoid file conflicts` })),
|
|
736
753
|
chain: Type.Optional(Type.Array(chainTaskSchema, { minItems: 1, maxItems: limits.maxTasks, description: "Sequential role tasks; {previous} inserts the prior result" })),
|
|
737
754
|
model: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Model for every task as provider/model; inherit uses each role setting or the active parent" })),
|
|
738
755
|
thinking: Type.Optional(Type.String({ minLength: 1, maxLength: 16, description: "Thinking effort for every task: off, minimal, low, medium, high, xhigh, max, or inherit" })),
|
|
@@ -805,6 +822,7 @@ function statusIcon(status: SubagentStatus): string {
|
|
|
805
822
|
interface ActiveThreadRuntime {
|
|
806
823
|
controller: AbortController;
|
|
807
824
|
handle?: SubagentProcessHandle;
|
|
825
|
+
handles: Set<SubagentProcessHandle>;
|
|
808
826
|
steering: string[];
|
|
809
827
|
restarting: boolean;
|
|
810
828
|
traceCount: number;
|
|
@@ -1030,11 +1048,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1030
1048
|
pi.registerTool({
|
|
1031
1049
|
name: "subagent",
|
|
1032
1050
|
label: "Subagents",
|
|
1033
|
-
description: `Spawn and manage named child threads. Children finish naturally. Parallel tasks
|
|
1051
|
+
description: `Spawn and manage named child threads. Children finish naturally. Parallel tasks with write-capable roles use one shared pool by default, up to ${limits.maxTasks}; read-only batches run concurrently up to ${limits.maxReadConcurrency}. Set writerConcurrency to cap the shared pool, or set it to 1 to serialize the entire writer-containing batch. Concurrent writers share the parent worktree, so callers must avoid file conflicts. The message parameter is only valid with action steer. Use action list, inspect, steer, interrupt, collect, and close to manage active and completed handoffs.`,
|
|
1034
1052
|
promptSnippet: "Delegate bounded specialist work to isolated KillerOS subagents",
|
|
1035
1053
|
promptGuidelines: [
|
|
1036
1054
|
"Use subagent for clearly separable specialist work; prefer read-only scout, planner, reviewer, or security roles before a writer.",
|
|
1037
|
-
`Parallel tasks
|
|
1055
|
+
`Parallel tasks with write-capable roles use one shared pool by default because all children share the parent worktree. Set writerConcurrency to cap the pool, or set it to 1 to serialize the entire writer-containing batch; callers remain responsible for file conflicts.`,
|
|
1038
1056
|
"Every child can load relevant skills with read and can use web_search, source_check, fetch_content, and get_search_content for external research.",
|
|
1039
1057
|
"When the user names a model or thinking effort, pass model and thinking separately; use inherit when the active parent or role setting should decide.",
|
|
1040
1058
|
"Keep completed and stopped threads inspectable until the parent explicitly closes them.",
|
|
@@ -1134,6 +1152,12 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1134
1152
|
|| hasChain && params.chain!.length === 0) {
|
|
1135
1153
|
throw new Error("Provide exactly one subagent mode: agent + task, tasks, or chain");
|
|
1136
1154
|
}
|
|
1155
|
+
if (params.writerConcurrency !== undefined) {
|
|
1156
|
+
if (!hasParallel) throw new Error("writerConcurrency is only valid with parallel tasks");
|
|
1157
|
+
if (!Number.isSafeInteger(params.writerConcurrency) || params.writerConcurrency < 1 || params.writerConcurrency > limits.maxTasks) {
|
|
1158
|
+
throw new Error(`writerConcurrency must be a positive integer no greater than ${limits.maxTasks}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1137
1161
|
|
|
1138
1162
|
const discovery = discoverAgentRoles(ctx.cwd, scope, ctx.isProjectTrusted(), options);
|
|
1139
1163
|
const roles = new Map(discovery.agents.map((agent) => [agent.name, agent]));
|
|
@@ -1171,9 +1195,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1171
1195
|
const writerIndexes = hasParallel
|
|
1172
1196
|
? inputs.map((input, index) => ({ input, index })).filter(({ input }) => roles.get(input.agent)!.access === "write").map(({ index }) => index)
|
|
1173
1197
|
: [];
|
|
1198
|
+
if (params.writerConcurrency !== undefined && hasParallel && writerIndexes.length === 0) {
|
|
1199
|
+
throw new Error("writerConcurrency requires at least one write-capable role");
|
|
1200
|
+
}
|
|
1201
|
+
const writerConcurrency = params.writerConcurrency ?? limits.maxTasks;
|
|
1202
|
+
const useSharedParallelPool = hasParallel && writerIndexes.length > 0;
|
|
1174
1203
|
const executionNote = hasParallel
|
|
1175
1204
|
? writerIndexes.length
|
|
1176
|
-
? `Parallel schedule:
|
|
1205
|
+
? `Parallel schedule: all tasks run through a shared pool of up to ${writerConcurrency}${params.writerConcurrency === undefined ? " (default)" : ""}; concurrent write-capable tasks share the parent worktree, so callers must avoid file conflicts.`
|
|
1177
1206
|
: `Parallel schedule: read-only tasks run concurrently up to ${limits.maxReadConcurrency}.`
|
|
1178
1207
|
: undefined;
|
|
1179
1208
|
|
|
@@ -1239,10 +1268,21 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1239
1268
|
emit();
|
|
1240
1269
|
return;
|
|
1241
1270
|
}
|
|
1271
|
+
const controller = new AbortController();
|
|
1272
|
+
const runtime: ActiveThreadRuntime = {
|
|
1273
|
+
controller,
|
|
1274
|
+
handles: new Set(),
|
|
1275
|
+
steering: [],
|
|
1276
|
+
restarting: false,
|
|
1277
|
+
traceCount: 0,
|
|
1278
|
+
startedAt: Date.now(),
|
|
1279
|
+
};
|
|
1280
|
+
activeRuntimes.set(threadId, runtime);
|
|
1242
1281
|
let sessionDirectory: string;
|
|
1243
1282
|
try {
|
|
1244
1283
|
sessionDirectory = await mkdtemp(path.join(os.tmpdir(), "killeros-subagent-session-"));
|
|
1245
1284
|
} catch (error) {
|
|
1285
|
+
activeRuntimes.delete(threadId);
|
|
1246
1286
|
const message = error instanceof Error ? error.message : String(error);
|
|
1247
1287
|
results[index] = {
|
|
1248
1288
|
...results[index]!,
|
|
@@ -1255,15 +1295,22 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1255
1295
|
emit();
|
|
1256
1296
|
return;
|
|
1257
1297
|
}
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1298
|
+
const currentThread = threads.inspect(threadId);
|
|
1299
|
+
if (controller.signal.aborted || threads.isDisposed || currentThread?.state !== "active") {
|
|
1300
|
+
activeRuntimes.delete(threadId);
|
|
1301
|
+
try {
|
|
1302
|
+
await rm(sessionDirectory, { recursive: true, force: true });
|
|
1303
|
+
} catch {
|
|
1304
|
+
// Temporary child session cleanup is best effort before process startup.
|
|
1305
|
+
}
|
|
1306
|
+
const reason = runtime.requestedReason ?? currentThread?.stopReason ?? (threads.isDisposed ? "session_shutdown" : "interrupted");
|
|
1307
|
+
results[index] = { ...results[index]!, status: "cancelled", terminationReason: reason };
|
|
1308
|
+
if (!threads.isDisposed && currentThread?.state === "active") threads.stop(threadId, { reason });
|
|
1309
|
+
savedResults.set(threadId, cloneResult(results[index]!));
|
|
1310
|
+
emit();
|
|
1311
|
+
return;
|
|
1264
1312
|
}
|
|
1265
|
-
const
|
|
1266
|
-
activeRuntimes.set(threadId, runtime);
|
|
1313
|
+
const sessionId = `killeros-${threadId.replace(/[^A-Za-z0-9_.-]/gu, "_")}`;
|
|
1267
1314
|
const agent = roles.get(input.agent)!;
|
|
1268
1315
|
const queuedSteering = initialThread.steering.map((entry) => entry.message);
|
|
1269
1316
|
let currentTask = queuedSteering.length ? buildSteeredTask(task, queuedSteering, limits.taskCharacters) : task;
|
|
@@ -1342,7 +1389,10 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1342
1389
|
...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd - usedCost }),
|
|
1343
1390
|
},
|
|
1344
1391
|
timeoutMs: remainingWallTimeMs,
|
|
1345
|
-
onHandle: (handle) => {
|
|
1392
|
+
onHandle: (handle) => {
|
|
1393
|
+
runtime.handle = handle;
|
|
1394
|
+
runtime.handles.add(handle);
|
|
1395
|
+
},
|
|
1346
1396
|
onChange: (changed) => {
|
|
1347
1397
|
results[index] = syncThread(threadId, changed, runtime);
|
|
1348
1398
|
emit();
|
|
@@ -1355,6 +1405,9 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1355
1405
|
savedResults.set(threadId, cloneResult(runtime.aggregate));
|
|
1356
1406
|
const shouldRestart = runtime.steering.length > 0 && !controller.signal.aborted && (runtime.restarting || next.status === "complete" || next.status === "cancelled");
|
|
1357
1407
|
if (!shouldRestart) break;
|
|
1408
|
+
const previousHandle = runtime.handle;
|
|
1409
|
+
if (previousHandle && !(await waitForConfirmedProcessExit(previousHandle))) break;
|
|
1410
|
+
if (controller.signal.aborted || threads.isDisposed) break;
|
|
1358
1411
|
const steering = runtime.steering.splice(0);
|
|
1359
1412
|
runtime.restarting = false;
|
|
1360
1413
|
runtime.requestedReason = undefined;
|
|
@@ -1369,12 +1422,18 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1369
1422
|
}
|
|
1370
1423
|
} finally {
|
|
1371
1424
|
activeRuntimes.delete(threadId);
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1425
|
+
const removeSessionDirectory = async (): Promise<void> => {
|
|
1426
|
+
try {
|
|
1427
|
+
await rm(sessionDirectory, { recursive: true, force: true });
|
|
1428
|
+
} catch {
|
|
1429
|
+
// Temporary child session cleanup is best effort after process termination.
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
const pendingExits = [...runtime.handles]
|
|
1433
|
+
.filter((handle) => !handle.hasExited)
|
|
1434
|
+
.map((handle) => handle.exited);
|
|
1435
|
+
if (!pendingExits.length) await removeSessionDirectory();
|
|
1436
|
+
else void Promise.all(pendingExits).then(removeSessionDirectory);
|
|
1378
1437
|
}
|
|
1379
1438
|
emit();
|
|
1380
1439
|
};
|
|
@@ -1408,8 +1467,13 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1408
1467
|
settleQueued("chain_stopped");
|
|
1409
1468
|
} else if (hasParallel) {
|
|
1410
1469
|
try {
|
|
1411
|
-
|
|
1412
|
-
|
|
1470
|
+
if (useSharedParallelPool) {
|
|
1471
|
+
const indexes = inputs.map((_, index) => index);
|
|
1472
|
+
await mapReadTasks(indexes, writerConcurrency, async (index) => runAt(index, inputs[index]!.task));
|
|
1473
|
+
} else {
|
|
1474
|
+
await mapReadTasks(readIndexes, limits.maxReadConcurrency, async ({ index }) => runAt(index, inputs[index]!.task));
|
|
1475
|
+
for (const index of writerIndexes) await runAt(index, inputs[index]!.task);
|
|
1476
|
+
}
|
|
1413
1477
|
} finally {
|
|
1414
1478
|
settleQueued("parallel_stopped");
|
|
1415
1479
|
}
|
|
@@ -1430,7 +1494,10 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1430
1494
|
renderCall(args, theme) {
|
|
1431
1495
|
const scope = args.agentScope ?? "user";
|
|
1432
1496
|
if (args.action && args.action !== "spawn") return new Text(`${theme.fg("toolTitle", theme.bold("threads "))}${theme.fg("accent", args.action)}${theme.fg("dim", args.threadId ? ` · ${args.threadId}` : "")}`, 0, 0);
|
|
1433
|
-
if (args.tasks?.length)
|
|
1497
|
+
if (args.tasks?.length) {
|
|
1498
|
+
const schedule = args.writerConcurrency === undefined ? "parallel default" : `shared pool ${args.writerConcurrency}`;
|
|
1499
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `parallel ${args.tasks.length} · ${schedule}`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1500
|
+
}
|
|
1434
1501
|
if (args.chain?.length) return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `chain ${args.chain.length}`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1435
1502
|
return new Text(`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "…")}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1436
1503
|
},
|