@yusukeshib/pi-babysit 0.6.1 → 0.6.2
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 +18 -11
- package/index.ts +98 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,24 +56,27 @@ 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` 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 |
|
|
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. Subagent `continueAfterStart: true` is accepted as a compatibility alias for this default background behavior. 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"` |
|
|
63
63
|
| `babysit_kill` | Terminate a session, verify terminal state, then suppress the exit notification |
|
|
64
64
|
|
|
65
65
|
The built-in `bash` tool is removed from the active tool set so the model does
|
|
66
|
-
not waste a failed tool turn before choosing `babysit_run`.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
66
|
+
not waste a failed tool turn before choosing `babysit_run`. Process commands
|
|
67
|
+
retain the built-in Bash tool's dynamic Pi metadata environment:
|
|
68
|
+
`PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, and
|
|
69
|
+
`PI_REASONING_LEVEL`. A fallback `tool_call` hook still blocks direct shell
|
|
70
|
+
calls if another extension or preset re-enables `bash`, including shell
|
|
71
|
+
backgrounding (`… &`, `nohup`, `setsid`, `disown`). Set
|
|
72
|
+
`PI_BABYSIT_ALLOW_BASH=1` to retain direct `bash` explicitly.
|
|
70
73
|
|
|
71
74
|
## Commands (human)
|
|
72
75
|
|
|
73
76
|
| Command | What it does |
|
|
74
77
|
| ------- | ------------ |
|
|
75
78
|
| `/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
|
-
| `/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;
|
|
79
|
+
| `/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; old empty roots are eligible once their lease is gone. Deletion uses a GC lock and atomic rename |
|
|
77
80
|
|
|
78
81
|
A widget below the editor separates session **kind** from task **state** at a glance:
|
|
79
82
|
summary counts use `RUNNING` / `IDLE`, and every row is labeled
|
|
@@ -157,7 +160,9 @@ because blindly rerunning an arbitrary command can duplicate side effects.
|
|
|
157
160
|
currently running member to stop and then share one notification even when
|
|
158
161
|
their exits span multiple polls.
|
|
159
162
|
`babysit_kill` and an exit already reported by `babysit_wait` suppress the
|
|
160
|
-
notification.
|
|
163
|
+
notification. Kill results are reconciled with persisted terminal state, so
|
|
164
|
+
a backend escalation warning that arrives after the child has already exited
|
|
165
|
+
does not produce a false failure followed by a redundant notification.
|
|
161
166
|
- **Subagent**: `foreground: true` or `babysit_wait` blocks on
|
|
162
167
|
`babysit expect '"type":"agent_settled"'`. A background task that settles or
|
|
163
168
|
exits before it is collected emits one ready-to-collect reminder; the parent
|
|
@@ -177,10 +182,12 @@ grace window (`PI_BABYSIT_REAP_AFTER`, default 120s) using the same parked-turn
|
|
|
177
182
|
rule, so a subagent waiting on a long build is never false-killed. Give bounded
|
|
178
183
|
recon/review tasks at least one cost, turn, tool-call, or token budget; omit
|
|
179
184
|
budgets only for intentionally open-ended work. Optional task budgets are
|
|
180
|
-
observed by the parent poller.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
observed by the parent poller. `maxUsageTokens` counts cumulative input, output,
|
|
186
|
+
and cache-token usage, so size it above the worker's initial context and
|
|
187
|
+
expected calls. At 80% of a limit the worker is steered to wrap up; reaching the
|
|
188
|
+
configured limit starts the hard grace immediately, even if a wedged worker
|
|
189
|
+
cannot accept steering. An in-flight model call or parallel tool batch can
|
|
190
|
+
still overshoot before the next poll. If the worker remains active
|
|
184
191
|
after `PI_BABYSIT_BUDGET_GRACE`, termination is verified before it is marked
|
|
185
192
|
budget-killed. Usage shown by check/wait is cumulative, and the first terminal
|
|
186
193
|
wait for each task charges that nested usage exactly once to the parent Pi
|
package/index.ts
CHANGED
|
@@ -220,9 +220,15 @@ function bs(
|
|
|
220
220
|
resolve({ stdout: "", stderr: "aborted", code: 130 });
|
|
221
221
|
return;
|
|
222
222
|
}
|
|
223
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
224
|
+
for (const [name, value] of Object.entries(opts.env ?? {})) {
|
|
225
|
+
if (value === undefined) delete env[name];
|
|
226
|
+
else env[name] = value;
|
|
227
|
+
}
|
|
228
|
+
env.BABYSIT_DIR = ROOT;
|
|
223
229
|
const child = spawn(BABYSIT_BIN, args, {
|
|
224
230
|
cwd: opts.cwd,
|
|
225
|
-
env
|
|
231
|
+
env,
|
|
226
232
|
});
|
|
227
233
|
let stdout = "";
|
|
228
234
|
let stderr = "";
|
|
@@ -236,7 +242,12 @@ function bs(
|
|
|
236
242
|
});
|
|
237
243
|
child.on("error", (e) => {
|
|
238
244
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
239
|
-
|
|
245
|
+
const installHint = babysitSpawnInstallHint(e);
|
|
246
|
+
if (installHint) {
|
|
247
|
+
babysitPreflightError = installHint;
|
|
248
|
+
babysitPreflightCheckedAt = Date.now();
|
|
249
|
+
}
|
|
250
|
+
resolve({ stdout, stderr: installHint ?? stderr + String(e), code: 1 });
|
|
240
251
|
});
|
|
241
252
|
child.on("close", (code) => {
|
|
242
253
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
@@ -261,6 +272,10 @@ const INSTALL_HINT =
|
|
|
261
272
|
`The \`babysit\` binary was not found (tried "${BABYSIT_BIN}").\n` + INSTALL_STEPS;
|
|
262
273
|
const MIN_BABYSIT_VERSION = [0, 13, 0] as const;
|
|
263
274
|
|
|
275
|
+
export function babysitSpawnInstallHint(error: unknown): string | null {
|
|
276
|
+
return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT" ? INSTALL_HINT : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
264
279
|
export function isSupportedBabysitVersion(output: string): boolean {
|
|
265
280
|
const match = /\b(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/.exec(output);
|
|
266
281
|
if (!match) return false;
|
|
@@ -271,7 +286,8 @@ export function isSupportedBabysitVersion(output: string): boolean {
|
|
|
271
286
|
return match[4] === undefined;
|
|
272
287
|
}
|
|
273
288
|
|
|
274
|
-
// Cached preflight
|
|
289
|
+
// Cached preflight. A supported binary stays cached until a later spawn reports
|
|
290
|
+
// ENOENT; failures are retried after a short delay so installation can recover.
|
|
275
291
|
// undefined = not probed, null = supported, string = actionable error.
|
|
276
292
|
let babysitPreflightError: string | null | undefined;
|
|
277
293
|
let babysitPreflightCheckedAt = 0;
|
|
@@ -355,6 +371,22 @@ export function validateKillResponse(stdout: string): string | null {
|
|
|
355
371
|
}
|
|
356
372
|
}
|
|
357
373
|
|
|
374
|
+
export function resolveKillConfirmation(
|
|
375
|
+
id: string,
|
|
376
|
+
backendError: string | null,
|
|
377
|
+
state: string | undefined,
|
|
378
|
+
): { confirmed: true; warning?: string } | { confirmed: false; error: string } {
|
|
379
|
+
if (state && isConfirmedTerminalState(state)) {
|
|
380
|
+
return backendError ? { confirmed: true, warning: backendError } : { confirmed: true };
|
|
381
|
+
}
|
|
382
|
+
if (backendError) return { confirmed: false, error: backendError };
|
|
383
|
+
if (!state) return { confirmed: false, error: `Kill could not be verified: session ${id} disappeared.` };
|
|
384
|
+
return {
|
|
385
|
+
confirmed: false,
|
|
386
|
+
error: `Kill was acknowledged but ${id} is still ${state}; completion notifications were restored.`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
358
390
|
async function awaitConfirmedTermination(id: string): Promise<BsSession | null> {
|
|
359
391
|
const initial = await statusOf(id);
|
|
360
392
|
if (!initial || isConfirmedTerminalState(initial.state) || initial.state === "dead") {
|
|
@@ -591,9 +623,12 @@ function gcRootIsSafe(root: string): boolean {
|
|
|
591
623
|
let sessionDirs: fs.Dirent[];
|
|
592
624
|
try {
|
|
593
625
|
sessionDirs = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
|
594
|
-
} catch {
|
|
595
|
-
|
|
626
|
+
} catch (error) {
|
|
627
|
+
// session_start acquires a lease before the first worker exists. Once that
|
|
628
|
+
// lease is gone, an old root with no sessions is safe to collect.
|
|
629
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
596
630
|
}
|
|
631
|
+
if (!sessionDirs.some((entry) => entry.isDirectory())) return true;
|
|
597
632
|
let sawStatus = false;
|
|
598
633
|
for (const sessionEntry of sessionDirs) {
|
|
599
634
|
if (!sessionEntry.isDirectory()) continue;
|
|
@@ -1893,12 +1928,26 @@ interface ProcOpts {
|
|
|
1893
1928
|
name?: string;
|
|
1894
1929
|
command: string;
|
|
1895
1930
|
cwd: string;
|
|
1931
|
+
env?: Record<string, string | undefined>;
|
|
1896
1932
|
timeout?: string; // default: none — dev servers may run indefinitely
|
|
1897
1933
|
idleTimeout?: string;
|
|
1898
1934
|
pty: boolean;
|
|
1899
1935
|
notificationGroup?: string;
|
|
1900
1936
|
}
|
|
1901
1937
|
|
|
1938
|
+
export function processSessionEnvironment(
|
|
1939
|
+
ctx: ExtensionContext,
|
|
1940
|
+
reasoningLevel = (ctx as ExtensionContext & { thinkingLevel?: string }).thinkingLevel,
|
|
1941
|
+
): Record<string, string | undefined> {
|
|
1942
|
+
return {
|
|
1943
|
+
PI_SESSION_ID: ctx.sessionManager.getSessionId(),
|
|
1944
|
+
PI_SESSION_FILE: ctx.sessionManager.getSessionFile(),
|
|
1945
|
+
PI_PROVIDER: ctx.model?.provider,
|
|
1946
|
+
PI_MODEL: ctx.model?.id,
|
|
1947
|
+
PI_REASONING_LEVEL: reasoningLevel,
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1902
1951
|
async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: string }> {
|
|
1903
1952
|
const bsArgs = ["run", "-d", "--json", "--size", "120x40"];
|
|
1904
1953
|
if (!opts.pty) bsArgs.push("--no-tty");
|
|
@@ -1911,7 +1960,7 @@ async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: s
|
|
|
1911
1960
|
|
|
1912
1961
|
let r: Awaited<ReturnType<typeof bs>>;
|
|
1913
1962
|
try {
|
|
1914
|
-
r = await bs(bsArgs, { cwd: opts.cwd });
|
|
1963
|
+
r = await bs(bsArgs, { cwd: opts.cwd, env: opts.env });
|
|
1915
1964
|
} finally {
|
|
1916
1965
|
if (reservedId) reservedSessionIds.delete(reservedId);
|
|
1917
1966
|
}
|
|
@@ -2843,6 +2892,21 @@ export function automaticNotificationGroup(entry: unknown): string | undefined {
|
|
|
2843
2892
|
return runs.length >= 2 ? group : undefined;
|
|
2844
2893
|
}
|
|
2845
2894
|
|
|
2895
|
+
export function prepareBabysitRunArguments(args: unknown): unknown {
|
|
2896
|
+
if (!args || typeof args !== "object") return args;
|
|
2897
|
+
const input = args as Record<string, unknown>;
|
|
2898
|
+
if (
|
|
2899
|
+
input.profile === "subagent" &&
|
|
2900
|
+
input.continueAfterStart === true &&
|
|
2901
|
+
input.foreground !== true
|
|
2902
|
+
) {
|
|
2903
|
+
const prepared = { ...input };
|
|
2904
|
+
delete prepared.continueAfterStart;
|
|
2905
|
+
return prepared;
|
|
2906
|
+
}
|
|
2907
|
+
return args;
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2846
2910
|
export function resolveSubagentSendMode(
|
|
2847
2911
|
requested: "auto" | "steer" | "task",
|
|
2848
2912
|
streaming?: boolean,
|
|
@@ -3430,14 +3494,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
3430
3494
|
promptSnippet: "Run supervised commands or bounded pi subagents with context-safe logs",
|
|
3431
3495
|
promptGuidelines: [
|
|
3432
3496
|
"Use babysit_run for shell commands and give meaningful sessions a stable name; bundle tiny related read-only observations into one command.",
|
|
3433
|
-
"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.",
|
|
3497
|
+
"Use babysit_run foreground mode for one process or subagent whose result is needed now; never issue sibling foreground runs in parallel. For parallel process checks, start background runs with continueAfterStart and collect them with one multi-session babysit_wait.",
|
|
3434
3498
|
"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.",
|
|
3435
|
-
"After a background process starts, stop the turn for its automatic notification; never poll or sleep. Use continueAfterStart only for specific non-polling work.",
|
|
3499
|
+
"After a background process starts, stop the turn for its automatic notification; never poll or sleep. Use continueAfterStart only for specific non-polling process work.",
|
|
3436
3500
|
"Inspect large logs with a narrow babysit_check pattern and maxBytes rather than broad tails.",
|
|
3437
3501
|
"Use retryOnWorkerDeath only once and only for idempotent commands; retries may duplicate side effects.",
|
|
3438
|
-
"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.",
|
|
3502
|
+
"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; maxUsageTokens counts cumulative input/cache tokens and can overshoot by one in-flight model call.",
|
|
3503
|
+
"Omit babysit_run.agent unless you know a named agent definition exists in the selected agentScope.",
|
|
3439
3504
|
"Subagent recursion defaults to depth 1; only a top-level caller may explicitly raise maxDepth.",
|
|
3440
3505
|
],
|
|
3506
|
+
prepareArguments(args) {
|
|
3507
|
+
return prepareBabysitRunArguments(args) as never;
|
|
3508
|
+
},
|
|
3441
3509
|
parameters: Type.Object({
|
|
3442
3510
|
command: Type.Optional(
|
|
3443
3511
|
Type.String({
|
|
@@ -3544,7 +3612,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3544
3612
|
continueAfterStart: Type.Optional(
|
|
3545
3613
|
Type.Boolean({
|
|
3546
3614
|
description:
|
|
3547
|
-
"Process mode
|
|
3615
|
+
"Process mode. Default false: starting a process ENDS the current turn (you are resumed by the exit notification). Set true only for immediate, specific, non-polling process work. In subagent mode true is accepted as a compatibility alias for the default background behavior.",
|
|
3548
3616
|
}),
|
|
3549
3617
|
),
|
|
3550
3618
|
retryOnWorkerDeath: Type.Optional(
|
|
@@ -3605,14 +3673,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3605
3673
|
details: {},
|
|
3606
3674
|
};
|
|
3607
3675
|
}
|
|
3608
|
-
if (
|
|
3609
|
-
return {
|
|
3610
|
-
content: [{ type: "text", text: "`continueAfterStart` is available only in process mode." }],
|
|
3611
|
-
isError: true,
|
|
3612
|
-
details: {},
|
|
3613
|
-
};
|
|
3614
|
-
}
|
|
3615
|
-
if (!isSubagent && params.foreground && params.continueAfterStart) {
|
|
3676
|
+
if (params.foreground && params.continueAfterStart) {
|
|
3616
3677
|
return {
|
|
3617
3678
|
content: [{ type: "text", text: "`foreground` and `continueAfterStart` are mutually exclusive." }],
|
|
3618
3679
|
isError: true,
|
|
@@ -3657,6 +3718,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3657
3718
|
timeout: params.timeout,
|
|
3658
3719
|
idleTimeout: params.idleTimeout,
|
|
3659
3720
|
pty: params.pty ?? true,
|
|
3721
|
+
env: processSessionEnvironment(ctx, pi.getThinkingLevel()),
|
|
3660
3722
|
notificationGroup: params.notificationGroup?.trim() || undefined,
|
|
3661
3723
|
};
|
|
3662
3724
|
let res = await spawnProcess(spawnOpts);
|
|
@@ -3785,7 +3847,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3785
3847
|
const avail = agents.map((a) => a.name).join(", ") || "none";
|
|
3786
3848
|
return {
|
|
3787
3849
|
content: [
|
|
3788
|
-
{ type: "text", text: `Unknown agent "${params.agent}". Available: ${avail}.` },
|
|
3850
|
+
{ type: "text", text: `Unknown agent "${params.agent}". Available: ${avail}. Omit \`agent\` to use the default subagent configuration.` },
|
|
3789
3851
|
],
|
|
3790
3852
|
isError: true,
|
|
3791
3853
|
details: {},
|
|
@@ -4581,29 +4643,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
4581
4643
|
};
|
|
4582
4644
|
|
|
4583
4645
|
const r = await bs(["kill", "-s", params.id, "--json"]);
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4646
|
+
const backendError = r.code !== 0
|
|
4647
|
+
? (r.stderr || r.stdout || "kill failed").trim()
|
|
4648
|
+
: validateKillResponse(r.stdout);
|
|
4649
|
+
// A backend can report an escalation error after the child has already
|
|
4650
|
+
// reached a persisted terminal state. Reconcile against authoritative
|
|
4651
|
+
// state before deciding whether to restore completion notifications.
|
|
4589
4652
|
const status = await awaitConfirmedTermination(params.id);
|
|
4653
|
+
const confirmation = resolveKillConfirmation(params.id, backendError, status?.state);
|
|
4654
|
+
if (!confirmation.confirmed) return fail(confirmation.error, status);
|
|
4590
4655
|
if (!status) return fail(`Kill could not be verified: session ${params.id} disappeared.`);
|
|
4591
|
-
if (!isConfirmedTerminalState(status.state)) {
|
|
4592
|
-
return fail(
|
|
4593
|
-
`Kill was acknowledged but ${params.id} is still ${status.state}; completion notifications were restored.`,
|
|
4594
|
-
status,
|
|
4595
|
-
);
|
|
4596
|
-
}
|
|
4597
4656
|
|
|
4598
4657
|
suppressNotify(params.id, "kill");
|
|
4599
4658
|
await refreshWidget(ctx);
|
|
4600
4659
|
return {
|
|
4601
|
-
content: [
|
|
4660
|
+
content: [
|
|
4661
|
+
{
|
|
4662
|
+
type: "text",
|
|
4663
|
+
text:
|
|
4664
|
+
`Killed ${params.id} (confirmed ${status.state}).` +
|
|
4665
|
+
(confirmation.warning ? ` Backend warning after termination: ${confirmation.warning}` : ""),
|
|
4666
|
+
},
|
|
4667
|
+
],
|
|
4602
4668
|
details: {
|
|
4603
4669
|
id: params.id,
|
|
4604
4670
|
status: status.state,
|
|
4605
4671
|
exitCode: status.exit_code,
|
|
4606
4672
|
logPath: logPath(params.id),
|
|
4673
|
+
backendWarning: confirmation.warning,
|
|
4607
4674
|
},
|
|
4608
4675
|
};
|
|
4609
4676
|
},
|