@yusukeshib/pi-babysit 0.3.14 → 0.3.15

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/README.md CHANGED
@@ -56,8 +56,8 @@ programs** (installers, wizards, REPLs): type with `babysit_send`
56
56
 
57
57
  | Tool | What it does |
58
58
  | ---- | ------------ |
59
- | `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`/`notificationGroup`) or start a subagent (`profile: "subagent"`, `task`, optional `agent`/`model`/`tools`/`maxDepth` and `maxCost`/`maxTurns`/`maxToolCalls`/`maxUsageTokens` budgets). `maxDepth` defaults to 1 and can only be set by the top-level caller. Quick commands return inline; longer ones continue in the background |
60
- | `babysit_check` | List all sessions, inspect one, tail its bounded recent output, or search its raw log with `pattern`; `screen: true` captures TUIs and subagents otherwise show structured live progress |
59
+ | `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`/`notificationGroup`). Set `foreground: true` when the next step needs the result in the same tool call, avoiding a separate `babysit_wait` turn. Or start a named subagent (`profile: "subagent"`, `task`, optional `name`/`agent`/`model`/`tools`/`maxDepth` and `maxCost`/`maxTurns`/`maxToolCalls`/`maxUsageTokens` budgets). `maxDepth` defaults to 1 and can only be set by the top-level caller. Quick commands return inline; longer ones continue in the background |
60
+ | `babysit_check` | Without an id, list running sessions by default (`state: "all"` includes history; `state`/`kind` filters are available). With an id, inspect one session, tail bounded recent output, or search its raw log with `pattern`; `screen: true` captures TUIs and subagents otherwise show structured live progress |
61
61
  | `babysit_send` | Process: type `text` / press `keys` into the PTY. Subagent: steer mid-run, or send a follow-up task when idle (`mode: auto/steer/task`) |
62
62
  | `babysit_wait` | Block until done: process exit (or `expect: "regex"` readiness marker), subagent task completion. Multi-wait: up to 32 unique `ids` + `mode: "any"\|"all"` |
63
63
  | `babysit_kill` | Terminate a session, verify terminal state, then suppress the exit notification |
@@ -106,13 +106,13 @@ a raw RPC JSON tail.
106
106
 
107
107
  Subagent logs compact Pi's streaming `message_update` events before they are
108
108
  recorded. Pi repeats the complete growing assistant message and partial snapshot
109
- on every token; pi-babysit retains only the incremental delta. The opt-in
110
- `compact` RPC log mode also removes duplicate payloads from `message_start`,
109
+ on every token; pi-babysit retains only the incremental delta. Compact RPC
110
+ logging is the default and also removes duplicate payloads from `message_start`,
111
111
  `turn_end`, successful `tool_execution_end`, and `agent_end`, while preserving
112
112
  authoritative `message_end`, failures, responses, and errors. Parked-process
113
113
  state is materialized as a small boolean so completion detection is unchanged.
114
- Set `PI_BABYSIT_RPC_LOG_MODE=standard` for the legacy lifecycle payloads. Live
115
- `/babysit` and attach views render either format.
114
+ Set `PI_BABYSIT_RPC_LOG_MODE=standard` only when legacy lifecycle payloads are
115
+ needed for debugging. Live `/babysit` and attach views render either format.
116
116
 
117
117
  All shell commands, including `pwd` and Git, go through `babysit_run`. Bundle
118
118
  closely related tiny observations when doing so safely reduces tool turns.
@@ -134,9 +134,11 @@ because blindly rerunning an arbitrary command can duplicate side effects.
134
134
  `pi.sendMessage(…, { triggerTurn: true, deliverAs: "steer" })` containing every
135
135
  deliverable exit observed in that poll (deduped via `meta/<id>.json`). Waiting
136
136
  for idleness prevents an immediately-following `babysit_wait` from racing the
137
- poller and receiving a duplicate completion. Processes with the same
138
- `notificationGroup` wait for every currently running group member to stop and
139
- then share one notification even when their exits span multiple polls.
137
+ poller and receiving a duplicate completion. Background process calls emitted
138
+ together in one assistant message automatically share a notification group;
139
+ an explicit `notificationGroup` overrides this. Group members wait for every
140
+ currently running member to stop and then share one notification even when
141
+ their exits span multiple polls.
140
142
  `babysit_kill` and an exit already reported by `babysit_wait` suppress the
141
143
  notification.
142
144
  - **Subagent**: `babysit_wait` blocks on `babysit expect '"type":"agent_settled"'`.
@@ -152,8 +154,10 @@ because blindly rerunning an arbitrary command can duplicate side effects.
152
154
 
153
155
  Subagents load `self-reap.ts`, which exits an idle finished subagent after a
154
156
  grace window (`PI_BABYSIT_REAP_AFTER`, default 120s) using the same parked-turn
