pi-better-background-tasks 0.2.20 → 0.3.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.
package/src/tools.ts CHANGED
@@ -1,17 +1,24 @@
1
1
  import { Type } from "typebox";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { failurePath, failureSummary } from "./failures.js";
4
- import { readFailureState } from "./shared-failure-observations.js";
5
- import { readLog } from "./logs.js";
6
3
  import { refreshBackgroundTasksNavigator } from "./navigator-provider.js";
4
+ import {
5
+ formatLaunch,
6
+ formatList,
7
+ formatLog,
8
+ formatStatus,
9
+ formatStopResult,
10
+ type OutputOptions,
11
+ } from "./output.js";
7
12
  import { cancelCallbackBatch } from "./shared-callback-batcher.js";
8
- import { listMetas, listMetasForOrigin, readMeta, writeMeta } from "./registry.js";
13
+ import { inspectMeta, listMetasForOrigin, writeMeta } from "./registry.js";
9
14
  import { resumeRunningTask, spawnTask, startWatchTask, stopTask } from "./runtime.js";
10
15
  import { runTaskMaintenance } from "./maintenance.js";
11
16
  import { ForegroundSandboxBlockedError } from "./sandbox.js";
12
17
  import type { BackgroundTaskCallbackOrigin, BackgroundTaskMeta } from "./types.js";
13
18
  import { isTerminalStatus } from "./types.js";
14
19
 
