@yusukeshib/pi-babysit 0.6.12 → 0.6.14

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.
Files changed (3) hide show
  1. package/README.md +12 -4
  2. package/index.ts +50 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -88,8 +88,11 @@ ten per page; `< 1/4 >` shows the current/total page when needed. Click the
88
88
  arrows to page. Collapsed rows occupy one line, previewing the command for
89
89
  processes and the task for agents (with `…` when clipped). Click a row for the
90
90
  complete wrapped command or task; agent rows also show their RPC worker launch
91
- argv (bold, readable text rather than a copy-paste shell command), and up to 20
92
- output lines. Expanded output lines wrap without a 100-character cutoff.
91
+ argv (bold, readable text rather than a copy-paste shell command), and up to 50
92
+ output lines. Agent output retains recent RPC text, tool calls, and tool results
93
+ in chronological order instead of showing only the latest message. Blank lines
94
+ and surrounding whitespace are preserved. Expanded output lines wrap without a
95
+ 100-character cutoff.
93
96
  Opening another row closes the previous
94
97
  one. A session with no available output shows an explicit placeholder when
95
98
  expanded. Mouse clicks
@@ -207,8 +210,13 @@ because blindly rerunning an arbitrary command can duplicate side effects.
207
210
  incrementally, which keeps follow-up tasks efficient.
208
211
 
209
212
  Subagents load `self-reap.ts`, which exits an idle finished subagent after a
210
- grace window (`PI_BABYSIT_REAP_AFTER`, default 120s) using the same parked-turn
211
- rule, so a subagent waiting on a long build is never false-killed. Give bounded
213
+ grace window using the same parked-turn rule, so a subagent waiting on a long
214
+ build is never false-killed. Callers can set `reapAfter: "5m"` on
215
+ `babysit_run { profile: "subagent", task, reapAfter: "5m" }`. Omission uses
216
+ `PI_BABYSIT_REAP_AFTER`, then `PI_SUBAGENT_REAP_AFTER`, then 120s.
217
+ `reapAfter: "none"` (also `"off"` or `"0"`) disables idle reaping; the absolute
218
+ `timeout` still applies. Follow-up tasks cancel the idle timer. This differs
219
+ from `idleTimeout`, which kills based on lack of output, even during work. Give bounded
212
220
  recon/review tasks at least one cost, turn, tool-call, or token budget; omit
213
221
  budgets only for intentionally open-ended work. Optional task budgets are
214
222
  observed by the parent poller. `maxUsageTokens` counts cumulative input, output,