155
- rule, so a subagent waiting on a long build is never false-killed. Optional task
156
- budgets are checked by the parent poller. On the first exceeded limit the worker
157
+ rule, so a subagent waiting on a long build is never false-killed. Give bounded
158
+ recon/review tasks at least one cost, turn, tool-call, or token budget; omit
159
+ budgets only for intentionally open-ended work. Optional task budgets are
160
+ checked by the parent poller. On the first exceeded limit the worker
157
161
  is steered to stop using tools and return its best answer; if it remains active
158
162
  after `PI_BABYSIT_BUDGET_GRACE`, termination is verified before the task is
159
163
  marked budget-killed. Usage shown by check/wait is cumulative for the task.
@@ -166,9 +170,10 @@ marked budget-killed. Usage shown by check/wait is cumulative for the task.
166
170
  | `PI_BABYSIT_BIN` | `pi` | agent binary for subagents |
167
171
  | `PI_BABYSIT_CLI` | `babysit` | babysit binary |
168
172
  | `PI_BABYSIT_VIEW_CMD` | bundled `format-stream.mjs` | live-attach pretty printer for subagent JSONL (`""` disables) |
173
+ | `PI_BABYSIT_QUICK_GRACE` | `2s` | interactive process grace before a still-running command is returned as background work; use `foreground: true` to wait explicitly |
169
174
  | `PI_BABYSIT_REAP_AFTER` | `120s` | idle grace before a finished subagent self-exits (`off`/`none`/`0` disables) |
170
175
  | `PI_BABYSIT_BUDGET_GRACE` | `30s` | grace after a subagent budget is exceeded before verified termination |
171
- | `PI_BABYSIT_RPC_LOG_MODE` | `standard` | `compact` removes duplicate RPC lifecycle payloads; `standard` retains legacy payloads |
176
+ | `PI_BABYSIT_RPC_LOG_MODE` | `compact` | `compact` removes duplicate RPC lifecycle payloads; `standard` opts into legacy payloads |
172
177
  | `PI_BABYSIT_RETENTION_DAYS` | unset | when set to a positive number, remove safe terminal roots older than this at session startup |
173
178
  | `PI_BABYSIT_TAIL_MAX_BYTES` | `8000` | cap for explicit log tails/screens returned by `babysit_check` |
174
179
  | `PI_BABYSIT_INLINE_OUTPUT_MAX_BYTES` | `8000` | cap for complete process output and aggregate multi-wait results |
package/index.ts CHANGED
@@ -148,16 +148,32 @@ const VIEW_CMD =
148
148
  // Appended to every subagent's system prompt. The subagent is a long-lived
149
149
  // headless `pi --mode rpc` worker: turns can end and resume, so babysit_run
150
150
  // (process kind) works normally inside it. It just cannot talk to a human.
151
- const SUBAGENT_GUIDANCE = [
152
- "You are a headless background worker driven over pi's RPC protocol.",
153
- "Work autonomously: you cannot ask the user questions, so state assumptions",
154
- "in your final answer instead. Long commands may be run synchronously via",
155
- "bash (blocking is fine) or via babysit_run (it works here). When your",
156
- "task is complete, produce a final answer message summarizing the outcome —",
157
- "your controller reads it from the event stream.",
158
- ].join(" ");
151
+ export function subagentGuidance(
152
+ depth: number,
153
+ maxDepth: number,
154
+ directBashAvailable: boolean,
155
+ babysitRunAvailable = true,
156
+ ): string {
157
+ const shellGuidance = directBashAvailable
158
+ ? "Direct bash is available, and babysit_run can supervise longer commands."
159
+ : babysitRunAvailable
160
+ ? "Direct bash is unavailable; run shell commands with babysit_run { command }."
161
+ : "No shell execution tool is available in this task's tool allowlist; do not attempt shell commands.";
162
+ const nestingGuidance = depth >= maxDepth
163
+ ? `You are at the inherited subagent depth limit (${depth}/${maxDepth}); do not attempt to spawn another subagent.`
164
+ : `Your inherited subagent depth is ${depth}/${maxDepth}; child subagents may not exceed maxDepth ${maxDepth}.`;
165
+ return [
166
+ "You are a headless background worker driven over pi's RPC protocol.",
167
+ "Work autonomously: you cannot ask the user questions, so state assumptions",
168
+ "in your final answer instead.",
169
+ shellGuidance,
170
+ nestingGuidance,
171
+ "When your task is complete, produce a final answer message summarizing the outcome —",
172
+ "your controller reads it from the event stream.",
173
+ ].join(" ");
174
+ }
159
175
  const POLL_MS = 2500;
