@yusukeshib/pi-babysit 0.3.0 → 0.3.1
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 +10 -1
- package/index.ts +46 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ programs** (installers, wizards, REPLs): type with `babysit_send`
|
|
|
49
49
|
|
|
50
50
|
| Tool | What it does |
|
|
51
51
|
| ---- | ------------ |
|
|
52
|
-
| `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`) or start a subagent (`profile: "subagent"`, `task`, optional `agent`/`model`/`tools`). Quick commands return inline; longer ones continue in the background |
|
|
52
|
+
| `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`) or start a subagent (`profile: "subagent"`, `task`, optional `agent`/`model`/`tools`). Quick commands return inline; longer ones continue in the background |
|
|
53
53
|
| `babysit_check` | List all sessions, or inspect one: process → state + log tail (or `screen: true` for TUIs); subagent → live progress (turns, recent tool calls, partial answer) |
|
|
54
54
|
| `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`) |
|
|
55
55
|
| `babysit_wait` | Block until done: process exit (or `expect: "regex"` readiness marker), subagent task completion. Multi-wait: `ids` + `mode: "any"\|"all"` |
|
|
@@ -84,6 +84,15 @@ rg -n 'FAIL|ERROR' /path/to/output.log
|
|
|
84
84
|
`babysit_check { id, lines }` remains available as a convenient bounded tail.
|
|
85
85
|
Do not read a potentially large log file in full.
|
|
86
86
|
|
|
87
|
+
## External worker death
|
|
88
|
+
|
|
89
|
+
If endpoint security or another external actor kills the babysit supervisor,
|
|
90
|
+
pi-babysit normalizes the stale `running` state to `worker-dead`, returns
|
|
91
|
+
immediately instead of hanging, and explains that the command may have started.
|
|
92
|
+
For commands known to be safe and idempotent, set `retryOnWorkerDeath: true` to
|
|
93
|
+
retry once with a new session id. It is opt-in because blindly rerunning an
|
|
94
|
+
arbitrary command can duplicate side effects.
|
|
95
|
+
|
|
87
96
|
## How completion detection works
|
|
88
97
|
|
|
89
98
|
- **Process**: a 2.5s poller watches for running→exited transitions and injects
|
package/index.ts
CHANGED
|
@@ -1047,6 +1047,7 @@ async function waitForExit(
|
|
|
1047
1047
|
}
|
|
1048
1048
|
suppressNotify(id); // the agent sees the exit here; don't notify again
|
|
1049
1049
|
const meta = readMeta(id);
|
|
1050
|
+
const workerDead = st.state === "dead" && st.exit_code == null;
|
|
1050
1051
|
const ok = st.exit_code === 0;
|
|
1051
1052
|
const output = await inlineOutput(id, st);
|
|
1052
1053
|
return {
|
|
@@ -1055,8 +1056,13 @@ async function waitForExit(
|
|
|
1055
1056
|
ok,
|
|
1056
1057
|
text:
|
|
1057
1058
|
`Process ${id}${meta?.command ? ` (${meta.command})` : ""} ` +
|
|
1058
|
-
|
|
1059
|
+
(workerDead
|
|
1060
|
+
? "worker-dead: the babysit supervisor disappeared without an exit status"
|
|
1061
|
+
: ok ? "completed successfully" : `exited with code ${st.exit_code ?? "?"}`) +
|
|
1059
1062
|
`${expectPattern ? ` before /${expectPattern}/ appeared` : ""}.` +
|
|
1063
|
+
(workerDead
|
|
1064
|
+
? " This commonly indicates an external kill (for example endpoint security). The command may have started, so retry only if it is safe and idempotent."
|
|
1065
|
+
: "") +
|
|
1060
1066
|
`\nLog: ${logPath(id)}` + output,
|
|
1061
1067
|
status: st,
|
|
1062
1068
|
};
|
|
@@ -1248,8 +1254,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1248
1254
|
"In non-interactive mode (`pi -p`, no UI), process mode blocks until exit because there is no " +
|
|
1249
1255
|
"notification loop. Two modes: (1) `command` — run any shell command, including builds, tests, " +
|
|
1250
1256
|
"dev servers, watchers, and interactive TUIs; you can type into it with babysit_send and read " +
|
|
1251
|
-
"its screen with babysit_check. " +
|
|
1252
|
-
"
|
|
1257
|
+
"its screen with babysit_check. If endpoint security kills a worker at startup, " +
|
|
1258
|
+
"`retryOnWorkerDeath` can retry one idempotent command once. " +
|
|
1253
1259
|
"(2) `profile: \"subagent\"` + `task` — spawn a pi subagent that works on the task in the " +
|
|
1254
1260
|
"background; poll with babysit_check, steer with babysit_send, block with babysit_wait, " +
|
|
1255
1261
|
"stop with babysit_kill.",
|
|
@@ -1259,7 +1265,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1259
1265
|
"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`.",
|
|
1260
1266
|
"Inspect a babysit log only with explicitly bounded commands such as `tail -n N` or focused `rg`; never read or cat a potentially large log in full. Those small inspection commands may use bash directly to avoid recursively creating babysit sessions.",
|
|
1261
1267
|
"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').",
|
|
1262
|
-
"
|
|
1268
|
+
"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.",
|
|
1263
1269
|
"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 }.",
|
|
1264
1270
|
"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.",
|
|
1265
1271
|
"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.",
|
|
@@ -1319,6 +1325,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1319
1325
|
"Process mode only. Default false: starting a process ENDS the current turn (you are resumed by the exit notification). Set true only when you have immediate, specific, non-polling work to do after starting.",
|
|
1320
1326
|
}),
|
|
1321
1327
|
),
|
|
1328
|
+
retryOnWorkerDeath: Type.Optional(
|
|
1329
|
+
Type.Boolean({
|
|
1330
|
+
description:
|
|
1331
|
+
"Process mode only. Retry once if the babysit worker is killed externally during startup. Use only for safe, idempotent commands because the first attempt may have produced side effects.",
|
|
1332
|
+
}),
|
|
1333
|
+
),
|
|
1322
1334
|
|
|
1323
1335
|
}),
|
|
1324
1336
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
@@ -1348,14 +1360,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1348
1360
|
|
|
1349
1361
|
// --- process mode ---
|
|
1350
1362
|
if (!isSubagent) {
|
|
1351
|
-
const
|
|
1363
|
+
const spawnOpts: ProcOpts = {
|
|
1352
1364
|
name: params.name,
|
|
1353
1365
|
command: params.command as string,
|
|
1354
1366
|
cwd: ctx.cwd,
|
|
1355
1367
|
timeout: params.timeout,
|
|
1356
1368
|
idleTimeout: params.idleTimeout,
|
|
1357
1369
|
pty: params.pty ?? true,
|
|
1358
|
-
|
|
1370
|
+
};
|
|
1371
|
+
let res = await spawnProcess(spawnOpts);
|
|
1359
1372
|
if ("error" in res) {
|
|
1360
1373
|
return {
|
|
1361
1374
|
content: [{ type: "text", text: `Failed to start process: ${res.error}` }],
|
|
@@ -1373,11 +1386,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
1373
1386
|
// outcome in THIS turn. The process still runs under babysit (logged,
|
|
1374
1387
|
// killable), we just wait for it here instead of fire-and-forget.
|
|
1375
1388
|
if (!ctx.hasUI) {
|
|
1376
|
-
|
|
1389
|
+
let outcome = await waitForExit(res.id, parseDurMs(params.timeout), _signal);
|
|
1390
|
+
let retried = false;
|
|
1391
|
+
if (params.retryOnWorkerDeath && outcome.status?.state === "dead" && outcome.status.exit_code == null) {
|
|
1392
|
+
const retry = await spawnProcess(spawnOpts);
|
|
1393
|
+
if (!("error" in retry)) {
|
|
1394
|
+
res = retry;
|
|
1395
|
+
retried = true;
|
|
1396
|
+
outcome = await waitForExit(res.id, parseDurMs(params.timeout), _signal);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1377
1399
|
return {
|
|
1378
|
-
content: [{ type: "text", text: outcome.text }],
|
|
1400
|
+
content: [{ type: "text", text: `${retried ? "Retried once after external worker death.\n" : ""}${outcome.text}` }],
|
|
1379
1401
|
isError: !outcome.ok,
|
|
1380
|
-
details: { id: res.id, kind: "process", command: params.command, logPath: logPath(res.id) },
|
|
1402
|
+
details: { id: res.id, kind: "process", command: params.command, logPath: logPath(res.id), retried },
|
|
1381
1403
|
};
|
|
1382
1404
|
}
|
|
1383
1405
|
|
|
@@ -1386,14 +1408,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
1386
1408
|
// A timeout means it is genuinely background work and follows the normal
|
|
1387
1409
|
// parked-turn / automatic-notification contract below.
|
|
1388
1410
|
await bs(["wait", "-s", res.id, "--timeout", QUICK_COMMAND_GRACE], { signal: _signal });
|
|
1389
|
-
|
|
1411
|
+
let quickStatus = await statusOf(res.id);
|
|
1412
|
+
let retried = false;
|
|
1413
|
+
if (params.retryOnWorkerDeath && quickStatus?.state === "dead" && quickStatus.exit_code == null) {
|
|
1414
|
+
const retry = await spawnProcess(spawnOpts);
|
|
1415
|
+
if (!("error" in retry)) {
|
|
1416
|
+
res = retry;
|
|
1417
|
+
retried = true;
|
|
1418
|
+
await bs(["wait", "-s", res.id, "--timeout", QUICK_COMMAND_GRACE], { signal: _signal });
|
|
1419
|
+
quickStatus = await statusOf(res.id);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1390
1422
|
if (quickStatus && quickStatus.state !== "running") {
|
|
1391
1423
|
const outcome = await waitForExit(res.id, null, _signal);
|
|
1392
1424
|
await refreshWidget(ctx);
|
|
1393
1425
|
return {
|
|
1394
|
-
content: [{ type: "text", text: outcome.text }],
|
|
1426
|
+
content: [{ type: "text", text: `${retried ? "Retried once after external worker death.\n" : ""}${outcome.text}` }],
|
|
1395
1427
|
isError: !outcome.ok,
|
|
1396
|
-
details: { id: res.id, kind: "process", command: params.command, logPath: logPath(res.id) },
|
|
1428
|
+
details: { id: res.id, kind: "process", command: params.command, logPath: logPath(res.id), retried },
|
|
1397
1429
|
};
|
|
1398
1430
|
}
|
|
1399
1431
|
|
|
@@ -1406,13 +1438,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1406
1438
|
{
|
|
1407
1439
|
type: "text",
|
|
1408
1440
|
text:
|
|
1441
|
+
`${retried ? "Retried once after external worker death.\n" : ""}` +
|
|
1409
1442
|
`Process started (id: ${res.id}). ${NOTIFY_MARKER}\nLog: ${logPath(res.id)}\n${nextStep}\n` +
|
|
1410
1443
|
`Inspect: babysit_check { id: "${res.id}" } (screen: true for TUIs) · ` +
|
|
1411
1444
|
`Wait: babysit_wait { id: "${res.id}" } · Kill: babysit_kill { id: "${res.id}" }\n` +
|
|
1412
1445
|
`Human can watch/take over: /babysit`,
|
|
1413
1446
|
},
|
|
1414
1447
|
],
|
|
1415
|
-
details: { id: res.id, kind: "process", command: params.command, logPath: logPath(res.id) },
|
|
1448
|
+
details: { id: res.id, kind: "process", command: params.command, logPath: logPath(res.id), retried },
|
|
1416
1449
|
// Do not return `terminate: true` here. In RPC/subagent hosts that hint
|
|
1417
1450
|
// can shut down the hosting pi worker, whose process-tree cleanup then
|
|
1418
1451
|
// kills the otherwise detached babysit supervisor and closes its PTY
|