@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.
Files changed (3) hide show
  1. package/README.md +10 -1
  2. package/index.ts +46 -13
  3. 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
- `${ok ? "completed successfully" : `exited with code ${st.exit_code ?? "?"}`}` +
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
- "you can type into it with babysit_send and read its screen with babysit_check. " +
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
- "babysit_run returns only minimal lifecycle information and a path to the complete log. Inspect that file with bounded shell commands such as `tail` or `rg`; never read a potentially large log in full.",
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 res = await spawnProcess({
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
- const outcome = await waitForExit(res.id, parseDurMs(params.timeout), _signal);
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
- const quickStatus = await statusOf(res.id);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",