@yusukeshib/pi-babysit 0.4.0 → 0.6.0

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
@@ -33,7 +33,7 @@ reachable from anywhere (`~/.pi-babysit/<pi-session-id>/`). Two kinds:
33
33
  | kind | started by | completion | on completion |
34
34
  | ---- | ---------- | ---------- | ------------- |
35
35
  | **process** | `babysit_run { command }` | process **exit** | automatic notification message (`triggerTurn`), batched for all exits observed in the same poll — the agent may end its turn after starting and is resumed on exit, same contract as the old `process` tool |
36
- | **subagent** | `babysit_run { profile: "subagent", task }` | `agent_settled` in the RPC event stream (worker remains reusable during its idle grace) | none — the agent polls `babysit_check` or blocks on `babysit_wait`; the idle session accepts follow-up tasks until self-reap |
36
+ | **subagent** | `babysit_run { profile: "subagent", task }` | `agent_settled` in the RPC event stream (worker remains reusable during its idle grace) | `foreground: true` returns the answer and nested usage in the same tool call; background workers must be collected with `babysit_wait` before the parent finishes, and remain reusable until self-reap |
37
37
 
38
38
  The **profile is a tool parameter, not a separate tool set**: domain knowledge
39
39
  (RPC bookkeeping, per-task byte offsets, parked-turn detection, PTY-safe
@@ -56,7 +56,7 @@ 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`). Set `foreground: true` when the next step needs the result in the same tool call; use `returnPattern`/`returnLines`/`maxBytes` to keep noisy output bounded without a second check turn. Or start a named subagent (`profile: "subagent"`, `task`, optional `name`/`agent`/`model`/`tools`/`maxDepth` and budget fields). `maxDepth` defaults to 1. Quick commands return inline; longer ones notify in the background |
59
+ | `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`/`notificationGroup`). Set `foreground: true` for one process or subagent whose result is needed in the same tool call; use `returnPattern`/`returnLines`/`maxBytes` to keep noisy process output bounded. Or start a named background subagent (`profile: "subagent"`, `task`, optional `name`/`agent`/`model`/`tools`/`maxDepth` and budget fields), then always collect it with `babysit_wait`. `maxDepth` defaults to 1. Quick commands return inline; longer process runs notify in the background |
60
60
  | `babysit_check` | Without an id, list sessions with state/kind filters. With an id, inspect bounded output, search with `pattern`, or capture a TUI with `screen: true`; `maxBytes` overrides the 4 KB default up to 24 KB |
61
61
  | `babysit_send` | Process: type `text` / press `keys` into the PTY. Subagent: steer mid-run, or send a follow-up task when confirmed settled (`mode: auto/steer/task`); explicit task mode rejects busy, parked, or unknown state |
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"` |
@@ -75,8 +75,10 @@ re-enables `bash`, including shell backgrounding (`… &`, `nohup`, `setsid`,
75
75
  | `/babysit` | Arrow-key picker over all sessions. Renders an **inline snapshot** (no tmux): running **process** → current rendered screen + recent output + a copy-paste `babysit attach` take-over hint (detach `Ctrl-\ Ctrl-\`); running **subagent** → read-only progress (RPC stdin stays untouchable); finished → summary. Re-run `/babysit` to refresh |
76
76
  | `/babysit gc [days]` | Preview and confirm deletion of old Pi-session roots (default 14 days). Active leases, live supervisor/child PIDs, unknown states, the current root, and recent roots are retained; deletion uses a GC lock and atomic rename |
77
77
 
78
- A minimal widget above the editor shows live counts
79
- (`N processes · M subagents working · K idle`).
78
+ A widget below the editor separates session **kind** from task **state** at a glance:
79
+ summary counts use `RUNNING` / `IDLE`, and every row is labeled
80
+ `[PROCESS] [RUNNING]`, `[AGENT] [RUNNING]`, or `[AGENT] [IDLE]`. The displayed
81
+ `age` is total session age, not time spent in the current state.
80
82
 
81
83
  ## Logs without context flooding
82
84
 
@@ -117,7 +119,12 @@ Set `PI_BABYSIT_RPC_LOG_MODE=standard` only when legacy lifecycle payloads are
117
119
  needed for debugging. Live `/babysit` and attach views render either format.
118
120
 
119
121
  All shell commands, including `pwd` and Git, go through `babysit_run`. Bundle
120
- closely related tiny observations when doing so safely reduces tool turns.
122
+ closely related tiny observations when doing so safely reduces tool turns. For
123
+ parallel checks, do not issue sibling `foreground: true` calls: start them with
124
+ `continueAfterStart: true` and collect them in one multi-session `babysit_wait`.
125
+ During fix loops, prefer targeted checks after each edit and one full validation
126
+ suite at the end. Background subagents must likewise be collected before the
127
+ parent task finishes so their answer and nested usage are not lost.
121
128
  Set `PI_BABYSIT_ALLOW_BASH=1` only as an explicit emergency escape hatch.
122
129
 
123
130
  ## Unexpected worker loss
@@ -143,7 +150,10 @@ because blindly rerunning an arbitrary command can duplicate side effects.
143
150
  their exits span multiple polls.
144
151
  `babysit_kill` and an exit already reported by `babysit_wait` suppress the
145
152
  notification.
146
- - **Subagent**: `babysit_wait` blocks on `babysit expect '"type":"agent_settled"'`.
153
+ - **Subagent**: `foreground: true` or `babysit_wait` blocks on
154
+ `babysit expect '"type":"agent_settled"'`. A background task that settles or
155
+ exits before it is collected emits one ready-to-collect reminder; the parent
156
+ must still call `babysit_wait` so the answer and nested usage are persisted.
147
157
  Unlike `agent_end`, `agent_settled` cannot precede an automatic retry,
148
158
  compaction retry, or queued continuation. A settled run containing a
149
159
  **parked** toolResult — a `babysit_run { command }` result carrying the
package/index.ts CHANGED
@@ -418,6 +418,13 @@ interface Meta {
418
418
  budgetKilled?: boolean;
419
419
  /** Prompt offset whose nested usage has already been charged to the parent session. */
420
420
  usageReportedOffset?: number;
421
+ /** Prompt offset explicitly collected by foreground mode or babysit_wait. */
422
+ subagentCollectedOffset?: number;
423
+ /** Prompt offset whose ready-to-collect reminder was sent to the parent. */
424
+ subagentNotifiedOffset?: number;
425
+ /** Current task completion first observed by the reminder poller. */
426
+ subagentCompletionObservedOffset?: number;
427
+ subagentCompletionObservedAt?: number;
421
428
  }
422
429
 
423
430
  const metaDir = () => path.join(ROOT, "meta");
@@ -731,6 +738,16 @@ export function shouldDeliverProcessCompletion(
731
738
  return meta?.kind === "process" && !meta.notified && !meta.notificationPaused;
732
739
  }
733
740
 
741
+ export function shouldDeliverSubagentCompletion(meta: Meta | null): meta is Meta & { kind: "subagent" } {
742
+ if (meta?.kind !== "subagent") return false;
743
+ const offset = meta.promptOffset ?? 0;
744
+ return (
745
+ meta.usageReportedOffset !== offset &&
746
+ meta.subagentCollectedOffset !== offset &&
747
+ meta.subagentNotifiedOffset !== offset
748
+ );
749
+ }
750
+
734
751
  export function isNotificationGroupReady(
735
752
  meta: Meta,
736
753
  sessions: BsSession[],
@@ -752,10 +769,14 @@ export function shouldKeepPolling(
752
769
  sessions: Array<{ id: string; state: string }>,
753
770
  metaFor: (id: string) => Meta | null,
754
771
  ): boolean {
755
- return sessions.some(
756
- (session) =>
757
- session.state === "running" || shouldDeliverProcessCompletion(metaFor(session.id)),
758
- );
772
+ return sessions.some((session) => {
773
+ const meta = metaFor(session.id);
774
+ return (
775
+ session.state === "running" ||
776
+ shouldDeliverProcessCompletion(meta) ||
777
+ (session.state !== "running" && shouldDeliverSubagentCompletion(meta))
778
+ );
779
+ });
759
780
  }
760
781
 
761
782
  export function shouldKeepPollingAfterList(
@@ -2167,13 +2188,56 @@ async function spawnSubagent(
2167
2188
  // A subagent whose task is done stays alive as an idle RPC worker (for
2168
2189
  // follow-ups), so "running" in babysit does NOT mean "working" — count
2169
2190
  // processes / busy subagents / idle subagents separately.
2170
- function renderWidgetLines(procs: number, busy: number, idle: number): string[] {
2191
+ export type WidgetSessionKind = "process" | "agent";
2192
+ export type WidgetSessionState = "running" | "idle";
2193
+
2194
+ export function widgetSummaryText(procs: number, busy: number, idle: number): string {
2195
+ const sections: string[] = [];
2196
+ const running: string[] = [];
2197
+ if (procs > 0) running.push(`${procs} process${procs > 1 ? "es" : ""}`);
2198
+ if (busy > 0) running.push(`${busy} agent${busy > 1 ? "s" : ""}`);
2199
+ if (running.length > 0) sections.push(`RUNNING ${running.join(" · ")}`);
2200
+ if (idle > 0) sections.push(`IDLE ${idle} agent${idle > 1 ? "s" : ""}`);
2201
+ return sections.join(" │ ");
2202
+ }
2203
+
2204
+ export function widgetSessionHeader(
2205
+ id: string,
2206
+ kind: WidgetSessionKind,
2207
+ state: WidgetSessionState,
2208
+ elapsed?: string,
2209
+ ): string {
2210
+ const icon = state === "running" ? "▶" : "○";
2211
+ return ` ${icon} ${id} [${kind.toUpperCase()}] [${state.toUpperCase()}]${elapsed ? ` · age ${elapsed}` : ""}`;
2212
+ }
2213
+
2214
+ function renderWidgetLines(procs: number, busy: number, idle: number, theme: Theme): string[] {
2171
2215
  if (procs === 0 && busy === 0 && idle === 0) return [];
2172
- const parts: string[] = [];
2173
- if (procs > 0) parts.push(`${procs} process${procs > 1 ? "es" : ""}`);
2174
- if (busy > 0) parts.push(`${busy} subagent${busy > 1 ? "s" : ""} working`);
2175
- if (idle > 0) parts.push(`${idle} idle`);
2176
- return [`\x1b[44;97m ${parts.join(" \u00b7 ")} \x1b[0m`];
2216
+ const sections: string[] = [];
2217
+ const running: string[] = [];
2218
+ if (procs > 0) running.push(`${procs} process${procs > 1 ? "es" : ""}`);
2219
+ if (busy > 0) running.push(`${busy} agent${busy > 1 ? "s" : ""}`);
2220
+ if (running.length > 0) {
2221
+ sections.push(`${theme.fg("success", theme.bold("RUNNING"))} ${running.join(" · ")}`);
2222
+ }
2223
+ if (idle > 0) {
2224
+ sections.push(`${theme.fg("muted", theme.bold("IDLE"))} ${idle} agent${idle > 1 ? "s" : ""}`);
2225
+ }
2226
+ return [theme.bg("toolPendingBg", ` ${sections.join(" │ ")} `)];
2227
+ }
2228
+
2229
+ function renderWidgetSessionHeader(
2230
+ id: string,
2231
+ kind: WidgetSessionKind,
2232
+ state: WidgetSessionState,
2233
+ elapsed: string,
2234
+ theme: Theme,
2235
+ ): string {
2236
+ const running = state === "running";
2237
+ const icon = theme.fg(running ? "success" : "muted", running ? "▶" : "○");
2238
+ const kindLabel = theme.fg(kind === "process" ? "accent" : "warning", theme.bold(`[${kind.toUpperCase()}]`));
2239
+ const stateLabel = theme.fg(running ? "success" : "muted", theme.bold(`[${state.toUpperCase()}]`));
2240
+ return ` ${icon} ${id} ${kindLabel} ${stateLabel}${elapsed ? theme.fg("dim", ` · age ${elapsed}`) : ""}`;
2177
2241
  }
2178
2242
 
2179
2243
  // How many trailing output lines to show per running session in the widget.
@@ -2457,16 +2521,27 @@ async function waitForTask(
2457
2521
  }
2458
2522
  }
2459
2523
 
2460
- // Mark a process session as already-reported so the exit-notification poller
2461
- // doesn't send a duplicate message for something the agent just observed.
2524
+ // Mark a session as already reported so completion pollers do not send a
2525
+ // duplicate message for something the agent just observed.
2462
2526
  function suppressNotify(id: string, reason: "observed" | "kill" = "observed"): void {
2463
2527
  const meta = readMeta(id);
2464
- if (meta && meta.kind === "process") {
2528
+ if (!meta) return;
2529
+ if (meta.kind === "process") {
2465
2530
  meta.notified = true;
2466
2531
  if (reason === "kill") meta.killNotificationSuppressed = true;
2467
2532
  delete meta.notificationPaused;
2468
- writeMeta(id, meta);
2533
+ } else {
2534
+ meta.subagentCollectedOffset = meta.promptOffset ?? 0;
2469
2535
  }
2536
+ writeMeta(id, meta);
2537
+ }
2538
+
2539
+ function collectSubagentOutcome(outcome: WaitOutcome): void {
2540
+ if (outcome.kind !== "done" && outcome.kind !== "exited") return;
2541
+ const meta = readMeta(outcome.id);
2542
+ if (!meta || meta.kind !== "subagent") return;
2543
+ meta.subagentCollectedOffset = meta.promptOffset ?? 0;
2544
+ writeMeta(outcome.id, meta);
2470
2545
  }
2471
2546
 
2472
2547
  export interface WaitReservationState {
@@ -2574,10 +2649,23 @@ async function waitForExit(
2574
2649
  // one concurrent wait timing out cannot re-enable notifications underneath
2575
2650
  // another wait that is still pending.
2576
2651
  updateWaitReservation(id, "reserve");
2577
- const w = await bs(["wait", "-s", id, "--timeout", t], { signal });
2578
- if (signal?.aborted) {
2579
- updateWaitReservation(id, "abandon");
2580
- return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
2652
+ let w: Awaited<ReturnType<typeof bs>>;
2653
+ let attempt = 0;
2654
+ for (;;) {
2655
+ w = await bs(["wait", "-s", id, "--timeout", t], { signal });
2656
+ if (signal?.aborted) {
2657
+ updateWaitReservation(id, "abandon");
2658
+ return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
2659
+ }
2660
+ if (w.code === 0 || w.code === 124 || w.code === 130) break;
2661
+
2662
+ // A freshly spawned session can be visible in `list` before the backend's
2663
+ // wait endpoint is ready, especially when sibling foreground tools start
2664
+ // concurrently. Retry that transient startup race instead of reporting the
2665
+ // still-running child as "exited with code ?".
2666
+ const retryStatus = await statusOf(id);
2667
+ if (retryStatus?.state !== "running" || attempt++ >= 3) break;
2668
+ await new Promise((resolve) => setTimeout(resolve, 50 * attempt));
2581
2669
  }
2582
2670
  if (w.code === 130) {
2583
2671
  const interruptedStatus = await statusOf(id);
@@ -2619,6 +2707,18 @@ async function waitForExit(
2619
2707
  if (!expectPattern) updateWaitReservation(id, "claim");
2620
2708
  return { id, kind: "exited", ok: false, text: `No such session: ${id}` };
2621
2709
  }
2710
+ if (st.state === "running") {
2711
+ if (!expectPattern) updateWaitReservation(id, "abandon");
2712
+ return {
2713
+ id,
2714
+ kind: "interrupted",
2715
+ ok: false,
2716
+ text:
2717
+ `The wait backend returned before process ${id} exited; the process is still running. ` +
2718
+ `Use babysit_wait to continue waiting.\nLog: ${logPath(id)}`,
2719
+ status: st,
2720
+ };
2721
+ }
2622
2722
  if (expectPattern) suppressNotify(id);
2623
2723
  else updateWaitReservation(id, "claim"); // the agent sees the exit here; don't notify again
2624
2724
  const meta = readMeta(id);
@@ -2964,6 +3064,64 @@ export default function (pi: ExtensionAPI) {
2964
3064
  );
2965
3065
  }
2966
3066
 
3067
+ async function notifySettledSubagents(
3068
+ ctx: ExtensionContext,
3069
+ snapshot?: BsSession[],
3070
+ ): Promise<void> {
3071
+ if (shouldDeferCompletionNotification(ctx.isIdle())) return;
3072
+ const sessions = snapshot ?? (await listSessions()).sessions;
3073
+ const ready: Array<{ id: string; offset: number; state: string; summary: string }> = [];
3074
+ for (const session of sessions) {
3075
+ const meta = readMeta(session.id);
3076
+ if (!shouldDeliverSubagentCompletion(meta)) continue;
3077
+ let progress: Progress;
3078
+ try {
3079
+ progress = taskProgressOf(session.id).progress;
3080
+ } catch {
3081
+ progress = emptyProgress();
3082
+ }
3083
+ if (session.state === "running" && !progress.done) continue;
3084
+
3085
+ const offset = meta.promptOffset ?? 0;
3086
+ if (meta.subagentCompletionObservedOffset !== offset) {
3087
+ meta.subagentCompletionObservedOffset = offset;
3088
+ meta.subagentCompletionObservedAt = Date.now();
3089
+ writeMeta(session.id, meta);
3090
+ continue;
3091
+ }
3092
+ if (Date.now() - (meta.subagentCompletionObservedAt ?? 0) < POLL_MS) continue;
3093
+ const summary = progress.done
3094
+ ? `task settled; ${progress.turns} turns, ${progress.toolCallCount} tools, $${progress.cost.toFixed(4)}`
3095
+ : `worker ${session.state} with exit code ${session.exit_code ?? "?"}; partial usage $${progress.cost.toFixed(4)}`;
3096
+ ready.push({ id: session.id, offset, state: session.state, summary });
3097
+ }
3098
+ if (ready.length === 0 || shouldDeferCompletionNotification(ctx.isIdle())) return;
3099
+
3100
+ const deliverable = ready.filter(({ id, offset }) => {
3101
+ const meta = readMeta(id);
3102
+ return shouldDeliverSubagentCompletion(meta) && (meta.promptOffset ?? 0) === offset;
3103
+ });
3104
+ if (deliverable.length === 0) return;
3105
+ pi.sendMessage(
3106
+ {
3107
+ customType: "pi-babysit-subagent-ready",
3108
+ content:
3109
+ `${deliverable.length === 1 ? "A background subagent is" : `${deliverable.length} background subagents are`} ready to collect:\n` +
3110
+ deliverable.map(({ id, summary }) => `- ${id}: ${summary}`).join("\n") +
3111
+ "\nCall babysit_wait now to retrieve the answer and charge nested usage before finishing the parent task.",
3112
+ display: true,
3113
+ details: { subagents: deliverable.map(({ id, state }) => ({ id, state })) },
3114
+ },
3115
+ { triggerTurn: true, deliverAs: "steer" },
3116
+ );
3117
+ for (const { id, offset } of deliverable) {
3118
+ const meta = readMeta(id);
3119
+ if (!meta || meta.kind !== "subagent" || (meta.promptOffset ?? 0) !== offset) continue;
3120
+ meta.subagentNotifiedOffset = offset;
3121
+ writeMeta(id, meta);
3122
+ }
3123
+ }
3124
+
2967
3125
  const refreshWidget = async (ctx: ExtensionContext, snapshot?: BsSession[]) => {
2968
3126
  if (!ctx.hasUI) return;
2969
3127
  const active = (snapshot ?? (await listSessions()).sessions).filter(
@@ -2980,7 +3138,8 @@ export default function (pi: ExtensionAPI) {
2980
3138
  }
2981
3139
  }
2982
3140
  const idle = subs.filter((session) => progressById.get(session.id)?.done).length;
2983
- const lines = renderWidgetLines(procs, subs.length - idle, idle);
3141
+ const theme = ctx.ui.theme;
3142
+ const lines = renderWidgetLines(procs, subs.length - idle, idle, theme);
2984
3143
  const tails = await Promise.all(
2985
3144
  active.map((session) => {
2986
3145
  const isSubagent = kindOf(session.id) === "subagent";
@@ -2993,11 +3152,17 @@ export default function (pi: ExtensionAPI) {
2993
3152
  );
2994
3153
  active.forEach((session, index) => {
2995
3154
  const isSubagent = kindOf(session.id) === "subagent";
2996
- const tag = isSubagent
2997
- ? progressById.get(session.id)?.done ? "sub idle" : "sub"
2998
- : "proc";
3155
+ const state: WidgetSessionState = isSubagent && progressById.get(session.id)?.done
3156
+ ? "idle"
3157
+ : "running";
2999
3158
  const elapsed = elapsedOf(session.id);
3000
- const header = ` ⏳ ${session.id} [${tag}]${elapsed ? ` ${elapsed}` : ""}`;
3159
+ const header = renderWidgetSessionHeader(
3160
+ session.id,
3161
+ isSubagent ? "agent" : "process",
3162
+ state,
3163
+ elapsed,
3164
+ theme,
3165
+ );
3001
3166
  if (tails[index].length === 1) {
3002
3167
  lines.push(`${header} │ ${tails[index][0]}`);
3003
3168
  } else {
@@ -3157,6 +3322,7 @@ export default function (pi: ExtensionAPI) {
3157
3322
  await enforceSubagentBudgets(snapshot);
3158
3323
  await Promise.all([
3159
3324
  notifyEndedProcesses(ctx, snapshot),
3325
+ notifySettledSubagents(ctx, snapshot),
3160
3326
  refreshWidget(ctx, snapshot),
3161
3327
  ]);
3162
3328
  pruneTerminalSessionCache(taskProgressCache, snapshot);
@@ -3228,12 +3394,13 @@ export default function (pi: ExtensionAPI) {
3228
3394
  "`returnPattern`/`returnLines` bound foreground output. Sessions support check, wait, send, and kill.",
3229
3395
  promptSnippet: "Run supervised commands or bounded pi subagents with context-safe logs",
3230
3396
  promptGuidelines: [
3231
- "Use babysit_run for shell commands and give meaningful sessions a stable name; bundle tiny related observations.",
3232
- "Use babysit_run foreground mode when the next step needs the result; use returnPattern/returnLines for noisy commands. Do not foreground unbounded servers.",
3397
+ "Use babysit_run for shell commands and give meaningful sessions a stable name; bundle tiny related read-only observations into one command.",
3398
+ "Use babysit_run foreground mode for one process or subagent whose result is needed now; never issue sibling foreground runs in parallel. For parallel checks, start background runs with continueAfterStart and collect them with one multi-session babysit_wait.",
3399
+ "Use returnPattern/returnLines for noisy commands. During edit/fix loops run targeted checks first and one full validation suite at the end instead of repeating every full gate.",
3233
3400
  "After a background process starts, stop the turn for its automatic notification; never poll or sleep. Use continueAfterStart only for specific non-polling work.",
3234
3401
  "Inspect large logs with a narrow babysit_check pattern and maxBytes rather than broad tails.",
3235
3402
  "Use retryOnWorkerDeath only once and only for idempotent commands; retries may duplicate side effects.",
3236
- "Delegate independent work with bounded babysit_run subagents; set at least one cost/turn/tool/token budget and keep making progress before babysit_wait.",
3403
+ "Delegate independent work with bounded babysit_run subagents. Prefer foreground for one result needed now; every background subagent must be collected with babysit_wait before the parent task finishes. Size budgets above the worker's initial context and expected tool count.",
3237
3404
  "Subagent recursion defaults to depth 1; only a top-level caller may explicitly raise maxDepth.",
3238
3405
  ],
3239
3406
  parameters: Type.Object({
@@ -3321,7 +3488,7 @@ export default function (pi: ExtensionAPI) {
3321
3488
  ),
3322
3489
  foreground: Type.Optional(
3323
3490
  Type.Boolean({
3324
- description: "Process: wait for exit and return the result now.",
3491
+ description: "Process or subagent: wait for completion and return the result in this tool call.",
3325
3492
  }),
3326
3493
  ),
3327
3494
  returnPattern: Type.Optional(
@@ -3396,16 +3563,16 @@ export default function (pi: ExtensionAPI) {
3396
3563
  details: {},
3397
3564
  };
3398
3565
  }
3399
- if (isSubagent && params.foreground) {
3566
+ if (isSubagent && (params.returnPattern || params.returnLines != null || params.maxBytes != null)) {
3400
3567
  return {
3401
- content: [{ type: "text", text: "`foreground` is available only in process mode; use babysit_wait for subagent task completion." }],
3568
+ content: [{ type: "text", text: "`returnPattern`, `returnLines`, and `maxBytes` are process-output options." }],
3402
3569
  isError: true,
3403
3570
  details: {},
3404
3571
  };
3405
3572
  }
3406
- if (isSubagent && (params.returnPattern || params.returnLines != null || params.maxBytes != null)) {
3573
+ if (isSubagent && params.continueAfterStart != null) {
3407
3574
  return {
3408
- content: [{ type: "text", text: "`returnPattern`, `returnLines`, and `maxBytes` are process-output options." }],
3575
+ content: [{ type: "text", text: "`continueAfterStart` is available only in process mode." }],
3409
3576
  isError: true,
3410
3577
  details: {},
3411
3578
  };
@@ -3625,15 +3792,37 @@ export default function (pi: ExtensionAPI) {
3625
3792
 
3626
3793
  pollNeeded = true;
3627
3794
  await refreshWidget(ctx);
3795
+ if (params.foreground || !ctx.hasUI) {
3796
+ const outcome = await waitForTask(res.id, null, _signal);
3797
+ collectSubagentOutcome(outcome);
3798
+ const usage = claimOutcomeUsage(outcome);
3799
+ if (ctx.hasUI) await refreshWidget(ctx);
3800
+ return {
3801
+ content: [{ type: "text", text: outcome.text }],
3802
+ isError: !outcome.ok,
3803
+ usage,
3804
+ details: {
3805
+ id: res.id,
3806
+ kind: "subagent",
3807
+ name: params.name ?? res.id,
3808
+ agent: agent?.name,
3809
+ model: res.model,
3810
+ task: params.task,
3811
+ depth: subagentNesting.childDepth,
3812
+ maxDepth: subagentNesting.maxDepth,
3813
+ status: outcomeStatus(outcome),
3814
+ },
3815
+ };
3816
+ }
3628
3817
  return {
3629
3818
  content: [
3630
3819
  {
3631
3820
  type: "text",
3632
3821
  text:
3633
3822
  `Subagent started (id: ${res.id})${agent ? ` [agent: ${agent.name}]` : ""}${res.model ? ` [model: ${res.model}]` : ""} [depth: ${subagentNesting.childDepth}/${subagentNesting.maxDepth}].\n` +
3634
- `Task accepted — running in the background; keep working (do NOT end your turn just to wait for it).\n` +
3635
- `Poll: babysit_check { id: "${res.id}" }\n` +
3636
- `Wait: babysit_wait { id: "${res.id}" }\n` +
3823
+ `Task accepted — running in the background. You MUST collect it with babysit_wait before finishing the parent task; use foreground: true next time when no independent work is available.\n` +
3824
+ `Progress: babysit_check { id: "${res.id}" } (only when inspection is needed)\n` +
3825
+ `Collect: babysit_wait { id: "${res.id}" }\n` +
3637
3826
  `Human can watch/steer: /babysit (pick ${res.id})`,
3638
3827
  },
3639
3828
  ],
@@ -4257,6 +4446,7 @@ export default function (pi: ExtensionAPI) {
4257
4446
 
4258
4447
  if (ids.length === 1) {
4259
4448
  const r = await waitFor(ids[0], limitMs, signal, params.expect);
4449
+ collectSubagentOutcome(r);
4260
4450
  const usage = claimOutcomeUsage(r);
4261
4451
  return {
4262
4452
  content: [{ type: "text", text: r.text }],
@@ -4277,6 +4467,7 @@ export default function (pi: ExtensionAPI) {
4277
4467
  ids.map((i) => waitFor(i, limitMs, signal, params.expect)),
4278
4468
  );
4279
4469
  const ok = results.every((r) => r.ok);
4470
+ results.forEach(collectSubagentOutcome);
4280
4471
  const usage = sumNestedUsage(results.map(claimOutcomeUsage));
4281
4472
  return {
4282
4473
  content: [
@@ -4306,6 +4497,7 @@ export default function (pi: ExtensionAPI) {
4306
4497
  ids.map((i) => waitFor(i, limitMs, ctrl.signal, params.expect)),
4307
4498
  );
4308
4499
  const others = ids.filter((i) => i !== first.id);
4500
+ collectSubagentOutcome(first);
4309
4501
  const usage = claimOutcomeUsage(first);
4310
4502
  return {
4311
4503
  content: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",
package/self-reap.ts CHANGED
@@ -100,12 +100,15 @@ export default function (pi: ExtensionAPI) {
100
100
 
101
101
  // agent_end can precede automatic retries, compaction retries, and queued
102
102
  // continuations. Start the grace timer only once Pi is truly settled.
103
- pi.on("agent_settled", (_event, ctx) => {
103
+ pi.on("agent_settled", () => {
104
104
  cancel();
105
105
  if (lastEndWasParked) return;
106
106
  timer = setTimeout(() => {
107
- // Graceful shutdown emits session_shutdown for every loaded extension.
108
- ctx.shutdown();
107
+ // In RPC mode ctx.shutdown() only sets a flag that is checked on the next
108
+ // command or agent_settled event. An already-idle worker receives neither,
109
+ // so it would remain alive indefinitely. At this point the grace elapsed,
110
+ // the task is settled, and parked turns were excluded above; exit cleanly.
111
+ process.exit(0);
109
112
  }, graceMs);
110
113
  timer.unref?.();
111
114
  });