package/index.ts CHANGED
@@ -464,6 +464,7 @@ interface Meta {
464
464
  // subagent
465
465
  task?: string;
466
466
  launchCommand?: string;
467
+ reapAfter?: string;
467
468
  promptOffset?: number;
468
469
  model?: string;
469
470
  tools?: string[];
@@ -1065,9 +1066,11 @@ const SUBAGENT_BUDGET_GRACE_MS =
1065
1066
  parseDurMs(process.env.PI_BABYSIT_BUDGET_GRACE ?? "90s") ?? 90_000;
1066
1067
  const SUBAGENT_REAP_AFTER =
1067
1068
  process.env.PI_BABYSIT_REAP_AFTER ?? process.env.PI_SUBAGENT_REAP_AFTER ?? "120s";
1068
- const SUBAGENT_REUSE_HINT = ["0", "off", "none"].includes(SUBAGENT_REAP_AFTER)
1069
- ? "Session remains available until its absolute timeout."
1070
- : `Session remains available for follow-ups during the ${SUBAGENT_REAP_AFTER} idle grace.`;
1069
+ export function subagentReuseHint(reapAfter = SUBAGENT_REAP_AFTER): string {
1070
+ return ["0", "off", "none"].includes(reapAfter)
1071
+ ? "Session remains available until its absolute timeout."
1072
+ : `Session remains available for follow-ups during the ${reapAfter} idle grace.`;
1073
+ }
1071
1074
 
1072
1075
  export function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
1073
1076
  if (maxBytes <= 0) return "";
@@ -1556,6 +1559,8 @@ export interface Progress {
1556
1559
  finalText: string;
1557
1560
  /** Best-effort text from the currently streaming assistant message. */
1558
1561
  streamingText: string;
1562
+ /** Recent readable RPC output, independent of the latest assistant message. */
1563
+ logText: string;
1559
1564
  /** Context size reported by the most recent assistant response. */
1560
1565
  tokens?: number;
1561
1566
  /** Cumulative model usage for the current subagent task. */
@@ -1616,6 +1621,7 @@ function emptyProgress(): Progress {
1616
1621
  toolCallCount: 0,
1617
1622
  finalText: "",
1618
1623
  streamingText: "",
1624
+ logText: "",
1619
1625
  modelCalls: 0,
1620
1626
  usageTokens: 0,
1621
1627
  inputTokens: 0,
@@ -1713,6 +1719,12 @@ function updateProgressState(progress: Progress): Progress {
1713
1719
  return progress;
1714
1720
  }
1715
1721
 
1722
+ function appendProgressLog(progress: Progress, text: string, separate = false): void {
1723
+ if (!text) return;
1724
+ const separator = separate && progress.logText && !progress.logText.endsWith("\n") ? "\n" : "";
1725
+ progress.logText = (progress.logText + separator + text).split("\n").slice(-50).join("\n");
1726
+ }
1727
+
1716
1728
  function parseEventLine(progress: Progress, raw: string): void {
1717
1729
  const line = raw.replace(/\r$/, "").trim();
1718
1730
  if (!line.startsWith("{")) return;
@@ -1732,12 +1744,14 @@ function parseEventLine(progress: Progress, raw: string): void {
1732
1744
  | { type?: string; delta?: string }
1733
1745
  | undefined;
1734
1746
  if (update?.type === "text_delta" && typeof update.delta === "string") {
1747
+ appendProgressLog(progress, update.delta, !progress.streamingText);
1735
1748
  progress.streamingText = clip(progress.streamingText + update.delta, ANSWER_MAX_BYTES);
1736
1749
  }
1737
1750
  break;
1738
1751
  }
1739
1752
  case "tool_execution_start": {
1740
1753
  const name = String(event.toolName ?? "tool");
1754
+ appendProgressLog(progress, summarizeToolCall(name, (event.args as Record<string, unknown>) ?? {}), true);
1741
1755
  progress.toolCallCount++;
1742
1756
  progress.toolCalls.push({
1743
1757
  name,
@@ -1771,12 +1785,22 @@ function parseEventLine(progress: Progress, raw: string): void {
1771
1785
  };
1772
1786
  }
1773
1787
  | undefined;
1788
+ // Compact RPC logs omit successful tool_execution_end payloads, but
1789
+ // retain the corresponding toolResult message once.
1790
+ if (message?.role === "toolResult") {
1791
+ for (const block of message.content ?? []) {
1792
+ if (block.type === "text" && block.text) appendProgressLog(progress, block.text, true);
1793
+ }
1794
+ }
1774
1795
  if (message?.role === "assistant") {
1775
1796
  const text = (message.content ?? [])
1776
1797
  .filter((content) => content.type === "text" && content.text)
1777
1798
  .map((content) => content.text)
1778
1799
  .join("");
1779
- if (text.trim()) progress.finalText = clip(text, ANSWER_MAX_BYTES);
1800
+ if (text.trim()) {
1801
+ if (!progress.streamingText) appendProgressLog(progress, text, true);
1802
+ progress.finalText = clip(text, ANSWER_MAX_BYTES);
1803
+ }
1780
1804
  if (message.stopReason === "error") {
1781
1805
  progress.errorMsg = message.errorMessage || "subagent model request failed";
1782
1806
  }
@@ -2080,6 +2104,7 @@ interface SubagentOpts {
2080
2104
  depth: number;
2081
2105
  maxDepth: number;
2082
2106
  budget?: SubagentBudget;
2107
+ reapAfter?: string;
2083
2108
  // Idle-timeout is OFF by default: an RPC-mode pi is silent while it works,
2084
2109
  // so idle detection would false-kill a busy subagent. The absolute timeout
2085
2110
  // is the safety valve instead.
@@ -2090,6 +2115,11 @@ interface SubagentOpts {
2090
2115
  async function spawnSubagent(
2091
2116
  opts: SubagentOpts,
2092
2117
  ): Promise<{ id: string; model?: string } | { error: string }> {
2118
+ const reapAfter = opts.reapAfter?.trim() ?? SUBAGENT_REAP_AFTER;
2119
+ if (opts.reapAfter !== undefined && !["0", "off", "none"].includes(reapAfter) &&
2120
+ (!/^\d+(ms|s|m|h)?$/.test(reapAfter) || !Number.isFinite(parseDurMs(reapAfter)))) {
2121
+ return { error: "Invalid reapAfter: use a duration such as 30s or 5m, or none/off/0 to disable." };
2122
+ }
2093
2123
  // Long-lived RPC worker: the task is NOT passed as argv — it is injected
2094
2124
  // below as an RPC `prompt` command, whose response we validate so spawn
2095
2125
  // failures (bad model, missing API key) are loud instead of a silent exit=1.
@@ -2168,6 +2198,7 @@ async function spawnSubagent(
2168
2198
  r = await bs(bsArgs, {
2169
2199
  cwd: opts.cwd,
2170
2200
  env: {
2201
+ PI_BABYSIT_REAP_AFTER: reapAfter,
2171
2202
  [SUBAGENT_DEPTH_ENV]: String(opts.depth),
2172
2203
  [SUBAGENT_MAX_DEPTH_ENV]: String(opts.maxDepth),
2173
2204
  },
@@ -2200,6 +2231,7 @@ async function spawnSubagent(
2200
2231
  depth: opts.depth,
2201
2232
  maxDepth: opts.maxDepth,
2202
2233
  budget: opts.budget,
2234
+ reapAfter,
2203
2235
  });
2204
2236
 
2205
2237
  // Wait for pi to boot (first JSON event in the log), then inject the task.
@@ -2293,6 +2325,7 @@ async function spawnSubagent(
2293
2325
  depth: opts.depth,
2294
2326
  maxDepth: opts.maxDepth,
2295
2327
  budget: opts.budget,
2328
+ reapAfter,
2296
2329
  startedAt: Date.now(),
2297
2330
  });
2298
2331
  return { id, model: resolvedModel };
@@ -2368,7 +2401,7 @@ function renderWidgetSessionHeader(
2368
2401
 
2369
2402
  // How many trailing output lines to show per session in the widget.
2370
2403
  const WIDGET_COLLAPSED_TAIL_LINES = 1;
2371
- const WIDGET_EXPANDED_TAIL_LINES = 20;
2404
+ const WIDGET_EXPANDED_TAIL_LINES = 50;
2372
2405
  const WIDGET_SESSION_PAGE_SIZE = 10;
2373
2406
 
2374
2407
  export interface WidgetSessionListState {
@@ -2565,10 +2598,12 @@ export function widgetTail(
2565
2598
  raw = readTailLines(logPath(id), lines);
2566
2599
  } else {
2567
2600
  const progress = subagentProgress ?? taskProgressOf(id).progress;
2568
- if (progress.streamingText.trim()) {
2569
- raw = progress.streamingText.trim().split("\n");
2601
+ if (progress.logText) {
2602
+ raw = progress.logText.split("\n");
2603
+ } else if (progress.streamingText.trim()) {
2604
+ raw = progress.streamingText.split("\n");
2570
2605
  } else if (progress.finalText.trim()) {
2571
- raw = progress.finalText.trim().split("\n");
2606
+ raw = progress.finalText.split("\n");
2572
2607
  } else if (progress.toolCalls.length > 0) {
2573
2608
  raw = progress.toolCalls.map((tool) => tool.summary);
2574
2609
  } else {
@@ -2577,7 +2612,6 @@ export function widgetTail(
2577
2612
  }
2578
2613
  return raw
2579
2614
  .map(sanitizeTailLine)
2580
- .filter((line) => line.trim().length > 0)
2581
2615
  .slice(-lines);
2582
2616
  }
2583
2617
 
@@ -2782,7 +2816,7 @@ async function waitForTask(
2782
2816
  ok: completed.ok,
2783
2817
  text:
2784
2818
  `Subagent ${id} finished its task (${stats}).\n` +
2785
- `${SUBAGENT_REUSE_HINT} Follow-up: babysit_send { id: "${id}" }, ` +
2819
+ `${subagentReuseHint(readMeta(id)?.reapAfter)} Follow-up: babysit_send { id: "${id}" }, ` +
2786
2820
  `or babysit_kill when done.\n\n${completed.body}`,
2787
2821
  status: st,
2788
2822
  progress: prog,
@@ -3879,6 +3913,11 @@ export default function (pi: ExtensionAPI) {
3879
3913
  "Absolute auto-kill after this long (e.g. 30m). Default: none for processes (dev servers may run forever), 15m for subagents. 'none' disables.",
3880
3914
  }),
3881
3915
  ),
3916
+ reapAfter: Type.Optional(
3917
+ Type.String({
3918
+ description: "Subagent only: stop the worker after this long in task-complete IDLE (e.g. 30s, 5m). Defaults to PI_BABYSIT_REAP_AFTER, then PI_SUBAGENT_REAP_AFTER, then 120s. none/off/0 disables; absolute timeout still applies. Follow-up tasks cancel the idle timer.",
3919
+ }),
3920
+ ),
3882
3921
  idleTimeout: Type.Optional(
3883
3922
  Type.String({
3884
3923
  description:
@@ -4199,6 +4238,7 @@ export default function (pi: ExtensionAPI) {
4199
4238
  maxUsageTokens: params.maxUsageTokens,
4200
4239
  }
4201
4240
  : undefined,
4241
+ reapAfter: params.reapAfter,
4202
4242
  timeout: params.timeout ?? "15m",
4203
4243
  idleTimeout: params.idleTimeout,
4204
4244
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",