killeros 1.5.6 → 1.5.7

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,14 @@ All notable changes to KillerOS are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.5.7] - 2026-08-06
8
+
9
+ ### Fixed
10
+
11
+ - Stopped runaway subagents at 64 turns or 2,000,000 reported tokens by default while preserving Pi's built-in model retries and the 30-minute wall limit.
12
+ - Kept in-flight model retries labeled `Running`, exposed requested stops as terminal at once, and kept failed or limited children out of the **Active** list.
13
+ - Replaced the frozen queued spawn result with a static launch receipt so only the separate live widget claims current state and usage.
14
+
7
15
  ## [1.5.6] - 2026-08-05
8
16
 
9
17
  ### Fixed
package/README.md CHANGED
@@ -122,9 +122,9 @@ The three spawn shapes cannot be mixed. On a single spawn, `message` aliases `ta
122
122
  {"agent":"reviewer","task":"Review the change","name":"auth-audit","model":"provider/model","thinking":"high"}
123
123
  ```
124
124
 
125
- Spawn returns the generated thread IDs immediately while the children continue in the background. This lets the parent use `list`, `inspect`, `wait`, `steer`, `interrupt`, `collect`, `resume`, and `close` in later tool calls. Compact thread records persist through Pi custom session entries. On parent restart, an active record restores as `orphaned`; `close` removes the child session only after confirmed process exit. 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.
125
+ Spawn returns the generated thread IDs immediately while the children continue in the background. The completed spawn card is a static launch receipt; the separate live widget shows current state and usage until the batch settles. This lets the parent use `list`, `inspect`, `wait`, `steer`, `interrupt`, `collect`, `resume`, and `close` in later tool calls. Compact thread records persist through Pi custom session entries. On parent restart, an active record restores as `orphaned`; `close` removes the child session only after confirmed process exit. 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.
126
126
 
127
- 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. An empty final assistant response is a failure. The default child wall time is 30 minutes; token and dollar quotas remain opt-in. 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 use a bounded 10-second process-exit wait.
127
+ 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. An empty final assistant response is a failure. Each child stops at the first default limit reached: 64 turns, 2,000,000 reported tokens, or 30 minutes. Dollar quotas remain opt-in. 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. Embedding options can adjust the turn, token, wall-time, and other named resource guards. Aborting the originating parent turn stops its queued and active children; explicit `interrupt` actions and session shutdown also terminate active children and use a bounded 10-second process-exit wait.
128
128
 
129
129
  The command grammar is:
130
130
 
@@ -150,7 +150,7 @@ Threads move through `queued`, `active`, `done`, `failed`, `stopped`, `orphaned`
150
150
 
151
151
  The parent can inspect a thread’s prompt, role, model, tools, trace, usage, and handoff; wait for one named or ID child or all queued and active children; steer an active or queued thread with a bounded follow-up (at most 20 pending messages; further steering is rejected explicitly until the child restarts or drains the queue); interrupt one child or all active and queued children; collect a concise handoff into parent context; resume a terminal or orphaned child; and close a finished, stopped, or orphaned 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.
152
152
 
153
- A child completes only when it returns usable final assistant text. The default wall time is 30 minutes; token and dollar quotas remain opt-in, while every JSONL record has an 8 MiB parser ceiling. Explicit embedding options can add 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 waits up to 10 seconds for confirmed process exit.
153
+ A child completes only when it returns usable final assistant text. The default child limits are 64 turns, 2,000,000 reported tokens, and 30 minutes; dollar quotas remain opt-in, and every JSONL record has an 8 MiB parser ceiling. Explicit embedding options can adjust those limits or add output, trace, stderr, JSONL, or cost guards; each guard reports its cause and returns 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 waits up to 10 seconds for confirmed process exit.
154
154
 
155
155
  The replacement lifecycle has nine phases:
156
156
 
@@ -160,7 +160,7 @@ The replacement lifecycle has nine phases:
160
160
  4. **Steer:** append a bounded parent follow-up to an active or queued thread.
161
161
  5. **Interrupt:** stop one or all active or queued children while preserving partial work.
162
162
  6. **Collect:** return a concise handoff while retaining the expanded trace.
