@yusukeshib/pi-babysit 0.5.0 → 0.6.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 +12 -2
- package/index.ts +97 -12
- package/package.json +1 -1
- package/self-reap.ts +6 -3
package/README.md
CHANGED
|
@@ -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
|
|
79
|
-
|
|
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
|
|
|
@@ -125,6 +127,14 @@ suite at the end. Background subagents must likewise be collected before the
|
|
|
125
127
|
parent task finishes so their answer and nested usage are not lost.
|
|
126
128
|
Set `PI_BABYSIT_ALLOW_BASH=1` only as an explicit emergency escape hatch.
|
|
127
129
|
|
|
130
|
+
## Pi shutdown and reload
|
|
131
|
+
|
|
132
|
+
A real Pi quit terminates every running process and subagent in the current Pi
|
|
133
|
+
session namespace. The workers are killed concurrently, so shutdown time does
|
|
134
|
+
not grow linearly when many sessions are active. Lifecycle transitions that keep
|
|
135
|
+
Pi running—`/reload`, `/new`, `/resume`, and `/fork`—leave detached workers
|
|
136
|
+
alone. Reloading or resuming the same Pi session reconnects to its namespace.
|
|
137
|
+
|
|
128
138
|
## Unexpected worker loss
|
|
129
139
|
|
|
130
140
|
If the babysit supervisor disappears without recording an exit, pi-babysit
|
package/index.ts
CHANGED
|
@@ -367,6 +367,35 @@ async function awaitConfirmedTermination(id: string): Promise<BsSession | null>
|
|
|
367
367
|
return statusOf(id);
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
export function shouldTerminateSessionsOnShutdown(reason: string | undefined): boolean {
|
|
371
|
+
return reason === "quit";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function terminateRunningSessions(): Promise<void> {
|
|
375
|
+
let listed: Awaited<ReturnType<typeof listSessions>>;
|
|
376
|
+
try {
|
|
377
|
+
listed = await listSessions();
|
|
378
|
+
} catch {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (listed.error) return;
|
|
382
|
+
|
|
383
|
+
// A namespace may contain many independent commands and agents. Terminate
|
|
384
|
+
// them concurrently so quitting Pi does not wait for serial kill timeouts.
|
|
385
|
+
// allSettled ensures one unreachable worker cannot skip cleanup for others.
|
|
386
|
+
await Promise.allSettled(
|
|
387
|
+
listed.sessions
|
|
388
|
+
.filter((session) => session.state === "running")
|
|
389
|
+
.map(async (session) => {
|
|
390
|
+
const result = await bs(["kill", "-s", session.id, "--json"]);
|
|
391
|
+
if (result.code !== 0 || validateKillResponse(result.stdout)) return;
|
|
392
|
+
// A later resume of this Pi session must not turn intentional shutdown
|
|
393
|
+
// cleanup into a process-completion notification.
|
|
394
|
+
suppressNotify(session.id, "kill");
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
370
399
|
// ---------------------------------------------------------------------------
|
|
371
400
|
// per-session metadata
|
|
372
401
|
// ---------------------------------------------------------------------------
|
|
@@ -2188,13 +2217,56 @@ async function spawnSubagent(
|
|
|
2188
2217
|
// A subagent whose task is done stays alive as an idle RPC worker (for
|
|
2189
2218
|
// follow-ups), so "running" in babysit does NOT mean "working" — count
|
|
2190
2219
|
// processes / busy subagents / idle subagents separately.
|
|
2191
|
-
|
|
2220
|
+
export type WidgetSessionKind = "process" | "agent";
|
|
2221
|
+
export type WidgetSessionState = "running" | "idle";
|
|
2222
|
+
|
|
2223
|
+
export function widgetSummaryText(procs: number, busy: number, idle: number): string {
|
|
2224
|
+
const sections: string[] = [];
|
|
2225
|
+
const running: string[] = [];
|
|
2226
|
+
if (procs > 0) running.push(`${procs} process${procs > 1 ? "es" : ""}`);
|
|
2227
|
+
if (busy > 0) running.push(`${busy} agent${busy > 1 ? "s" : ""}`);
|
|
2228
|
+
if (running.length > 0) sections.push(`RUNNING ${running.join(" · ")}`);
|
|
2229
|
+
if (idle > 0) sections.push(`IDLE ${idle} agent${idle > 1 ? "s" : ""}`);
|
|
2230
|
+
return sections.join(" │ ");
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
export function widgetSessionHeader(
|
|
2234
|
+
id: string,
|
|
2235
|
+
kind: WidgetSessionKind,
|
|
2236
|
+
state: WidgetSessionState,
|
|
2237
|
+
elapsed?: string,
|
|
2238
|
+
): string {
|
|
2239
|
+
const icon = state === "running" ? "▶" : "○";
|
|
2240
|
+
return ` ${icon} ${id} [${kind.toUpperCase()}] [${state.toUpperCase()}]${elapsed ? ` · age ${elapsed}` : ""}`;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
function renderWidgetLines(procs: number, busy: number, idle: number, theme: Theme): string[] {
|
|
2192
2244
|
if (procs === 0 && busy === 0 && idle === 0) return [];
|
|
2193
|
-
const
|
|
2194
|
-
|
|
2195
|
-
if (
|
|
2196
|
-
if (
|
|
2197
|
-
|
|
2245
|
+
const sections: string[] = [];
|
|
2246
|
+
const running: string[] = [];
|
|
2247
|
+
if (procs > 0) running.push(`${procs} process${procs > 1 ? "es" : ""}`);
|
|
2248
|
+
if (busy > 0) running.push(`${busy} agent${busy > 1 ? "s" : ""}`);
|
|
2249
|
+
if (running.length > 0) {
|
|
2250
|
+
sections.push(`${theme.fg("success", theme.bold("RUNNING"))} ${running.join(" · ")}`);
|
|
2251
|
+
}
|
|
2252
|
+
if (idle > 0) {
|
|
2253
|
+
sections.push(`${theme.fg("muted", theme.bold("IDLE"))} ${idle} agent${idle > 1 ? "s" : ""}`);
|
|
2254
|
+
}
|
|
2255
|
+
return [theme.bg("toolPendingBg", ` ${sections.join(" │ ")} `)];
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function renderWidgetSessionHeader(
|
|
2259
|
+
id: string,
|
|
2260
|
+
kind: WidgetSessionKind,
|
|
2261
|
+
state: WidgetSessionState,
|
|
2262
|
+
elapsed: string,
|
|
2263
|
+
theme: Theme,
|
|
2264
|
+
): string {
|
|
2265
|
+
const running = state === "running";
|
|
2266
|
+
const icon = theme.fg(running ? "success" : "muted", running ? "▶" : "○");
|
|
2267
|
+
const kindLabel = theme.fg(kind === "process" ? "accent" : "warning", theme.bold(`[${kind.toUpperCase()}]`));
|
|
2268
|
+
const stateLabel = theme.fg(running ? "success" : "muted", theme.bold(`[${state.toUpperCase()}]`));
|
|
2269
|
+
return ` ${icon} ${id} ${kindLabel} ${stateLabel}${elapsed ? theme.fg("dim", ` · age ${elapsed}`) : ""}`;
|
|
2198
2270
|
}
|
|
2199
2271
|
|
|
2200
2272
|
// How many trailing output lines to show per running session in the widget.
|
|
@@ -3095,7 +3167,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
3095
3167
|
}
|
|
3096
3168
|
}
|
|
3097
3169
|
const idle = subs.filter((session) => progressById.get(session.id)?.done).length;
|
|
3098
|
-
const
|
|
3170
|
+
const theme = ctx.ui.theme;
|
|
3171
|
+
const lines = renderWidgetLines(procs, subs.length - idle, idle, theme);
|
|
3099
3172
|
const tails = await Promise.all(
|
|
3100
3173
|
active.map((session) => {
|
|
3101
3174
|
const isSubagent = kindOf(session.id) === "subagent";
|
|
@@ -3108,11 +3181,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
3108
3181
|
);
|
|
3109
3182
|
active.forEach((session, index) => {
|
|
3110
3183
|
const isSubagent = kindOf(session.id) === "subagent";
|
|
3111
|
-
const
|
|
3112
|
-
?
|
|
3113
|
-
: "
|
|
3184
|
+
const state: WidgetSessionState = isSubagent && progressById.get(session.id)?.done
|
|
3185
|
+
? "idle"
|
|
3186
|
+
: "running";
|
|
3114
3187
|
const elapsed = elapsedOf(session.id);
|
|
3115
|
-
const header =
|
|
3188
|
+
const header = renderWidgetSessionHeader(
|
|
3189
|
+
session.id,
|
|
3190
|
+
isSubagent ? "agent" : "process",
|
|
3191
|
+
state,
|
|
3192
|
+
elapsed,
|
|
3193
|
+
theme,
|
|
3194
|
+
);
|
|
3116
3195
|
if (tails[index].length === 1) {
|
|
3117
3196
|
lines.push(`${header} │ ${tails[index][0]}`);
|
|
3118
3197
|
} else {
|
|
@@ -3288,9 +3367,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
3288
3367
|
}, POLL_MS);
|
|
3289
3368
|
});
|
|
3290
3369
|
|
|
3291
|
-
pi.on("session_shutdown", async () => {
|
|
3370
|
+
pi.on("session_shutdown", async (event) => {
|
|
3292
3371
|
if (pollTimer) clearInterval(pollTimer);
|
|
3293
3372
|
pollTimer = undefined;
|
|
3373
|
+
// Pi also emits session_shutdown while hot-reloading and switching sessions.
|
|
3374
|
+
// Preserve detached work for those lifecycle transitions; only a real Pi
|
|
3375
|
+
// quit owns and terminates every worker in the current session namespace.
|
|
3376
|
+
if (shouldTerminateSessionsOnShutdown(event?.reason)) {
|
|
3377
|
+
await terminateRunningSessions();
|
|
3378
|
+
}
|
|
3294
3379
|
releaseRootLease(rootLeasePath);
|
|
3295
3380
|
rootLeasePath = undefined;
|
|
3296
3381
|
});
|
package/package.json
CHANGED
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", (
|
|
103
|
+
pi.on("agent_settled", () => {
|
|
104
104
|
cancel();
|
|
105
105
|
if (lastEndWasParked) return;
|
|
106
106
|
timer = setTimeout(() => {
|
|
107
|
-
//
|
|
108
|
-
|
|
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
|
});
|