@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.
Files changed (3) hide show
  1. package/README.md +33 -12
  2. package/index.ts +160 -57
  3. 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 at every
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 command for processes and
136
- the task for agents (with `…` when clipped). Click an AGENT or PROCESS row to
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.summary}\nLog: ${notice.logPath}`).join("\n\n") +
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.summary}\nCommand: ${summarizeNotificationCommand(notice.command)}\nLog: ${notice.logPath}`;
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 detail = session?.details?.[0] ?? "";
2693
- const context = detail.startsWith("task: ") ? detail.slice(6) : detail;
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
- let cursor: RemoteLogCursor = { remoteOffset: 0, localBytes: 0, complete: false };
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
- if (Number.isSafeInteger(parsed.remoteOffset) && Number.isSafeInteger(parsed.localBytes) && parsed.remoteOffset >= 0 && parsed.localBytes >= 0) {
2873
- cursor = { remoteOffset: parsed.remoteOffset, localBytes: parsed.localBytes, complete: parsed.complete === true };
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
- let size = 0;
2877
- try {
2878
- size = fs.statSync(file).size;
2879
- } catch {}
2880
- if (size < cursor.localBytes) return { remoteOffset: 0, localBytes: 0, complete: false };
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 result = await bs(["log", "-s", id, "--since", String(cursor.remoteOffset), "--raw", "--json"], { unboundedStdout: true });
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
- writeRemoteLogCursor(id, { remoteOffset: 0, localBytes: 0, complete: false });
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: response.offset as number, localBytes: cursor.localBytes + bytes.length, complete });
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?.command ? ` (${summarizeNotificationCommand(meta.command)})` : ""} ` +
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.bold(escapeCommandForDisplay(readableLaunchCommand(meta.launchCommand)))] : []),
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.bold(escapeCommandForDisplay(meta.command))] : [];
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.command
4074
- ? ` ${summarizeNotificationCommand(d.command)}`
4075
- : d.count && d.count > 1
4076
- ? ` ×${d.count}`
4077
- : "";
4078
- const header = theme.fg(
4079
- "warning",
4080
- theme.bold(`babysit_run COMMAND${payload}`),
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(theme.fg("toolOutput", content), 0, 0));
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
- `Task 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` +
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
- // Make the execution kind and payload identifiable before the result arrives.
4734
- // Use semantic theme colors rather than the fallback gray tool-call text.
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 detail =
4742
- typeof payload === "string" && payload.length > 0
4743
- ? ` ${escapeCommandForDisplay(payload)}`
4744
- : "";
4745
- return new Text(theme.fg("warning", `${title}${agent}${detail}`), 0, 0);
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 = (kind === "subagent" ? meta?.task : meta?.command) ?? "";
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?.kind === "subagent" ? meta.task : meta?.command) ?? "")
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.6.20",
3
+ "version": "0.6.22",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",