zob-harness 0.9.2 → 0.10.0

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.
@@ -0,0 +1,289 @@
1
+ import { closeSync, existsSync, openSync, readSync, statSync } from "node:fs";
2
+ import { resolve, sep } from "node:path";
3
+
4
+ import { isRecord } from "../core/utils/records.js";
5
+ import { sanitizeDelegationText } from "./delegation-markdown.js";
6
+
7
+ const ACTIVITY_MAX_BYTES = 180_000;
8
+ const DEFAULT_RECENT_LIMIT = 5;
9
+
10
+ export type DelegationActivityStatus = "running" | "success" | "error";
11
+
12
+ export interface DelegationActivity {
13
+ toolCallId: string;
14
+ toolName: string;
15
+ status: DelegationActivityStatus;
16
+ startedAtMs?: number;
17
+ endedAtMs?: number;
18
+ elapsedMs?: number;
19
+ command?: string;
20
+ timeoutMs?: number;
21
+ target?: string;
22
+ summary?: string;
23
+ outputSummary?: string;
24
+ errorSummary?: string;
25
+ }
26
+
27
+ export interface DelegationActivitySnapshot {
28
+ status: "ok" | "missing" | "blocked" | "empty";
29
+ note?: string;
30
+ fingerprint: string;
31
+ current?: DelegationActivity;
32
+ recent: DelegationActivity[];
33
+ lastUpdatedMs?: number;
34
+ quietMs?: number;
35
+ }
36
+
37
+ function normalizePath(path: string): string {
38
+ return resolve(path);
39
+ }
40
+
41
+ function isInside(root: string, candidate: string): boolean {
42
+ const normalizedRoot = normalizePath(root);
43
+ const normalizedCandidate = normalizePath(candidate);
44
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}${sep}`);
45
+ }
46
+
47
+ function readTail(path: string, size: number, maxBytes: number): string {
48
+ if (size <= maxBytes) {
49
+ const buffer = Buffer.alloc(size);
50
+ const fd = openSync(path, "r");
51
+ try {
52
+ readSync(fd, buffer, 0, size, 0);
53
+ } finally {
54
+ closeSync(fd);
55
+ }
56
+ return buffer.toString("utf8");
57
+ }
58
+ const buffer = Buffer.alloc(maxBytes);
59
+ const fd = openSync(path, "r");
60
+ try {
61
+ readSync(fd, buffer, 0, maxBytes, Math.max(0, size - maxBytes));
62
+ } finally {
63
+ closeSync(fd);
64
+ }
65
+ const text = buffer.toString("utf8");
66
+ const firstNewline = text.indexOf("\n");
67
+ const safeTail = firstNewline >= 0 ? text.slice(firstNewline + 1) : text;
68
+ return `{"type":"custom","customType":"zob-activity-tail-capped","data":{"omittedBytes":${size - maxBytes}}}\n${safeTail}`;
69
+ }
70
+
71
+ function timestampMs(entry: Record<string, unknown>): number | undefined {
72
+ if (typeof entry.timestamp !== "string") return undefined;
73
+ const parsed = Date.parse(entry.timestamp);
74
+ return Number.isFinite(parsed) ? parsed : undefined;
75
+ }
76
+
77
+ function compactWhitespace(value: string): string {
78
+ return value.replace(/\s+/g, " ").trim();
79
+ }
80
+
81
+ export function redactDelegationCommand(command: string): string {
82
+ return sanitizeDelegationText(command)
83
+ .replace(/\b([A-Z0-9_]*(?:DATABASE_URL|TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|ACCESS_KEY|PRIVATE_KEY)[A-Z0-9_]*)=("[^"]*"|'[^']*'|\S+)/gi, "$1=<redacted>")
84
+ .replace(/(Authorization\s*:\s*Bearer\s+)("[^"]*"|'[^']*'|\S+)/gi, "$1<redacted>")
85
+ .replace(/\b(--(?:password|passwd|token|secret|api-key|access-key|private-key|database-url)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi, "$1<redacted>")
86
+ .replace(/\b(-H\s+["']?Authorization\s*:\s*Bearer\s+)([^"'\s]+)/gi, "$1<redacted>");
87
+ }
88
+
89
+ function capInline(value: string, limit: number): string {
90
+ const compact = compactWhitespace(sanitizeDelegationText(value));
91
+ if (compact.length <= limit) return compact;
92
+ const head = Math.max(8, Math.floor(limit * 0.65));
93
+ const tail = Math.max(4, limit - head - 1);
94
+ return `${compact.slice(0, head)}…${compact.slice(-tail)}`;
95
+ }
96
+
97
+ function contentText(content: unknown): string {
98
+ if (typeof content === "string") return sanitizeDelegationText(content);
99
+ if (!Array.isArray(content)) return "";
100
+ const parts: string[] = [];
101
+ for (const part of content) {
102
+ if (!isRecord(part) || typeof part.type !== "string") continue;
103
+ if (part.type === "text" && typeof part.text === "string") parts.push(sanitizeDelegationText(part.text));
104
+ else if (part.type === "image") parts.push("[image]");
105
+ }
106
+ return parts.join("\n");
107
+ }
108
+
109
+ function toolArguments(part: Record<string, unknown>): Record<string, unknown> {
110
+ const direct = part.arguments ?? part.args ?? part.input;
111
+ return isRecord(direct) ? direct : {};
112
+ }
113
+
114
+ function toolName(part: Record<string, unknown>): string {
115
+ return typeof part.name === "string" ? part.name : typeof part.toolName === "string" ? part.toolName : "tool";
116
+ }
117
+
118
+ function timeoutMsFromArgs(args: Record<string, unknown>): number | undefined {
119
+ const raw = args.timeout ?? args.timeout_ms ?? args.timeoutMs;
120
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return undefined;
121
+ // Pi bash tool accepts timeout in seconds; keep millisecond values if callers already use them.
122
+ return raw > 10_000 ? Math.floor(raw) : Math.floor(raw * 1000);
123
+ }
124
+
125
+ function activityFromToolCall(entry: Record<string, unknown>, part: Record<string, unknown>): DelegationActivity | undefined {
126
+ const id = typeof part.id === "string" ? part.id : undefined;
127
+ if (!id) return undefined;
128
+ const name = toolName(part);
129
+ const lower = name.toLowerCase();
130
+ const args = toolArguments(part);
131
+ const startedAtMs = timestampMs(entry);
132
+ const activity: DelegationActivity = {
133
+ toolCallId: id,
134
+ toolName: name,
135
+ status: "running",
136
+ startedAtMs,
137
+ timeoutMs: timeoutMsFromArgs(args),
138
+ };
139
+
140
+ const path = typeof args.path === "string" ? args.path : typeof args.file === "string" ? args.file : undefined;
141
+ if (lower === "bash") {
142
+ const command = typeof args.command === "string" ? redactDelegationCommand(args.command) : undefined;
143
+ activity.command = command;
144
+ activity.summary = command ? capInline(command, 140) : "shell command";
145
+ } else if (lower === "read") {
146
+ activity.target = path;
147
+ activity.summary = path ? `read ${path}` : "read file";
148
+ } else if (lower === "grep") {
149
+ const pattern = typeof args.pattern === "string" ? args.pattern : typeof args.query === "string" ? args.query : undefined;
150
+ activity.target = path;
151
+ activity.summary = `grep ${pattern ? capInline(pattern, 60) : "pattern"}${path ? ` in ${path}` : ""}`;
152
+ } else if (lower === "find") {
153
+ const pattern = typeof args.pattern === "string" ? args.pattern : "files";
154
+ activity.target = path;
155
+ activity.summary = `find ${capInline(pattern, 60)}${path ? ` in ${path}` : ""}`;
156
+ } else if (lower === "edit" || lower === "write") {
157
+ activity.target = path;
158
+ const edits = Array.isArray(args.edits) ? ` · ${args.edits.length} edit${args.edits.length === 1 ? "" : "s"}` : "";
159
+ activity.summary = `${lower} ${path ?? "file"}${edits}`;
160
+ } else if (lower === "delegate_task" || lower === "delegate_agent") {
161
+ const agent = typeof args.agent === "string" ? args.agent : undefined;
162
+ activity.summary = `${lower}${agent ? ` → ${agent}` : ""}`;
163
+ } else {
164
+ activity.summary = Object.entries(args).length > 0
165
+ ? `${name} ${Object.entries(args).slice(0, 3).map(([key, value]) => `${key}=${typeof value === "string" ? capInline(value, 40) : Array.isArray(value) ? `${value.length} items` : String(value)}`).join(" ")}`
166
+ : name;
167
+ }
168
+
169
+ return activity;
170
+ }
171
+
172
+ function resultSummary(text: string): string | undefined {
173
+ const trimmed = text.trim();
174
+ if (!trimmed) return undefined;
175
+ const compact = compactWhitespace(trimmed);
176
+ const timeout = compact.match(/Command timed out after\s+([0-9.]+)\s+seconds/i);
177
+ if (timeout) return `timed out after ${timeout[1]}s`;
178
+ const lineCount = trimmed.split("\n").length;
179
+ if (lineCount > 1 || trimmed.length > 120) return `${lineCount} line${lineCount === 1 ? "" : "s"} · ${trimmed.length} chars`;
180
+ return capInline(trimmed, 140);
181
+ }
182
+
183
+ function applyToolResult(entry: Record<string, unknown>, message: Record<string, unknown>, byId: Map<string, DelegationActivity>, order: DelegationActivity[]): void {
184
+ const id = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
185
+ const name = typeof message.toolName === "string" ? message.toolName : "tool";
186
+ if (!id) return;
187
+ let activity = byId.get(id);
188
+ if (!activity) {
189
+ activity = { toolCallId: id, toolName: name, status: "running" };
190
+ byId.set(id, activity);
191
+ order.push(activity);
192
+ }
193
+ const endedAtMs = timestampMs(entry);
194
+ activity.endedAtMs = endedAtMs;
195
+ activity.elapsedMs = activity.startedAtMs && endedAtMs ? Math.max(0, endedAtMs - activity.startedAtMs) : undefined;
196
+ activity.status = message.isError === true ? "error" : "success";
197
+ const text = contentText(message.content);
198
+ const summary = resultSummary(text);
199
+ if (message.isError === true) activity.errorSummary = summary ?? "tool error";
200
+ else activity.outputSummary = summary ?? "completed";
201
+ }
202
+
203
+ function parseActivityText(text: string, nowMs: number): { recent: DelegationActivity[]; current?: DelegationActivity; lastUpdatedMs?: number } {
204
+ const byId = new Map<string, DelegationActivity>();
205
+ const order: DelegationActivity[] = [];
206
+ let lastUpdatedMs: number | undefined;
207
+
208
+ for (const rawLine of text.split("\n")) {
209
+ if (!rawLine.trim()) continue;
210
+ let parsed: unknown;
211
+ try {
212
+ parsed = JSON.parse(rawLine);
213
+ } catch {
214
+ continue;
215
+ }
216
+ if (!isRecord(parsed)) continue;
217
+ const at = timestampMs(parsed);
218
+ if (at !== undefined) lastUpdatedMs = lastUpdatedMs === undefined ? at : Math.max(lastUpdatedMs, at);
219
+
220
+ if (parsed.type === "message" && isRecord(parsed.message)) {
221
+ const message = parsed.message;
222
+ if (message.role === "assistant" && Array.isArray(message.content)) {
223
+ for (const part of message.content) {
224
+ if (!isRecord(part) || part.type !== "toolCall") continue;
225
+ const activity = activityFromToolCall(parsed, part);
226
+ if (!activity) continue;
227
+ byId.set(activity.toolCallId, activity);
228
+ order.push(activity);
229
+ }
230
+ } else if (message.role === "toolResult") {
231
+ applyToolResult(parsed, message, byId, order);
232
+ }
233
+ }
234
+ }
235
+
236
+ for (const activity of order) {
237
+ if (activity.status === "running" && activity.startedAtMs !== undefined) activity.elapsedMs = Math.max(0, nowMs - activity.startedAtMs);
238
+ }
239
+ const current = [...order].reverse().find((activity) => activity.status === "running");
240
+ return { recent: order.slice(-DEFAULT_RECENT_LIMIT), current, lastUpdatedMs };
241
+ }
242
+
243
+ export function readDelegationActivitySnapshot(sessionPath: string | undefined, repoRoot: string, nowMs = Date.now()): DelegationActivitySnapshot {
244
+ if (!sessionPath) return { status: "missing", note: "Child session is not captured yet.", fingerprint: "no-session", recent: [] };
245
+ const resolved = resolve(sessionPath);
246
+ if (!isInside(repoRoot, resolved)) {
247
+ return { status: "blocked", note: `Child session path is outside repo root: ${sessionPath}`, fingerprint: `blocked:${sessionPath}`, recent: [] };
248
+ }
249
+ if (!existsSync(resolved)) return { status: "missing", note: `Child session file not found yet: ${sessionPath}`, fingerprint: `missing:${sessionPath}`, recent: [] };
250
+ const stat = statSync(resolved);
251
+ if (stat.size === 0) return { status: "empty", note: "Child session file is empty.", fingerprint: `${resolved}:0:${Math.floor(stat.mtimeMs)}`, recent: [], lastUpdatedMs: Math.floor(stat.mtimeMs), quietMs: Math.max(0, nowMs - stat.mtimeMs) };
252
+ const text = readTail(resolved, stat.size, ACTIVITY_MAX_BYTES);
253
+ const parsed = parseActivityText(text, nowMs);
254
+ const lastUpdatedMs = parsed.lastUpdatedMs ?? Math.floor(stat.mtimeMs);
255
+ const cappedNote = stat.size > ACTIVITY_MAX_BYTES ? `Activity feed read from last ${ACTIVITY_MAX_BYTES} bytes of ${stat.size} bytes.` : undefined;
256
+ return {
257
+ status: "ok",
258
+ note: cappedNote,
259
+ fingerprint: `${resolved}:${stat.size}:${Math.floor(stat.mtimeMs)}`,
260
+ recent: parsed.recent,
261
+ current: parsed.current,
262
+ lastUpdatedMs,
263
+ quietMs: Math.max(0, nowMs - Math.max(lastUpdatedMs, Math.floor(stat.mtimeMs))),
264
+ };
265
+ }
266
+
267
+ export function formatActivityDuration(ms: number | undefined): string | undefined {
268
+ if (typeof ms !== "number" || !Number.isFinite(ms)) return undefined;
269
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
270
+ const seconds = totalSeconds % 60;
271
+ const minutes = Math.floor(totalSeconds / 60) % 60;
272
+ const hours = Math.floor(totalSeconds / 3600);
273
+ const pad2 = (value: number): string => String(value).padStart(2, "0");
274
+ if (hours > 0) return `${hours}:${pad2(minutes)}:${pad2(seconds)}`;
275
+ return `${pad2(minutes)}:${pad2(seconds)}`;
276
+ }
277
+
278
+ export function formatActivitySummary(activity: DelegationActivity, options: { includeCommand?: boolean; nowMs?: number } = {}): string[] {
279
+ const icon = activity.status === "running" ? "◉" : activity.status === "success" ? "✓" : "✗";
280
+ const duration = formatActivityDuration(activity.elapsedMs ?? (activity.status === "running" && activity.startedAtMs ? (options.nowMs ?? Date.now()) - activity.startedAtMs : undefined));
281
+ const timeout = formatActivityDuration(activity.timeoutMs);
282
+ const detail = activity.status === "running"
283
+ ? [duration ? `running ${duration}` : "running", timeout ? `timeout ${timeout}` : undefined].filter(Boolean).join(" / ")
284
+ : activity.errorSummary ?? activity.outputSummary ?? (activity.status === "success" ? "done" : "error");
285
+ const summary = activity.summary ?? activity.command ?? activity.target ?? activity.toolName;
286
+ const lines = [`${icon} ${activity.toolName}${detail ? ` · ${detail}` : ""}${summary && activity.toolName.toLowerCase() !== summary.toLowerCase() ? ` · ${summary}` : ""}`];
287
+ if (options.includeCommand && activity.command) lines.push(`$ ${activity.command}`);
288
+ return lines;
289
+ }
@@ -4,6 +4,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
4
4
  import { Markdown, truncateToWidth, visibleWidth, type MarkdownTheme } from "@earendil-works/pi-tui";
5
5
 
6
6
  import { delegationDurationMs, delegationSignalBadge, delegationSignalColor, formatDelegationContextLabel, formatDelegationCostLabel, formatDelegationModelLabel, formatDelegationSignalBadge, formatDuration, statusIcon, type DelegationRunView } from "./delegation-monitor.js";
7
+ import { formatActivityDuration, formatActivitySummary, readDelegationActivitySnapshot } from "./delegation-activity.js";
7
8
  import { sanitizeDelegationText } from "./delegation-markdown.js";
8
9
  import { isRecord } from "../core/utils/records.js";
9
10
 
@@ -180,6 +181,31 @@ function actionCard(lines: string[], title: string, details: string[], width: nu
180
181
  }
181
182
  }
182
183
 
184
+ function renderLiveActivityCard(run: DelegationRunView, repoRoot: string, lines: string[], width: number, theme: Theme, nowMs = Date.now()): void {
185
+ const snapshot = readDelegationActivitySnapshot(run.sessionPath, repoRoot, nowMs);
186
+ if (snapshot.recent.length === 0) return;
187
+ pushSpacer(lines);
188
+ const bgKey = snapshot.current ? "toolPendingBg" : "toolSuccessBg";
189
+ lines.push(theme.bg(bgKey, padToWidth(theme.fg(snapshot.current ? "toolTitle" : "success", theme.bold(" Live activity")), width)));
190
+ if (snapshot.current) {
191
+ for (const line of formatActivitySummary(snapshot.current, { includeCommand: true, nowMs })) {
192
+ lines.push(theme.bg(bgKey, padToWidth(theme.fg(line.startsWith("$") ? "toolOutput" : "warning", ` ${line}`), width)));
193
+ }
194
+ if (typeof snapshot.quietMs === "number" && snapshot.quietMs >= 120_000) {
195
+ const quiet = `quiet ${formatActivityDuration(snapshot.quietMs)} since last transcript update`;
196
+ lines.push(theme.bg(bgKey, padToWidth(theme.fg(snapshot.quietMs >= 300_000 ? "warning" : "muted", ` ${quiet}`), width)));
197
+ }
198
+ }
199
+ const recent = snapshot.recent.slice(-5).filter((activity) => activity.toolCallId !== snapshot.current?.toolCallId);
200
+ if (recent.length > 0) lines.push(theme.bg(bgKey, padToWidth(theme.fg("muted", " last actions"), width)));
201
+ for (const activity of recent) {
202
+ const colorKey = activity.status === "error" ? "error" : activity.status === "running" ? "warning" : "success";
203
+ const first = formatActivitySummary(activity, { nowMs })[0];
204
+ if (first) lines.push(theme.bg(bgKey, padToWidth(theme.fg(colorKey, ` ${first}`), width)));
205
+ }
206
+ if (snapshot.note) lines.push(theme.bg(bgKey, padToWidth(theme.fg("muted", ` ${snapshot.note}`), width)));
207
+ }
208
+
183
209
  function failureDetails(run: DelegationRunView, fallback: string): string[] {
184
210
  const details: string[] = [];
185
211
  if (run.errorMessage) details.push(`message: ${sanitizeDelegationText(run.errorMessage)}`);
@@ -372,6 +398,7 @@ export function renderDelegationFeedLines(run: DelegationRunView | undefined, re
372
398
  lines.push(theme.fg("dim", `${statusIcon(run.status)} ${run.agent}${signalText ? ` · ${theme.fg(delegationSignalColor(signalBadge), signalText)}` : ""}${modelLabel ? ` · ${theme.fg("muted", `(${modelLabel})`)}` : ""} · ${run.status}${run.failureKind ? ` · ${run.failureKind}` : ""} · ${formatDuration(delegationDurationMs(run))} · ${formatDelegationCostLabel(run)} · ${formatDelegationContextLabel(run)}`));
373
399
  if (run.usage) lines.push(theme.fg("muted", `usage · in ${run.usage.input} · out ${run.usage.output} · cache ${run.usage.cacheRead}/${run.usage.cacheWrite} · context ${run.usage.contextTokens}`));
374
400
  if (run.taskPreview) lines.push(theme.fg("muted", `task · ${sanitizeDelegationText(run.taskPreview)}`));
401
+ renderLiveActivityCard(run, repoRoot, lines, safeWidth, theme);
375
402
  let renderedFailureSummary = false;
376
403
  if (run.failureKind === "preflight" || run.failureKind === "config") {
377
404
  renderedFailureSummary = true;
@@ -40,6 +40,7 @@ export interface DelegationRunView {
40
40
  childChangedPaths?: ChildResult["childChangedPaths"];
41
41
  usage?: ChildResult["usage"];
42
42
  model?: string;
43
+ background?: boolean;
43
44
  }
44
45
 
45
46
  export interface DelegationMonitorState {
@@ -73,6 +74,8 @@ const TRANSCRIPT_MAX_BYTES = 240_000;
73
74
  const TRANSCRIPT_MAX_LINES = 1_200;
74
75
  const WIDGET_COMPLETE_TTL_MS = 30_000;
75
76
  const WIDGET_FAILURE_TTL_MS = 120_000;
77
+ const WIDGET_BACKGROUND_COMPLETE_TTL_MS = 10 * 60_000;
78
+ const WIDGET_BACKGROUND_FAILURE_TTL_MS = 30 * 60_000;
76
79
 
77
80
  function capPreview(text: string | undefined, limit = PREVIEW_LIMIT): string {
78
81
  const value = text ?? "";
@@ -362,6 +365,10 @@ export function shouldShowRunInWidget(run: DelegationRunView, nowMs = Date.now()
362
365
  if (run.status === "queued" || run.status === "running") return true;
363
366
  if (!run.endedAtMs) return false;
364
367
  const ageMs = Math.max(0, nowMs - run.endedAtMs);
368
+ if (run.background) {
369
+ if (run.status === "complete") return ageMs <= WIDGET_BACKGROUND_COMPLETE_TTL_MS;
370
+ return ageMs <= WIDGET_BACKGROUND_FAILURE_TTL_MS;
371
+ }
365
372
  if (run.status === "complete") return ageMs <= WIDGET_COMPLETE_TTL_MS;
366
373
  return ageMs <= WIDGET_FAILURE_TTL_MS;
367
374
  }
@@ -466,6 +473,7 @@ export function updateDelegationRun(state: DelegationMonitorState, id: string, p
466
473
  if (patch.errorMessage !== undefined) run.errorMessage = patch.errorMessage;
467
474
  if (patch.usage !== undefined) run.usage = patch.usage;
468
475
  if (patch.model !== undefined) run.model = patch.model;
476
+ if (patch.background !== undefined) run.background = patch.background;
469
477
  return run;
470
478
  }
471
479
 
@@ -414,6 +414,8 @@ function restoreDelegationRunsFromEntries(state: HarnessRuntimeState, entries: u
414
414
  errorMessage: Array.isArray(data.errors) ? data.errors.map(String).join("; ") : existing?.errorMessage,
415
415
  childChangedPaths: normalizeZcommitChildChangedPathRefs(data.childChangedPathRefs) ?? existing?.childChangedPaths,
416
416
  usage: usageField(data) ?? existing?.usage,
417
+ model: stringField(data, "model") ?? existing?.model,
418
+ background: (typeof data.background === "boolean" ? data.background : undefined) ?? existing?.background,
417
419
  };
418
420
  if (event === "start") restoredRun.endedAtMs = existing?.endedAtMs;
419
421
  restored.set(runId, restoredRun);
@@ -40,7 +40,9 @@ import {
40
40
  statusIcon,
41
41
  updateDelegationRun,
42
42
  type DelegationRunMode,
43
+ type DelegationRunView,
43
44
  } from "../delegation-monitor.js";
45
+ import { formatActivityDuration, formatActivitySummary, readDelegationActivitySnapshot } from "../delegation-activity.js";
44
46
  import { delegateViewLink } from "../delegation-mouse.js";
45
47
  import type { BackgroundDelegationRuntimeRun, HarnessRuntimeState } from "../state.js";
46
48
  import { strictGoalErrors, strictGoalSpecErrors } from "../state.js";
@@ -728,6 +730,32 @@ export function formatDelegationCatalogSummary(catalog: Record<string, unknown>)
728
730
  return lines.join("\n");
729
731
  }
730
732
 
733
+ function renderMiniActivityLines(run: DelegationRunView, nowMs: number, expanded: boolean, theme: { fg: (color: any, text: string) => string }): string[] {
734
+ const lines: string[] = [];
735
+ const snapshot = readDelegationActivitySnapshot(run.sessionPath, process.cwd(), nowMs);
736
+ const recent = snapshot.recent.slice(-5);
737
+ if (recent.length === 0) {
738
+ if ((run.status === "running" || run.status === "queued") && snapshot.note && snapshot.status !== "missing") {
739
+ lines.push(` ${theme.fg("muted", snapshot.note)}`);
740
+ }
741
+ return lines;
742
+ }
743
+ const label = expanded ? "last actions" : "last";
744
+ lines.push(` ${theme.fg("dim", `${label} ${recent.length}`)}`);
745
+ for (const activity of recent) {
746
+ const color = activity.status === "running" ? "warning" : activity.status === "error" ? "error" : "success";
747
+ const includeCommand = activity.status === "running" && activity.toolName.toLowerCase() === "bash";
748
+ const rendered = formatActivitySummary(activity, { includeCommand, nowMs });
749
+ const [first, ...rest] = rendered;
750
+ if (first) lines.push(` ${theme.fg(color, first)}`);
751
+ for (const extra of rest.slice(0, expanded ? 2 : 1)) lines.push(` ${theme.fg("muted", extra)}`);
752
+ }
753
+ if (snapshot.current && typeof snapshot.quietMs === "number" && snapshot.quietMs >= 120_000) {
754
+ lines.push(` ${theme.fg(snapshot.quietMs >= 300_000 ? "warning" : "muted", `quiet ${formatActivityDuration(snapshot.quietMs)} since last transcript update`)}`);
755
+ }
756
+ return lines;
757
+ }
758
+
731
759
  export function renderDelegationToolResultText(source: "delegate_agent" | "delegate_task", details: DelegationDetails | undefined, state: HarnessRuntimeState, toolCallId: string | undefined, isPartial: boolean, expanded: boolean, theme: { fg: (color: any, text: string) => string; bold: (text: string) => string }): string {
732
760
  hydrateDelegationRunsFromDetails(source, details, state, toolCallId);
733
761
  const nowMs = Date.now();
@@ -754,11 +782,13 @@ export function renderDelegationToolResultText(source: "delegate_agent" | "deleg
754
782
  const prefix = index === visibleRuns.length - 1 && !hasMore ? "└─" : "├─";
755
783
  const viewHint = delegateViewLink(run.id);
756
784
  const kind = run.failureKind ? ` ${run.failureKind}` : "";
785
+ const background = run.background ? " bg" : "";
757
786
  const badge = delegationSignalBadge(run);
758
787
  const badgeText = formatDelegationSignalBadge(badge);
759
788
  const modelLabel = formatDelegationModelLabel(run);
760
789
  const modelSuffix = modelLabel ? ` ${theme.fg("muted", `(${modelLabel})`)}` : "";
761
- lines.push(`${theme.fg("dim", prefix)} ${theme.fg(color, `${statusIcon(run.status)} ${run.agent}${kind}`)}${badgeText ? ` ${theme.fg(delegationSignalColor(badge), badgeText)}` : ""}${modelSuffix} ${theme.fg("dim", formatDuration(delegationDurationMs(run, nowMs)))} ${theme.fg("muted", viewHint)}`);
790
+ lines.push(`${theme.fg("dim", prefix)} ${theme.fg(color, `${statusIcon(run.status)} ${run.agent}${kind}${background}`)}${badgeText ? ` ${theme.fg(delegationSignalColor(badge), badgeText)}` : ""}${modelSuffix} ${theme.fg("dim", formatDuration(delegationDurationMs(run, nowMs)))} ${theme.fg("muted", viewHint)}`);
791
+ lines.push(...renderMiniActivityLines(run, nowMs, expanded, theme));
762
792
  if (expanded && (run.errorMessage || run.stopReason || run.gateErrors?.length)) lines.push(` ${theme.fg("dim", run.errorMessage ?? run.gateErrors?.join("; ") ?? run.stopReason ?? "")}`);
763
793
  }
764
794
  if (hasMore) lines.push(theme.fg("dim", `└─ … ${monitoredRuns.length - maxRows} more child run(s)`));
@@ -827,6 +827,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
827
827
  taskHash: sha256(structuredTask),
828
828
  originalUserAskHash: sha256(params.original_user_ask ?? state.activeGoal?.originalUserAsk ?? ""),
829
829
  outputContract,
830
+ background: params.run_in_background === true,
830
831
  startedAt,
831
832
  });
832
833
 
@@ -975,6 +976,8 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
975
976
  };
976
977
 
977
978
  if (params.run_in_background) {
979
+ updateDelegationRun(state.delegations, runId, { background: true });
980
+ renderDelegationMonitor();
978
981
  const backgroundController = new AbortController();
979
982
  const backgroundPromise = runDelegateTaskChild(backgroundController.signal, false);
980
983
  const backgroundRun: BackgroundDelegationRuntimeRun = { runId, startedAtMs, promise: backgroundPromise, abortController: backgroundController };
@@ -8,7 +8,8 @@ import { isRecord } from "../core/utils/records.js";
8
8
  import type { AssistantLikeMessage, ModeName } from "../types.js";
9
9
  import { readHarnessReadinessWidgetData } from "../domains/orchestration/widget-readers.js";
10
10
  import { buildZpeerPeerRoomSummaries, type ZpeerPeerRoomSummary } from "../domains/coms/coms-v2/zpeer.js";
11
- import { delegationCost, delegationDurationMs, formatDelegationCost, formatDuration, summarizeDelegations } from "./delegation-monitor.js";
11
+ import { delegationCost, delegationDurationMs, formatDelegationCost, formatDuration, listWidgetDelegationRuns, summarizeDelegations } from "./delegation-monitor.js";
12
+ import { formatActivitySummary, readDelegationActivitySnapshot } from "./delegation-activity.js";
12
13
  import { disposeDelegationMouseSupport } from "./delegation-mouse.js";
13
14
  import { formatZcompactHudLine } from "./auto-compaction.js";
14
15
  import type { HarnessRuntimeState } from "./state.js";
@@ -339,9 +340,18 @@ export function renderHarnessWidget(pi: ExtensionAPI, state: HarnessRuntimeState
339
340
  ? "working"
340
341
  : "idle";
341
342
  const assistantsCount = delegationSummary.running + delegationSummary.queued + (runtimeTodoSummary?.delegated ?? 0);
342
- const assistantsState = assistantsCount > 0
343
- ? `${assistantsCount} active`
344
- : "none";
343
+ const widgetDelegationRuns = listWidgetDelegationRuns(state.delegations, renderNowMs);
344
+ const activeDelegationRun = widgetDelegationRuns.find((run) => run.status === "running" || run.status === "queued") ?? widgetDelegationRuns[0];
345
+ const activeDelegationSnapshot = activeDelegationRun ? readDelegationActivitySnapshot(activeDelegationRun.sessionPath, ctx.cwd, renderNowMs) : undefined;
346
+ const activeDelegationActivity = activeDelegationSnapshot?.current ?? activeDelegationSnapshot?.recent.at(-1);
347
+ const activeDelegationLine = activeDelegationRun && activeDelegationActivity
348
+ ? `${activeDelegationRun.agent}: ${formatActivitySummary(activeDelegationActivity, { nowMs: renderNowMs })[0] ?? activeDelegationActivity.toolName}`
349
+ : undefined;
350
+ const assistantsState = activeDelegationLine
351
+ ? truncateToWidth(`${assistantsCount > 0 ? `${assistantsCount} active` : "recent"} · ${activeDelegationLine}`, 96, "…")
352
+ : assistantsCount > 0
353
+ ? `${assistantsCount} active`
354
+ : "none";
345
355
  const zpeerRoomSummaries = cachedZpeerRoomSummaries(state, ctx, renderNowMs);
346
356
  const zpeerLast = state.zobLive.lastEvent
347
357
  ? `${state.zobLive.lastEvent.kind} ${state.zobLive.lastEvent.fromAlias ? `@${state.zobLive.lastEvent.fromAlias}` : "?"}→${state.zobLive.lastEvent.toAlias ? `@${state.zobLive.lastEvent.toAlias}` : "?"} ${state.zobLive.lastEvent.status}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zob-harness",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "A governed Agent Factory for Pi: launch communicating agent teams, run tmux-backed factories, validate artifacts, and package repeatable workflows.",
6
6
  "license": "MIT",