160
- const QUICK_COMMAND_GRACE = process.env.PI_BABYSIT_QUICK_GRACE ?? "1s";
176
+ const QUICK_COMMAND_GRACE = process.env.PI_BABYSIT_QUICK_GRACE ?? "2s";
161
177
  const KILL_CONFIRM_TIMEOUT = "4s";
162
178
 
163
179
  interface BsSession {
@@ -1587,13 +1603,16 @@ function taskProgressOf(id: string): { progress: Progress; offset: number } {
1587
1603
  // ---------------------------------------------------------------------------
1588
1604
 
1589
1605
  // Friendly name → unique babysit session id (babysit ids allow [\w.-]).
1590
- async function uniqueSessionId(name: string): Promise<string> {
1606
+ const reservedSessionIds = new Set<string>();
1607
+
1608
+ async function reserveUniqueSessionId(name: string): Promise<string> {
1591
1609
  const base = name.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "proc";
1592
1610
  const taken = new Set((await listSessions()).sessions.map((s) => s.id));
1593
- if (!taken.has(base)) return base;
1594
- for (let i = 2; ; i++) {
1595
- if (!taken.has(`${base}-${i}`)) return `${base}-${i}`;
1596
- }
1611
+ for (const id of reservedSessionIds) taken.add(id);
1612
+ let id = base;
1613
+ for (let i = 2; taken.has(id); i++) id = `${base}-${i}`;
1614
+ reservedSessionIds.add(id);
1615
+ return id;
1597
1616
  }
1598
1617
 
1599
1618
  interface ProcOpts {
@@ -1612,10 +1631,16 @@ async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: s
1612
1631
  if (opts.timeout && opts.timeout !== "none") bsArgs.push("--timeout", opts.timeout);
1613
1632
  if (opts.idleTimeout && opts.idleTimeout !== "none")
1614
1633
  bsArgs.push("--idle-timeout", opts.idleTimeout);
1615
- if (opts.name) bsArgs.push("--id", await uniqueSessionId(opts.name));
1634
+ const reservedId = opts.name ? await reserveUniqueSessionId(opts.name) : undefined;
1635
+ if (reservedId) bsArgs.push("--id", reservedId);
1616
1636
  bsArgs.push("--", SHELL, "-c", opts.command);
1617
1637
 
1618
- const r = await bs(bsArgs, { cwd: opts.cwd });
1638
+ let r: Awaited<ReturnType<typeof bs>>;
1639
+ try {
1640
+ r = await bs(bsArgs, { cwd: opts.cwd });
1641
+ } finally {
1642
+ if (reservedId) reservedSessionIds.delete(reservedId);
1643
+ }
1619
1644
  if (r.code !== 0) {
1620
1645
  return {
1621
1646
  error:
@@ -1687,6 +1712,7 @@ function discardDelivery(delivery: DeliverableMessage): void {
1687
1712
  }
1688
1713
 
1689
1714
  interface SubagentOpts {
1715
+ name?: string;
1690
1716
  agent?: AgentConfig;
1691
1717
  task: string;
1692
1718
  model?: string;
@@ -1720,7 +1746,15 @@ async function spawnSubagent(
1720
1746
  };
1721
1747
  }
1722
1748
  if (tools && tools.length > 0) piArgs.push("--tools", tools.join(","));
1723
- piArgs.push("--append-system-prompt", SUBAGENT_GUIDANCE);
1749
+ piArgs.push(
1750
+ "--append-system-prompt",
1751
+ subagentGuidance(
1752
+ opts.depth,
1753
+ opts.maxDepth,
1754
+ isAllowedDirectBash("") && (!tools || tools.includes("bash")),
1755
+ !tools || tools.includes("babysit_run"),
1756
+ ),
1757
+ );
1724
1758
  let promptTempFile: string | undefined;
1725
1759
  if (opts.agent?.systemPrompt?.trim()) {
1726
1760
  promptTempFile = writePromptTempFile(opts.agent.name, opts.agent.systemPrompt);
@@ -1758,6 +1792,8 @@ async function spawnSubagent(
1758
1792
  if (opts.idleTimeout && opts.idleTimeout !== "none") {
1759
1793
  bsArgs.push("--idle-timeout", opts.idleTimeout);
1760
1794
  }
1795
+ const reservedId = opts.name ? await reserveUniqueSessionId(opts.name) : undefined;
1796
+ if (reservedId) bsArgs.push("--id", reservedId);
1761
1797
  bsArgs.push(
1762
1798
  "--",
1763
1799
  process.execPath,
@@ -1767,13 +1803,18 @@ async function spawnSubagent(
1767
1803
  ...piArgs,
1768
1804
  );
1769
1805
 
1770
- const r = await bs(bsArgs, {
1771
- cwd: opts.cwd,
1772
- env: {
1773
- [SUBAGENT_DEPTH_ENV]: String(opts.depth),
1774
- [SUBAGENT_MAX_DEPTH_ENV]: String(opts.maxDepth),
1775
- },
1776
- });
1806
+ let r: Awaited<ReturnType<typeof bs>>;
1807
+ try {
1808
+ r = await bs(bsArgs, {
1809
+ cwd: opts.cwd,
1810
+ env: {
1811
+ [SUBAGENT_DEPTH_ENV]: String(opts.depth),
1812
+ [SUBAGENT_MAX_DEPTH_ENV]: String(opts.maxDepth),
1813
+ },
1814
+ });
1815
+ } finally {
1816
+ if (reservedId) reservedSessionIds.delete(reservedId);
1817
+ }
1777
1818
  if (r.code !== 0) {
1778
1819
  cleanupPromptTemp();
1779
1820
  return { error: r.stderr || r.stdout || `babysit run failed (exit ${r.code}, no output) — check that \`${BABYSIT_BIN}\` works and ${ROOT} is writable` };
@@ -1792,6 +1833,7 @@ async function spawnSubagent(
1792
1833
  // The success path overwrites this with the full task meta.
1793
1834
  writeMeta(id, {
1794
1835
  kind: "subagent",
1836
+ name: opts.name ?? id,
1795
1837
  task: opts.task,
1796
1838
  notified: true,
1797
1839
  depth: opts.depth,
@@ -1851,6 +1893,7 @@ async function spawnSubagent(
1851
1893
 
1852
1894
  writeMeta(id, {
1853
1895
  kind: "subagent",
1896
+ name: opts.name ?? id,
1854
1897
  task: opts.task,
1855
1898
  promptOffset: resp.offset,
1856
1899
  model: resolvedModel,
@@ -2136,9 +2179,15 @@ async function waitForExit(
2136
2179
 
2137
2180
  if (expectPattern) {
2138
2181
  const e = await bs(["expect", "-s", id, "--timeout", t, expectPattern], { signal });
2139
- if (signal?.aborted || e.code === 130) {
2182
+ if (signal?.aborted) {
2140
2183
  return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
2141
2184
  }
2185
+ if (e.code === 130) {
2186
+ const interruptedStatus = await statusOf(id);
2187
+ if (interruptedStatus?.state === "running") {
2188
+ return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
2189
+ }
2190
+ }
2142
2191
  if (e.code === 0) {
2143
2192
  return {
2144
2193
  id,
@@ -2164,10 +2213,17 @@ async function waitForExit(
2164
2213
  // another wait that is still pending.
2165
2214
  updateWaitReservation(id, "reserve");
2166
2215
  const w = await bs(["wait", "-s", id, "--timeout", t], { signal });
2167
- if (signal?.aborted || w.code === 130) {
2216
+ if (signal?.aborted) {
2168
2217
  updateWaitReservation(id, "abandon");
2169
2218
  return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
2170
2219
  }
2220
+ if (w.code === 130) {
2221
+ const interruptedStatus = await statusOf(id);
2222
+ if (interruptedStatus?.state === "running") {
2223
+ updateWaitReservation(id, "abandon");
2224
+ return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
2225
+ }
2226
+ }
2171
2227
  if (w.code === 124) {
2172
2228
  // 124 is ambiguous (timeout vs child exiting 124) — disambiguate.
2173
2229
  const st0 = await statusOf(id);
@@ -2258,6 +2314,44 @@ export function activeToolsWithoutDirectBash(activeTools: string[], allowDirectB
2258
2314
  return allowDirectBash ? activeTools : activeTools.filter((name) => name !== "bash");
2259
2315
  }
2260
2316
 
2317
+ export function automaticNotificationGroup(entry: unknown): string | undefined {
2318
+ const candidate = entry as {
2319
+ id?: unknown;
2320
+ type?: unknown;
2321
+ message?: { role?: unknown; content?: unknown };
2322
+ };
2323
+ if (
2324
+ candidate?.type !== "message" ||
2325
+ candidate.message?.role !== "assistant" ||
2326
+ typeof candidate.id !== "string" ||
2327
+ !Array.isArray(candidate.message.content)
2328
+ ) {
2329
+ return undefined;
2330
+ }
2331
+ const group = `turn-${candidate.id}`;
2332
+ const runs = candidate.message.content.filter((part) => {
2333
+ if (!part || typeof part !== "object") return false;
2334
+ const call = part as {
2335
+ type?: unknown;
2336
+ name?: unknown;
2337
+ arguments?: Record<string, unknown>;
2338
+ };
2339
+ if (call.type !== "toolCall" || call.name !== "babysit_run") return false;
2340
+ const args = call.arguments ?? {};
2341
+ const existing = typeof args.notificationGroup === "string"
2342
+ ? args.notificationGroup.trim()
2343
+ : "";
2344
+ return (
2345
+ typeof args.command === "string" &&
2346
+ args.command.length > 0 &&
2347
+ args.profile !== "subagent" &&
2348
+ args.foreground !== true &&
2349
+ (existing === "" || existing === group)
2350
+ );
2351
+ });
2352
+ return runs.length >= 2 ? group : undefined;
2353
+ }
2354
+
2261
2355
  // ---------------------------------------------------------------------------
2262
2356
  // extension
2263
2357
  // ---------------------------------------------------------------------------
@@ -2688,7 +2782,25 @@ export default function (pi: ExtensionAPI) {
2688
2782
  rootLeasePath = undefined;
2689
2783
  });
2690
2784
 
2691
- pi.on("tool_call", async (event) => {
2785
+ pi.on("tool_call", async (event, ctx) => {
2786
+ if (event.toolName === "babysit_run") {
2787
+ const input = event.input as {
2788
+ command?: unknown;
2789
+ profile?: unknown;
2790
+ foreground?: unknown;
2791
+ notificationGroup?: unknown;
2792
+ };
2793
+ if (
2794
+ typeof input.command === "string" &&
2795
+ input.profile !== "subagent" &&
2796
+ input.foreground !== true &&
2797
+ (typeof input.notificationGroup !== "string" || input.notificationGroup.trim() === "")
2798
+ ) {
2799
+ const group = automaticNotificationGroup(ctx.sessionManager.getLeafEntry());
2800
+ if (group) input.notificationGroup = group;
2801
+ }
2802
+ return;
2803
+ }
2692
2804
  if (event.toolName !== "bash") return;
2693
2805
  const command = String((event.input as { command?: unknown }).command ?? "");
2694
2806
  if (backgroundsItself(command)) {
@@ -2714,10 +2826,12 @@ export default function (pi: ExtensionAPI) {
2714
2826
  name: "babysit_run",
2715
2827
  label: "Babysit: run",
2716
2828
  description:
2717
- "Run any shell command in a supervised babysit session. Commands that finish within a short " +
2718
- "grace period return completion metadata immediately; longer commands continue in the background " +
2719
- "and trigger an automatic notification on exit. Complete output is returned inline only when it is " +
2720
- "small; larger output stays in the log path for bounded inspection with babysit_check. " +
2829
+ "Run any shell command in a supervised babysit session. Set `foreground: true` when the next step " +
2830
+ "needs the exit result in this tool call. Otherwise commands that finish within a short grace period " +
2831
+ "return completion metadata immediately; longer commands continue in the background and trigger an " +
2832
+ "automatic notification on exit. Sibling background runs in one assistant message are grouped " +
2833
+ "automatically. Complete output is returned inline only when it is small; larger output stays " +
2834
+ "in the log path for bounded inspection with babysit_check. " +
2721
2835
  "In non-interactive mode (`pi -p`, no UI), process mode blocks until exit because there is no " +
2722
2836
  "notification loop. Two modes: (1) `command` — run any shell command, including builds, tests, " +
2723
2837
  "dev servers, watchers, and interactive TUIs; you can type into it with babysit_send and read " +
@@ -2730,13 +2844,15 @@ export default function (pi: ExtensionAPI) {
2730
2844
  promptSnippet:
2731
2845
  "Run any shell command with context-safe captured output; quick commands return metadata, longer ones continue in background",
2732
2846
  promptGuidelines: [
2733
- "Use babysit_run as the default for shell commands, not only long-running work. Small output is returned directly; large stdout/stderr stays out of model context in the returned log path. Give meaningful commands a clear stable `name`.",
2847
+ "Use babysit_run as the default for shell commands, not only long-running work. Small output is returned directly; large stdout/stderr stays out of model context in the returned log path. Give every meaningful process or subagent a clear stable `name`.",
2848
+ "Use babysit_run { command, foreground: true } when the result is required before the next step; this avoids a separate babysit_wait model turn. Do not use foreground for servers, watchers, or commands of unknown duration without a timeout.",
2734
2849
  "Bundle closely related tiny observations into one babysit_run command when that reduces tool turns without obscuring lifecycle or failure handling.",
2735
2850
  "Inspect a babysit log with babysit_check { id, lines, pattern? }; never read or cat a potentially large log file in full. Prefer a targeted `pattern` search over returning a broad tail.",
2736
2851
  "After babysit_run { command } starts a process, end your response immediately so the automatic process-end notification can resume you; NEVER poll with babysit_check or sleep. Set continueAfterStart: true only when you have immediate, specific, non-polling work to do next. Call babysit_wait when you must consume the result inside the current turn (optionally with `expect` to wait for a readiness line like 'listening on').",
2737
2852
  "If a babysit worker is killed externally, babysit_run reports it as worker-dead rather than hanging. Set retryOnWorkerDeath: true only for safe, idempotent commands; it retries at most once and may otherwise duplicate side effects.",
2738
2853
  "babysit_run gives full PTY control: drive interactive programs (installers, wizards, REPLs) with babysit_send (text or named keys) and read the rendered screen with babysit_check { screen: true }.",
2739
2854
  "Delegate self-contained tasks (codebase recon, a parallelizable subtask, work that would pollute your context) with babysit_run { profile: \"subagent\", task }. Launch several for independent subtasks; they run concurrently.",
2855
+ "Set at least one subagent budget (`maxCost`, `maxTurns`, `maxToolCalls`, or `maxUsageTokens`) for bounded recon and review tasks. Omit budgets only for intentionally open-ended work; the absolute timeout remains a separate safety limit.",
2740
2856
  "Subagents cannot create further subagents by default (maximum depth 1). Only the top-level caller can explicitly opt in by setting maxDepth when it creates the first worker; nested workers inherit that limit and cannot raise it.",
2741
2857
  "After spawning subagents, do not idle-wait and do not end your turn to wait for them: keep making progress, then call babysit_wait (ids + mode any/all) when you need their results. Steer or send follow-up tasks with babysit_send; kill runaways with babysit_kill.",
2742
2858
  ],
@@ -2748,7 +2864,7 @@ export default function (pi: ExtensionAPI) {
2748
2864
  ),
2749
2865
  name: Type.Optional(
2750
2866
  Type.String({
2751
- description: "Friendly stable name for a process (becomes the session id), e.g. 'cargo-build'.",
2867
+ description: "Friendly stable name for a process or subagent (becomes the session id), e.g. 'cargo-build' or 'review-api'.",
2752
2868
  }),
2753
2869
  ),
2754
2870
  profile: Type.Optional(
@@ -2814,10 +2930,16 @@ export default function (pi: ExtensionAPI) {
2814
2930
  "Process mode: run in a PTY (default true; enables interactive input/screen). false = plain pipes for cleaner line-oriented logs.",
2815
2931
  }),
2816
2932
  ),
2933
+ foreground: Type.Optional(
2934
+ Type.Boolean({
2935
+ description:
2936
+ "Process mode: wait for exit and return the result in this tool call. Use when the next step needs the result; avoid for servers/watchers unless bounded by timeout.",
2937
+ }),
2938
+ ),
2817
2939
  notificationGroup: Type.Optional(
2818
2940
  Type.String({
2819
2941
  description:
2820
- "Process mode: defer automatic completion until every running process with this group has stopped, then send one batched notification.",
2942
+ "Process mode: defer automatic completion until every running process with this group has stopped, then send one batched notification. Sibling background runs are auto-grouped when omitted.",
2821
2943
  }),
2822
2944
  ),
2823
2945
  continueAfterStart: Type.Optional(
@@ -2877,6 +2999,20 @@ export default function (pi: ExtensionAPI) {
2877
2999
  details: {},
2878
3000
  };
2879
3001
  }
3002
+ if (isSubagent && params.foreground) {
3003
+ return {
3004
+ content: [{ type: "text", text: "`foreground` is available only in process mode; use babysit_wait for subagent task completion." }],
3005
+ isError: true,
3006
+ details: {},
3007
+ };
3008
+ }
3009
+ if (!isSubagent && params.foreground && params.continueAfterStart) {
3010
+ return {
3011
+ content: [{ type: "text", text: "`foreground` and `continueAfterStart` are mutually exclusive." }],
3012
+ isError: true,
3013
+ details: {},
3014
+ };
3015
+ }
2880
3016
 
2881
3017
  // Compute nesting only for subagent mode. Ordinary command processes remain
2882
3018
  // available even when the hosting agent is at its subagent depth limit.
@@ -2913,24 +3049,27 @@ export default function (pi: ExtensionAPI) {
2913
3049
  pollNeeded = true;
2914
3050
  await refreshWidget(ctx);
2915
3051
 
2916
- // One-shot / non-interactive mode (e.g. `pi -p`): there is no live
2917
- // event loop to deliver the exit notification, and ending the turn
2918
- // would exit pi entirely — orphaning the babysat process and losing
2919
- // the session (the agent "settles" the moment it is told the turn will
2920
- // stop). So block inline like a normal command and return the full
2921
- // outcome in THIS turn. The process still runs under babysit (logged,
2922
- // killable), we just wait for it here instead of fire-and-forget.
2923
- if (!ctx.hasUI) {
2924
- let outcome = await waitForExit(res.id, parseDurMs(params.timeout), _signal);
3052
+ // One-shot / non-interactive mode has no event loop that can deliver an
3053
+ // exit notification. `foreground: true` provides the same single-tool-call
3054
+ // result in interactive mode, avoiding a separate babysit_wait model turn.
3055
+ // The command remains supervised, logged, killable, and subject to its
3056
+ // babysit timeout in either case.
3057
+ if (!ctx.hasUI || params.foreground) {
3058
+ // The babysit supervisor owns the absolute command timeout. Waiting with
3059
+ // the same deadline here races its terminal-state write and can return a
3060
+ // false "still running" result at the boundary, so wait for the
3061
+ // supervisor's definitive exit instead.
3062
+ let outcome = await waitForExit(res.id, null, _signal);
2925
3063
  let retried = false;
2926
3064
  if (params.retryOnWorkerDeath && outcome.status?.state === "dead" && outcome.status.exit_code == null) {
2927
3065
  const retry = await spawnProcess(spawnOpts);
2928
3066
  if (!("error" in retry)) {
2929
3067
  res = retry;
2930
3068
  retried = true;
2931
- outcome = await waitForExit(res.id, parseDurMs(params.timeout), _signal);
3069
+ outcome = await waitForExit(res.id, null, _signal);
2932
3070
  }
2933
3071
  }
3072
+ if (ctx.hasUI) await refreshWidget(ctx);
2934
3073
  return {
2935
3074
  content: [{ type: "text", text: `${retried ? "Retried once after external worker death.\n" : ""}${outcome.text}` }],
2936
3075
  isError: !outcome.ok,
@@ -2946,9 +3085,9 @@ export default function (pi: ExtensionAPI) {
2946
3085
  }
2947
3086
 
2948
3087
  // Keep ordinary quick commands ergonomic. Give the process a short grace
2949
- // period; if it exits, return only lifecycle metadata + log path now.
2950
- // A timeout means it is genuinely background work and follows the normal
2951
- // parked-turn / automatic-notification contract below.
3088
+ // period; if it exits, return lifecycle metadata + log path immediately.
3089
+ // A process still running after the grace follows the parked-turn /
3090
+ // automatic-notification contract below.
2952
3091
  await bs(["wait", "-s", res.id, "--timeout", QUICK_COMMAND_GRACE], { signal: _signal });
2953
3092
  let quickStatus = await statusOf(res.id);
2954
3093
  let retried = false;
@@ -3036,6 +3175,7 @@ export default function (pi: ExtensionAPI) {
3036
3175
  // The branch above guarantees a successful plan in subagent mode.
3037
3176
  const subagentNesting = nesting as Extract<SubagentSpawnPlan, { allowed: true }>;
3038
3177
  const res = await spawnSubagent({
3178
+ name: params.name,
3039
3179
  agent,
3040
3180
  task: params.task as string,
3041
3181
  model: params.model,
@@ -3081,6 +3221,7 @@ export default function (pi: ExtensionAPI) {
3081
3221
  details: {
3082
3222
  id: res.id,
3083
3223
  kind: "subagent",
3224
+ name: params.name ?? res.id,
3084
3225
  agent: agent?.name,
3085
3226
  model: res.model,
3086
3227
  task: params.task,
@@ -3140,14 +3281,25 @@ export default function (pi: ExtensionAPI) {
3140
3281
  name: "babysit_check",
3141
3282
  label: "Babysit: check",
3142
3283
  description:
3143
- "Inspect babysit session(s). Without an id: lists all sessions (processes + subagents). " +
3144
- "With an id: a process shows state + recent output, searches its log with `pattern`, " +
3284
+ "Inspect babysit session(s). Without an id: lists running sessions by default; use `state: \"all\"` " +
3285
+ "for history, or filter by terminal state/kind. With an id: a process shows state + recent " +
3286
+ "output, searches its log with `pattern`, " +
3145
3287
  "or captures the rendered screen with `screen: true`; a subagent shows live progress " +
3146
3288
  "(or raw log matches with `pattern`). Results are bounded by `lines` and clipped. " +
3147
3289
  "Do NOT poll this while merely waiting for a process to end — the exit notification is automatic.",
3148
3290
  promptSnippet: "Check status/progress of babysit sessions (processes and subagents)",
3149
3291
  parameters: Type.Object({
3150
- id: Type.Optional(Type.String({ description: "Session id. Omit to list all sessions." })),
3292
+ id: Type.Optional(Type.String({ description: "Session id. Omit to list sessions." })),
3293
+ state: Type.Optional(
3294
+ StringEnum(["running", "terminal", "all"] as const, {
3295
+ description: "List mode only: state filter. Defaults to running; terminal means any non-running state.",
3296
+ }),
3297
+ ),
3298
+ kind: Type.Optional(
3299
+ StringEnum(["process", "subagent", "all"] as const, {
3300
+ description: "List mode only: session kind filter. Defaults to all.",
3301
+ }),
3302
+ ),
3151
3303
  tools: Type.Optional(
3152
3304
  Type.Number({ description: "Subagent: how many recent tool calls to show (default 8, max 50)." }),
3153
3305
  ),
@@ -3179,9 +3331,38 @@ export default function (pi: ExtensionAPI) {
3179
3331
  };
3180
3332
  }
3181
3333
  if (sessions.length === 0) {
3182
- return { content: [{ type: "text", text: "No babysit sessions." }], details: {} };
3334
+ return { content: [{ type: "text", text: "No babysit sessions." }], details: { sessions: [] } };
3335
+ }
3336
+ const stateFilter = params.state ?? "running";
3337
+ const kindFilter = params.kind ?? "all";
3338
+ const stateMatches = (session: BsSession) => stateFilter === "all"
3339
+ ? true
3340
+ : stateFilter === "running"
3341
+ ? session.state === "running"
3342
+ : session.state !== "running";
3343
+ const kindOfSession = (session: BsSession) => readMeta(session.id)?.kind ?? "process";
3344
+ const selected = sessions.filter((session) =>
3345
+ stateMatches(session) && (kindFilter === "all" || kindOfSession(session) === kindFilter),
3346
+ );
3347
+ const reveal: string[] = [];
3348
+ if (stateFilter !== "all" && sessions.some((session) => !stateMatches(session))) {
3349
+ reveal.push('state: "all"');
3350
+ }
3351
+ if (kindFilter !== "all" && sessions.some((session) => kindOfSession(session) !== kindFilter)) {
3352
+ reveal.push('kind: "all"');
3353
+ }
3354
+ const revealHint = reveal.length > 0 ? `; use ${reveal.join(" and ")} to widen the list` : "";
3355
+ if (selected.length === 0) {
3356
+ const hidden = sessions.length;
3357
+ return {
3358
+ content: [{
3359
+ type: "text",
3360
+ text: `No ${stateFilter}${kindFilter === "all" ? "" : ` ${kindFilter}`} babysit sessions.${hidden > 0 ? ` ${hidden} session(s) hidden${revealHint}.` : ""}`,
3361
+ }],
3362
+ details: { sessions: [], total: sessions.length, hidden, state: stateFilter, kind: kindFilter },
3363
+ };
3183
3364
  }
3184
- const lines = sessions.map((s) => {
3365
+ const lines = selected.map((s) => {
3185
3366
  const meta = readMeta(s.id);
3186
3367
  const kind = meta?.kind ?? "process";
3187
3368
  const flag = s.note ? ` ⚑ ${s.note}` : "";
@@ -3194,7 +3375,12 @@ export default function (pi: ExtensionAPI) {
3194
3375
  const preview = what.length > 60 ? `${what.slice(0, 57)}…` : what;
3195
3376
  return `${s.id} [${kind}] ${s.state}${ec}${depth}${flag}${preview ? ` — ${preview}` : ""}`;
3196
3377
  });
3197
- return { content: [{ type: "text", text: clip(lines.join("\n")) }], details: { sessions } };
3378
+ const hidden = sessions.length - selected.length;
3379
+ const suffix = hidden > 0 ? `\n… ${hidden} session(s) hidden${revealHint}.` : "";
3380
+ return {
3381
+ content: [{ type: "text", text: clip(lines.join("\n") + suffix) }],
3382
+ details: { sessions: selected, total: sessions.length, hidden, state: stateFilter, kind: kindFilter },
3383
+ };
3198
3384
  }
3199
3385
 
3200
3386
  const st = await statusOf(params.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -8,10 +8,10 @@
8
8
  * message. Persisting every snapshot makes babysit logs grow quadratically.
9
9
  *
10
10
  * This proxy forwards stdin byte-for-byte. `message_update` snapshots are
11
- * always compacted. In opt-in `compact` log mode, redundant payloads on
12
- * lifecycle/tool events are also removed while authoritative `message_end`,
11
+ * always compacted. Compact log mode is the default: redundant payloads on
12
+ * lifecycle/tool events are removed while authoritative `message_end`,
13
13
  * response, and error events remain intact. Set PI_BABYSIT_RPC_LOG_MODE=standard
14
- * to retain the legacy lifecycle payloads.
14
+ * to opt back into the legacy lifecycle payloads.
15
15
  */
16
16
  import { spawn } from "node:child_process";
17
17
  import { StringDecoder } from "node:string_decoder";
@@ -44,7 +44,7 @@ function isParkedMessages(messages) {
44
44
 
45
45
  export function compactRpcLine(
46
46
  line,
47
- mode = process.env.PI_BABYSIT_RPC_LOG_MODE ?? "standard",
47
+ mode = process.env.PI_BABYSIT_RPC_LOG_MODE ?? "compact",
48
48
  ) {
49
49
  if (!line.trimStart().startsWith("{")) return line;
50
50