20
+ export { formatLaunch } from "./output.js";
21
+
15
22
  const JsonPathSchema = Type.String({
16
23
  pattern: "^\\$",
17
24
  description: "Root-prefixed JSON path, e.g. $.status, $.terminalFailure, or $.steps[0].status. Bare keys and wildcards are unsupported.",
@@ -67,17 +74,27 @@ const WatchParams = Type.Object({
67
74
  failure_when: Type.Optional(ConditionSchema),
68
75
  });
69
76
 
77
+ const CursorFields = {
78
+ cursor: Type.Optional(Type.String({ description: "Caller-owned continuation cursor from a previous page. Replay returns the same page or a no-change/failure-only notice; nextCursor continues. Independent callers do not consume each other. Cursors are bound to the selected session scope." })),
79
+ max_bytes: Type.Optional(Type.Number({ description: "Optional UTF-8 byte budget. Defaults: status/log/list 1 KiB, raw evidence 16 KiB. Larger explicit pages are allowed up to the shared hard cap (status 2 KiB, log/list 4 KiB, raw 64 KiB) Hard caps are OUTPUT_BUDGET_MAX_BYTES." })),
80
+ all: Type.Optional(Type.Boolean({ description: "Inspect or list tasks across every session. Default false is current-session only. Unknown ownership is reported as a gap, never as missing or healthy." })),
81
+ };
82
+
70
83
  const IdParams = Type.Object({
71
84
  id: Type.String({ description: "Background task id." }),
72
- verbose: Type.Optional(Type.Boolean({ description: "Return full raw metadata JSON. Default false returns the compact model-facing summary. Use true only for debugging or explicit recovery." })),
85
+ verbose: Type.Optional(Type.Boolean({ description: "Return raw metadata JSON with environment values omitted, bounded like raw evidence (16 KiB default, max_bytes up to 64 KiB, paged with cursor when larger). Default false returns the compact model-facing summary. Use true only for debugging or explicit recovery." })),
86
+ ...CursorFields,
73
87
  });
74
88
  const ListParams = Type.Object({
75
89
  status: Type.Optional(Type.Array(Type.String({ description: "Statuses to include." }))),
76
- limit: Type.Optional(Type.Number({ description: "Maximum tasks to show. Default 20." })),
90
+ limit: Type.Optional(Type.Number({ description: "Maximum tasks to show. Default 10, max 100." })),
91
+ ...CursorFields,
77
92
  });
78
93
  const LogParams = Type.Object({
79
94
  id: Type.String({ description: "Background task id." }),
80
- tail_lines: Type.Optional(Type.Number({ description: "Number of trailing lines. Default 5 for compact model ingestion. Set <=0 only when the full log is explicitly required." })),
95
+ tail_lines: Type.Optional(Type.Number({ description: "Number of trailing display rows. Default 10 for compact model ingestion. Set 0 to page retained raw bytes from the oldest retained offset." })),
96
+ raw: Type.Optional(Type.Boolean({ description: "Page retained raw log bytes (16 KiB default, 64 KiB hard cap) instead of the compact excerpt. Capture and retention loss are disclosed; this is not a full-history archive." })),
97
+ ...CursorFields,
81
98
  });
82
99
 
83
100
  const ActionParams = Type.Object({
@@ -99,6 +116,8 @@ const ActionParams = Type.Object({
99
116
  interval_seconds: Type.Optional(Type.Number()),
100
117
  success_when: Type.Optional(ConditionSchema),
101
118
  failure_when: Type.Optional(ConditionSchema),
119
+ raw: Type.Optional(Type.Boolean()),
120
+ ...CursorFields,
102
121
  });
103
122
 
104
123
  const StatusActionParams = Type.Object({
@@ -108,6 +127,8 @@ const StatusActionParams = Type.Object({
108
127
  limit: Type.Optional(Type.Number()),
109
128
  tail_lines: Type.Optional(Type.Number()),
110
129
  verbose: Type.Optional(Type.Boolean()),
130
+ raw: Type.Optional(Type.Boolean()),
131
+ ...CursorFields,
111
132
  });
112
133
 
113
134
  const BACKGROUND_ORCHESTRATION_GUIDELINES = [
@@ -169,34 +190,36 @@ export function registerTools(pi: ExtensionAPI): void {
169
190
  pi.registerTool({
170
191
  name: "bg_task_list",
171
192
  label: "BG List",
172
- description: "List durable background tasks. Nonblocking.",
193
+ description: "List durable background tasks as compact rows under a 1 KiB UTF-8 budget (10 entries default), newest first. Current-session only unless all:true. Nonblocking. Pass the returned nextCursor as cursor to page further (every task is reachable), or statusCursor to get a small no-change response or failure-only updates; a higher limit/max_bytes gives a larger explicit page.",
173
194
  parameters: ListParams,
174
- async execute(_toolCallId, params) {
175
- return text(formatList(resolveList(params.status, params.limit)));
195
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
196
+ activeSession = getCallbackOrigin(ctx);
197
+ return text(formatList(listOptions(params, activeSession)));
176
198
  },
177
199
  });
178
200
 
179
201
  pi.registerTool({
180
202
  name: "bg_task_status",
181
203
  label: "BG Status",
182
- description: "Inspect one background task. Default output is a compact model-facing summary; pass verbose:true only when full raw metadata is explicitly needed. After a terminal callback, call this first and call bg_task_log only if the summary is insufficient.",
204
+ description: "Inspect one background task. Default output is a compact model-facing summary (1 KiB UTF-8) with matched condition, exit/signal, stop error, and failure counts before progress. Current-session only unless all:true; unknown ownership is a gap, not missing or healthy. Pass verbose:true only when full raw metadata is explicitly needed. Environment values are omitted. After a terminal callback, call this first and call bg_task_log only if the summary is insufficient.",
183
205
  parameters: IdParams,
184
- async execute(_toolCallId, params) {
185
- const meta = readMeta(params.id);
186
- return text(formatStatus(meta, params.id, { verbose: params.verbose === true }));
206
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
207
+ activeSession = getCallbackOrigin(ctx);
208
+ return text(formatStatus(inspectMeta(params.id), statusOptions(params, activeSession)));
187
209
  },
188
210
  });
189
211
 
190
212
  pi.registerTool({
191
213
  name: "bg_task_log",
192
214
  label: "BG Log",
193
- description: "Read a background task log. Default output is a compact 5-line terminal-aware tail for model ingestion. Pass tail_lines for a bounded tail; tail_lines:0 returns the retained raw log, capped at 512 KiB for safe recovery. Nonblocking.",
215
+ description: "Read a background task log. Default output is a compact 10-line terminal-aware tail for model ingestion (1 KiB UTF-8). Current-session only unless all:true. Pass tail_lines for a bounded tail; tail_lines:0 pages the retained raw log from the oldest retained offset (16 KiB pages, 64 KiB hard cap) with a caller-owned cursor. Capture and retention loss are disclosed; this is not a full-history archive. Nonblocking.",
194
216
  parameters: LogParams,
195
217
  renderResult(result: unknown, options: unknown, theme: unknown) {
196
218
  return renderBackgroundTaskLogDisplay(result, options, theme);
197
219
  },
198
- async execute(_toolCallId, params) {
199
- return logText(params.id, params.tail_lines);
220
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
221
+ activeSession = ctx ? getCallbackOrigin(ctx) : activeSession;
222
+ return logText(params.id, logOptions(params, activeSession));
200
223
  },
201
224
  });
202
225
 
@@ -207,7 +230,7 @@ export function registerTools(pi: ExtensionAPI): void {
207
230
  parameters: IdParams,
208
231
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
209
232
  activeSession = getCallbackOrigin(ctx);
210
- const result = await formatStop(pi, params.id, ctx, getActiveSession);
233
+ const result = await formatStop(pi, params.id, ctx, getActiveSession, statusOptions(params, activeSession));
211
234
  refreshBackgroundTasksNavigator(ctx);
212
235
  return text(result);
213
236
  },
@@ -216,7 +239,7 @@ export function registerTools(pi: ExtensionAPI): void {
216
239
  pi.registerTool({
217
240
  name: "bg_task",
218
241
  label: "BG Task",
219
- description: "Action wrapper for background tasks: spawn, watch, list, status, log, stop, or clear. For remote work, prefer structured ssh: pass ssh:{host,user} and provide the remote command in command. For short synchronous remote commands that should return output now, use remote_bash from pi-better-ssh. SSH spawn defaults to durable tmux; SSH watches use direct one-shot polls without tmux installation; remote.session=direct is a weaker-stop spawn escape hatch. Spawn/watch return immediately; do not poll in foreground. For action:status, default compact output and use verbose:true only for full metadata. For action:log, default compact tail and use tail_lines:0 only for explicit full logs.",
242
+ description: "Action wrapper for background tasks: spawn, watch, list, status, log, stop, or clear. For remote work, prefer structured ssh: pass ssh:{host,user} and provide the remote command in command. For short synchronous remote commands that should return output now, use remote_bash from pi-better-ssh. SSH spawn defaults to durable tmux; SSH watches use direct one-shot polls without tmux installation; remote.session=direct is a weaker-stop spawn escape hatch. Spawn/watch return immediately; do not poll in foreground. For action:status, default compact output and use verbose:true only for full metadata. For action:log, default compact tail and use tail_lines:0 to page retained raw bytes. Standalone tools and these wrappers share the same output assembler. List/status/log default to the current session; pass all:true to override.",
220
243
  promptGuidelines: BACKGROUND_ORCHESTRATION_GUIDELINES,
221
244
  parameters: ActionParams,
222
245
  renderResult(result: unknown, options: unknown, theme: unknown) {
@@ -231,7 +254,7 @@ export function registerTools(pi: ExtensionAPI): void {
231
254
  pi.registerTool({
232
255
  name: "bg_status",
233
256
  label: "BG Status",
234
- description: "Action wrapper for inspecting background tasks: list, status, log, stop, or clear. Nonblocking. Status is compact by default; log returns a compact tail by default. Use verbose:true or tail_lines:0 only for explicit full-data recovery.",
257
+ description: "Action wrapper for inspecting background tasks: list, status, log, stop, or clear. Nonblocking. Status is compact by default; log returns a compact tail by default. Use verbose:true or tail_lines:0 only for explicit full-data recovery. Shares the same output assembler as the standalone tools. Current-session default; pass all:true to override.",
235
258
  parameters: StatusActionParams,
236
259
  renderResult(result: unknown, options: unknown, theme: unknown) {
237
260
  return renderBackgroundTaskLogDisplay(result, options, theme);
@@ -247,6 +270,38 @@ export function text(textValue: string, details?: unknown) {
247
270
  return { content: [{ type: "text" as const, text: textValue }], details };
248
271
  }
249
272
 
273
+ function statusOptions(params: Record<string, unknown>, origin?: BackgroundTaskCallbackOrigin): OutputOptions {
274
+ return {
275
+ verbose: params.verbose === true,
276
+ cursor: typeof params.cursor === "string" ? params.cursor : undefined,
277
+ maxBytes: typeof params.max_bytes === "number" ? params.max_bytes : undefined,
278
+ all: params.all === true,
279
+ ...scopeOptions(origin),
280
+ };
281
+ }
282
+
283
+ function logOptions(params: Record<string, unknown>, origin?: BackgroundTaskCallbackOrigin): OutputOptions {
284
+ return {
285
+ tailLines: typeof params.tail_lines === "number" ? params.tail_lines : undefined,
286
+ raw: params.raw === true,
287
+ cursor: typeof params.cursor === "string" ? params.cursor : undefined,
288
+ maxBytes: typeof params.max_bytes === "number" ? params.max_bytes : undefined,
289
+ all: params.all === true,
290
+ ...scopeOptions(origin),
291
+ };
292
+ }
293
+
294
+ function listOptions(params: Record<string, unknown>, origin?: BackgroundTaskCallbackOrigin): OutputOptions {
295
+ return {
296
+ statuses: params.status as string[] | undefined,
297
+ limit: typeof params.limit === "number" ? params.limit : undefined,
298
+ cursor: typeof params.cursor === "string" ? params.cursor : undefined,
299
+ maxBytes: typeof params.max_bytes === "number" ? params.max_bytes : undefined,
300
+ all: params.all === true,
301
+ ...scopeOptions(origin),
302
+ };
303
+ }
304
+
250
305
  async function actionText(
251
306
  pi: ExtensionAPI,
252
307
  params: Record<string, unknown>,
@@ -255,7 +310,7 @@ async function actionText(
255
310
  getActiveSession: () => BackgroundTaskCallbackOrigin | undefined,
256
311
  ) {
257
312
  if (params.action === "log" && params.id) {
258
- return logText(String(params.id), params.tail_lines as number | undefined);
313
+ return logText(String(params.id), logOptions(params, callbackOrigin));
259
314
  }
260
315
  return text(await runAction(pi, params, ctx, callbackOrigin, getActiveSession));
261
316
  }
@@ -274,16 +329,16 @@ async function runAction(
274
329
  if (!params.success_when) return "Invalid parameters: watch requires success_when.";
275
330
  return withNavigatorRefresh(ctx, reportLaunch(() => startWatchTask(pi, params as never, ctx.cwd, callbackOrigin, getActiveSession)));
276
331
  case "list":
277
- return formatList(resolveList(params.status as string[] | undefined, params.limit as number | undefined));
332
+ return formatList(listOptions(params, callbackOrigin));
278
333
  case "status":
279
334
  if (!params.id) return "Invalid parameters: status requires id.";
280
- return formatStatus(readMeta(String(params.id)) ?? undefined, String(params.id), { verbose: params.verbose === true });
335
+ return formatStatus(inspectMeta(String(params.id)), statusOptions(params, callbackOrigin));
281
336
  case "log":
282
337
  if (!params.id) return "Invalid parameters: log requires id.";
283
- return formatLog(String(params.id), params.tail_lines as number | undefined);
338
+ return formatLog(String(params.id), logOptions(params, callbackOrigin));
284
339
  case "stop":
285
340
  if (!params.id) return "Invalid parameters: stop requires id.";
286
- return withNavigatorRefresh(ctx, await formatStop(pi, String(params.id), ctx, getActiveSession));
341
+ return withNavigatorRefresh(ctx, await formatStop(pi, String(params.id), ctx, getActiveSession, statusOptions(params, callbackOrigin)));
287
342
  case "clear":
288
343
  return withNavigatorRefresh(ctx, formatClear(params.status as string[] | undefined, callbackOrigin));
289
344
  default:
@@ -311,111 +366,33 @@ function withNavigatorRefresh(ctx: ExtensionContext, result: string): string {
311
366
  return result;
312
367
  }
313
368
 
314
- function getCallbackOrigin(ctx: ExtensionContext): BackgroundTaskCallbackOrigin {
369
+ /** Origins whose session identity could not be read (the host threw). */
370
+ const unavailableSessionOrigins = new WeakSet<BackgroundTaskCallbackOrigin>();
371
+
372
+ function getCallbackOrigin(ctx: ExtensionContext | undefined): BackgroundTaskCallbackOrigin {
315
373
  let sessionId: string | undefined;
374
+ let unavailable = false;
316
375
  try {
317
- sessionId = ctx.sessionManager?.getSessionId();
376
+ sessionId = ctx?.sessionManager?.getSessionId();
318
377
  } catch {
319
378
  sessionId = undefined;
379
+ unavailable = true;
320
380
  }
321
- return { cwd: ctx.cwd, sessionId };
322
- }
323
-
324
- function resolveList(statuses?: string[], limit?: number): BackgroundTaskMeta[] {
325
- let metas = listMetas();
326
- if (statuses && statuses.length > 0) {
327
- const wanted = new Set(statuses);
328
- metas = metas.filter((meta) => wanted.has(meta.status));
329
- }
330
- return metas.slice(0, Math.max(1, Math.min(limit ?? 20, 100)));
331
- }
332
-
333
- export function formatLaunch(meta: BackgroundTaskMeta): string {
334
- const label = meta.name ? `${meta.name} (${meta.id})` : meta.id;
335
- const remote = meta.ssh
336
- ? ` Remote: ${meta.ssh.target}${meta.remote?.session ? ` mode=${meta.remote.session}` : ""}${meta.remote?.sessionName ? ` session=${meta.remote.sessionName}` : ""}.`
337
- : "";
338
- const setup = meta.remote?.bootstrapMessage ? ` Remote setup: ${meta.remote.bootstrapMessage}` : "";
339
- const warning = meta.remote?.warning ? ` Warning: ${meta.remote.warning}` : "";
340
- const failure = failureSummary(meta.id);
341
- return `${failure ? `${failure}\n` : ""}Started background ${meta.kind} ${label}. Status: ${meta.status}.${remote}${setup}${warning} Log: ${meta.logPath}`;
342
- }
343
-
344
- function formatList(metas: BackgroundTaskMeta[]): string {
345
- if (metas.length === 0) return "No background tasks found.";
346
- return metas.map((meta) => {
347
- const failure = failureSummary(meta.id);
348
- const age = formatDuration((meta.endedAt ?? Date.now()) - meta.startedAt);
349
- const label = meta.name ? `${meta.name} ` : "";
350
- const remote = meta.ssh ? ` ${meta.ssh.target}${meta.remote?.session ? ` ${meta.remote.session}` : ""}` : "";
351
- return `${failure ? `${failure}\n` : ""}${meta.id} ${label}${meta.kind} ${meta.status} ${age}${remote}`;
352
- }).join("\n");
353
- }
354
-
355
- function formatStatus(meta: BackgroundTaskMeta | undefined, id?: string, options: { verbose?: boolean } = {}): string {
356
- if (!meta) return `No background task found${id ? ` for id ${id}` : ""}.`;
357
- const failure = failureSummary(meta.id);
358
- if (!options.verbose) return [failure, formatCompactStatus(meta)].filter(Boolean).join("\n");
359
- const state = readFailureState(failurePath(meta.id));
360
- const observations = Object.values(state.observations);
361
- return JSON.stringify(observations.length ? {
362
- failureSummary: failure, failureJournal: failurePath(meta.id),
363
- failureObservations: observations.map((observation) => ({ ...observation, attentionDeliveredAt: state.delivered[observation.id] })),
364
- ...meta,
365
- } : meta, null, 2);
366
- }
367
-
368
- function formatCompactStatus(meta: BackgroundTaskMeta): string {
369
- const lines = [
370
- `Background task ${meta.id}${meta.name ? ` (${meta.name})` : ""} is ${meta.status}.`,
371
- `kind: ${meta.kind}`,
372
- `elapsed: ${formatDuration((meta.endedAt ?? Date.now()) - meta.startedAt)}`,
373
- ];
374
- if (meta.ssh) lines.push(`remote: ${meta.ssh.target}`);
375
- if (meta.remote?.session) lines.push(`remote mode: ${meta.remote.session}`);
376
- if (meta.remote?.sessionName) lines.push(`remote session: ${meta.remote.sessionName}`);
377
- if (meta.remote?.bootstrapMessage) lines.push(`remote setup: ${oneLine(meta.remote.bootstrapMessage, 500)}`);
378
- if (meta.remote?.warning) lines.push(`warning: ${oneLine(meta.remote.warning, 500)}`);
379
- if (meta.remote?.stopMessage) lines.push(`remote stop: ${oneLine(meta.remote.stopMessage, 500)}`);
380
- if (meta.deadlineAt && meta.status === "running") lines.push(`deadline: ${formatDuration(meta.deadlineAt - Date.now())} left`);
381
- if (meta.lastExitCode !== undefined || meta.lastSignal !== undefined) lines.push(`last exit: ${meta.lastExitCode ?? "null"}${meta.lastSignal ? ` signal=${meta.lastSignal}` : ""}`);
382
- const reason = resultReason(meta.result);
383
- if (reason) lines.push(`result: ${reason}`);
384
- if (meta.error) lines.push(`error: ${oneLine(meta.error, 500)}`);
385
- if (meta.lastState !== undefined) lines.push(`last state: ${oneLine(meta.lastState, 800)}`);
386
- if (meta.logDiscardedBytes) lines.push(`log retention: ${meta.logDiscardedBytes} bytes discarded in ${meta.logRetentionEvents ?? 1} compaction(s).`);
387
- lines.push(`log: ${meta.logPath}`);
388
- lines.push(`For full metadata use bg_task_status id=${meta.id} verbose=true. For logs use bg_task_log id=${meta.id} tail_lines=5, or tail_lines=0 for the retained raw log.`);
389
- return lines.join("\n");
390
- }
391
-
392
- function resultReason(result: unknown): string | undefined {
393
- if (!result) return undefined;
394
- if (typeof result === "object" && result !== null && "reason" in result) {
395
- const reason = (result as { reason?: unknown }).reason;
396
- return reason === undefined ? undefined : oneLine(reason, 500);
397
- }
398
- return oneLine(result, 500);
381
+ const origin = { cwd: ctx?.cwd ?? "", sessionId };
382
+ if (unavailable) unavailableSessionOrigins.add(origin);
383
+ return origin;
399
384
  }
400
385
 
401
- function oneLine(value: unknown, maxLength: number): string {
402
- const raw = typeof value === "string" ? value : JSON.stringify(value);
403
- const single = String(raw ?? "").replace(/\s+/g, " ").trim();
404
- return single.length <= maxLength ? single : `${single.slice(0, Math.max(0, maxLength - 1))}…`;
405
- }
406
-
407
- function formatLog(id: string, tailLines?: number): string {
408
- const meta = readMeta(id);
409
- if (!meta) return `No background task found for id ${id}.`;
410
- const log = readLog(meta.logPath, tailLines ?? 5);
411
- const prefix = log.truncated ? `[showing tail of ${meta.logPath}]\n` : `[${meta.logPath}]\n`;
412
- const failure = failureSummary(id);
413
- return `${failure ? `${failure}\n` : ""}${prefix}${log.text || "(log is empty)"}`;
386
+ function scopeOptions(origin?: BackgroundTaskCallbackOrigin): Pick<OutputOptions, "origin" | "sessionUnavailable"> {
387
+ return {
388
+ origin,
389
+ ...(origin && unavailableSessionOrigins.has(origin) ? { sessionUnavailable: true } : {}),
390
+ };
414
391
  }
415
392
 
416
- function logText(id: string, tailLines?: number) {
417
- const body = formatLog(id, tailLines);
418
- if (!readMeta(id)) return text(body);
393
+ function logText(id: string, options: OutputOptions) {
394
+ const body = formatLog(id, options);
395
+ if (!inspectMeta(id).meta) return text(body);
419
396
  return text(body, buildBackgroundTaskLogDisplayDetails(body));
420
397
  }
421
398
 
@@ -464,8 +441,8 @@ export function renderBackgroundTaskLogDisplay(result: unknown, options: unknown
464
441
 
465
442
  function resultTextContent(result: unknown): string {
466
443
  const content = (result as { content?: Array<{ text?: string }> })?.content;
467
- if (!Array.isArray(content)) return String(result ?? "");
468
- return content.map((part) => part.text ?? "").join("\n");
444
+ if (Array.isArray(content)) return content.map((part) => part.text ?? "").join("\n");
445
+ return String(result ?? "");
469
446
  }
470
447
 
471
448
  function nonEmptyPreviewLines(lines: string[]): string[] {
@@ -519,12 +496,13 @@ async function formatStop(
519
496
  id: string,
520
497
  _ctx: ExtensionContext,
521
498
  getActiveSession?: () => BackgroundTaskCallbackOrigin | undefined,
499
+ options: OutputOptions = {},
522
500
  ): Promise<string> {
501
+ const existing = inspectMeta(id);
502
+ if (!existing.meta && !existing.found) return formatStopResult(existing, options);
523
503
  const meta = await stopTask(pi, id, getActiveSession);
524
- if (!meta) return `No background task found for id ${id}.`;
525
- const remoteStop = meta.remote?.stopMessage ? ` ${meta.remote.stopMessage}` : "";
526
- const weakStop = meta.remote?.session === "direct" ? ` Warning: ${meta.remote.warning}` : "";
527
- return `Background task ${id} is ${meta.status}.${remoteStop}${weakStop}`;
504
+ if (!meta) return formatStopResult({ id, found: existing.found, readable: false, error: existing.error }, options);
505
+ return formatStopResult({ id: meta.id, meta, found: true, readable: true }, options);
528
506
  }
529
507
 
530
508
  function formatClear(statuses: string[] | undefined, active: BackgroundTaskCallbackOrigin): string {
@@ -554,11 +532,3 @@ function belongsToActiveToolSession(meta: BackgroundTaskMeta, active: Background
554
532
  if (active.sessionId) return false;
555
533
  return meta.cwd === active.cwd;
556
534
  }
557
-
558
- function formatDuration(ms: number): string {
559
- const seconds = Math.max(0, Math.round(ms / 1000));
560
- if (seconds < 60) return `${seconds}s`;
561
- const minutes = Math.floor(seconds / 60);
562
- const rest = seconds % 60;
563
- return `${minutes}m${rest.toString().padStart(2, "0")}s`;
564
- }
package/src/types.ts CHANGED
@@ -34,6 +34,12 @@ export interface CommandResult {
34
34
  startedAt: number;
35
35
  endedAt: number;
36
36
  timedOut?: boolean;
37
+ /** Bytes of stdout never stored because the capture cap was reached. */
38
+ stdoutDiscardedBytes?: number;
39
+ /** Bytes of stderr never stored because the capture cap was reached. */
40
+ stderrDiscardedBytes?: number;
41
+ /** True when stdout or stderr capture overflowed. Distinct from log retention. */
42
+ captureTruncated?: boolean;
37
43
  }
38
44
 
39
45
  export interface RemoteTaskParams {
@@ -102,6 +108,13 @@ export interface BackgroundTaskMeta {
102
108
  maxLogBytes?: number;
103
109
  logDiscardedBytes?: number;
104
110
  logRetentionEvents?: number;
111
+ /** Incremented on every same-inode compaction so log-page cursors can detect it. */
112
+ logGeneration?: number;
113
+ /** Bytes never captured into watch/command buffers. Distinct from logDiscardedBytes. */
114
+ captureDiscardedBytes?: number;
115
+ captureOverflowEvents?: number;
116
+ stdoutDiscardedBytes?: number;
117
+ stderrDiscardedBytes?: number;
105
118
  pid?: number;
106
119
  /** Opaque process-start token used to reject recycled child PIDs. */
107
120
  pidStartTime?: string;
@@ -114,6 +127,8 @@ export interface BackgroundTaskMeta {
114
127
  notifyOn?: "terminal";
115
128
  stopRequestedAt?: number;
116
129
  error?: string;
130
+ /** Set when stop failed while the task is still running. Distinct from poll errors. */
131
+ stopError?: string;
117
132
  ssh?: ResolvedSshIdentity;
118
133
  remote?: ResolvedRemoteTaskMetadata;
119
134
  }
@@ -122,6 +137,7 @@ export interface TerminalResult {
122
137
  status: Exclude<BackgroundTaskStatus, "running">;
123
138
  reason: string;
124
139
  matchedCondition?: Condition;
140
+ matchedValue?: unknown;
125
141
  commandResult?: CommandResult;
126
142
  }
127
143