@yusukeshib/pi-babysit 0.3.4 → 0.3.7
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 +40 -37
- package/index.ts +339 -90
- package/package.json +2 -1
- package/search-log.mjs +42 -0
package/README.md
CHANGED
|
@@ -13,8 +13,8 @@ It retires both `@mjakl/pi-processes` (the `process` tool) and the old
|
|
|
13
13
|
pi install npm:@yusukeshib/pi-babysit
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Then install
|
|
17
|
-
(the extension does **not** auto-install it):
|
|
16
|
+
Then install [`babysit`](https://github.com/yusukeshib/babysit) **0.13.0 or
|
|
17
|
+
newer** (the extension does **not** auto-install it):
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
20
|
cargo install --git https://github.com/yusukeshib/babysit
|
|
@@ -22,8 +22,8 @@ cargo install --git https://github.com/yusukeshib/babysit
|
|
|
22
22
|
|
|
23
23
|
or grab a prebuilt binary from the
|
|
24
24
|
[releases](https://github.com/yusukeshib/babysit/releases) and put it on your
|
|
25
|
-
`PATH`. If it
|
|
26
|
-
instructions.
|
|
25
|
+
`PATH`. If it is missing or older than 0.13.0, every tool and the `/babysit`
|
|
26
|
+
command fail with upgrade instructions.
|
|
27
27
|
|
|
28
28
|
## The model
|
|
29
29
|
|
|
@@ -50,14 +50,13 @@ programs** (installers, wizards, REPLs): type with `babysit_send`
|
|
|
50
50
|
| Tool | What it does |
|
|
51
51
|
| ---- | ------------ |
|
|
52
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
|
-
| `babysit_check` | List all sessions,
|
|
53
|
+
| `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 |
|
|
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"` |
|
|
56
|
-
| `babysit_kill` | Terminate a session
|
|
56
|
+
| `babysit_kill` | Terminate a session, verify terminal state, then suppress the exit notification |
|
|
57
57
|
|
|
58
|
-
A `tool_call` hook
|
|
59
|
-
|
|
60
|
-
(carried over from pi-processes' `blockBackgroundCommands`).
|
|
58
|
+
A `tool_call` hook blocks shell backgrounding (`… &`, `nohup`, `setsid`,
|
|
59
|
+
`disown`) and redirects all direct `bash` commands to `babysit_run`.
|
|
61
60
|
|
|
62
61
|
## Commands (human)
|
|
63
62
|
|
|
@@ -72,33 +71,31 @@ A minimal widget above the editor shows live counts
|
|
|
72
71
|
|
|
73
72
|
`babysit_run`, `babysit_wait`, and automatic completion notifications always
|
|
74
73
|
return lifecycle metadata and the absolute path to the complete `output.log`.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
Explicit run/wait results inline complete output up to 8 KB; unsolicited
|
|
75
|
+
completion notifications use a stricter 2 KB cap. Larger output stays out of
|
|
76
|
+
model context. Inspect it through the session id without creating another shell
|
|
77
|
+
session:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
babysit_check { id: "cargo-test", lines: 50 }
|
|
81
|
+
babysit_check { id: "cargo-test", pattern: "FAIL|ERROR", lines: 50 }
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
Tail and search results are capped at 200 lines and clipped to 8 KB. Pattern
|
|
85
|
+
search returns the latest matching lines with line numbers. Do not read a
|
|
86
|
+
potentially large log file in full.
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
are explicitly bounded to at most 100 lines. Broad searches, diffs, API calls,
|
|
90
|
-
multiple commands, redirects, and shell wrappers are redirected to
|
|
91
|
-
`babysit_run`. Set `PI_BABYSIT_ALLOW_BASH=1` only as an emergency escape hatch
|
|
92
|
-
to disable this gate.
|
|
88
|
+
All shell commands, including `pwd` and Git, are redirected to `babysit_run`.
|
|
89
|
+
Set `PI_BABYSIT_ALLOW_BASH=1` only as an explicit emergency escape hatch.
|
|
93
90
|
|
|
94
|
-
##
|
|
91
|
+
## Unexpected worker loss
|
|
95
92
|
|
|
96
|
-
If
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
For commands known to be safe and idempotent,
|
|
100
|
-
retry once with a new session id. It is opt-in
|
|
101
|
-
arbitrary command can duplicate side effects.
|
|
93
|
+
If the babysit supervisor disappears without recording an exit, pi-babysit
|
|
94
|
+
normalizes the stale `running` state to `worker-dead` and returns immediately
|
|
95
|
+
instead of hanging. Possible causes include host process cleanup, endpoint
|
|
96
|
+
security, or a supervisor crash. For commands known to be safe and idempotent,
|
|
97
|
+
set `retryOnWorkerDeath: true` to retry once with a new session id. It is opt-in
|
|
98
|
+
because blindly rerunning an arbitrary command can duplicate side effects.
|
|
102
99
|
|
|
103
100
|
## How completion detection works
|
|
104
101
|
|
|
@@ -127,12 +124,18 @@ rule, so a subagent waiting on a long build is never false-killed.
|
|
|
127
124
|
| `PI_BABYSIT_CLI` | `babysit` | babysit binary |
|
|
128
125
|
| `PI_BABYSIT_VIEW_CMD` | bundled `format-stream.mjs` | live-attach pretty printer for subagent JSONL (`""` disables) |
|
|
129
126
|
| `PI_BABYSIT_REAP_AFTER` | `120s` | idle grace before a finished subagent self-exits (`off`/`none`/`0` disables) |
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
`
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
127
|
+
| `PI_BABYSIT_TAIL_MAX_BYTES` | `8000` | cap for explicit log tails/screens returned by `babysit_check` |
|
|
128
|
+
| `PI_BABYSIT_INLINE_OUTPUT_MAX_BYTES` | `8000` | cap for complete output in explicitly requested run/wait results |
|
|
129
|
+
| `PI_BABYSIT_NOTIFY_OUTPUT_MAX_BYTES` | `2000` | smaller cap for unsolicited process-completion notifications (`0` omits all output) |
|
|
130
|
+
| `PI_BABYSIT_NOTIFY_COMMAND_MAX_BYTES` | `240` | cap for the command preview in completion notifications |
|
|
131
|
+
| `PI_BABYSIT_ALLOW_BASH` | unset | set to `1` to bypass direct-Bash redirection (emergency escape hatch) |
|
|
132
|
+
|
|
133
|
+
Requires `babysit` 0.13.0 or newer and `pi` on `PATH`. The extension does **not**
|
|
134
|
+
auto-install `babysit`: if the binary is missing or too old, every tool and the
|
|
135
|
+
`/babysit` command fail with install instructions (`cargo install --git
|
|
136
|
+
https://github.com/yusukeshib/babysit` or a prebuilt release), and a warning is
|
|
137
|
+
shown at session start. Point `$PI_BABYSIT_CLI` at a custom binary path if
|
|
138
|
+
needed.
|
|
136
139
|
|
|
137
140
|
(No tmux dependency — `/babysit` renders inline; take over a live process
|
|
138
141
|
manually with the `babysit attach` command it shows.)
|
package/index.ts
CHANGED
|
@@ -81,6 +81,7 @@ const SUBAGENT_GUIDANCE = [
|
|
|
81
81
|
].join(" ");
|
|
82
82
|
const POLL_MS = 2500;
|
|
83
83
|
const QUICK_COMMAND_GRACE = process.env.PI_BABYSIT_QUICK_GRACE ?? "1s";
|
|
84
|
+
const KILL_CONFIRM_TIMEOUT = "4s";
|
|
84
85
|
|
|
85
86
|
interface BsSession {
|
|
86
87
|
id: string;
|
|
@@ -154,27 +155,49 @@ function bs(
|
|
|
154
155
|
// Every session shells out to `babysit`; without it the extension can do
|
|
155
156
|
// nothing. We don't auto-install (that's the user's job) — we fail loudly with
|
|
156
157
|
// install instructions the moment a tool or command is used.
|
|
157
|
-
const
|
|
158
|
-
`
|
|
159
|
-
`Install it, then retry:\n` +
|
|
158
|
+
const INSTALL_STEPS =
|
|
159
|
+
`Install babysit 0.13.0 or newer, then retry:\n` +
|
|
160
160
|
` cargo install --git https://github.com/yusukeshib/babysit\n` +
|
|
161
161
|
`or download a prebuilt binary from https://github.com/yusukeshib/babysit/releases and put it on your PATH.\n` +
|
|
162
162
|
`(Override the binary path with $PI_BABYSIT_CLI.)`;
|
|
163
|
+
const INSTALL_HINT =
|
|
164
|
+
`The \`babysit\` binary was not found (tried "${BABYSIT_BIN}").\n` + INSTALL_STEPS;
|
|
165
|
+
const MIN_BABYSIT_VERSION = [0, 13, 0] as const;
|
|
166
|
+
|
|
167
|
+
export function isSupportedBabysitVersion(output: string): boolean {
|
|
168
|
+
const match = /\b(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/.exec(output);
|
|
169
|
+
if (!match) return false;
|
|
170
|
+
const actual = [Number(match[1]), Number(match[2]), Number(match[3])] as const;
|
|
171
|
+
for (let i = 0; i < MIN_BABYSIT_VERSION.length; i++) {
|
|
172
|
+
if (actual[i] !== MIN_BABYSIT_VERSION[i]) return actual[i] > MIN_BABYSIT_VERSION[i];
|
|
173
|
+
}
|
|
174
|
+
return match[4] === undefined;
|
|
175
|
+
}
|
|
163
176
|
|
|
164
177
|
// Cached preflight — probe `babysit --version` exactly once per process.
|
|
165
|
-
|
|
178
|
+
// undefined = not probed, null = supported, string = actionable error.
|
|
179
|
+
let babysitPreflightError: string | null | undefined;
|
|
166
180
|
async function babysitAvailable(): Promise<boolean> {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
// Cache only success. A missing or outdated binary may be installed while pi
|
|
182
|
+
// stays open, so subsequent tool calls must be able to recover without a restart.
|
|
183
|
+
if (babysitPreflightError === null) return true;
|
|
184
|
+
const r = await bs(["--version"]);
|
|
185
|
+
if (r.code !== 0) {
|
|
186
|
+
babysitPreflightError = INSTALL_HINT;
|
|
187
|
+
} else if (!isSupportedBabysitVersion(r.stdout)) {
|
|
188
|
+
babysitPreflightError =
|
|
189
|
+
`pi-babysit requires babysit 0.13.0 or newer; found ${r.stdout.trim() || "an unknown version"}.\n` +
|
|
190
|
+
INSTALL_STEPS;
|
|
191
|
+
} else {
|
|
192
|
+
babysitPreflightError = null;
|
|
170
193
|
}
|
|
171
|
-
return
|
|
194
|
+
return babysitPreflightError === null;
|
|
172
195
|
}
|
|
173
196
|
|
|
174
197
|
// Throwing form for tool `execute` handlers: a thrown error marks the tool
|
|
175
|
-
// result isError and reports the
|
|
198
|
+
// result isError and reports the preflight error to the model.
|
|
176
199
|
async function requireBabysit(): Promise<void> {
|
|
177
|
-
if (!(await babysitAvailable())) throw new Error(INSTALL_HINT);
|
|
200
|
+
if (!(await babysitAvailable())) throw new Error(babysitPreflightError ?? INSTALL_HINT);
|
|
178
201
|
}
|
|
179
202
|
|
|
180
203
|
// Error-aware: `babysit list` failing is NOT the same as "no sessions" —
|
|
@@ -219,6 +242,34 @@ async function statusOf(id: string): Promise<BsSession | null> {
|
|
|
219
242
|
}
|
|
220
243
|
}
|
|
221
244
|
|
|
245
|
+
export function isConfirmedTerminalState(state: string): boolean {
|
|
246
|
+
return state === "killed" || state === "exited";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function validateKillResponse(stdout: string): string | null {
|
|
250
|
+
try {
|
|
251
|
+
const response = JSON.parse(stdout);
|
|
252
|
+
if (response.killed !== true || response.confirmed === false) {
|
|
253
|
+
return `Kill was not confirmed by babysit: ${stdout.trim()}`;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
} catch {
|
|
257
|
+
return `Invalid kill response from babysit: ${stdout.trim() || "(empty)"}`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function awaitConfirmedTermination(id: string): Promise<BsSession | null> {
|
|
262
|
+
const initial = await statusOf(id);
|
|
263
|
+
if (!initial || isConfirmedTerminalState(initial.state) || initial.state === "dead") {
|
|
264
|
+
return initial;
|
|
265
|
+
}
|
|
266
|
+
// New babysit versions return only after persistence, so this is normally
|
|
267
|
+
// skipped. It is a bounded compatibility guard for older binaries that
|
|
268
|
+
// acknowledged signal delivery before the process actually exited.
|
|
269
|
+
await bs(["wait", "-s", id, "--timeout", KILL_CONFIRM_TIMEOUT]);
|
|
270
|
+
return statusOf(id);
|
|
271
|
+
}
|
|
272
|
+
|
|
222
273
|
// ---------------------------------------------------------------------------
|
|
223
274
|
// per-session metadata
|
|
224
275
|
// ---------------------------------------------------------------------------
|
|
@@ -232,6 +283,12 @@ interface Meta {
|
|
|
232
283
|
name?: string;
|
|
233
284
|
command?: string;
|
|
234
285
|
notified?: boolean;
|
|
286
|
+
// A confirmed kill permanently owns completion delivery. An interrupted
|
|
287
|
+
// concurrent wait must not re-enable the automatic notification afterward.
|
|
288
|
+
killNotificationSuppressed?: boolean;
|
|
289
|
+
// Temporary reservation while kill is in flight. Unlike `notified`, this
|
|
290
|
+
// must be cleared on failure so a real completion remains deliverable.
|
|
291
|
+
notificationPaused?: boolean;
|
|
235
292
|
completionObservedAt?: number;
|
|
236
293
|
startedAt?: number;
|
|
237
294
|
// subagent
|
|
@@ -367,8 +424,19 @@ function parseDurMs(s?: string): number | null {
|
|
|
367
424
|
// megabytes, so we also cap bytes, eliding the middle so both the head and
|
|
368
425
|
// the tail of the output stay visible.
|
|
369
426
|
|
|
370
|
-
|
|
371
|
-
const
|
|
427
|
+
function byteLimitFromEnv(name: string, fallback: number): number {
|
|
428
|
+
const raw = process.env[name];
|
|
429
|
+
if (raw == null || raw.trim() === "") return fallback;
|
|
430
|
+
const value = Number(raw);
|
|
431
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : fallback;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const TAIL_MAX_BYTES = byteLimitFromEnv("PI_BABYSIT_TAIL_MAX_BYTES", 8_000);
|
|
435
|
+
// Direct run/wait results can carry more context because the caller explicitly
|
|
436
|
+
// requested them. Unsolicited completion notifications default much smaller.
|
|
437
|
+
const INLINE_OUTPUT_MAX_BYTES = byteLimitFromEnv("PI_BABYSIT_INLINE_OUTPUT_MAX_BYTES", 8_000);
|
|
438
|
+
const NOTIFY_OUTPUT_MAX_BYTES = byteLimitFromEnv("PI_BABYSIT_NOTIFY_OUTPUT_MAX_BYTES", 2_000);
|
|
439
|
+
const NOTIFY_COMMAND_MAX_BYTES = byteLimitFromEnv("PI_BABYSIT_NOTIFY_COMMAND_MAX_BYTES", 240);
|
|
372
440
|
const ANSWER_MAX_BYTES = 24_000; // subagent answers / error messages
|
|
373
441
|
|
|
374
442
|
function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
|
|
@@ -381,7 +449,77 @@ function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
|
|
|
381
449
|
return `${head}\n… [${buf.length - maxBytes} bytes elided] …\n${tail}`;
|
|
382
450
|
}
|
|
383
451
|
|
|
384
|
-
async function
|
|
452
|
+
async function searchLog(
|
|
453
|
+
id: string,
|
|
454
|
+
pattern: string,
|
|
455
|
+
maxLines: number,
|
|
456
|
+
signal?: AbortSignal,
|
|
457
|
+
): Promise<{ text: string; error?: string }> {
|
|
458
|
+
const file = logPath(id);
|
|
459
|
+
if (!fs.existsSync(file)) return { text: "", error: `Log file is missing: ${file}` };
|
|
460
|
+
if (signal?.aborted) return { text: "", error: "Log search was interrupted." };
|
|
461
|
+
|
|
462
|
+
// Run regex evaluation out of process so catastrophic backtracking or a huge
|
|
463
|
+
// no-newline log cannot freeze or exhaust pi's main Node process. The helper
|
|
464
|
+
// clips each retained line; this parent also enforces a hard wall-clock limit.
|
|
465
|
+
return new Promise((resolve) => {
|
|
466
|
+
const helper = path.join(EXT_DIR, "search-log.mjs");
|
|
467
|
+
const nodeOptions = [process.env.NODE_OPTIONS, "--max-old-space-size=32"]
|
|
468
|
+
.filter(Boolean)
|
|
469
|
+
.join(" ");
|
|
470
|
+
const child = spawn(process.execPath, [helper, file, pattern, String(maxLines)], {
|
|
471
|
+
env: { ...process.env, NODE_OPTIONS: nodeOptions },
|
|
472
|
+
});
|
|
473
|
+
let stdout = "";
|
|
474
|
+
let stderr = "";
|
|
475
|
+
let finished = false;
|
|
476
|
+
let timedOut = false;
|
|
477
|
+
const finish = (result: { text: string; error?: string }) => {
|
|
478
|
+
if (finished) return;
|
|
479
|
+
finished = true;
|
|
480
|
+
clearTimeout(timer);
|
|
481
|
+
signal?.removeEventListener("abort", onAbort);
|
|
482
|
+
resolve(result);
|
|
483
|
+
};
|
|
484
|
+
const onAbort = () => {
|
|
485
|
+
child.kill("SIGTERM");
|
|
486
|
+
finish({ text: "", error: "Log search was interrupted." });
|
|
487
|
+
};
|
|
488
|
+
const timer = setTimeout(() => {
|
|
489
|
+
timedOut = true;
|
|
490
|
+
child.kill("SIGTERM");
|
|
491
|
+
}, 3_000);
|
|
492
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
493
|
+
child.stdout?.on("data", (data) => {
|
|
494
|
+
stdout += data.toString();
|
|
495
|
+
});
|
|
496
|
+
child.stderr?.on("data", (data) => {
|
|
497
|
+
stderr = clip(stderr + data.toString());
|
|
498
|
+
});
|
|
499
|
+
child.on("error", (error) => {
|
|
500
|
+
finish({ text: "", error: `Could not start log search: ${String(error)}` });
|
|
501
|
+
});
|
|
502
|
+
child.on("close", (code) => {
|
|
503
|
+
if (timedOut) {
|
|
504
|
+
finish({ text: "", error: "Log search timed out after 3s; narrow the pattern or log." });
|
|
505
|
+
} else if (code !== 0) {
|
|
506
|
+
finish({ text: "", error: stderr.trim() || `Log search failed (exit ${code ?? "?"}).` });
|
|
507
|
+
} else {
|
|
508
|
+
finish({ text: clip(stdout.trimEnd()) });
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function shouldInlineCompleteOutput(outputBytes: number, maxBytes: number): boolean {
|
|
515
|
+
return maxBytes > 0 && outputBytes <= maxBytes;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function inlineOutput(
|
|
519
|
+
id: string,
|
|
520
|
+
status: BsSession,
|
|
521
|
+
maxBytes = INLINE_OUTPUT_MAX_BYTES,
|
|
522
|
+
): Promise<string> {
|
|
385
523
|
let bytes = status.output_bytes;
|
|
386
524
|
if (bytes == null) {
|
|
387
525
|
try {
|
|
@@ -390,17 +528,38 @@ async function inlineOutput(id: string, status: BsSession): Promise<string> {
|
|
|
390
528
|
bytes = Number.POSITIVE_INFINITY;
|
|
391
529
|
}
|
|
392
530
|
}
|
|
393
|
-
if (bytes
|
|
531
|
+
if (!shouldInlineCompleteOutput(bytes, maxBytes)) {
|
|
394
532
|
const size = Number.isFinite(bytes) ? `${bytes} bytes` : "size unavailable";
|
|
395
|
-
return `\nOutput omitted (${size}; inline limit ${
|
|
533
|
+
return `\nOutput omitted (${size}; inline limit ${maxBytes}).`;
|
|
396
534
|
}
|
|
397
535
|
const output = (await bs(["log", "-s", id])).stdout.trimEnd();
|
|
398
|
-
if (Buffer.byteLength(output) >
|
|
399
|
-
return `\nOutput omitted (exceeds inline limit ${
|
|
536
|
+
if (Buffer.byteLength(output) > maxBytes) {
|
|
537
|
+
return `\nOutput omitted (exceeds inline limit ${maxBytes} bytes).`;
|
|
400
538
|
}
|
|
401
539
|
return output ? `\n\nOutput:\n${output}` : "";
|
|
402
540
|
}
|
|
403
541
|
|
|
542
|
+
export function summarizeNotificationCommand(command: string | undefined): string {
|
|
543
|
+
const preview =
|
|
544
|
+
(command ?? "?")
|
|
545
|
+
.trim()
|
|
546
|
+
.replace(/\r/g, "\\r")
|
|
547
|
+
.replace(/\n/g, "\\n")
|
|
548
|
+
.replace(/\t/g, "\\t") || "?";
|
|
549
|
+
const bytes = Buffer.from(preview, "utf8");
|
|
550
|
+
if (bytes.length <= NOTIFY_COMMAND_MAX_BYTES) return preview;
|
|
551
|
+
if (NOTIFY_COMMAND_MAX_BYTES === 0) return "";
|
|
552
|
+
const ellipsis = Buffer.from("…", "utf8");
|
|
553
|
+
if (NOTIFY_COMMAND_MAX_BYTES <= ellipsis.length) {
|
|
554
|
+
return ".".repeat(NOTIFY_COMMAND_MAX_BYTES);
|
|
555
|
+
}
|
|
556
|
+
const prefix = bytes
|
|
557
|
+
.subarray(0, NOTIFY_COMMAND_MAX_BYTES - ellipsis.length)
|
|
558
|
+
.toString("utf8")
|
|
559
|
+
.replace(/\uFFFD+$/, "");
|
|
560
|
+
return `${prefix}…`;
|
|
561
|
+
}
|
|
562
|
+
|
|
404
563
|
|
|
405
564
|
// ---------------------------------------------------------------------------
|
|
406
565
|
// parked-turn detection (shared rule with self-reap.ts)
|
|
@@ -966,22 +1125,47 @@ async function waitForTask(
|
|
|
966
1125
|
|
|
967
1126
|
// Mark a process session as already-reported so the exit-notification poller
|
|
968
1127
|
// doesn't send a duplicate message for something the agent just observed.
|
|
969
|
-
function suppressNotify(id: string): void {
|
|
1128
|
+
function suppressNotify(id: string, reason: "observed" | "kill" = "observed"): void {
|
|
970
1129
|
const meta = readMeta(id);
|
|
971
|
-
if (meta && meta.kind === "process"
|
|
1130
|
+
if (meta && meta.kind === "process") {
|
|
972
1131
|
meta.notified = true;
|
|
1132
|
+
if (reason === "kill") meta.killNotificationSuppressed = true;
|
|
1133
|
+
delete meta.notificationPaused;
|
|
973
1134
|
writeMeta(id, meta);
|
|
974
1135
|
}
|
|
975
1136
|
}
|
|
976
1137
|
|
|
1138
|
+
export function canRestoreNotificationAfterWait(meta: {
|
|
1139
|
+
notified?: boolean;
|
|
1140
|
+
killNotificationSuppressed?: boolean;
|
|
1141
|
+
}): boolean {
|
|
1142
|
+
return meta.notified === true && meta.killNotificationSuppressed !== true;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
977
1145
|
function enableNotify(id: string): void {
|
|
978
1146
|
const meta = readMeta(id);
|
|
979
|
-
if (meta && meta.kind === "process" && meta
|
|
1147
|
+
if (meta && meta.kind === "process" && canRestoreNotificationAfterWait(meta)) {
|
|
980
1148
|
meta.notified = false;
|
|
981
1149
|
writeMeta(id, meta);
|
|
982
1150
|
}
|
|
983
1151
|
}
|
|
984
1152
|
|
|
1153
|
+
function pauseNotify(id: string): void {
|
|
1154
|
+
const meta = readMeta(id);
|
|
1155
|
+
if (meta && meta.kind === "process" && !meta.notificationPaused) {
|
|
1156
|
+
meta.notificationPaused = true;
|
|
1157
|
+
writeMeta(id, meta);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function resumeNotify(id: string): void {
|
|
1162
|
+
const meta = readMeta(id);
|
|
1163
|
+
if (meta && meta.kind === "process" && meta.notificationPaused) {
|
|
1164
|
+
delete meta.notificationPaused;
|
|
1165
|
+
writeMeta(id, meta);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
985
1169
|
// Wait for a PROCESS session: either until a regex appears in its output
|
|
986
1170
|
// (`expect` — e.g. "server listening") or until the process exits.
|
|
987
1171
|
async function waitForExit(
|
|
@@ -1055,13 +1239,13 @@ async function waitForExit(
|
|
|
1055
1239
|
kind: "exited",
|
|
1056
1240
|
ok,
|
|
1057
1241
|
text:
|
|
1058
|
-
`Process ${id}${meta?.command ? ` (${meta.command})` : ""} ` +
|
|
1242
|
+
`Process ${id}${meta?.command ? ` (${summarizeNotificationCommand(meta.command)})` : ""} ` +
|
|
1059
1243
|
(workerDead
|
|
1060
1244
|
? "worker-dead: the babysit supervisor disappeared without an exit status"
|
|
1061
1245
|
: ok ? "completed successfully" : `exited with code ${st.exit_code ?? "?"}`) +
|
|
1062
1246
|
`${expectPattern ? ` before /${expectPattern}/ appeared` : ""}.` +
|
|
1063
1247
|
(workerDead
|
|
1064
|
-
? "
|
|
1248
|
+
? " The supervisor disappeared without recording an exit; possible causes include host process cleanup, endpoint security, or a supervisor crash. The command may have started, so retry only if it is safe and idempotent."
|
|
1065
1249
|
: "") +
|
|
1066
1250
|
`\nLog: ${logPath(id)}` + output,
|
|
1067
1251
|
status: st,
|
|
@@ -1079,7 +1263,7 @@ const waitFor = (
|
|
|
1079
1263
|
: waitForExit(id, limitMs, signal, expectPattern);
|
|
1080
1264
|
|
|
1081
1265
|
// ---------------------------------------------------------------------------
|
|
1082
|
-
// bash
|
|
1266
|
+
// direct bash policy
|
|
1083
1267
|
// ---------------------------------------------------------------------------
|
|
1084
1268
|
|
|
1085
1269
|
// Heuristic (no shell AST): catch `... &` backgrounding (not `&&`), nohup,
|
|
@@ -1092,28 +1276,9 @@ function backgroundsItself(command: string): boolean {
|
|
|
1092
1276
|
return false;
|
|
1093
1277
|
}
|
|
1094
1278
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
return Math.max(...matches.map((m) => Number(m[1])));
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
/** Commands allowed to bypass babysit: tiny scalar observations and strictly
|
|
1102
|
-
* bounded reads of captured log files. This is deliberately conservative;
|
|
1103
|
-
* uncertain shell syntax belongs in babysit_run where output is capped. */
|
|
1104
|
-
export function isAllowedDirectBash(command: string): boolean {
|
|
1105
|
-
if (process.env.PI_BABYSIT_ALLOW_BASH === "1") return true;
|
|
1106
|
-
const s = command.trim();
|
|
1107
|
-
if (/^(pwd|git status --short|git branch --show-current)$/.test(s)) return true;
|
|
1108
|
-
if (!s || /[;&`\n\r]|\$\(|>|\b(?:bash|sh|zsh)\s+-c\b/.test(s)) return false;
|
|
1109
|
-
if (!/(?:output\.log|\/(?:tmp|var\/tmp)\/[^\s'\"]*\.log)\b/.test(s)) return false;
|
|
1110
|
-
if (/^wc\s+-(?:l|c)\s+/.test(s) && !s.includes("|")) return true;
|
|
1111
|
-
if (!/^(?:tail|head|rg)\b/.test(s)) return false;
|
|
1112
|
-
const limit = boundedLineCount(s);
|
|
1113
|
-
if (limit == null || limit > 100) return false;
|
|
1114
|
-
// rg must feed a bounded head/tail; direct head/tail is already bounded.
|
|
1115
|
-
if (/^rg\b/.test(s) && !/\|\s*(?:head|tail)\b/.test(s)) return false;
|
|
1116
|
-
return true;
|
|
1279
|
+
/** Emergency escape hatch only. All ordinary shell commands go through babysit_run. */
|
|
1280
|
+
export function isAllowedDirectBash(_command: string): boolean {
|
|
1281
|
+
return process.env.PI_BABYSIT_ALLOW_BASH === "1";
|
|
1117
1282
|
}
|
|
1118
1283
|
|
|
1119
1284
|
// ---------------------------------------------------------------------------
|
|
@@ -1133,7 +1298,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1133
1298
|
for (const s of sessions) {
|
|
1134
1299
|
if (s.state === "running") continue;
|
|
1135
1300
|
const meta = readMeta(s.id);
|
|
1136
|
-
if (!meta || meta.kind !== "process" || meta.notified) continue;
|
|
1301
|
+
if (!meta || meta.kind !== "process" || meta.notified || meta.notificationPaused) continue;
|
|
1137
1302
|
// Delay delivery by one poll interval. This gives an agent that chose
|
|
1138
1303
|
// babysit_wait immediately after babysit_run enough time to claim the
|
|
1139
1304
|
// completion and suppress the otherwise duplicate automatic message.
|
|
@@ -1143,15 +1308,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1143
1308
|
continue;
|
|
1144
1309
|
}
|
|
1145
1310
|
if (Date.now() - meta.completionObservedAt < POLL_MS) continue;
|
|
1146
|
-
meta.notified = true;
|
|
1147
|
-
writeMeta(s.id, meta);
|
|
1148
1311
|
const ok = s.exit_code === 0;
|
|
1149
1312
|
const status: DisplayStatus = ok
|
|
1150
1313
|
? "success"
|
|
1151
1314
|
: s.state === "dead" || s.exit_code == null
|
|
1152
1315
|
? "terminated"
|
|
1153
1316
|
: "failed";
|
|
1154
|
-
const output = await inlineOutput(s.id, s);
|
|
1317
|
+
const output = await inlineOutput(s.id, s, NOTIFY_OUTPUT_MAX_BYTES);
|
|
1155
1318
|
const runtime = meta.startedAt
|
|
1156
1319
|
? `${Math.round((Date.now() - meta.startedAt) / 1000)}s`
|
|
1157
1320
|
: "?";
|
|
@@ -1160,24 +1323,43 @@ export default function (pi: ExtensionAPI) {
|
|
|
1160
1323
|
: s.state === "dead" || s.exit_code == null
|
|
1161
1324
|
? `Process "${s.id}" was terminated after ${runtime}.`
|
|
1162
1325
|
: `Process "${s.id}" exited with code ${s.exit_code} after ${runtime}.`;
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1326
|
+
// Output loading is asynchronous. A kill/wait can claim completion in
|
|
1327
|
+
// that window, so re-read metadata immediately before delivery.
|
|
1328
|
+
const current = readMeta(s.id);
|
|
1329
|
+
if (
|
|
1330
|
+
!current ||
|
|
1331
|
+
current.kind !== "process" ||
|
|
1332
|
+
current.notified ||
|
|
1333
|
+
current.notificationPaused
|
|
1334
|
+
) {
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
try {
|
|
1338
|
+
pi.sendMessage(
|
|
1339
|
+
{
|
|
1340
|
+
customType: "pi-babysit-process-end",
|
|
1341
|
+
content:
|
|
1342
|
+
`${summary}\nCommand: ${summarizeNotificationCommand(current.command)}\nLog: ${logPath(s.id)}${output}` +
|
|
1343
|
+
"\n\nAutomatic completion notification. Inspect the bounded log with babysit_check only if needed.",
|
|
1344
|
+
display: true,
|
|
1345
|
+
details: {
|
|
1346
|
+
id: s.id,
|
|
1347
|
+
exitCode: s.exit_code,
|
|
1348
|
+
success: ok,
|
|
1349
|
+
status,
|
|
1350
|
+
runtime,
|
|
1351
|
+
logPath: logPath(s.id),
|
|
1352
|
+
},
|
|
1177
1353
|
},
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1354
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
1355
|
+
);
|
|
1356
|
+
} catch {
|
|
1357
|
+
// Leave it pending so the next poll can retry delivery.
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
current.notified = true;
|
|
1361
|
+
delete current.notificationPaused;
|
|
1362
|
+
writeMeta(s.id, current);
|
|
1181
1363
|
}
|
|
1182
1364
|
}
|
|
1183
1365
|
|
|
@@ -1282,7 +1464,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1282
1464
|
}
|
|
1283
1465
|
// Warn early if the binary is missing so the user isn't surprised only when
|
|
1284
1466
|
// a tool later fails. Tools/commands still enforce it via requireBabysit.
|
|
1285
|
-
if (ctx.hasUI && !(await babysitAvailable()))
|
|
1467
|
+
if (ctx.hasUI && !(await babysitAvailable())) {
|
|
1468
|
+
ctx.ui.notify(babysitPreflightError ?? INSTALL_HINT, "warn");
|
|
1469
|
+
}
|
|
1286
1470
|
if (pollTimer) clearInterval(pollTimer);
|
|
1287
1471
|
pollTimer = setInterval(() => {
|
|
1288
1472
|
// Skip if the previous (async) poll hasn't finished, so slow babysit
|
|
@@ -1304,9 +1488,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1304
1488
|
pollTimer = undefined;
|
|
1305
1489
|
});
|
|
1306
1490
|
|
|
1307
|
-
// Keep unpredictable command output out of model context. Direct bash is
|
|
1308
|
-
// reserved for tiny scalar observations and tightly bounded log inspection;
|
|
1309
|
-
// everything else belongs in babysit_run.
|
|
1310
1491
|
pi.on("tool_call", async (event) => {
|
|
1311
1492
|
if (event.toolName !== "bash") return;
|
|
1312
1493
|
const command = String((event.input as { command?: unknown }).command ?? "");
|
|
@@ -1322,8 +1503,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1322
1503
|
return {
|
|
1323
1504
|
block: true,
|
|
1324
1505
|
reason:
|
|
1325
|
-
"Use babysit_run for
|
|
1326
|
-
"
|
|
1506
|
+
"Use babysit_run for shell commands so output is supervised and captured outside model context. " +
|
|
1507
|
+
"Inspect an existing session log with babysit_check { id, lines, pattern? }. " +
|
|
1327
1508
|
`Retry as babysit_run({ command: ${JSON.stringify(command)} }).`,
|
|
1328
1509
|
};
|
|
1329
1510
|
});
|
|
@@ -1336,11 +1517,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1336
1517
|
"Run any shell command in a supervised babysit session. Commands that finish within a short " +
|
|
1337
1518
|
"grace period return completion metadata immediately; longer commands continue in the background " +
|
|
1338
1519
|
"and trigger an automatic notification on exit. Complete output is returned inline only when it is " +
|
|
1339
|
-
"small; larger output stays in the log path for bounded inspection with
|
|
1520
|
+
"small; larger output stays in the log path for bounded inspection with babysit_check. " +
|
|
1340
1521
|
"In non-interactive mode (`pi -p`, no UI), process mode blocks until exit because there is no " +
|
|
1341
1522
|
"notification loop. Two modes: (1) `command` — run any shell command, including builds, tests, " +
|
|
1342
1523
|
"dev servers, watchers, and interactive TUIs; you can type into it with babysit_send and read " +
|
|
1343
|
-
"its screen with babysit_check. If
|
|
1524
|
+
"its screen with babysit_check. If a worker disappears during startup without recording an exit, " +
|
|
1344
1525
|
"`retryOnWorkerDeath` can retry one idempotent command once. " +
|
|
1345
1526
|
"(2) `profile: \"subagent\"` + `task` — spawn a pi subagent that works on the task in the " +
|
|
1346
1527
|
"background; poll with babysit_check, steer with babysit_send, block with babysit_wait, " +
|
|
@@ -1349,7 +1530,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1349
1530
|
"Run any shell command with context-safe captured output; quick commands return metadata, longer ones continue in background",
|
|
1350
1531
|
promptGuidelines: [
|
|
1351
1532
|
"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`.",
|
|
1352
|
-
"Inspect a babysit log
|
|
1533
|
+
"Inspect a babysit log with babysit_check { id, lines, pattern? }; never read or cat a potentially large log file in full.",
|
|
1353
1534
|
"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').",
|
|
1354
1535
|
"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.",
|
|
1355
1536
|
"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 }.",
|
|
@@ -1667,10 +1848,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1667
1848
|
label: "Babysit: check",
|
|
1668
1849
|
description:
|
|
1669
1850
|
"Inspect babysit session(s). Without an id: lists all sessions (processes + subagents). " +
|
|
1670
|
-
"With an id: a process shows state + recent output
|
|
1671
|
-
"
|
|
1672
|
-
"(
|
|
1673
|
-
"a process to end — the exit notification is automatic.",
|
|
1851
|
+
"With an id: a process shows state + recent output, searches its log with `pattern`, " +
|
|
1852
|
+
"or captures the rendered screen with `screen: true`; a subagent shows live progress " +
|
|
1853
|
+
"(or raw log matches with `pattern`). Results are bounded by `lines` and clipped. " +
|
|
1854
|
+
"Do NOT poll this while merely waiting for a process to end — the exit notification is automatic.",
|
|
1674
1855
|
promptSnippet: "Check status/progress of babysit sessions (processes and subagents)",
|
|
1675
1856
|
parameters: Type.Object({
|
|
1676
1857
|
id: Type.Optional(Type.String({ description: "Session id. Omit to list all sessions." })),
|
|
@@ -1678,7 +1859,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1678
1859
|
Type.Number({ description: "Subagent: how many recent tool calls to show (default 8, max 50)." }),
|
|
1679
1860
|
),
|
|
1680
1861
|
lines: Type.Optional(
|
|
1681
|
-
Type.Number({ description: "
|
|
1862
|
+
Type.Number({ description: "How many tail lines or latest matches to show (default 30, max 200)." }),
|
|
1863
|
+
),
|
|
1864
|
+
pattern: Type.Optional(
|
|
1865
|
+
Type.String({
|
|
1866
|
+
description:
|
|
1867
|
+
"Search this session's raw log with a regular expression; returns the latest bounded matches.",
|
|
1868
|
+
}),
|
|
1682
1869
|
),
|
|
1683
1870
|
screen: Type.Optional(
|
|
1684
1871
|
Type.Boolean({
|
|
@@ -1687,7 +1874,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1687
1874
|
}),
|
|
1688
1875
|
),
|
|
1689
1876
|
}),
|
|
1690
|
-
async execute(_id, params) {
|
|
1877
|
+
async execute(_id, params, signal) {
|
|
1691
1878
|
await requireBabysit();
|
|
1692
1879
|
if (!params.id) {
|
|
1693
1880
|
const { sessions, error } = await listSessions();
|
|
@@ -1722,6 +1909,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
1722
1909
|
};
|
|
1723
1910
|
}
|
|
1724
1911
|
const meta = readMeta(params.id);
|
|
1912
|
+
const nLines = Math.min(Math.max(1, Math.floor(params.lines ?? 30)), 200);
|
|
1913
|
+
if (params.pattern !== undefined) {
|
|
1914
|
+
if (params.screen) {
|
|
1915
|
+
return {
|
|
1916
|
+
content: [{ type: "text", text: "`pattern` and `screen` are mutually exclusive." }],
|
|
1917
|
+
isError: true,
|
|
1918
|
+
details: {},
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
if (params.pattern.length === 0) {
|
|
1922
|
+
return {
|
|
1923
|
+
content: [{ type: "text", text: "`pattern` must not be empty." }],
|
|
1924
|
+
isError: true,
|
|
1925
|
+
details: {},
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
const result = await searchLog(params.id, params.pattern, nLines, signal);
|
|
1929
|
+
if (result.error) {
|
|
1930
|
+
return {
|
|
1931
|
+
content: [{ type: "text", text: result.error }],
|
|
1932
|
+
isError: true,
|
|
1933
|
+
details: {},
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
const kind = meta?.kind ?? "process";
|
|
1937
|
+
const header = `[${kind}] state=${st.state}\nlog: ${logPath(params.id)}`;
|
|
1938
|
+
const body = result.text
|
|
1939
|
+
? `--- latest matches /${params.pattern}/ ---\n${result.text}`
|
|
1940
|
+
: `(no output matching /${params.pattern}/)`;
|
|
1941
|
+
return {
|
|
1942
|
+
content: [{ type: "text", text: `${header}\n${body}` }],
|
|
1943
|
+
details: { status: st, kind, logPath: logPath(params.id), pattern: params.pattern },
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1725
1946
|
|
|
1726
1947
|
// --- process ---
|
|
1727
1948
|
if (meta?.kind !== "subagent") {
|
|
@@ -1740,7 +1961,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1740
1961
|
const sc = await bs(["screenshot", "-s", params.id, "--trim"]);
|
|
1741
1962
|
parts.push(`--- screen ---\n${clip(sc.stdout.trimEnd()) || "(blank screen)"}`);
|
|
1742
1963
|
} else {
|
|
1743
|
-
const nLines = Math.min(Math.max(1, params.lines ?? 30), 200);
|
|
1744
1964
|
const tail = clip(
|
|
1745
1965
|
(await bs(["log", "-s", params.id, "--tail", String(nLines)])).stdout.trimEnd(),
|
|
1746
1966
|
);
|
|
@@ -2078,17 +2298,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
2078
2298
|
parameters: Type.Object({ id: Type.String({ description: "Session id." }) }),
|
|
2079
2299
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2080
2300
|
await requireBabysit();
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2301
|
+
// Prevent the exit poller racing a requested kill, but restore delivery
|
|
2302
|
+
// on every failure. Permanent suppression happens only after terminal
|
|
2303
|
+
// state is independently confirmed.
|
|
2304
|
+
pauseNotify(params.id);
|
|
2305
|
+
const fail = async (message: string, status?: BsSession | null) => {
|
|
2306
|
+
resumeNotify(params.id);
|
|
2307
|
+
await refreshWidget(ctx);
|
|
2085
2308
|
return {
|
|
2086
|
-
content: [{ type: "text", text:
|
|
2309
|
+
content: [{ type: "text" as const, text: message }],
|
|
2087
2310
|
isError: true,
|
|
2088
|
-
details: {},
|
|
2311
|
+
details: { id: params.id, status: status?.state, logPath: logPath(params.id) },
|
|
2089
2312
|
};
|
|
2313
|
+
};
|
|
2314
|
+
|
|
2315
|
+
const r = await bs(["kill", "-s", params.id, "--json"]);
|
|
2316
|
+
if (r.code !== 0) return fail((r.stderr || r.stdout || "kill failed").trim());
|
|
2317
|
+
|
|
2318
|
+
const responseError = validateKillResponse(r.stdout);
|
|
2319
|
+
if (responseError) return fail(responseError);
|
|
2320
|
+
|
|
2321
|
+
const status = await awaitConfirmedTermination(params.id);
|
|
2322
|
+
if (!status) return fail(`Kill could not be verified: session ${params.id} disappeared.`);
|
|
2323
|
+
if (!isConfirmedTerminalState(status.state)) {
|
|
2324
|
+
return fail(
|
|
2325
|
+
`Kill was acknowledged but ${params.id} is still ${status.state}; completion notifications were restored.`,
|
|
2326
|
+
status,
|
|
2327
|
+
);
|
|
2090
2328
|
}
|
|
2091
|
-
|
|
2329
|
+
|
|
2330
|
+
suppressNotify(params.id, "kill");
|
|
2331
|
+
await refreshWidget(ctx);
|
|
2332
|
+
return {
|
|
2333
|
+
content: [{ type: "text", text: `Killed ${params.id} (confirmed ${status.state}).` }],
|
|
2334
|
+
details: {
|
|
2335
|
+
id: params.id,
|
|
2336
|
+
status: status.state,
|
|
2337
|
+
exitCode: status.exit_code,
|
|
2338
|
+
logPath: logPath(params.id),
|
|
2339
|
+
},
|
|
2340
|
+
};
|
|
2092
2341
|
},
|
|
2093
2342
|
});
|
|
2094
2343
|
|
|
@@ -2101,7 +2350,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2101
2350
|
description: "Pick a babysit session (↑/↓) to snapshot/inspect",
|
|
2102
2351
|
handler: async (_args, ctx) => {
|
|
2103
2352
|
if (!(await babysitAvailable())) {
|
|
2104
|
-
ctx.ui.notify(INSTALL_HINT, "error");
|
|
2353
|
+
ctx.ui.notify(babysitPreflightError ?? INSTALL_HINT, "error");
|
|
2105
2354
|
return;
|
|
2106
2355
|
}
|
|
2107
2356
|
const sessions = (await listSessions()).sessions.sort((a, b) =>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yusukeshib/pi-babysit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"agents.ts",
|
|
24
24
|
"self-reap.ts",
|
|
25
25
|
"format-stream.mjs",
|
|
26
|
+
"search-log.mjs",
|
|
26
27
|
"README.md",
|
|
27
28
|
"LICENSE"
|
|
28
29
|
],
|
package/search-log.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
|
|
4
|
+
const [file, source, maxLinesRaw] = process.argv.slice(2);
|
|
5
|
+
const maxLines = Math.min(Math.max(1, Number.parseInt(maxLinesRaw ?? "30", 10) || 30), 200);
|
|
6
|
+
const MAX_LINE_BYTES = 4_000;
|
|
7
|
+
|
|
8
|
+
let pattern;
|
|
9
|
+
try {
|
|
10
|
+
pattern = new RegExp(source);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error(`Invalid pattern: ${String(error)}`);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function clipLine(line) {
|
|
17
|
+
const bytes = Buffer.from(line, "utf8");
|
|
18
|
+
if (bytes.length <= MAX_LINE_BYTES) return line;
|
|
19
|
+
const half = Math.floor(MAX_LINE_BYTES / 2);
|
|
20
|
+
const head = bytes.subarray(0, half).toString("utf8").replace(/\uFFFD+$/, "");
|
|
21
|
+
const tail = bytes.subarray(bytes.length - half).toString("utf8").replace(/^\uFFFD+/, "");
|
|
22
|
+
return `${head}… [line clipped] …${tail}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const matches = [];
|
|
26
|
+
let lineNumber = 0;
|
|
27
|
+
try {
|
|
28
|
+
const input = fs.createReadStream(file, { encoding: "utf8" });
|
|
29
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
30
|
+
for await (const line of lines) {
|
|
31
|
+
lineNumber++;
|
|
32
|
+
pattern.lastIndex = 0;
|
|
33
|
+
if (!pattern.test(line)) continue;
|
|
34
|
+
matches.push(`${lineNumber}:${clipLine(line)}`);
|
|
35
|
+
if (matches.length > maxLines) matches.shift();
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(`Could not search log: ${String(error)}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.stdout.write(matches.join("\n"));
|