killeros 1.5.2 → 1.5.3
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 +10 -0
- package/README.md +5 -3
- package/killeros/commands.ts +4 -1
- package/killeros/subagents.ts +127 -34
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.5.3] - 2026-08-03
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Returned spawned thread IDs immediately and delivered completed handoffs as Pi follow-ups, making active `inspect`, `steer`, and `interrupt` actions reachable through normal parent turns.
|
|
12
|
+
- Ignored provider-generated `threadId` values during spawn argument preparation and TUI rendering while retaining strict action validation during execution.
|
|
13
|
+
- Connected Pi's parent cancellation signal to active child processes so Escape stops the subagent, suppresses replacement follow-up turns, and returns control to the terminal.
|
|
14
|
+
- Made `/exit` abort an active run before requesting Pi's graceful shutdown, and made session teardown await bounded background-child settlement.
|
|
15
|
+
- Corrected the README cancellation contract so it matches active-child termination.
|
|
16
|
+
|
|
7
17
|
## [1.5.2] - 2026-08-03
|
|
8
18
|
|
|
9
19
|
### Changed
|
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.5.
|
|
36
|
+
pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.5.3
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -118,7 +118,9 @@ The three spawn shapes cannot be mixed. The `message` field is only valid with `
|
|
|
118
118
|
{"agent":"reviewer","task":"Review the change","model":"provider/model","thinking":"high"}
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
Spawn returns the generated thread IDs immediately while the children continue in the background. This lets the parent use `list`, `inspect`, `steer`, `interrupt`, `collect`, and `close` in later tool calls. When the batch settles, KillerOS delivers its bounded handoff as a Pi follow-up and triggers the parent turn. A batch cancelled by parent Escape remains inspectable but does not trigger a replacement turn.
|
|
122
|
+
|
|
123
|
+
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, trace, stderr, or returned-output execution quota; each JSONL record still has a bounded 8 MiB parser ceiling. 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. Aborting the originating parent turn stops its queued and active children; explicit `interrupt` actions and session shutdown also terminate active children and escalate after five seconds.
|
|
122
124
|
|
|
123
125
|
### Thread lifecycle
|
|
124
126
|
|
|
@@ -128,7 +130,7 @@ Threads move through `queued`, `active`, `done`, `failed`, `stopped`, and `close
|
|
|
128
130
|
|
|
129
131
|
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. Closing removes a thread from the active workspace; heavy trace and result payloads are evicted as needed under the bounded retention budget, leaving a small inspectable tombstone.
|
|
130
132
|
|
|
131
|
-
A child completes naturally when it returns a final answer. The default path has no per-child execution quota, while every JSONL record has an 8 MiB parser ceiling. 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.
|
|
133
|
+
A child completes naturally when it returns a final answer. The default path has no per-child execution quota, while every JSONL record has an 8 MiB parser ceiling. 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. Aborting the originating parent turn settles queued work as cancelled and terminates active children; explicit `interrupt` actions and real child-process failures remain visible. Session shutdown also terminates active children and escalates after five seconds.
|
|
132
134
|
|
|
133
135
|
The replacement lifecycle has nine phases:
|
|
134
136
|
|
package/killeros/commands.ts
CHANGED
|
@@ -15,7 +15,10 @@ export function registerAliases(pi: ExtensionAPI): void {
|
|
|
15
15
|
pi.registerCommand("clear", { description: "Start a new session after confirmation", handler: startNewSession });
|
|
16
16
|
pi.registerCommand("exit", {
|
|
17
17
|
description: "Quit Pi gracefully",
|
|
18
|
-
handler: async (_args, ctx) =>
|
|
18
|
+
handler: async (_args, ctx) => {
|
|
19
|
+
if (!ctx.isIdle()) ctx.abort();
|
|
20
|
+
ctx.shutdown();
|
|
21
|
+
},
|
|
19
22
|
});
|
|
20
23
|
}
|
|
21
24
|
|
package/killeros/subagents.ts
CHANGED
|
@@ -164,6 +164,8 @@ export interface SubagentRuntimeOptions {
|
|
|
164
164
|
webExtension?: string;
|
|
165
165
|
spawnProcess?: (args: string[], cwd: string, environment?: NodeJS.ProcessEnv) => SpawnedProcess;
|
|
166
166
|
limits?: Partial<SubagentLimits>;
|
|
167
|
+
/** Test and embedding compatibility mode; production spawns return immediately. */
|
|
168
|
+
awaitSpawnCompletion?: boolean;
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
class AgentConfigurationError extends Error {
|
|
@@ -950,6 +952,19 @@ export function tryNormalizeSubagentRequest(
|
|
|
950
952
|
}
|
|
951
953
|
}
|
|
952
954
|
|
|
955
|
+
function prepareSubagentRequest(
|
|
956
|
+
value: unknown,
|
|
957
|
+
limits: Pick<SubagentLimits, "maxTasks" | "taskCharacters">,
|
|
958
|
+
): NormalizedSubagentRequest {
|
|
959
|
+
const record = requireRecord(value, "subagent request");
|
|
960
|
+
const action = record.action ?? SUBAGENT_ACTION.spawn;
|
|
961
|
+
if (action === SUBAGENT_ACTION.spawn && Object.hasOwn(record, "threadId")) {
|
|
962
|
+
const { threadId: _generatedThreadId, ...spawnRecord } = record;
|
|
963
|
+
return normalizeSubagentRequest(spawnRecord, limits);
|
|
964
|
+
}
|
|
965
|
+
return normalizeSubagentRequest(record, limits);
|
|
966
|
+
}
|
|
967
|
+
|
|
953
968
|
function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxReadConcurrency" | "taskCharacters">) {
|
|
954
969
|
const taskSchema = Type.Object({
|
|
955
970
|
agent: Type.String({ minLength: 1, maxLength: 64, description: "Agent role name" }),
|
|
@@ -959,9 +974,9 @@ function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxRead
|
|
|
959
974
|
agent: Type.String({ minLength: 1, maxLength: 64, description: "Agent role name" }),
|
|
960
975
|
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task with optional {previous} handoff placeholder" }),
|
|
961
976
|
}, { additionalProperties: false });
|
|
962
|
-
const threadId = Type.String({ minLength: 1, maxLength: 128, description: "
|
|
977
|
+
const threadId = Type.String({ minLength: 1, maxLength: 128, description: "Existing child thread ID; omit when spawning because KillerOS creates it" });
|
|
963
978
|
return Type.Object({
|
|
964
|
-
action: Type.Optional(StringEnum(SUBAGENT_ACTIONS, { default: SUBAGENT_ACTION.spawn, description: "Spawn, list, inspect, steer, interrupt, collect, or close" })),
|
|
979
|
+
action: Type.Optional(StringEnum(SUBAGENT_ACTIONS, { default: SUBAGENT_ACTION.spawn, description: "Spawn, list, inspect, steer, interrupt, collect, or close. Omit threadId when spawning" })),
|
|
965
980
|
threadId: Type.Optional(threadId),
|
|
966
981
|
message: Type.Optional(Type.String({ minLength: 1, maxLength: 4_000, description: "Steering message; valid only with action steer" })),
|
|
967
982
|
all: Type.Optional(Type.Literal(true, { description: "Interrupt every active child thread" })),
|
|
@@ -1187,6 +1202,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1187
1202
|
const spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
|
|
1188
1203
|
const threads = new SubagentThreadRegistry();
|
|
1189
1204
|
const activeRuntimes = new Map<string, ActiveThreadRuntime>();
|
|
1205
|
+
const backgroundBatches = new Set<Promise<unknown>>();
|
|
1190
1206
|
const savedResults = new Map<string, SubagentTaskResult>();
|
|
1191
1207
|
const evictedThreadParents = new Map<string, string | undefined>();
|
|
1192
1208
|
const maxClosedThreads = Number.isSafeInteger(limits.threadRetentionRecords) && limits.threadRetentionRecords > 0
|
|
@@ -1334,7 +1350,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1334
1350
|
};
|
|
1335
1351
|
|
|
1336
1352
|
if (typeof pi.on === "function") {
|
|
1337
|
-
pi.on("session_shutdown", () => {
|
|
1353
|
+
pi.on("session_shutdown", async () => {
|
|
1338
1354
|
for (const runtime of activeRuntimes.values()) {
|
|
1339
1355
|
runtime.restarting = false;
|
|
1340
1356
|
runtime.requestedReason = "session_shutdown";
|
|
@@ -1344,6 +1360,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1344
1360
|
threads.dispose();
|
|
1345
1361
|
savedResults.clear();
|
|
1346
1362
|
evictedThreadParents.clear();
|
|
1363
|
+
await Promise.allSettled([...backgroundBatches]);
|
|
1347
1364
|
});
|
|
1348
1365
|
}
|
|
1349
1366
|
|
|
@@ -1361,7 +1378,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1361
1378
|
],
|
|
1362
1379
|
parameters: createSubagentParams(limits),
|
|
1363
1380
|
prepareArguments(args) {
|
|
1364
|
-
return
|
|
1381
|
+
return prepareSubagentRequest(args, limits).input;
|
|
1365
1382
|
},
|
|
1366
1383
|
executionMode: "parallel",
|
|
1367
1384
|
|
|
@@ -1523,7 +1540,9 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1523
1540
|
const results = threadRecords.map((thread, index) => {
|
|
1524
1541
|
return makeQueuedResult(thread.id, inputs[index]!.agent, inputs[index]!.task, hasChain ? index + 1 : undefined);
|
|
1525
1542
|
});
|
|
1543
|
+
let updatesOpen = true;
|
|
1526
1544
|
const emit = (message = `${mode}: ${results.filter((result) => !["queued", "running"].includes(result.status)).length}/${results.length} settled`): void => {
|
|
1545
|
+
if (!updatesOpen) return;
|
|
1527
1546
|
const board = detailsFor(parentId, mode, scope, discovery.projectAgentsDir);
|
|
1528
1547
|
const currentResults = results.map(cloneResult);
|
|
1529
1548
|
(onUpdate as ToolUpdate | undefined)?.({
|
|
@@ -1584,11 +1603,20 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1584
1603
|
traceCount: 0,
|
|
1585
1604
|
startedAt: Date.now(),
|
|
1586
1605
|
};
|
|
1606
|
+
const abortFromParent = (): void => {
|
|
1607
|
+
runtime.restarting = false;
|
|
1608
|
+
runtime.requestedReason = "abort";
|
|
1609
|
+
runtime.handle?.stop("abort");
|
|
1610
|
+
controller.abort();
|
|
1611
|
+
};
|
|
1612
|
+
signal?.addEventListener("abort", abortFromParent, { once: true });
|
|
1613
|
+
if (signal?.aborted) abortFromParent();
|
|
1587
1614
|
activeRuntimes.set(threadId, runtime);
|
|
1588
1615
|
let sessionDirectory: string;
|
|
1589
1616
|
try {
|
|
1590
1617
|
sessionDirectory = await mkdtemp(path.join(os.tmpdir(), "killeros-subagent-session-"));
|
|
1591
1618
|
} catch (error) {
|
|
1619
|
+
signal?.removeEventListener("abort", abortFromParent);
|
|
1592
1620
|
activeRuntimes.delete(threadId);
|
|
1593
1621
|
const message = error instanceof Error ? error.message : String(error);
|
|
1594
1622
|
results[index] = {
|
|
@@ -1604,6 +1632,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1604
1632
|
}
|
|
1605
1633
|
const currentThread = threads.inspect(threadId);
|
|
1606
1634
|
if (controller.signal.aborted || threads.isDisposed || currentThread?.state !== "active") {
|
|
1635
|
+
signal?.removeEventListener("abort", abortFromParent);
|
|
1607
1636
|
activeRuntimes.delete(threadId);
|
|
1608
1637
|
try {
|
|
1609
1638
|
await rm(sessionDirectory, { recursive: true, force: true });
|
|
@@ -1748,6 +1777,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1748
1777
|
currentTask = buildSteeredTask(task, steering, limits.taskCharacters);
|
|
1749
1778
|
}
|
|
1750
1779
|
} finally {
|
|
1780
|
+
signal?.removeEventListener("abort", abortFromParent);
|
|
1751
1781
|
activeRuntimes.delete(threadId);
|
|
1752
1782
|
const removeSessionDirectory = async (): Promise<void> => {
|
|
1753
1783
|
try {
|
|
@@ -1782,48 +1812,111 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1782
1812
|
}
|
|
1783
1813
|
};
|
|
1784
1814
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1815
|
+
const finishBatch = async () => {
|
|
1816
|
+
if (hasChain) {
|
|
1817
|
+
let previous = "";
|
|
1818
|
+
for (let index = 0; index < inputs.length; index += 1) {
|
|
1819
|
+
const task = expandChainTask(inputs[index]!.task, previous, limits.taskCharacters);
|
|
1820
|
+
if (task === undefined) {
|
|
1821
|
+
failQueuedTask(index, "task_limit", `Expanded task exceeds ${limits.taskCharacters} characters`);
|
|
1822
|
+
break;
|
|
1823
|
+
}
|
|
1824
|
+
await runAt(index, task);
|
|
1825
|
+
if (results[index]!.status !== "complete") break;
|
|
1826
|
+
previous = results[index]!.output;
|
|
1827
|
+
}
|
|
1828
|
+
settleQueued("chain_stopped");
|
|
1829
|
+
} else if (hasParallel) {
|
|
1830
|
+
try {
|
|
1831
|
+
if (useSharedParallelPool) {
|
|
1832
|
+
const indexes = inputs.map((_, index) => index);
|
|
1833
|
+
await mapReadTasks(indexes, writerConcurrency, async (index) => runAt(index, inputs[index]!.task));
|
|
1834
|
+
} else {
|
|
1835
|
+
await mapReadTasks(readIndexes, limits.maxReadConcurrency, async ({ index }) => runAt(index, inputs[index]!.task));
|
|
1836
|
+
for (const index of writerIndexes) await runAt(index, inputs[index]!.task);
|
|
1837
|
+
}
|
|
1838
|
+
} finally {
|
|
1839
|
+
settleQueued("parallel_stopped");
|
|
1793
1840
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
previous = results[index]!.output;
|
|
1841
|
+
} else {
|
|
1842
|
+
await runAt(0, inputs[0]!.task);
|
|
1797
1843
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1844
|
+
|
|
1845
|
+
const board = detailsFor(parentId, mode, scope, discovery.projectAgentsDir);
|
|
1846
|
+
const currentResults = results.map(cloneResult);
|
|
1847
|
+
const details: SubagentDetails = { ...board, executionNote, results: currentResults, aggregateUsage: aggregateUsage(currentResults) };
|
|
1848
|
+
return {
|
|
1849
|
+
content: [{ type: "text" as const, text: buildToolContent(mode, details.results, limits.toolOutputBytes) }],
|
|
1850
|
+
details,
|
|
1851
|
+
usage: details.aggregateUsage,
|
|
1852
|
+
};
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
emit(`${mode}: ${results.length} queued`);
|
|
1856
|
+
if (options.awaitSpawnCompletion === true) {
|
|
1857
|
+
const foregroundBatch = finishBatch();
|
|
1858
|
+
backgroundBatches.add(foregroundBatch);
|
|
1800
1859
|
try {
|
|
1801
|
-
|
|
1802
|
-
const indexes = inputs.map((_, index) => index);
|
|
1803
|
-
await mapReadTasks(indexes, writerConcurrency, async (index) => runAt(index, inputs[index]!.task));
|
|
1804
|
-
} else {
|
|
1805
|
-
await mapReadTasks(readIndexes, limits.maxReadConcurrency, async ({ index }) => runAt(index, inputs[index]!.task));
|
|
1806
|
-
for (const index of writerIndexes) await runAt(index, inputs[index]!.task);
|
|
1807
|
-
}
|
|
1860
|
+
return await foregroundBatch;
|
|
1808
1861
|
} finally {
|
|
1809
|
-
|
|
1862
|
+
backgroundBatches.delete(foregroundBatch);
|
|
1810
1863
|
}
|
|
1811
|
-
} else {
|
|
1812
|
-
await runAt(0, inputs[0]!.task);
|
|
1813
1864
|
}
|
|
1814
1865
|
|
|
1815
|
-
const
|
|
1816
|
-
const
|
|
1817
|
-
const
|
|
1866
|
+
const queuedBoard = detailsFor(parentId, mode, scope, discovery.projectAgentsDir);
|
|
1867
|
+
const queuedResults = results.map(cloneResult);
|
|
1868
|
+
const queuedDetails: SubagentDetails = {
|
|
1869
|
+
...queuedBoard,
|
|
1870
|
+
executionNote,
|
|
1871
|
+
results: queuedResults,
|
|
1872
|
+
aggregateUsage: aggregateUsage(queuedResults),
|
|
1873
|
+
};
|
|
1874
|
+
const threadList = threadRecords.map((thread) => `${thread.id} (${thread.role})`).join(", ");
|
|
1875
|
+
updatesOpen = false;
|
|
1876
|
+
const backgroundBatch = finishBatch().then((completed) => {
|
|
1877
|
+
if (threads.isDisposed || completed.details.results.some((result) => result.terminationReason === "abort")) return;
|
|
1878
|
+
try {
|
|
1879
|
+
pi.sendMessage({
|
|
1880
|
+
customType: "killeros-subagent-settled",
|
|
1881
|
+
content: `Subagent batch settled: ${threadList}\n\n${completed.content[0].text}`,
|
|
1882
|
+
display: true,
|
|
1883
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1884
|
+
} catch {
|
|
1885
|
+
// The completed handoff remains available through list, inspect, and collect.
|
|
1886
|
+
}
|
|
1887
|
+
}).catch((error) => {
|
|
1888
|
+
if (threads.isDisposed) return;
|
|
1889
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1890
|
+
try {
|
|
1891
|
+
pi.sendMessage({
|
|
1892
|
+
customType: "killeros-subagent-settled",
|
|
1893
|
+
content: `Subagent batch failed: ${threadList}\n\n${message}`,
|
|
1894
|
+
display: true,
|
|
1895
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1896
|
+
} catch {
|
|
1897
|
+
// The thread registry retains any partial state for inspection.
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
backgroundBatches.add(backgroundBatch);
|
|
1901
|
+
void backgroundBatch.finally(() => backgroundBatches.delete(backgroundBatch));
|
|
1818
1902
|
return {
|
|
1819
|
-
content: [{
|
|
1820
|
-
|
|
1821
|
-
|
|
1903
|
+
content: [{
|
|
1904
|
+
type: "text",
|
|
1905
|
+
text: boundedText(`Started child threads: ${threadList}. They continue in the background; use list, inspect, steer, interrupt, collect, or close while they run.`, limits.toolOutputBytes, "\n\n[Spawn output truncated.]"),
|
|
1906
|
+
}],
|
|
1907
|
+
details: queuedDetails,
|
|
1908
|
+
usage: queuedDetails.aggregateUsage,
|
|
1822
1909
|
};
|
|
1823
1910
|
},
|
|
1824
1911
|
|
|
1825
1912
|
renderCall(args, theme) {
|
|
1826
|
-
|
|
1913
|
+
let renderArgs = args;
|
|
1914
|
+
try {
|
|
1915
|
+
renderArgs = prepareSubagentRequest(args, limits).input;
|
|
1916
|
+
} catch {
|
|
1917
|
+
// Strict rendering below displays malformed requests as invalid.
|
|
1918
|
+
}
|
|
1919
|
+
const parsed = tryNormalizeSubagentRequest(renderArgs, limits);
|
|
1827
1920
|
if (!parsed.ok) {
|
|
1828
1921
|
return new Text(`${theme.fg("toolTitle", theme.bold("subagent"))}${theme.fg("error", " · invalid request")}`, 0, 0);
|
|
1829
1922
|
}
|