killeros 1.5.1 → 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 CHANGED
@@ -4,6 +4,26 @@ 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
+
17
+ ## [1.5.2] - 2026-08-03
18
+
19
+ ### Changed
20
+
21
+ - Hardened always-on concise guidance around low-friction action, visible multi-turn state, evidence-backed outcomes, material ambiguity, diagnostic resets, and explicit safety and correctness precedence.
22
+
23
+ ### Fixed
24
+
25
+ - Resolved bundled subagent role discovery from the package `agents/` directory; the module-relative path broke when subagent modules moved under `killeros/` in v1.5.0, leaving fresh installs with `Unknown subagent "<role>". Available: none`.
26
+
7
27
  ## [1.5.1] - 2026-08-03
8
28
 
9
29
  ### 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.1
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
- 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. 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.
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. 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.
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
 
@@ -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) => ctx.shutdown(),
18
+ handler: async (_args, ctx) => {
19
+ if (!ctx.isIdle()) ctx.abort();
20
+ ctx.shutdown();
21
+ },
19
22
  });
20
23
  }
21
24
 
@@ -1,16 +1,33 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  export const CONCISE_SYSTEM_PROMPT = `
4
- # Concise output rules
5
- 1. Start with the answer or next action; omit conversational preambles.
6
- 2. Use numbered steps only when order matters, with one bounded action per step.
7
- 3. Finish the primary task before mentioning optional follow-up work.
8
- 4. State failures directly and include the recovery action.
9
- 5. Keep lists focused; group long inventories under clear headings.
10
- 6. Do not invent time estimates, completion claims, or facts.
11
- 7. Preserve exact code, commands, paths, quoted text, warnings, and user-requested formats.
12
- 8. Omit recap sections and generic closing pleasantries.
13
- 9. Use a pragmatic style: direct, plain, and focused on the task.
4
+ # Action-oriented response guidance
5
+
6
+ Make each response easy to start and easy to follow. Keep the state the user needs on screen instead of expecting them to remember missing context. Concise means low friction, not minimal detail: preserve everything required for safety, correctness, and action.
7
+
8
+ ## Serve the immediate need
9
+
10
+ The first line serves the user's immediate need: give the answer for an informational request, the next action for executable work, or the failure and recovery when blocked. Omit conversational preambles. If the agent can continue the work, continue instead of asking permission; if the task is complete, do not manufacture a next step.
11
+
12
+ Finish the primary task before raising a separate concern. Mention an unrelated issue only when it materially affects safety, correctness, or the user's next decision. Ask one concrete question only when a material ambiguity remains after checking available code and context.
13
+
14
+ ## Reduce action and memory friction
15
+
16
+ Use structure by meaning: numbered steps for sequence, bullets for parallel facts or options, and short headings when they make a longer answer easier to scan or re-enter. Keep each step bounded. When a list grows beyond about five items, rank or group it instead of omitting required detail.
17
+
18
+ Use a compact state anchor only when multi-step work spans turns or resumes after interruption. During tool-driven work, report meaningful phase changes, long-running verification, failures, and required decisions rather than narrating routine actions.
19
+
20
+ ## Make outcomes and failures concrete
21
+
22
+ When work completes, make the verified outcome prominent: state what now works and the evidence that proves it. Do not append a redundant recap. State failures matter-of-factly with the cause, impact, and recovery action. After three consecutive turns leave the same issue broken, stop speculative edits, name the likely invalid assumption from the observed evidence, and ask one diagnostic question.
23
+
24
+ Never invent facts, completion claims, or timing. Give a concrete human execution estimate only when requested or supported by evidence, state its assumptions, and never predict the agent's own completion time. Preserve exact code, commands, paths, quoted text, warnings, and user-requested formats.
25
+
26
+ ## Resolve conflicts deliberately
27
+
28
+ Safety and harness constraints come first, followed by the user's explicit depth or format request, then correctness and completeness, then these concise defaults. Explain fully when asked, confirm before destructive actions, and retain necessary uncertainty rather than manufacturing confidence.
29
+
30
+ Before sending, check that the first line serves the immediate need, filler and tangents are gone, exact artifacts and necessary uncertainty remain, and the ending is either the verified outcome or one action the user must take. Use a pragmatic tone: direct, plain, and focused on the task. Omit generic praise, recap sections, and closing pleasantries.
14
31
  `.trim();
15
32
 
16
33
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -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 {
@@ -357,7 +359,7 @@ export function discoverAgentRoles(
357
359
  options: Pick<SubagentRuntimeOptions, "bundledAgentsDir" | "userAgentsDir" | "limits"> = {},
358
360
  ): AgentDiscoveryResult {
359
361
  const limits = { ...SUBAGENT_LIMITS, ...options.limits };
360
- const bundledDir = options.bundledAgentsDir ?? fileURLToPath(new URL("./agents/", import.meta.url));
362
+ const bundledDir = options.bundledAgentsDir ?? fileURLToPath(new URL("../agents/", import.meta.url));
361
363
  const userDir = options.userAgentsDir ?? path.join(getAgentDir(), "agents");
362
364
  const wantsProject = scope === "project" || scope === "both";
363
365
  if (wantsProject && !projectTrusted) throw new Error("Project agents require a trusted project");
@@ -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: "Stable child thread ID" });
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 normalizeSubagentRequest(args, limits).input;
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
- emit(`${mode}: ${results.length} queued`);
1786
- if (hasChain) {
1787
- let previous = "";
1788
- for (let index = 0; index < inputs.length; index += 1) {
1789
- const task = expandChainTask(inputs[index]!.task, previous, limits.taskCharacters);
1790
- if (task === undefined) {
1791
- failQueuedTask(index, "task_limit", `Expanded task exceeds ${limits.taskCharacters} characters`);
1792
- break;
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
- await runAt(index, task);
1795
- if (results[index]!.status !== "complete") break;
1796
- previous = results[index]!.output;
1841
+ } else {
1842
+ await runAt(0, inputs[0]!.task);
1797
1843
  }
1798
- settleQueued("chain_stopped");
1799
- } else if (hasParallel) {
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
- if (useSharedParallelPool) {
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
- settleQueued("parallel_stopped");
1862
+ backgroundBatches.delete(foregroundBatch);
1810
1863
  }
1811
- } else {
1812
- await runAt(0, inputs[0]!.task);
1813
1864
  }
1814
1865
 
1815
- const board = detailsFor(parentId, mode, scope, discovery.projectAgentsDir);
1816
- const currentResults = results.map(cloneResult);
1817
- const details: SubagentDetails = { ...board, executionNote, results: currentResults, aggregateUsage: aggregateUsage(currentResults) };
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: [{ type: "text", text: buildToolContent(mode, details.results, limits.toolOutputBytes) }],
1820
- details,
1821
- usage: details.aggregateUsage,
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
- const parsed = tryNormalizeSubagentRequest(args, limits);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "A production-hardened TUI and workflow extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "keywords": [