@yusukeshib/pi-babysit 0.6.20 → 0.6.22
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 +33 -12
- package/index.ts +160 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,8 +32,8 @@ reachable from anywhere (`~/.pi-babysit/<pi-session-id>/`). Two kinds:
|
|
|
32
32
|
|
|
33
33
|
| kind | started by | completion | on completion |
|
|
34
34
|
| ---- | ---------- | ---------- | ------------- |
|
|
35
|
-
| **process** | `babysit_run { command }` | process **exit** | automatic notification message (`triggerTurn`), batched for all exits observed in the same poll — the agent may end its turn after starting and is resumed on exit, same contract as the old `process` tool |
|
|
36
|
-
| **subagent** | `babysit_run { profile: "subagent", task }` | `agent_settled` in the RPC event stream (worker remains reusable during its idle grace) | `foreground: true` returns the answer and nested usage in the same tool call; background workers must be collected with `babysit_wait` before the parent finishes, and remain reusable until self-reap |
|
|
35
|
+
| **process** | `babysit_run { description, command }` | process **exit** | automatic notification message (`triggerTurn`), batched for all exits observed in the same poll — the agent may end its turn after starting and is resumed on exit, same contract as the old `process` tool |
|
|
36
|
+
| **subagent** | `babysit_run { description, profile: "subagent", task }` | `agent_settled` in the RPC event stream (worker remains reusable during its idle grace) | `foreground: true` returns the answer and nested usage in the same tool call; background workers must be collected with `babysit_wait` before the parent finishes, and remain reusable until self-reap |
|
|
37
37
|
|
|
38
38
|
The **profile is a tool parameter, not a separate tool set**: domain knowledge
|
|
39
39
|
(RPC bookkeeping, per-task byte offsets, parked-turn detection, PTY-safe
|
|
@@ -44,8 +44,8 @@ Subagent recursion is **disabled by default**: a top-level pi may create workers
|
|
|
44
44
|
(depth 1), but those workers cannot create more workers. A top-level caller can
|
|
45
45
|
explicitly opt in for a specific tree with `maxDepth: 2` (or higher) when it
|
|
46
46
|
creates the first worker. Descendants inherit that ceiling and cannot raise it.
|
|
47
|
-
Normal `babysit_run { command }` process execution remains available
|
|
48
|
-
depth.
|
|
47
|
+
Normal `babysit_run { description, command }` process execution remains available
|
|
48
|
+
at every depth.
|
|
49
49
|
|
|
50
50
|
Because sessions are real PTYs, the agent can also **drive interactive
|
|
51
51
|
programs** (installers, wizards, REPLs): type with `babysit_send`
|
|
@@ -56,7 +56,7 @@ 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`/`lifecycle`). 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. Foreground and non-interactive process waits default to lifecycle `attached`, which terminates the process tree if that owning tool call is interrupted; background starts default to `detached`. 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 |
|
|
59
|
+
| `babysit_run` | Run any command (`description` + `command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`/`notificationGroup`/`lifecycle`). 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. Foreground and non-interactive process waits default to lifecycle `attached`, which terminates the process tree if that owning tool call is interrupted; background starts default to `detached`. Or start a named background subagent (`description`, `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"` |
|
|
@@ -71,6 +71,27 @@ calls if another extension or preset re-enables `bash`, including shell
|
|
|
71
71
|
backgrounding (`… &`, `nohup`, `setsid`, `disown`). Set
|
|
72
72
|
`PI_BABYSIT_ALLOW_BASH=1` to retain direct `bash` explicitly.
|
|
73
73
|
|
|
74
|
+
## Describe every run
|
|
75
|
+
|
|
76
|
+
Every `babysit_run` requires a non-blank `description`, for both commands and
|
|
77
|
+
subagents. Explain the purpose briefly in language the user can understand;
|
|
78
|
+
`name` remains a stable session identifier, while `command` / `task` contains
|
|
79
|
+
the actual execution instructions. Descriptions are trimmed before saving and
|
|
80
|
+
are display metadata, not shell code or instructions sent to the subagent.
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
babysit_run { description: "Run regression tests", command: "bun test", foreground: true }
|
|
84
|
+
babysit_run { description: "Review the authentication changes", profile: "subagent", task: "Review the authentication diff for security regressions", maxTurns: 5 }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
In Pi's main transcript the description is the prominent yellow heading, with
|
|
88
|
+
the execution kind in muted gray. The full command or task is always visible
|
|
89
|
+
in gray beneath it, even when tool results are collapsed. Session lists, the
|
|
90
|
+
widget, and completion notifications also show the description. Older saved sessions and tool calls
|
|
91
|
+
without one remain readable, falling back to their command or task. Callers
|
|
92
|
+
must supply a description for new runs; blank or missing values are rejected
|
|
93
|
+
before launch.
|
|
94
|
+
|
|
74
95
|
## Remote processes over SSH
|
|
75
96
|
|
|
76
97
|
Install a `babysit` CLI with `--host` support on both machines (tested with
|
|
@@ -79,8 +100,8 @@ SSH aliases, keys, known hosts, and ProxyJump are handled by babysit/SSH;
|
|
|
79
100
|
pi-babysit does not install remote software or copy credentials.
|
|
80
101
|
|
|
81
102
|
```text
|
|
82
|
-
babysit_run { host: "yusuke-t4", cwd: "/home/yusuke/project", command: "bun test", foreground: true, returnLines: 30 }
|
|
83
|
-
babysit_run { host: "user@server", command: "python3 -i", name: "remote-repl", continueAfterStart: true }
|
|
103
|
+
babysit_run { description: "Run tests on Linux", host: "yusuke-t4", cwd: "/home/yusuke/project", command: "bun test", foreground: true, returnLines: 30 }
|
|
104
|
+
babysit_run { description: "Open a remote Python REPL", host: "user@server", command: "python3 -i", name: "remote-repl", continueAfterStart: true }
|
|
84
105
|
babysit_check { id: "remote-repl", screen: true }
|
|
85
106
|
babysit_send { id: "remote-repl", text: "print(42)" }
|
|
86
107
|
babysit_kill { id: "remote-repl" }
|
|
@@ -132,9 +153,9 @@ summary counts use `RUNNING` / `IDLE`, and every row is labeled
|
|
|
132
153
|
Click the header to toggle to `babysits(n all)`, which also shows finished and failed
|
|
133
154
|
sessions, and click again to return to running-only. Both modes show newest first,
|
|
134
155
|
ten per page; `< 1/4 >` shows the current/total page when needed. Click the
|
|
135
|
-
arrows to page. Rows occupy one line, previewing the
|
|
136
|
-
|
|
137
|
-
replace the list with a tall **log viewer in the same widget area**, not an
|
|
156
|
+
arrows to page. Rows occupy one line, previewing the description (or the command
|
|
157
|
+
for older processes and task for older agents), with `…` when clipped. Click an
|
|
158
|
+
AGENT or PROCESS row to replace the list with a tall **log viewer in the same widget area**, not an
|
|
138
159
|
overlay or separate screen. `Esc` or the right-hand **Close** button restores
|
|
139
160
|
the list, preserving its mode and page. The viewer uses Pi's `ScrollView` for
|
|
140
161
|
scroll position and follow behavior, with a widget-local scrollbar styled using
|
|
@@ -269,7 +290,7 @@ because blindly rerunning an arbitrary command can duplicate side effects.
|
|
|
269
290
|
must still call `babysit_wait` so the answer and nested usage are persisted.
|
|
270
291
|
Unlike `agent_end`, `agent_settled` cannot precede an automatic retry,
|
|
271
292
|
compaction retry, or queued continuation. A settled run containing a
|
|
272
|
-
**parked** toolResult — a `babysit_run { command }` result carrying the
|
|
293
|
+
**parked** toolResult — a `babysit_run { description, command }` result carrying the
|
|
273
294
|
`[notify-on-exit]` marker (or the legacy `process` tool) — only means "turn
|
|
274
295
|
parked awaiting a process-exit notification; pi resumes on its own", so the
|
|
275
296
|
wait continues. The marker-bearing result may be followed by a short
|
|
@@ -280,7 +301,7 @@ because blindly rerunning an arbitrary command can duplicate side effects.
|
|
|
280
301
|
Subagents load `self-reap.ts`, which exits an idle finished subagent after a
|
|
281
302
|
grace window using the same parked-turn rule, so a subagent waiting on a long
|
|
282
303
|
build is never false-killed. Callers can set `reapAfter: "5m"` on
|
|
283
|
-
`babysit_run { profile: "subagent", task, reapAfter: "5m" }`. Omission uses
|
|
304
|
+
`babysit_run { description, profile: "subagent", task, reapAfter: "5m" }`. Omission uses
|
|
284
305
|
`PI_BABYSIT_REAP_AFTER`, then `PI_SUBAGENT_REAP_AFTER`, then 120s.
|
|
285
306
|
`reapAfter: "none"` (also `"off"` or `"0"`) disables idle reaping; the absolute
|
|
286
307
|
`timeout` still applies. Follow-up tasks cancel the idle timer. This differs
|
package/index.ts
CHANGED
|
@@ -127,7 +127,7 @@ export function planSubagentSpawn(
|
|
|
127
127
|
error:
|
|
128
128
|
`Nested subagent creation is disabled at depth ${currentDepth}: spawning would reach depth ${childDepth}, ` +
|
|
129
129
|
`but the inherited maxDepth is ${maxDepth}. Have the top-level parent explicitly opt in with ` +
|
|
130
|
-
`babysit_run { profile: "subagent", task, maxDepth: ${childDepth} } when creating the first worker.`,
|
|
130
|
+
`babysit_run { description, profile: "subagent", task, maxDepth: ${childDepth} } when creating the first worker.`,
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
return { allowed: true, childDepth, maxDepth };
|
|
@@ -160,7 +160,7 @@ export function subagentGuidance(
|
|
|
160
160
|
const shellGuidance = directBashAvailable
|
|
161
161
|
? "Direct bash is available, and babysit_run can supervise longer commands."
|
|
162
162
|
: babysitRunAvailable
|
|
163
|
-
? "Direct bash is unavailable; run shell commands with babysit_run { command }."
|
|
163
|
+
? "Direct bash is unavailable; run shell commands with babysit_run { description, command }."
|
|
164
164
|
: "No shell execution tool is available in this task's tool allowlist; do not attempt shell commands.";
|
|
165
165
|
const nestingGuidance = depth >= maxDepth
|
|
166
166
|
? `You are at the inherited subagent depth limit (${depth}/${maxDepth}); do not attempt to spawn another subagent.`
|
|
@@ -521,6 +521,8 @@ interface Meta {
|
|
|
521
521
|
remoteId?: string;
|
|
522
522
|
remoteTerminalState?: string;
|
|
523
523
|
kind: "process" | "subagent";
|
|
524
|
+
/** Human-facing purpose. Optional only for metadata written by older versions. */
|
|
525
|
+
description?: string;
|
|
524
526
|
// process
|
|
525
527
|
name?: string;
|
|
526
528
|
command?: string;
|
|
@@ -568,6 +570,11 @@ interface Meta {
|
|
|
568
570
|
}
|
|
569
571
|
|
|
570
572
|
const metaDir = () => path.join(ROOT, "meta");
|
|
573
|
+
|
|
574
|
+
/** Prefer the human-facing purpose while preserving previews for old sessions. */
|
|
575
|
+
export function sessionPreview(meta: Pick<Meta, "description" | "kind" | "task" | "command"> | undefined): string {
|
|
576
|
+
return meta?.description?.trim() || (meta?.kind === "subagent" ? meta.task : meta?.command) || "";
|
|
577
|
+
}
|
|
571
578
|
const localLogPath = (id: string) => path.join(ROOT, "sessions", id, "output.log");
|
|
572
579
|
const logPath = (id: string) => {
|
|
573
580
|
const meta = readMeta(id);
|
|
@@ -1397,6 +1404,7 @@ export interface ProcessCompletionNotice {
|
|
|
1397
1404
|
status: CompletionStatus;
|
|
1398
1405
|
runtime: string;
|
|
1399
1406
|
summary: string;
|
|
1407
|
+
description?: string;
|
|
1400
1408
|
command: string | undefined;
|
|
1401
1409
|
logPath: string;
|
|
1402
1410
|
output: string;
|
|
@@ -1414,6 +1422,7 @@ interface ProcessCompletionMessage {
|
|
|
1414
1422
|
runtime?: string;
|
|
1415
1423
|
logPath?: string;
|
|
1416
1424
|
command?: string;
|
|
1425
|
+
description?: string;
|
|
1417
1426
|
count: number;
|
|
1418
1427
|
totalCount: number;
|
|
1419
1428
|
remainingCount: number;
|
|
@@ -1424,6 +1433,7 @@ interface ProcessCompletionMessage {
|
|
|
1424
1433
|
status: CompletionStatus;
|
|
1425
1434
|
runtime: string;
|
|
1426
1435
|
command: string | undefined;
|
|
1436
|
+
description?: string;
|
|
1427
1437
|
logPath: string;
|
|
1428
1438
|
}>;
|
|
1429
1439
|
};
|
|
@@ -1465,9 +1475,11 @@ export function buildProcessCompletionMessage(
|
|
|
1465
1475
|
count < totalCount
|
|
1466
1476
|
? `\n\n${totalCount - count} completion${totalCount - count === 1 ? "" : "s"} deferred to the next poll.`
|
|
1467
1477
|
: "";
|
|
1478
|
+
const noticeSummary = (notice: ProcessCompletionNotice) =>
|
|
1479
|
+
notice.summary + (notice.description ? `\nDescription: ${summarizeNotificationCommand(notice.description)}` : "");
|
|
1468
1480
|
const renderCompact = (batch: ProcessCompletionNotice[]) =>
|
|
1469
1481
|
header(batch.length) +
|
|
1470
|
-
batch.map((notice) => `${notice
|
|
1482
|
+
batch.map((notice) => `${noticeSummary(notice)}\nLog: ${notice.logPath}`).join("\n\n") +
|
|
1471
1483
|
deferred(batch.length) +
|
|
1472
1484
|
footer;
|
|
1473
1485
|
|
|
@@ -1492,7 +1504,7 @@ export function buildProcessCompletionMessage(
|
|
|
1492
1504
|
}
|
|
1493
1505
|
|
|
1494
1506
|
const blockBase = (notice: ProcessCompletionNotice) =>
|
|
1495
|
-
`${notice
|
|
1507
|
+
`${noticeSummary(notice)}\nCommand: ${summarizeNotificationCommand(notice.command)}\nLog: ${notice.logPath}`;
|
|
1496
1508
|
const outputs = batch.map((notice) =>
|
|
1497
1509
|
notice.output.startsWith("\n\nOutput:") ? AGGREGATE_OUTPUT_OMISSION : notice.output,
|
|
1498
1510
|
);
|
|
@@ -1533,6 +1545,7 @@ export function buildProcessCompletionMessage(
|
|
|
1533
1545
|
status: notice.status,
|
|
1534
1546
|
runtime: notice.runtime,
|
|
1535
1547
|
command: notice.command,
|
|
1548
|
+
description: notice.description,
|
|
1536
1549
|
logPath: notice.logPath,
|
|
1537
1550
|
}));
|
|
1538
1551
|
const single = totalCount === 1 ? batch[0] : undefined;
|
|
@@ -1548,6 +1561,7 @@ export function buildProcessCompletionMessage(
|
|
|
1548
1561
|
runtime: single?.runtime,
|
|
1549
1562
|
logPath: single?.logPath,
|
|
1550
1563
|
command: single?.command,
|
|
1564
|
+
description: single?.description,
|
|
1551
1565
|
count: batch.length,
|
|
1552
1566
|
totalCount,
|
|
1553
1567
|
remainingCount: totalCount - batch.length,
|
|
@@ -2109,6 +2123,7 @@ async function reserveUniqueSessionId(name: string): Promise<string> {
|
|
|
2109
2123
|
}
|
|
2110
2124
|
|
|
2111
2125
|
interface ProcOpts {
|
|
2126
|
+
description: string;
|
|
2112
2127
|
name?: string;
|
|
2113
2128
|
command: string;
|
|
2114
2129
|
cwd?: string;
|
|
@@ -2161,7 +2176,7 @@ async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: s
|
|
|
2161
2176
|
const reservedId = remote ? await reserveUniqueSessionId(opts.name ?? "remote-proc") : opts.name ? await reserveUniqueSessionId(opts.name) : undefined;
|
|
2162
2177
|
const remoteId = remote ? remoteProcessId(ROOT) : undefined;
|
|
2163
2178
|
if (reservedId) bsArgs.push("--id", remoteId ?? reservedId);
|
|
2164
|
-
if (remote && !writeMeta(reservedId!, { kind: "process", host: opts.host, remoteId, name: opts.name ?? reservedId, command: opts.command, notificationGroup: opts.notificationGroup, notified: false, startedAt: Date.now() })) {
|
|
2179
|
+
if (remote && !writeMeta(reservedId!, { kind: "process", host: opts.host, remoteId, name: opts.name ?? reservedId, description: opts.description, command: opts.command, notificationGroup: opts.notificationGroup, notified: false, startedAt: Date.now() })) {
|
|
2165
2180
|
reservedSessionIds.delete(reservedId!);
|
|
2166
2181
|
return { error: "Cannot persist remote identity; process was not started." };
|
|
2167
2182
|
}
|
|
@@ -2196,6 +2211,7 @@ async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: s
|
|
|
2196
2211
|
writeMeta(id, {
|
|
2197
2212
|
kind: "process",
|
|
2198
2213
|
name: opts.name ?? id,
|
|
2214
|
+
description: opts.description,
|
|
2199
2215
|
command: opts.command,
|
|
2200
2216
|
notificationGroup: opts.notificationGroup,
|
|
2201
2217
|
notified: false,
|
|
@@ -2251,6 +2267,7 @@ function discardDelivery(delivery: DeliverableMessage): void {
|
|
|
2251
2267
|
}
|
|
2252
2268
|
|
|
2253
2269
|
interface SubagentOpts {
|
|
2270
|
+
description: string;
|
|
2254
2271
|
name?: string;
|
|
2255
2272
|
agent?: AgentConfig;
|
|
2256
2273
|
task: string;
|
|
@@ -2381,6 +2398,7 @@ async function spawnSubagent(
|
|
|
2381
2398
|
writeMeta(id, {
|
|
2382
2399
|
kind: "subagent",
|
|
2383
2400
|
name: opts.name ?? id,
|
|
2401
|
+
description: opts.description,
|
|
2384
2402
|
task: opts.task,
|
|
2385
2403
|
launchCommand,
|
|
2386
2404
|
notified: true,
|
|
@@ -2470,6 +2488,7 @@ async function spawnSubagent(
|
|
|
2470
2488
|
writeMeta(id, {
|
|
2471
2489
|
kind: "subagent",
|
|
2472
2490
|
name: opts.name ?? id,
|
|
2491
|
+
description: opts.description,
|
|
2473
2492
|
task: opts.task,
|
|
2474
2493
|
launchCommand,
|
|
2475
2494
|
promptOffset: resp.offset,
|
|
@@ -2566,6 +2585,7 @@ export interface WidgetSessionListState {
|
|
|
2566
2585
|
|
|
2567
2586
|
export interface WidgetSessionDisplay {
|
|
2568
2587
|
id: string;
|
|
2588
|
+
description?: string;
|
|
2569
2589
|
header: string;
|
|
2570
2590
|
tail: string[];
|
|
2571
2591
|
viewable: boolean;
|
|
@@ -2603,7 +2623,7 @@ export function createWidgetComponent(
|
|
|
2603
2623
|
const pushSession = (session: WidgetSessionDisplay) => {
|
|
2604
2624
|
const target = session.viewable ? { type: "session" as const, id: session.id } : undefined;
|
|
2605
2625
|
const detail = session.details?.[0];
|
|
2606
|
-
const preview = detail?.startsWith("task: ") ? detail.slice(6) : detail ?? session.tail.at(-1);
|
|
2626
|
+
const preview = session.description ?? (detail?.startsWith("task: ") ? detail.slice(6) : detail ?? session.tail.at(-1));
|
|
2607
2627
|
push(`${session.header}${preview ? ` ${preview}` : ""}`, target, "…");
|
|
2608
2628
|
};
|
|
2609
2629
|
|
|
@@ -2689,8 +2709,9 @@ export function createSessionWidget(options: {
|
|
|
2689
2709
|
const log = options.readLog(selectedId);
|
|
2690
2710
|
const session = displays.find((session) => session.id === selectedId);
|
|
2691
2711
|
const header = session?.header ?? selectedId;
|
|
2692
|
-
const
|
|
2693
|
-
|
|
2712
|
+
const context = (session?.details ?? [])
|
|
2713
|
+
.map((detail) => detail.startsWith("task: ") ? detail.slice(6) : detail)
|
|
2714
|
+
.join("\n");
|
|
2694
2715
|
if (log.file) {
|
|
2695
2716
|
if (!source || source.file !== log.file || source.isAgent !== log.isAgent)
|
|
2696
2717
|
source = new FileLogSource(log.file, log.isAgent);
|
|
@@ -2851,34 +2872,55 @@ const remoteTerminalTails = new Set<string>();
|
|
|
2851
2872
|
// append-only mirror built from incremental `log --since --raw --json` reads.
|
|
2852
2873
|
// The cursor records the remote raw-byte offset and the local mirror size it
|
|
2853
2874
|
// corresponds to, so an interrupted append is truncated instead of duplicated.
|
|
2854
|
-
type RemoteLogCursor = { remoteOffset: number; localBytes: number; complete: boolean };
|
|
2875
|
+
type RemoteLogCursor = { remoteOffset: number; localBytes: number; timestampBytes: number; complete: boolean };
|
|
2855
2876
|
type RemoteLogSyncState = { inFlight?: Promise<boolean>; lastSyncAt: number; lastChanged: boolean; error?: string };
|
|
2856
2877
|
const remoteLogSyncs = new Map<string, RemoteLogSyncState>();
|
|
2857
2878
|
const REMOTE_LOG_SYNC_INTERVAL_MS = 1000;
|
|
2879
|
+
// Hosts whose babysit predates `log --timestamps`; their mirrors have no dates.
|
|
2880
|
+
const remoteLogTimestampUnsupported = new Set<string>();
|
|
2858
2881
|
|
|
2859
2882
|
export function remoteLogMirrorPath(id: string): string {
|
|
2860
2883
|
return path.join(ROOT, "remote-logs", id, "output.log");
|
|
2861
2884
|
}
|
|
2862
2885
|
|
|
2886
|
+
function remoteLogTimestampsPath(id: string): string {
|
|
2887
|
+
return path.join(path.dirname(remoteLogMirrorPath(id)), "output.timestamps.jsonl");
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2863
2890
|
function remoteLogCursorPath(id: string): string {
|
|
2864
2891
|
return path.join(path.dirname(remoteLogMirrorPath(id)), "cursor.json");
|
|
2865
2892
|
}
|
|
2866
2893
|
|
|
2867
2894
|
function readRemoteLogCursor(id: string): RemoteLogCursor {
|
|
2868
2895
|
const file = remoteLogMirrorPath(id);
|
|
2869
|
-
|
|
2896
|
+
const sidecar = remoteLogTimestampsPath(id);
|
|
2897
|
+
const empty: RemoteLogCursor = { remoteOffset: 0, localBytes: 0, timestampBytes: 0, complete: false };
|
|
2898
|
+
let cursor = empty;
|
|
2870
2899
|
try {
|
|
2871
2900
|
const parsed = JSON.parse(fs.readFileSync(remoteLogCursorPath(id), "utf8"));
|
|
2872
|
-
|
|
2873
|
-
|
|
2901
|
+
const valid = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0;
|
|
2902
|
+
if (valid(parsed.remoteOffset) && valid(parsed.localBytes)) {
|
|
2903
|
+
cursor = { remoteOffset: parsed.remoteOffset, localBytes: parsed.localBytes, timestampBytes: valid(parsed.timestampBytes) ? parsed.timestampBytes : 0, complete: parsed.complete === true };
|
|
2874
2904
|
}
|
|
2875
2905
|
} catch {}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2906
|
+
const sizeOf = (target: string) => {
|
|
2907
|
+
try {
|
|
2908
|
+
return fs.statSync(target).size;
|
|
2909
|
+
} catch {
|
|
2910
|
+
return 0;
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
2913
|
+
const size = sizeOf(file);
|
|
2914
|
+
const sidecarSize = sizeOf(sidecar);
|
|
2915
|
+
if (size < cursor.localBytes || sidecarSize < cursor.timestampBytes) {
|
|
2916
|
+
// Committed data is missing; restart the mirror from scratch.
|
|
2917
|
+
if (size > 0) fs.truncateSync(file, 0);
|
|
2918
|
+
if (sidecarSize > 0) fs.truncateSync(sidecar, 0);
|
|
2919
|
+
return empty;
|
|
2920
|
+
}
|
|
2921
|
+
// Drop an append that was interrupted before its cursor was committed.
|
|
2881
2922
|
if (size > cursor.localBytes) fs.truncateSync(file, cursor.localBytes);
|
|
2923
|
+
if (sidecarSize > cursor.timestampBytes) fs.truncateSync(sidecar, cursor.timestampBytes);
|
|
2882
2924
|
return cursor;
|
|
2883
2925
|
}
|
|
2884
2926
|
|
|
@@ -2900,14 +2942,23 @@ export async function syncRemoteLog(id: string): Promise<boolean> {
|
|
|
2900
2942
|
if (!fs.existsSync(file)) fs.writeFileSync(file, "");
|
|
2901
2943
|
const cursor = readRemoteLogCursor(id);
|
|
2902
2944
|
if (cursor.complete) return false;
|
|
2903
|
-
const
|
|
2945
|
+
const host = readMeta(id)?.host ?? "";
|
|
2946
|
+
const readArgs = ["log", "-s", id, "--since", String(cursor.remoteOffset), "--raw", "--json"];
|
|
2947
|
+
let withTimestamps = !remoteLogTimestampUnsupported.has(host);
|
|
2948
|
+
let result = await bs(withTimestamps ? [...readArgs, "--timestamps"] : readArgs, { unboundedStdout: true });
|
|
2949
|
+
if (withTimestamps && result.code !== 0 && /--timestamps/.test(result.stderr)) {
|
|
2950
|
+
// Older remote babysit: keep mirroring text, without capture dates.
|
|
2951
|
+
remoteLogTimestampUnsupported.add(host);
|
|
2952
|
+
withTimestamps = false;
|
|
2953
|
+
result = await bs(readArgs, { unboundedStdout: true });
|
|
2954
|
+
}
|
|
2904
2955
|
if (result.code !== 0) {
|
|
2905
2956
|
const error = clip(result.stderr.trim() || result.stdout.trim() || `babysit log exited ${result.code}`, 500);
|
|
2906
2957
|
const changed = state.error !== error;
|
|
2907
2958
|
state.error = error;
|
|
2908
2959
|
return changed;
|
|
2909
2960
|
}
|
|
2910
|
-
let response: { text?: unknown; offset?: unknown; done?: unknown };
|
|
2961
|
+
let response: { text?: unknown; offset?: unknown; done?: unknown; timestamps?: unknown };
|
|
2911
2962
|
try {
|
|
2912
2963
|
response = JSON.parse(result.stdout);
|
|
2913
2964
|
} catch {
|
|
@@ -2921,18 +2972,38 @@ export async function syncRemoteLog(id: string): Promise<boolean> {
|
|
|
2921
2972
|
if ((response.offset as number) < cursor.remoteOffset) {
|
|
2922
2973
|
// The remote log was replaced or truncated (e.g. restart); rebuild the mirror.
|
|
2923
2974
|
fs.truncateSync(file, 0);
|
|
2924
|
-
|
|
2975
|
+
fs.rmSync(remoteLogTimestampsPath(id), { force: true });
|
|
2976
|
+
writeRemoteLogCursor(id, { remoteOffset: 0, localBytes: 0, timestampBytes: 0, complete: false });
|
|
2925
2977
|
return true;
|
|
2926
2978
|
}
|
|
2927
2979
|
const hadError = state.error !== undefined;
|
|
2928
2980
|
state.error = undefined;
|
|
2929
2981
|
const bytes = Buffer.from(response.text, "utf8");
|
|
2930
2982
|
if (bytes.length > 0) fs.appendFileSync(file, bytes);
|
|
2983
|
+
const remoteStart = cursor.remoteOffset;
|
|
2984
|
+
const remoteEnd = response.offset as number;
|
|
2985
|
+
let timestampBytes = cursor.timestampBytes;
|
|
2986
|
+
// Remote rows use remote raw offsets. They map 1:1 onto the mirror only
|
|
2987
|
+
// when the chunk arrived byte-for-byte; a lossy UTF-8 replacement shifts
|
|
2988
|
+
// offsets, so that chunk is left undated rather than mis-dated.
|
|
2989
|
+
if (withTimestamps && Array.isArray(response.timestamps) && bytes.length === remoteEnd - remoteStart) {
|
|
2990
|
+
const rows = response.timestamps.flatMap((row) => {
|
|
2991
|
+
if (!Array.isArray(row) || row.length !== 3 || !row.every((value) => Number.isSafeInteger(value) && value >= 0)) return [];
|
|
2992
|
+
const [start, length, time] = row as number[];
|
|
2993
|
+
const begin = Math.max(start, remoteStart);
|
|
2994
|
+
const end = Math.min(start + length, remoteEnd);
|
|
2995
|
+
return end > begin ? [`[${cursor.localBytes + begin - remoteStart},${end - begin},${time}]\n`] : [];
|
|
2996
|
+
}).join("");
|
|
2997
|
+
if (rows) {
|
|
2998
|
+
fs.appendFileSync(remoteLogTimestampsPath(id), rows);
|
|
2999
|
+
timestampBytes += Buffer.byteLength(rows);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
2931
3002
|
const advanced = (response.offset as number) > cursor.remoteOffset || bytes.length > 0;
|
|
2932
3003
|
// `done` is reported before the worker's final flush, so completion
|
|
2933
3004
|
// requires a terminal read that returned nothing new.
|
|
2934
3005
|
const complete = response.done === true && !advanced;
|
|
2935
|
-
writeRemoteLogCursor(id, { remoteOffset:
|
|
3006
|
+
writeRemoteLogCursor(id, { remoteOffset: remoteEnd, localBytes: cursor.localBytes + bytes.length, timestampBytes, complete });
|
|
2936
3007
|
return advanced || complete || hadError;
|
|
2937
3008
|
})().then((changed) => {
|
|
2938
3009
|
state.lastChanged = changed;
|
|
@@ -3490,7 +3561,7 @@ async function waitForExit(
|
|
|
3490
3561
|
kind: "exited",
|
|
3491
3562
|
ok,
|
|
3492
3563
|
text:
|
|
3493
|
-
`Process ${id}${meta
|
|
3564
|
+
`Process ${id}${sessionPreview(meta) ? ` (${summarizeNotificationCommand(sessionPreview(meta))})` : ""} ` +
|
|
3494
3565
|
(workerDead
|
|
3495
3566
|
? "worker-dead: the babysit supervisor disappeared without an exit status"
|
|
3496
3567
|
: ok ? "completed successfully" : `exited with code ${st.exit_code ?? "?"}`) +
|
|
@@ -3802,6 +3873,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3802
3873
|
runtime,
|
|
3803
3874
|
summary,
|
|
3804
3875
|
command: meta.command,
|
|
3876
|
+
description: meta.description,
|
|
3805
3877
|
logPath: logPath(session.id),
|
|
3806
3878
|
output,
|
|
3807
3879
|
});
|
|
@@ -3823,7 +3895,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3823
3895
|
if (!latest || latest.transportError || latest.state === "running") return [];
|
|
3824
3896
|
if (!isNotificationGroupReady(current, finalSessions, readMeta)) return [];
|
|
3825
3897
|
metadataById.set(notice.id, current);
|
|
3826
|
-
return [{ ...notice, command: current.command }];
|
|
3898
|
+
return [{ ...notice, command: current.command, description: current.description }];
|
|
3827
3899
|
});
|
|
3828
3900
|
// Output collection above yields to the event loop. Re-check idleness so a
|
|
3829
3901
|
// newly-started agent turn cannot receive a duplicate completion mid-turn.
|
|
@@ -3847,7 +3919,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3847
3919
|
): Promise<void> {
|
|
3848
3920
|
if (shouldDeferCompletionNotification(ctx.isIdle())) return;
|
|
3849
3921
|
const sessions = snapshot ?? (await listSessions()).sessions;
|
|
3850
|
-
const ready: Array<{ id: string; offset: number; state: string; summary: string }> = [];
|
|
3922
|
+
const ready: Array<{ id: string; offset: number; state: string; summary: string; description?: string }> = [];
|
|
3851
3923
|
for (const session of sessions) {
|
|
3852
3924
|
const meta = readMeta(session.id);
|
|
3853
3925
|
if (!shouldDeliverSubagentCompletion(meta)) continue;
|
|
@@ -3870,7 +3942,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3870
3942
|
const summary = progress.done
|
|
3871
3943
|
? `task settled; ${progress.turns} turns, ${progress.toolCallCount} tools, $${progress.cost.toFixed(4)}`
|
|
3872
3944
|
: `worker ${session.state} with exit code ${session.exit_code ?? "?"}; partial usage $${progress.cost.toFixed(4)}`;
|
|
3873
|
-
ready.push({ id: session.id, offset, state: session.state, summary });
|
|
3945
|
+
ready.push({ id: session.id, offset, state: session.state, summary, description: meta.description });
|
|
3874
3946
|
}
|
|
3875
3947
|
if (ready.length === 0 || shouldDeferCompletionNotification(ctx.isIdle())) return;
|
|
3876
3948
|
|
|
@@ -3884,10 +3956,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3884
3956
|
customType: "pi-babysit-subagent-ready",
|
|
3885
3957
|
content:
|
|
3886
3958
|
`${deliverable.length === 1 ? "A background subagent is" : `${deliverable.length} background subagents are`} ready to collect:\n` +
|
|
3887
|
-
deliverable.map(({ id, summary }) => `- ${id}: ${summary}`).join("\n") +
|
|
3959
|
+
deliverable.map(({ id, summary, description }) => `- ${id}${description ? ` (${summarizeNotificationCommand(description)})` : ""}: ${summary}`).join("\n") +
|
|
3888
3960
|
"\nCall babysit_wait now to retrieve the answer and charge nested usage before finishing the parent task.",
|
|
3889
3961
|
display: true,
|
|
3890
|
-
details: { subagents: deliverable.map(({ id, state }) => ({ id, state })) },
|
|
3962
|
+
details: { subagents: deliverable.map(({ id, state, description }) => ({ id, state, description })) },
|
|
3891
3963
|
},
|
|
3892
3964
|
{ triggerTurn: true, deliverAs: "steer" },
|
|
3893
3965
|
);
|
|
@@ -3942,12 +4014,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
3942
4014
|
const id = session.id;
|
|
3943
4015
|
const meta = readMeta(id);
|
|
3944
4016
|
const isSubagent = meta?.kind === "subagent";
|
|
4017
|
+
const description = meta?.description?.trim()
|
|
4018
|
+
? theme.fg("warning", theme.bold(escapeCommandForDisplay(meta.description.trim())))
|
|
4019
|
+
: undefined;
|
|
3945
4020
|
const details = isSubagent
|
|
3946
4021
|
? [
|
|
3947
|
-
...(meta?.task ? [`task: ${escapeCommandForDisplay(meta.task)}`] : []),
|
|
3948
|
-
...(meta?.launchCommand ? [theme.
|
|
4022
|
+
...(meta?.task ? [theme.fg("muted", `task: ${escapeCommandForDisplay(meta.task)}`)] : []),
|
|
4023
|
+
...(meta?.launchCommand ? [theme.fg("muted", escapeCommandForDisplay(readableLaunchCommand(meta.launchCommand)))] : []),
|
|
3949
4024
|
]
|
|
3950
|
-
: meta?.command ? [theme.
|
|
4025
|
+
: meta?.command ? [theme.fg("muted", escapeCommandForDisplay(meta.command))] : [];
|
|
4026
|
+
if (description) details.unshift(description);
|
|
3951
4027
|
if (meta?.host) details.unshift(`host: ${meta.host}${session.transportError ? " (status unknown)" : ""} · ${attachCmd(id)}`);
|
|
3952
4028
|
if (session.state === "running") {
|
|
3953
4029
|
const state: WidgetSessionState = isSubagent && progressById.get(id)?.done ? "idle" : "running";
|
|
@@ -3955,6 +4031,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3955
4031
|
id,
|
|
3956
4032
|
header: renderWidgetSessionHeader(id, isSubagent ? "agent" : "process", state, elapsedOf(id), theme) + (session.transportError ? " · status unknown" : ""),
|
|
3957
4033
|
tail: activeTails.get(id) ?? [],
|
|
4034
|
+
description,
|
|
3958
4035
|
viewable: true,
|
|
3959
4036
|
active: true,
|
|
3960
4037
|
details,
|
|
@@ -3970,6 +4047,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3970
4047
|
theme,
|
|
3971
4048
|
),
|
|
3972
4049
|
details,
|
|
4050
|
+
description,
|
|
3973
4051
|
// Only read/parse visible sessions on demand.
|
|
3974
4052
|
get tail() {
|
|
3975
4053
|
try {
|
|
@@ -4065,25 +4143,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
4065
4143
|
success?: boolean;
|
|
4066
4144
|
exitCode?: number | null;
|
|
4067
4145
|
command?: string;
|
|
4146
|
+
description?: string;
|
|
4068
4147
|
count?: number;
|
|
4069
4148
|
};
|
|
4070
4149
|
const status =
|
|
4071
4150
|
d.status ??
|
|
4072
4151
|
(d.success ? "success" : d.exitCode == null ? "terminated" : "failed");
|
|
4073
|
-
const payload = d.
|
|
4074
|
-
?
|
|
4075
|
-
: d.
|
|
4076
|
-
?
|
|
4077
|
-
:
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
);
|
|
4152
|
+
const payload = d.description?.trim()
|
|
4153
|
+
? theme.fg("warning", theme.bold(summarizeNotificationCommand(d.description)))
|
|
4154
|
+
: d.command
|
|
4155
|
+
? theme.fg("muted", summarizeNotificationCommand(d.command))
|
|
4156
|
+
: d.count && d.count > 1
|
|
4157
|
+
? theme.fg("muted", `×${d.count}`)
|
|
4158
|
+
: "";
|
|
4159
|
+
const header = theme.fg("muted", theme.bold("babysit_run COMMAND")) + (payload ? ` ${payload}` : "");
|
|
4082
4160
|
const box = new Box(1, 1, (t) => theme.bg("toolSuccessBg", t));
|
|
4083
4161
|
box.addChild(new Text(header, 0, 0));
|
|
4084
4162
|
box.addChild(new Text(renderStatus(status, theme), 0, 0));
|
|
4085
4163
|
if (expanded && content) {
|
|
4086
|
-
box.addChild(new Text(
|
|
4164
|
+
box.addChild(new Text(content.split("\n").map((line) =>
|
|
4165
|
+
theme.fg(line.startsWith("Command: ") ? "muted" : "toolOutput", line),
|
|
4166
|
+
).join("\n"), 0, 0));
|
|
4087
4167
|
}
|
|
4088
4168
|
return box;
|
|
4089
4169
|
},
|
|
@@ -4127,6 +4207,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4127
4207
|
remoteTails.clear();
|
|
4128
4208
|
remoteTerminalTails.clear();
|
|
4129
4209
|
remoteLogSyncs.clear();
|
|
4210
|
+
remoteLogTimestampUnsupported.clear();
|
|
4130
4211
|
pollNeeded = true;
|
|
4131
4212
|
const retentionDays = Number(process.env.PI_BABYSIT_RETENTION_DAYS ?? "3");
|
|
4132
4213
|
if (
|
|
@@ -4230,7 +4311,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4230
4311
|
block: true,
|
|
4231
4312
|
reason:
|
|
4232
4313
|
`This bash command tries to run in the background. Use babysit_run instead, e.g. ` +
|
|
4233
|
-
`babysit_run({ name: "background-process", command: ${JSON.stringify(command.replace(/\s*&\s*$/, ""))} })`,
|
|
4314
|
+
`babysit_run({ description: "Describe this background process's purpose", name: "background-process", command: ${JSON.stringify(command.replace(/\s*&\s*$/, ""))} })`,
|
|
4234
4315
|
};
|
|
4235
4316
|
}
|
|
4236
4317
|
if (isAllowedDirectBash(command)) return;
|
|
@@ -4239,7 +4320,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4239
4320
|
reason:
|
|
4240
4321
|
"Use babysit_run for shell commands so output is supervised and captured outside model context. " +
|
|
4241
4322
|
"Inspect an existing session log with babysit_check { id, lines, pattern? }. " +
|
|
4242
|
-
`Retry as babysit_run({ command: ${JSON.stringify(command)} }).`,
|
|
4323
|
+
`Retry with a short purpose description as babysit_run({ description: "Explain the purpose of this command", command: ${JSON.stringify(command)} }).`,
|
|
4243
4324
|
};
|
|
4244
4325
|
});
|
|
4245
4326
|
|
|
@@ -4249,11 +4330,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
4249
4330
|
label: "Babysit: run",
|
|
4250
4331
|
description:
|
|
4251
4332
|
"Run a supervised shell command, or start a reusable pi subagent with `profile: \"subagent\"`. " +
|
|
4333
|
+
"Always provide a short human-readable `description` of what this run does and why. " +
|
|
4252
4334
|
"Use `foreground` for results needed now; otherwise long commands notify on exit. Full logs stay on disk. " +
|
|
4253
4335
|
"`returnPattern`/`returnLines` bound foreground output. Sessions support check, wait, send, and kill.",
|
|
4254
4336
|
promptSnippet: "Run supervised commands or bounded pi subagents with context-safe logs",
|
|
4255
4337
|
promptGuidelines: [
|
|
4256
4338
|
"Use babysit_run for shell commands and give meaningful sessions a stable name; bundle tiny related read-only observations into one command.",
|
|
4339
|
+
"Every babysit_run requires a short description explaining its purpose for the user, for both commands and subagents. Describe the actual work, not a generic 'run command' or 'start agent'.",
|
|
4257
4340
|
"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.",
|
|
4258
4341
|
"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.",
|
|
4259
4342
|
"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.",
|
|
@@ -4267,6 +4350,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4267
4350
|
return prepareBabysitRunArguments(args) as never;
|
|
4268
4351
|
},
|
|
4269
4352
|
parameters: Type.Object({
|
|
4353
|
+
description: Type.String({
|
|
4354
|
+
minLength: 1,
|
|
4355
|
+
pattern: "\\S",
|
|
4356
|
+
description: "Required short human-readable purpose for this command or subagent. Explain what it does and why; shown prominently to the user.",
|
|
4357
|
+
}),
|
|
4270
4358
|
host: Type.Optional(Type.String({ description: "Process SSH destination/config alias (default local). Remote subagents are unsupported." })),
|
|
4271
4359
|
cwd: Type.Optional(Type.String({ description: "Process working directory. Remote default is login home; never inferred from local cwd." })),
|
|
4272
4360
|
command: Type.Optional(
|
|
@@ -4397,6 +4485,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
4397
4485
|
|
|
4398
4486
|
}),
|
|
4399
4487
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
4488
|
+
if (typeof params.description !== "string" || !params.description.trim()) {
|
|
4489
|
+
throw new Error("`description` is required and must be a non-blank string for both commands and subagents.");
|
|
4490
|
+
}
|
|
4491
|
+
const description = params.description.trim();
|
|
4400
4492
|
await requireBabysit();
|
|
4401
4493
|
const isSubagent = params.profile === "subagent";
|
|
4402
4494
|
validateProcessHost(params.host);
|
|
@@ -4504,6 +4596,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4504
4596
|
? { pattern: params.returnPattern, lines: params.returnLines, maxBytes: params.maxBytes }
|
|
4505
4597
|
: undefined;
|
|
4506
4598
|
const spawnOpts: ProcOpts = {
|
|
4599
|
+
description,
|
|
4507
4600
|
name: params.name,
|
|
4508
4601
|
command: params.command as string,
|
|
4509
4602
|
host: params.host,
|
|
@@ -4554,6 +4647,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4554
4647
|
kind: "process",
|
|
4555
4648
|
command: params.command,
|
|
4556
4649
|
logPath: logPath(res.id),
|
|
4650
|
+
description,
|
|
4557
4651
|
retried,
|
|
4558
4652
|
status: outcomeStatus(outcome),
|
|
4559
4653
|
},
|
|
@@ -4590,6 +4684,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4590
4684
|
kind: "process",
|
|
4591
4685
|
command: params.command,
|
|
4592
4686
|
logPath: logPath(res.id),
|
|
4687
|
+
description,
|
|
4593
4688
|
retried,
|
|
4594
4689
|
status: outcomeStatus(outcome),
|
|
4595
4690
|
},
|
|
@@ -4606,7 +4701,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4606
4701
|
type: "text",
|
|
4607
4702
|
text:
|
|
4608
4703
|
`${retried ? "Retried once after external worker death.\n" : ""}` +
|
|
4609
|
-
`Process started (id: ${res.id}). ${NOTIFY_MARKER}\nLog: ${logPath(res.id)}\n${nextStep}\n` +
|
|
4704
|
+
`Process started (id: ${res.id}). ${NOTIFY_MARKER}\nDescription: ${escapeCommandForDisplay(description)}\nLog: ${logPath(res.id)}\n${nextStep}\n` +
|
|
4610
4705
|
`Inspect: babysit_check { id: "${res.id}" } (screen: true for TUIs) · ` +
|
|
4611
4706
|
`Wait: babysit_wait { id: "${res.id}" } · Kill: babysit_kill { id: "${res.id}" }\n` +
|
|
4612
4707
|
`Human can watch/take over: /babysit`,
|
|
@@ -4616,6 +4711,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4616
4711
|
id: res.id,
|
|
4617
4712
|
kind: "process",
|
|
4618
4713
|
command: params.command,
|
|
4714
|
+
description,
|
|
4619
4715
|
logPath: logPath(res.id),
|
|
4620
4716
|
retried,
|
|
4621
4717
|
status: "started" satisfies DisplayStatus,
|
|
@@ -4651,6 +4747,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4651
4747
|
// The branch above guarantees a successful plan in subagent mode.
|
|
4652
4748
|
const subagentNesting = nesting as Extract<SubagentSpawnPlan, { allowed: true }>;
|
|
4653
4749
|
const res = await spawnSubagent({
|
|
4750
|
+
description,
|
|
4654
4751
|
name: params.name,
|
|
4655
4752
|
agent,
|
|
4656
4753
|
task: params.task as string,
|
|
@@ -4699,6 +4796,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4699
4796
|
agent: agent?.name,
|
|
4700
4797
|
model: res.model,
|
|
4701
4798
|
task: params.task,
|
|
4799
|
+
description,
|
|
4702
4800
|
depth: subagentNesting.childDepth,
|
|
4703
4801
|
maxDepth: subagentNesting.maxDepth,
|
|
4704
4802
|
status: outcomeStatus(outcome),
|
|
@@ -4711,7 +4809,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4711
4809
|
type: "text",
|
|
4712
4810
|
text:
|
|
4713
4811
|
`Subagent started (id: ${res.id})${agent ? ` [agent: ${agent.name}]` : ""}${res.model ? ` [model: ${res.model}]` : ""} [depth: ${subagentNesting.childDepth}/${subagentNesting.maxDepth}].\n` +
|
|
4714
|
-
`
|
|
4812
|
+
`Description: ${escapeCommandForDisplay(description)}\nTask accepted — running in the background. You MUST collect it with babysit_wait before finishing the parent task; use foreground: true next time when no independent work is available.\n` +
|
|
4715
4813
|
`Progress: babysit_check { id: "${res.id}" } (only when inspection is needed)\n` +
|
|
4716
4814
|
`Collect: babysit_wait { id: "${res.id}" }\n` +
|
|
4717
4815
|
`Human can watch/steer: /babysit (pick ${res.id})`,
|
|
@@ -4724,25 +4822,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
4724
4822
|
agent: agent?.name,
|
|
4725
4823
|
model: res.model,
|
|
4726
4824
|
task: params.task,
|
|
4825
|
+
description,
|
|
4727
4826
|
depth: subagentNesting.childDepth,
|
|
4728
4827
|
maxDepth: subagentNesting.maxDepth,
|
|
4729
4828
|
status: "started" satisfies DisplayStatus,
|
|
4730
4829
|
},
|
|
4731
4830
|
};
|
|
4732
4831
|
},
|
|
4733
|
-
//
|
|
4734
|
-
//
|
|
4832
|
+
// The human-readable purpose is primary; the executable payload is secondary.
|
|
4833
|
+
// Legacy calls without descriptions still render their original payload safely.
|
|
4735
4834
|
renderCall(args, theme) {
|
|
4736
4835
|
const isSubagent = args.profile === "subagent";
|
|
4737
4836
|
const kind = isSubagent ? "AGENT" : "COMMAND";
|
|
4738
4837
|
const payload = isSubagent ? args.task : args.command;
|
|
4739
|
-
const agent = isSubagent && args.agent ? ` [${args.agent}]` : "";
|
|
4740
|
-
const title = theme.bold(`babysit_run ${kind}`);
|
|
4741
|
-
const
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4838
|
+
const agent = isSubagent && args.agent ? ` [${escapeCommandForDisplay(args.agent)}]` : "";
|
|
4839
|
+
const title = theme.fg("muted", `${theme.bold(`babysit_run ${kind}`)}${agent}`);
|
|
4840
|
+
const description = typeof args.description === "string" ? args.description.trim() : "";
|
|
4841
|
+
const raw = typeof payload === "string" ? escapeCommandForDisplay(payload) : "";
|
|
4842
|
+
const heading = description
|
|
4843
|
+
? `${title} ${theme.fg("warning", theme.bold(escapeCommandForDisplay(description)))}`
|
|
4844
|
+
: `${title}${raw ? theme.fg("muted", ` ${raw}`) : ""}`;
|
|
4845
|
+
const detail = description && raw
|
|
4846
|
+
? `\n${theme.fg("muted", raw)}`
|
|
4847
|
+
: "";
|
|
4848
|
+
return new Text(heading + detail, 0, 0);
|
|
4746
4849
|
},
|
|
4747
4850
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
4748
4851
|
const details = (result.details ?? {}) as {
|
|
@@ -4881,7 +4984,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4881
4984
|
kind === "subagent" && meta?.depth != null
|
|
4882
4985
|
? ` depth=${meta.depth}/${meta.maxDepth ?? "?"}`
|
|
4883
4986
|
: "";
|
|
4884
|
-
const what = (
|
|
4987
|
+
const what = escapeCommandForDisplay(sessionPreview(meta));
|
|
4885
4988
|
const preview = what.length > 60 ? `${what.slice(0, 57)}…` : what;
|
|
4886
4989
|
return `${s.id} [${kind}${s.host ? ` host=${s.host}` : ""}] ${s.transportError ? "unknown" : s.state}${ec}${depth}${flag}${preview ? ` — ${preview}` : ""}`;
|
|
4887
4990
|
});
|
|
@@ -4949,6 +5052,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4949
5052
|
if (el) header += ` elapsed=${el}`;
|
|
4950
5053
|
}
|
|
4951
5054
|
if (st.exit_code != null) header += ` exit_code=${st.exit_code}`;
|
|
5055
|
+
if (meta?.description) header += `\ndescription: ${escapeCommandForDisplay(meta.description)}`;
|
|
4952
5056
|
if (meta?.command) header += `\ncommand: ${summarizeNotificationCommand(meta.command)}`;
|
|
4953
5057
|
header += `\nlog: ${logPath(params.id)}`;
|
|
4954
5058
|
if (st.note) header += ` ⚑ ${st.note}`;
|
|
@@ -4994,6 +5098,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4994
5098
|
if (prog.tokens != null) header += ` ctx=${prog.tokens}`;
|
|
4995
5099
|
if (prog.modelCalls > 0) header += ` usage=${prog.usageTokens} $${prog.cost.toFixed(4)}`;
|
|
4996
5100
|
if (st.note) header += ` ⚑ ${st.note}`;
|
|
5101
|
+
if (meta.description) header += `\ndescription: ${escapeCommandForDisplay(meta.description)}`;
|
|
4997
5102
|
parts.push(header);
|
|
4998
5103
|
|
|
4999
5104
|
if (prog.errorMsg) parts.push(`⚠ error: ${clip(prog.errorMsg, ANSWER_MAX_BYTES)}`);
|
|
@@ -5542,9 +5647,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5542
5647
|
|
|
5543
5648
|
const whatOf = (s: BsSession): string => {
|
|
5544
5649
|
const meta = readMeta(s.id);
|
|
5545
|
-
return ((meta
|
|
5546
|
-
.replace(/\s+/g, " ")
|
|
5547
|
-
.trim();
|
|
5650
|
+
return escapeCommandForDisplay(sessionPreview(meta).replace(/\s+/g, " ").trim());
|
|
5548
5651
|
};
|
|
5549
5652
|
|
|
5550
5653
|
// Labels must be unique for index mapping; the id makes them unique.
|