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/README.md +5 -2
- package/package.json +1 -1
- package/src/failures.ts +25 -4
- package/src/logs.ts +51 -7
- package/src/navigator-provider.ts +2 -2
- package/src/output.ts +753 -0
- package/src/process.ts +95 -8
- package/src/registry.ts +84 -7
- package/src/runtime.ts +55 -16
- package/src/shared-callback-batcher.ts +327 -29
- package/src/shared-failure-observations.ts +501 -22
- package/src/shared-log-utils.ts +1025 -2
- package/src/tools.ts +105 -135
- package/src/types.ts +16 -0
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
186
|
-
return text(formatStatus(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
332
|
+
return formatList(listOptions(params, callbackOrigin));
|
|
278
333
|
case "status":
|
|
279
334
|
if (!params.id) return "Invalid parameters: status requires id.";
|
|
280
|
-
return formatStatus(
|
|
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
|
|
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
|
-
|
|
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
|
|
376
|
+
sessionId = ctx?.sessionManager?.getSessionId();
|
|
318
377
|
} catch {
|
|
319
378
|
sessionId = undefined;
|
|
379
|
+
unavailable = true;
|
|
320
380
|
}
|
|
321
|
-
|
|
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
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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,
|
|
417
|
-
const body = formatLog(id,
|
|
418
|
-
if (!
|
|
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 (
|
|
468
|
-
return
|
|
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
|
|
525
|
-
|
|
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
|
|