163
- 7. **Guard:** honor only explicitly configured child resource guards; do not impose a routine turn stop.
163
+ 7. **Guard:** stop at the default turn, token, or wall-time limit and honor explicit resource-limit overrides.
164
164
  8. **Close:** remove a finished or stopped thread from the workspace while retaining a small inspectable tombstone; heavy payloads may be evicted under the retention budget.
165
165
  9. **Prove:** test identity, visibility, controls, natural completion, guards, partial handoffs, bounded retention, and closure.
166
166
 
@@ -81,9 +81,10 @@ export interface SubagentProcessOptions {
81
81
  spawnProcess?: (args: string[], cwd: string, environment?: NodeJS.ProcessEnv) => SubagentProcessChild;
82
82
  }
83
83
 
84
- export interface SubagentProcessLimits {
85
- wallTimeMs?: number;
86
- jsonlLineBytes?: number;
84
+ export interface SubagentProcessLimits {
85
+ wallTimeMs?: number;
86
+ maxTurns?: number;
87
+ jsonlLineBytes?: number;
87
88
  traceBytes?: number;
88
89
  stderrBytes?: number;
89
90
  outputBytes?: number;
@@ -223,7 +224,7 @@ function hasIsolatedSession(args: readonly string[]): boolean {
223
224
 
224
225
  function normalizeLimits(overrides: Partial<SubagentProcessLimits> | undefined): SubagentProcessLimits {
225
226
  const limits = { ...SUBAGENT_PROCESS_LIMITS, ...overrides };
226
- for (const name of ["wallTimeMs", "jsonlLineBytes", "traceBytes", "stderrBytes", "outputBytes", "killGraceMs", "processExitWaitMs"] as const) {
227
+ for (const name of ["wallTimeMs", "maxTurns", "jsonlLineBytes", "traceBytes", "stderrBytes", "outputBytes", "killGraceMs", "processExitWaitMs"] as const) {
227
228
  const value = limits[name];
228
229
  if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0 || value > MAX_NODE_TIMER_MS
229
230
  && (name === "wallTimeMs" || name === "killGraceMs" || name === "processExitWaitMs"))) {
@@ -450,11 +451,14 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
450
451
  publish();
451
452
  resolveResult(cloneResult(state));
452
453
  };
453
- const requestTermination = (status: Exclude<SubagentProcessStatus, "running" | "complete">, reason: string, errorMessage?: string): void => {
454
- if (requestedStatus || closed) return;
455
- requestedStatus = status;
456
- requestedReason = reason;
457
- if (errorMessage) state.errorMessage = errorMessage;
454
+ const requestTermination = (status: Exclude<SubagentProcessStatus, "running" | "complete">, reason: string, errorMessage?: string): void => {
455
+ if (requestedStatus || closed) return;
456
+ requestedStatus = status;
457
+ requestedReason = reason;
458
+ if (errorMessage) state.errorMessage = errorMessage;
459
+ state.status = status;
460
+ state.terminationReason = reason;
461
+ publish();
458
462
  if (!child) {
459
463
  finish(null);
460
464
  return;
@@ -485,8 +489,8 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
485
489
  state.usage.turns += 1;
486
490
  addUsage(state.usage, { ...message.usage, turns: 0 });
487
491
  state.toolCallCount += toolCallCount(message);
488
- if (limits.quotaTokens !== undefined && state.usage.totalTokens > limits.quotaTokens) {
489
- requestTermination("limited", "quota_tokens", `Child token usage exceeds ${limits.quotaTokens}`);
492
+ if (limits.quotaTokens !== undefined && state.usage.totalTokens >= limits.quotaTokens) {
493
+ requestTermination("limited", "quota_tokens", `Child token usage reaches ${limits.quotaTokens}`);
490
494
  } else if (limits.quotaUsd !== undefined && state.usage.cost.total > limits.quotaUsd) {
491
495
  requestTermination("limited", "quota_cost", `Child cost exceeds $${limits.quotaUsd}`);
492
496
  }
@@ -515,8 +519,11 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
515
519
  hasUsableAssistantResponse = true;
516
520
  }
517
521
  }
518
- if (typeof message.errorMessage === "string") state.errorMessage = message.errorMessage;
519
- publish();
522
+ if (typeof message.errorMessage === "string") state.errorMessage = message.errorMessage;
523
+ if (message.stopReason === "toolUse" && limits.maxTurns !== undefined && state.usage.turns >= limits.maxTurns) {
524
+ requestTermination("limited", "turn_limit", `Child turn count reached ${limits.maxTurns}`);
525
+ }
526
+ publish();
520
527
  } else if (event?.type === "tool_result_end" && event.message) {
521
528
  const name = typeof event.message.toolName === "string" ? event.message.toolName : "tool";
522
529
  const traceTruncatedBefore = state.traceTruncatedBytes;
@@ -131,7 +131,8 @@ function toRecord(item: ThreadListItem): ThreadRecord {
131
131
  function threadStatus(thread: Pick<ThreadRecord, "status" | "terminationReason">): ThreadStatus {
132
132
  const reason = thread.terminationReason;
133
133
  if (thread.status === "orphaned") return "orphaned";
134
- if (thread.status === "failed" || reason === "error" || reason === "spawn_error" || reason === "process_closed" || reason === "missing_assistant_message" || reason === "malformed_jsonl" || reason === "invalid_usage" || reason?.startsWith("exit_")) return "failed";
134
+ if (thread.status === "queued" || thread.status === "running") return thread.status;
135
+ if (thread.status === "failed" || reason === "error" || reason === "model_error" || reason === "spawn_error" || reason === "process_closed" || reason === "missing_assistant_message" || reason === "malformed_jsonl" || reason === "invalid_usage" || reason?.startsWith("exit_")) return "failed";
135
136
  if (thread.status === "cancelled" || reason === "abort" || reason === "interrupt") return "cancelled";
136
137
  if (thread.status === "complete" || reason === "completed") return "complete";
137
138
  return thread.status;
@@ -194,6 +195,7 @@ export function formatThreadControls(status: ThreadStatus): readonly ThreadBoard
194
195
  }
195
196
 
196
197
  export function formatThreadInspection(thread: ThreadRecord): ThreadInspectionView {
198
+ const state = formatThreadState(thread);
197
199
  return {
198
200
  id: thread.id,
199
201
  displayName: thread.displayName,
@@ -201,11 +203,11 @@ export function formatThreadInspection(thread: ThreadRecord): ThreadInspectionVi
201
203
  agent: thread.agent,
202
204
  task: thread.task,
203
205
  step: thread.step,
204
- state: formatThreadState(thread),
206
+ state,
205
207
  usage: formatThreadUsage(thread.usage),
206
208
  trace: formatThreadTrace(thread),
207
209
  handoff: formatThreadHandoff(thread),
208
- controls: formatThreadControls(thread.status),
210
+ controls: formatThreadControls(state.status),
209
211
  };
210
212
  }
211
213
 
@@ -231,8 +233,8 @@ export function formatThreadBoard(input: ThreadBoardInput): ParentThreadBoard {
231
233
  .filter((thread) => !closed.has(thread.id) && !current.has(thread.id))
232
234
  .map(toRecord) ?? [];
233
235
  const threads = [...current.values(), ...retained];
234
- const active = threads.filter((thread) => !isDone(thread.status)).map((thread) => formatThreadListItem(thread, input.selectedThreadId));
235
- const done = threads.filter((thread) => isDone(thread.status)).map((thread) => formatThreadListItem(thread, input.selectedThreadId));
236
+ const active = threads.filter((thread) => !isDone(threadStatus(thread))).map((thread) => formatThreadListItem(thread, input.selectedThreadId));
237
+ const done = threads.filter((thread) => isDone(threadStatus(thread))).map((thread) => formatThreadListItem(thread, input.selectedThreadId));
236
238
  const selectedThread = input.selectedThreadId ? threads.find((thread) => thread.id === input.selectedThreadId) : undefined;
237
239
  return {
238
240
  title: input.title ?? "Threads",
@@ -29,9 +29,11 @@ export const SUBAGENT_LIMITS = {
29
29
  threadRetentionRecords: 64,
30
30
  threadRetentionBytes: 128 * 1024 * 1024,
31
31
  roleFileBytes: 64 * 1024,
32
- taskCharacters: 20_000,
33
- killGraceMs: 5_000,
34
- defaultWallTimeMs: 1_800_000,
32
+ taskCharacters: 20_000,
33
+ killGraceMs: 5_000,
34
+ defaultMaxTurns: 64,
35
+ defaultQuotaTokens: 2_000_000,
36
+ defaultWallTimeMs: 1_800_000,
35
37
  processExitWaitMs: 10_000,
36
38
  } as const;
37
39
 
@@ -136,8 +138,9 @@ export interface SubagentDetails {
136
138
  threads?: SubagentThread[];
137
139
  activeThreads?: SubagentThread[];
138
140
  doneThreads?: SubagentThread[];
139
- selectedThreadId?: string;
140
- wait?: SubagentWaitSummary;
141
+ selectedThreadId?: string;
142
+ wait?: SubagentWaitSummary;
143
+ backgroundStarted?: boolean;
141
144
  }
142
145
 
143
146
  export interface SubagentWaitSummary {
@@ -192,9 +195,10 @@ interface SpawnedProcess {
192
195
  once(event: "close", listener: (code: number | null) => void): this;
193
196
  }
194
197
 
195
- type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
196
- wallTimeMs?: number;
197
- jsonlLineBytes?: number;
198
+ type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
199
+ wallTimeMs?: number;
200
+ maxTurns?: number;
201
+ jsonlLineBytes?: number;
198
202
  traceBytes?: number;
199
203
  stderrBytes?: number;
200
204
  taskOutputBytes?: number;
@@ -723,9 +727,10 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
723
727
  cwd: options.cwd,
724
728
  signal: options.signal,
725
729
  spawnProcess: options.spawnProcess,
726
- limits: {
727
- ...(options.timeoutMs === undefined ? {} : { wallTimeMs: options.timeoutMs }),
728
- ...(limits.jsonlLineBytes === undefined ? {} : { jsonlLineBytes: limits.jsonlLineBytes }),
730
+ limits: {
731
+ ...(options.timeoutMs === undefined ? {} : { wallTimeMs: options.timeoutMs }),
732
+ ...(limits.maxTurns === undefined ? {} : { maxTurns: limits.maxTurns }),
733
+ ...(limits.jsonlLineBytes === undefined ? {} : { jsonlLineBytes: limits.jsonlLineBytes }),
729
734
  ...(limits.traceBytes === undefined ? {} : { traceBytes: limits.traceBytes }),
730
735
  ...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes }),
731
736
  ...(limits.taskOutputBytes === undefined ? {} : { outputBytes: limits.taskOutputBytes }),
@@ -2592,9 +2597,12 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
2592
2597
  const remainingWallTimeMs = wallTimeMs === undefined ? undefined : wallTimeMs - (Date.now() - runtime.startedAt);
2593
2598
  const usedTraceBytes = (aggregate?.traceBytes ?? 0) + (aggregate?.traceTruncatedBytes ?? 0);
2594
2599
  const usedStderrBytes = aggregate?.stderrBytes ?? 0;
2595
- const usedOutputBytes = aggregate?.outputBytes ?? 0;
2596
- const usedTokens = aggregate?.usage.totalTokens ?? 0;
2597
- const usedCost = aggregate?.usage.cost.total ?? 0;
2600
+ const usedOutputBytes = aggregate?.outputBytes ?? 0;
2601
+ const usedTokens = aggregate?.usage.totalTokens ?? 0;
2602
+ const usedCost = aggregate?.usage.cost.total ?? 0;
2603
+ const usedTurns = aggregate?.usage.turns ?? 0;
2604
+ const maxTurns = limits.maxTurns ?? limits.defaultMaxTurns;
2605
+ const quotaTokens = limits.quotaTokens ?? limits.defaultQuotaTokens;
2598
2606
  if (remainingWallTimeMs !== undefined && remainingWallTimeMs <= 0) {
2599
2607
  stopForBudget("wall_time_limit", `Child thread exceeds ${wallTimeMs} ms`);
2600
2608
  break;
@@ -2611,10 +2619,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
2611
2619
  stopForBudget("output_limit", `Child thread emits more than ${limits.taskOutputBytes} output bytes`);
2612
2620
  break;
2613
2621
  }
2614
- if (limits.quotaTokens !== undefined && usedTokens >= limits.quotaTokens) {
2615
- stopForBudget("quota_tokens", `Child thread exceeds ${limits.quotaTokens} tokens`);
2616
- break;
2617
- }
2622
+ if (usedTurns >= maxTurns) {
2623
+ stopForBudget("turn_limit", `Child thread reaches ${maxTurns} turns`);
2624
+ break;
2625
+ }
2626
+ if (usedTokens >= quotaTokens) {
2627
+ stopForBudget("quota_tokens", `Child thread reaches ${quotaTokens} tokens`);
2628
+ break;
2629
+ }
2618
2630
  if (limits.quotaUsd !== undefined && usedCost >= limits.quotaUsd) {
2619
2631
  stopForBudget("quota_cost", `Child thread exceeds $${limits.quotaUsd}`);
2620
2632
  break;
@@ -2638,10 +2650,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
2638
2650
  limits: {
2639
2651
  ...limits,
2640
2652
  ...(limits.traceBytes === undefined ? {} : { traceBytes: limits.traceBytes - usedTraceBytes }),
2641
- ...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes - usedStderrBytes }),
2642
- ...(limits.taskOutputBytes === undefined ? {} : { taskOutputBytes: limits.taskOutputBytes - usedOutputBytes }),
2643
- ...(limits.quotaTokens === undefined ? {} : { quotaTokens: limits.quotaTokens - usedTokens }),
2644
- ...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd - usedCost }),
2653
+ ...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes - usedStderrBytes }),
2654
+ ...(limits.taskOutputBytes === undefined ? {} : { taskOutputBytes: limits.taskOutputBytes - usedOutputBytes }),
2655
+ maxTurns: maxTurns - usedTurns,
2656
+ quotaTokens: quotaTokens - usedTokens,
2657
+ ...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd - usedCost }),
2645
2658
  },
2646
2659
  timeoutMs: remainingWallTimeMs,
2647
2660
  onHandle: (handle) => {
@@ -2876,9 +2889,10 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
2876
2889
 
2877
2890
  const queuedBoard = detailsFor(parentId, mode, scope, discovery.projectAgentsDir, isResume ? resumeTarget?.id : undefined);
2878
2891
  const queuedResults = results.map(cloneResult);
2879
- const queuedDetails: SubagentDetails = {
2880
- ...queuedBoard,
2881
- executionNote,
2892
+ const queuedDetails: SubagentDetails = {
2893
+ ...queuedBoard,
2894
+ backgroundStarted: true,
2895
+ executionNote,
2882
2896
  results: queuedResults,
2883
2897
  aggregateUsage: aggregateUsage(queuedResults),
2884
2898
  };
@@ -2960,11 +2974,20 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
2960
2974
 
2961
2975
  renderResult(result, { expanded }, theme) {
2962
2976
  const details = result.details as SubagentDetails | undefined;
2963
- if (!details?.results.length) {
2964
- const first = result.content[0];
2965
- return new Text(first?.type === "text" ? first.text : "(no output)", 0, 0);
2966
- }
2967
- const board = formatThreadBoard({
2977
+ if (!details?.results.length) {
2978
+ const first = result.content[0];
2979
+ return new Text(first?.type === "text" ? first.text : "(no output)", 0, 0);
2980
+ }
2981
+ if (details.backgroundStarted) {
2982
+ const lines = [
2983
+ theme.fg("toolTitle", theme.bold(`Started in background (${details.results.length})`)),
2984
+ ...details.results.map((task) => `${theme.fg("toolTitle", theme.bold(task.name ?? task.agent))}${theme.fg("dim", ` · role ${task.agent} · ${task.id} · attempt ${task.attempt ?? 1}`)}`),
2985
+ ];
2986
+ if (details.executionNote) lines.push(theme.fg("dim", details.executionNote));
2987
+ lines.push(theme.fg("dim", "Live status appears below while child threads run."));
2988
+ return new Text(lines.join("\n"), 0, 0);
2989
+ }
2990
+ const board = formatThreadBoard({
2968
2991
  title: `Subagents · ${details.mode}`,
2969
2992
  threads: details.results.map(threadBoardRecord),
2970
2993
  selectedThreadId: details.selectedThreadId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "1.5.6",
3
+ "version": "1.5.7",
4
4
  "description": "A production-hardened TUI and workflow extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "keywords": [