pi-better-background-tasks 0.2.19 → 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/shared-sandbox-core.ts +11 -8
- package/src/tools.ts +105 -135
- package/src/types.ts +16 -0
package/src/output.ts
ADDED
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
activeFailures,
|
|
4
|
+
failureRevision,
|
|
5
|
+
formatFailureLines,
|
|
6
|
+
formatFailureSummary,
|
|
7
|
+
formatIncidentSummary,
|
|
8
|
+
isIncidentCursor,
|
|
9
|
+
pageFailureIncidents,
|
|
10
|
+
readFailureState,
|
|
11
|
+
type FailureState,
|
|
12
|
+
} from "./shared-failure-observations.js";
|
|
13
|
+
import {
|
|
14
|
+
assemblePriorityEnvelope,
|
|
15
|
+
clampBudgetBytes,
|
|
16
|
+
cursorKind,
|
|
17
|
+
formatUnchangedEvidence,
|
|
18
|
+
inspectStatusRevision,
|
|
19
|
+
pageRows,
|
|
20
|
+
pageVerbatimText,
|
|
21
|
+
revisionOf,
|
|
22
|
+
sliceUtf8Bytes,
|
|
23
|
+
utf8ByteLength,
|
|
24
|
+
OUTPUT_BUDGET_BYTES,
|
|
25
|
+
OUTPUT_BUDGET_MAX_BYTES,
|
|
26
|
+
OUTPUT_PAGE_DEFAULTS,
|
|
27
|
+
type EnvelopeSections,
|
|
28
|
+
type EvidenceGap,
|
|
29
|
+
type VerbatimPage,
|
|
30
|
+
} from "./shared-log-utils.js";
|
|
31
|
+
import { failurePath } from "./failures.js";
|
|
32
|
+
import { captureGapsFor, pageTaskLog, readLog, type LogRead } from "./logs.js";
|
|
33
|
+
import { belongsToOrigin, inspectMeta, listTaskRecords, originOf, type MetaInspection } from "./registry.js";
|
|
34
|
+
import type { BackgroundTaskCallbackOrigin, BackgroundTaskMeta, Condition } from "./types.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Issue #312 consumer budgets. Defaults follow OUTPUT-POLICY / shared
|
|
38
|
+
* `OUTPUT_BUDGET_BYTES`. Explicit larger pages clamp to `OUTPUT_BUDGET_MAX_BYTES`
|
|
39
|
+
* (hard caps). Totals are UTF-8 bytes of the whole model-facing `content`,
|
|
40
|
+
* including headers, failures, gaps, and continuation.
|
|
41
|
+
*/
|
|
42
|
+
export const BACKGROUND_OUTPUT_BUDGET_BYTES = {
|
|
43
|
+
status: OUTPUT_BUDGET_BYTES.status,
|
|
44
|
+
log: OUTPUT_BUDGET_BYTES.log,
|
|
45
|
+
list: OUTPUT_BUDGET_BYTES.list,
|
|
46
|
+
rawPage: OUTPUT_BUDGET_BYTES.rawPage,
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
export const BACKGROUND_OUTPUT_HARD_CAP_BYTES = {
|
|
50
|
+
status: OUTPUT_BUDGET_MAX_BYTES.status,
|
|
51
|
+
log: OUTPUT_BUDGET_MAX_BYTES.log,
|
|
52
|
+
list: OUTPUT_BUDGET_MAX_BYTES.list,
|
|
53
|
+
rawPage: OUTPUT_BUDGET_MAX_BYTES.rawPage,
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
export const DEFAULT_LOG_TAIL_ROWS = OUTPUT_PAGE_DEFAULTS.logLines;
|
|
57
|
+
export const DEFAULT_LIST_ENTRIES = OUTPUT_PAGE_DEFAULTS.listEntries;
|
|
58
|
+
const MAX_LIST_ENTRIES = 100;
|
|
59
|
+
const STATUS_EXCERPT_ROWS = 3;
|
|
60
|
+
/** Longest incident preview a list lead-in may show; the rest is on bg_task_status. */
|
|
61
|
+
const LIST_LEAD_PREVIEW_BYTES = 200;
|
|
62
|
+
|
|
63
|
+
export type BackgroundOutputSurface = keyof typeof BACKGROUND_OUTPUT_BUDGET_BYTES;
|
|
64
|
+
|
|
65
|
+
export interface OutputOptions {
|
|
66
|
+
cursor?: string;
|
|
67
|
+
maxBytes?: number;
|
|
68
|
+
verbose?: boolean;
|
|
69
|
+
tailLines?: number;
|
|
70
|
+
raw?: boolean;
|
|
71
|
+
statuses?: string[];
|
|
72
|
+
limit?: number;
|
|
73
|
+
origin?: BackgroundTaskCallbackOrigin;
|
|
74
|
+
all?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The current session's identity could not be read. Without `all`, every
|
|
77
|
+
* task's ownership is then unverifiable: reads report an ownership gap and
|
|
78
|
+
* lists hide rows while counting them.
|
|
79
|
+
*/
|
|
80
|
+
sessionUnavailable?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function backgroundBudget(surface: BackgroundOutputSurface, requested?: number): number {
|
|
84
|
+
const fallback = BACKGROUND_OUTPUT_BUDGET_BYTES[surface];
|
|
85
|
+
const hard = BACKGROUND_OUTPUT_HARD_CAP_BYTES[surface];
|
|
86
|
+
return Math.min(clampBudgetBytes(requested, fallback), hard);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function oneLine(value: unknown, maxLength: number): string {
|
|
90
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
91
|
+
const single = String(raw ?? "").replace(/\s+/g, " ").trim();
|
|
92
|
+
return single.length <= maxLength ? single : `${single.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatDuration(ms: number): string {
|
|
96
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
97
|
+
if (seconds < 60) return `${seconds}s`;
|
|
98
|
+
const minutes = Math.floor(seconds / 60);
|
|
99
|
+
const rest = seconds % 60;
|
|
100
|
+
return `${minutes}m${rest.toString().padStart(2, "0")}s`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function stringifyObserved(value: unknown): string {
|
|
104
|
+
if (value === undefined) return "undefined";
|
|
105
|
+
if (typeof value === "string") return oneLine(value, 200);
|
|
106
|
+
try {
|
|
107
|
+
return oneLine(JSON.stringify(value), 200);
|
|
108
|
+
} catch {
|
|
109
|
+
return oneLine(String(value), 200);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Cursor scope: pagination and revision cursors never cross session scopes. */
|
|
114
|
+
function scopeKey(options: OutputOptions): string {
|
|
115
|
+
if (options.all) return "all";
|
|
116
|
+
if (options.sessionUnavailable) return "unavailable";
|
|
117
|
+
const origin = options.origin;
|
|
118
|
+
if (!origin) return "none";
|
|
119
|
+
return revisionOf([origin.cwd, origin.sessionId ?? ""]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function taskGaps(meta: BackgroundTaskMeta): EvidenceGap[] {
|
|
123
|
+
const gaps: EvidenceGap[] = [];
|
|
124
|
+
if (meta.logDiscardedBytes) {
|
|
125
|
+
gaps.push({
|
|
126
|
+
kind: "retention",
|
|
127
|
+
bytes: meta.logDiscardedBytes,
|
|
128
|
+
detail: `${meta.logRetentionEvents ?? 1} compaction(s); discarded bytes are not recoverable`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
for (const gap of captureGapsFor(meta)) {
|
|
132
|
+
gaps.push({ kind: "capture", bytes: gap.bytes, detail: gap.detail });
|
|
133
|
+
}
|
|
134
|
+
return gaps;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function failureStateFor(id: string): FailureState {
|
|
138
|
+
return readFailureState(failurePath(id));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Failure section computed for the exact bytes the envelope grants it: whole
|
|
143
|
+
* incident rows when they fit, otherwise a count line (total / shown /
|
|
144
|
+
* omitted) with an incident cursor that resumes at the first byte not shown.
|
|
145
|
+
*/
|
|
146
|
+
function incidentSection(id: string, options: OutputOptions, state = failureStateFor(id)): ((budget: number) => string | undefined) | undefined {
|
|
147
|
+
if (activeFailures(state).length === 0) return undefined;
|
|
148
|
+
const resource = `incidents:${scopeKey(options)}:${id}`;
|
|
149
|
+
return (budget) => formatIncidentSummary(state, {
|
|
150
|
+
maxBytes: budget,
|
|
151
|
+
resource,
|
|
152
|
+
retrieval: `pass as cursor to bg_task_status id=${id}`,
|
|
153
|
+
}).text || undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assembleIncidentPage(meta: BackgroundTaskMeta, options: OutputOptions): string {
|
|
157
|
+
const state = failureStateFor(meta.id);
|
|
158
|
+
const resource = `incidents:${scopeKey(options)}:${meta.id}`;
|
|
159
|
+
const total = activeFailures(state).length;
|
|
160
|
+
return assembleBackgroundContent({
|
|
161
|
+
surface: "status",
|
|
162
|
+
maxBytes: options.maxBytes,
|
|
163
|
+
sections: {
|
|
164
|
+
identity: `${identityLine(meta)} Incident page of ${total} active failure observation${total === 1 ? "" : "s"}.`,
|
|
165
|
+
decision: formatDecision(meta),
|
|
166
|
+
},
|
|
167
|
+
verbatim: (budget) => {
|
|
168
|
+
const page = pageFailureIncidents(state, { cursor: options.cursor, maxBytes: budget, resource });
|
|
169
|
+
return {
|
|
170
|
+
text: page.text || (page.total === 0 ? "No active failure observations." : ""),
|
|
171
|
+
hasMore: page.hasMore,
|
|
172
|
+
cursor: page.cursor,
|
|
173
|
+
nextCursor: page.nextCursor,
|
|
174
|
+
omittedBytes: 0,
|
|
175
|
+
omittedRows: page.omitted,
|
|
176
|
+
reset: page.reset,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatCondition(condition: Condition, observed: unknown): string {
|
|
183
|
+
switch (condition.type) {
|
|
184
|
+
case "exit_code":
|
|
185
|
+
return `Condition matched: exit_code = ${condition.equals}\nobserved: ${stringifyObserved(observed ?? condition.equals)}`;
|
|
186
|
+
case "json_path_equals":
|
|
187
|
+
return `Condition matched: ${condition.path} = ${stringifyObserved(condition.value)}\nobserved: ${stringifyObserved(observed)}`;
|
|
188
|
+
case "json_path_exists":
|
|
189
|
+
return `Condition matched: ${condition.path} exists\nobserved: ${stringifyObserved(observed)}`;
|
|
190
|
+
case "stdout_contains":
|
|
191
|
+
return `Condition matched: stdout_contains ${JSON.stringify(condition.value)}`;
|
|
192
|
+
case "stderr_contains":
|
|
193
|
+
return `Condition matched: stderr_contains ${JSON.stringify(condition.value)}`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function resultFields(meta: BackgroundTaskMeta): {
|
|
198
|
+
reason?: string;
|
|
199
|
+
matchedCondition?: Condition;
|
|
200
|
+
matchedValue?: unknown;
|
|
201
|
+
} {
|
|
202
|
+
if (!meta.result || typeof meta.result !== "object") return {};
|
|
203
|
+
return meta.result as { reason?: string; matchedCondition?: Condition; matchedValue?: unknown };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Decision facts: stop error, matched condition and observed value, exit/signal, recorded error. */
|
|
207
|
+
function formatDecision(meta: BackgroundTaskMeta): string | undefined {
|
|
208
|
+
const lines: string[] = [];
|
|
209
|
+
if (meta.status === "running" && meta.stopError) {
|
|
210
|
+
lines.push(`stop failed: ${oneLine(meta.stopError, 300)}`);
|
|
211
|
+
lines.push("The task may still be executing.");
|
|
212
|
+
}
|
|
213
|
+
const result = resultFields(meta);
|
|
214
|
+
if (result.matchedCondition) {
|
|
215
|
+
lines.push(formatCondition(result.matchedCondition, result.matchedValue));
|
|
216
|
+
}
|
|
217
|
+
if (meta.lastExitCode !== undefined || meta.lastSignal) {
|
|
218
|
+
lines.push(`exit=${meta.lastExitCode ?? "null"}${meta.lastSignal ? ` signal=${meta.lastSignal}` : ""}`);
|
|
219
|
+
}
|
|
220
|
+
if (result.reason && !result.matchedCondition) lines.push(oneLine(result.reason, 240));
|
|
221
|
+
if (meta.error && meta.error !== meta.stopError && meta.error !== result.reason) {
|
|
222
|
+
lines.push(`error: ${oneLine(meta.error, 300)}`);
|
|
223
|
+
}
|
|
224
|
+
return lines.length ? lines.join("\n") : undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Completion-callback facts (no env/command dump). Incident rows are passed
|
|
229
|
+
* whole so the batch can count exactly which it shows.
|
|
230
|
+
*/
|
|
231
|
+
export function formatCallbackFacts(meta: BackgroundTaskMeta): {
|
|
232
|
+
outcome: string;
|
|
233
|
+
failureRows?: string[];
|
|
234
|
+
decision?: string;
|
|
235
|
+
incidentCount?: number;
|
|
236
|
+
} {
|
|
237
|
+
const state = failureStateFor(meta.id);
|
|
238
|
+
const rows = formatFailureLines(state);
|
|
239
|
+
const gapLines = [
|
|
240
|
+
meta.logDiscardedBytes ? `retention discarded ${meta.logDiscardedBytes} bytes; not recoverable` : undefined,
|
|
241
|
+
meta.captureDiscardedBytes ? `capture overflow discarded ${meta.captureDiscardedBytes} bytes; not full history` : undefined,
|
|
242
|
+
].filter((line): line is string => Boolean(line));
|
|
243
|
+
const decision = [formatDecision(meta), ...gapLines].filter(Boolean).join("\n") || undefined;
|
|
244
|
+
return {
|
|
245
|
+
outcome: meta.status,
|
|
246
|
+
...(rows.length ? { failureRows: rows, incidentCount: rows.length } : {}),
|
|
247
|
+
decision,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatDiagnostics(meta: BackgroundTaskMeta, extra: string[] = []): string | undefined {
|
|
252
|
+
const lines = [...extra];
|
|
253
|
+
if (meta.ssh) lines.push(`remote: ${meta.ssh.target}`);
|
|
254
|
+
if (meta.remote?.session) lines.push(`remote mode: ${meta.remote.session}`);
|
|
255
|
+
if (meta.remote?.sessionName) lines.push(`remote session: ${meta.remote.sessionName}`);
|
|
256
|
+
if (meta.remote?.bootstrapMessage) lines.push(`remote setup: ${oneLine(meta.remote.bootstrapMessage, 180)}`);
|
|
257
|
+
if (meta.remote?.warning) lines.push(`warning: ${oneLine(meta.remote.warning, 180)}`);
|
|
258
|
+
if (meta.remote?.stopMessage) lines.push(`remote stop: ${oneLine(meta.remote.stopMessage, 180)}`);
|
|
259
|
+
if (meta.logDiscardedBytes) {
|
|
260
|
+
lines.push(`retention discarded ${meta.logDiscardedBytes} bytes in ${meta.logRetentionEvents ?? 1} compaction(s); not recoverable`);
|
|
261
|
+
}
|
|
262
|
+
if (meta.captureDiscardedBytes) {
|
|
263
|
+
lines.push(`capture overflow discarded ${meta.captureDiscardedBytes} bytes in ${meta.captureOverflowEvents ?? 1} event(s); not full history`);
|
|
264
|
+
}
|
|
265
|
+
return lines.length ? lines.join("\n") : undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function formatProgress(meta: BackgroundTaskMeta): string | undefined {
|
|
269
|
+
const lines = [`kind: ${meta.kind}`];
|
|
270
|
+
if (meta.name) lines.push(`name: ${oneLine(meta.name, 80)}`);
|
|
271
|
+
lines.push(`elapsed: ${formatDuration((meta.endedAt ?? Date.now()) - meta.startedAt)}`);
|
|
272
|
+
if (meta.deadlineAt && meta.status === "running") {
|
|
273
|
+
lines.push(`deadline: ${formatDuration(meta.deadlineAt - Date.now())} left`);
|
|
274
|
+
}
|
|
275
|
+
if (meta.lastCheckedAt) lines.push(`last check: ${formatDuration(Date.now() - meta.lastCheckedAt)} ago`);
|
|
276
|
+
if (meta.lastState !== undefined) lines.push(`last state: ${oneLine(meta.lastState, 120)}`);
|
|
277
|
+
return lines.join("\n");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function identityLine(meta: BackgroundTaskMeta): string {
|
|
281
|
+
const stop = meta.status === "running" && meta.stopError ? " · stop failed" : "";
|
|
282
|
+
return `Background task ${meta.id} is ${meta.status}${stop}.`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Lifecycle, result, and retained-log facts. A deleted or rewritten log is a change. */
|
|
286
|
+
function contentRevision(meta: BackgroundTaskMeta): string {
|
|
287
|
+
let log: unknown;
|
|
288
|
+
try {
|
|
289
|
+
const stats = statSync(meta.logPath);
|
|
290
|
+
log = [stats.dev, stats.ino, stats.size, Math.trunc(stats.mtimeMs)];
|
|
291
|
+
} catch (error) {
|
|
292
|
+
log = ["unreadable", (error as NodeJS.ErrnoException).code ?? String(error)];
|
|
293
|
+
}
|
|
294
|
+
return revisionOf([
|
|
295
|
+
meta.status,
|
|
296
|
+
meta.endedAt ?? null,
|
|
297
|
+
meta.lastCheckedAt ?? null,
|
|
298
|
+
meta.lastProgressAt ?? null,
|
|
299
|
+
meta.lastExitCode ?? null,
|
|
300
|
+
meta.lastSignal ?? null,
|
|
301
|
+
meta.error ?? null,
|
|
302
|
+
meta.stopError ?? null,
|
|
303
|
+
meta.logGeneration ?? 0,
|
|
304
|
+
meta.logDiscardedBytes ?? 0,
|
|
305
|
+
meta.captureDiscardedBytes ?? 0,
|
|
306
|
+
meta.result ?? null,
|
|
307
|
+
meta.lastState ?? null,
|
|
308
|
+
meta.remote?.bootstrapStatus ?? null,
|
|
309
|
+
meta.remote?.stopMessage ?? null,
|
|
310
|
+
log,
|
|
311
|
+
]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function assembleBackgroundContent(input: {
|
|
315
|
+
surface: BackgroundOutputSurface;
|
|
316
|
+
maxBytes?: number;
|
|
317
|
+
sections?: EnvelopeSections;
|
|
318
|
+
verbatim?: (budget: number) => VerbatimPage;
|
|
319
|
+
gaps?: EvidenceGap[];
|
|
320
|
+
statusCursor?: string;
|
|
321
|
+
}): string {
|
|
322
|
+
const maxBytes = backgroundBudget(input.surface, input.maxBytes);
|
|
323
|
+
return assemblePriorityEnvelope({
|
|
324
|
+
maxBytes,
|
|
325
|
+
sections: input.sections,
|
|
326
|
+
verbatim: input.verbatim,
|
|
327
|
+
// Explicit evidence pages always advance: half the page is held for bytes.
|
|
328
|
+
// Explicit evidence pages and list rows always get room: half the page
|
|
329
|
+
// is held back from failure/diagnostic sections.
|
|
330
|
+
verbatimReserve: input.surface === "rawPage" || input.surface === "list" ? Math.floor(maxBytes / 2) : undefined,
|
|
331
|
+
gaps: input.gaps,
|
|
332
|
+
statusCursor: input.statusCursor,
|
|
333
|
+
// ADR 0006 surface contract: background summaries lead with failures.
|
|
334
|
+
failureFirst: true,
|
|
335
|
+
}).text;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function formatMissingTask(inspection: MetaInspection, options: OutputOptions = {}): string {
|
|
339
|
+
if (inspection.found || inspection.error) {
|
|
340
|
+
return assembleBackgroundContent({
|
|
341
|
+
surface: "status",
|
|
342
|
+
maxBytes: options.maxBytes,
|
|
343
|
+
sections: {
|
|
344
|
+
identity: `Background task ${inspection.id} metadata is unreadable.`,
|
|
345
|
+
diagnostics: [
|
|
346
|
+
inspection.error ?? "metadata could not be read",
|
|
347
|
+
"Cannot treat this as an empty or nonexistent task.",
|
|
348
|
+
].join("\n"),
|
|
349
|
+
},
|
|
350
|
+
gaps: [{ kind: "read", detail: inspection.error ?? "unreadable metadata" }],
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return assembleBackgroundContent({
|
|
354
|
+
surface: "status",
|
|
355
|
+
maxBytes: options.maxBytes,
|
|
356
|
+
sections: {
|
|
357
|
+
identity: `No background task found for id ${inspection.id}.`,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function formatOwnershipGap(id: string, kind: "foreign" | "unknown", options: OutputOptions): string {
|
|
363
|
+
const detail = kind === "foreign"
|
|
364
|
+
? "This task belongs to another session. Pass all:true to inspect it."
|
|
365
|
+
: options.sessionUnavailable
|
|
366
|
+
? "The current session identity is unavailable, so ownership cannot be verified. Cannot treat this as nonexistent or healthy. Pass all:true to inspect."
|
|
367
|
+
: "Task ownership is unavailable or unreadable. Cannot treat this as nonexistent or healthy. Pass all:true to inspect.";
|
|
368
|
+
return assembleBackgroundContent({
|
|
369
|
+
surface: "status",
|
|
370
|
+
maxBytes: options.maxBytes,
|
|
371
|
+
sections: {
|
|
372
|
+
identity: `Background task ${id} is outside the current session scope.`,
|
|
373
|
+
diagnostics: detail,
|
|
374
|
+
},
|
|
375
|
+
gaps: [{ kind: "read", detail: kind === "foreign" ? "foreign-session" : "ownership-unavailable" }],
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
type Ownership = "allow" | "foreign" | "unknown";
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Current-session ownership. Without a current session id, ownership is only
|
|
383
|
+
* verified for a task this process launched with the same sessionless origin;
|
|
384
|
+
* a legacy task with no recorded origin is never assumed to be ours.
|
|
385
|
+
*/
|
|
386
|
+
function classifyOwnership(meta: BackgroundTaskMeta, options: OutputOptions): Ownership {
|
|
387
|
+
if (options.all === true) return "allow";
|
|
388
|
+
if (options.sessionUnavailable) return "unknown";
|
|
389
|
+
const origin = options.origin;
|
|
390
|
+
if (!origin) return "allow";
|
|
391
|
+
if (!origin.sessionId) {
|
|
392
|
+
const recorded = meta.callbackOrigin;
|
|
393
|
+
if (!recorded) return "unknown";
|
|
394
|
+
if (recorded.cwd === origin.cwd && !recorded.sessionId && meta.spawnPid === process.pid) return "allow";
|
|
395
|
+
return recorded.sessionId ? "foreign" : "unknown";
|
|
396
|
+
}
|
|
397
|
+
if (belongsToOrigin(meta, origin)) return "allow";
|
|
398
|
+
const taskOrigin = originOf(meta);
|
|
399
|
+
if (!meta.callbackOrigin || (!taskOrigin.sessionId && origin.sessionId)) return "unknown";
|
|
400
|
+
return "foreign";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function asInspection(inspection: MetaInspection | BackgroundTaskMeta | undefined, idOrOptions?: string | OutputOptions): MetaInspection {
|
|
404
|
+
if (!inspection) {
|
|
405
|
+
return { id: typeof idOrOptions === "string" ? idOrOptions : "", found: false, readable: false };
|
|
406
|
+
}
|
|
407
|
+
if (typeof inspection === "object" && ("found" in inspection || "readable" in inspection)) {
|
|
408
|
+
return inspection as MetaInspection;
|
|
409
|
+
}
|
|
410
|
+
const meta = inspection as BackgroundTaskMeta;
|
|
411
|
+
return { id: meta.id, meta, found: true, readable: true };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function formatLaunch(meta: BackgroundTaskMeta): string {
|
|
415
|
+
const label = meta.name ? `${meta.name} (${meta.id})` : meta.id;
|
|
416
|
+
const remoteLines = [
|
|
417
|
+
...(meta.ssh ? [`Remote: ${meta.ssh.target}${meta.remote?.session ? ` mode=${meta.remote.session}` : ""}${meta.remote?.sessionName ? ` session=${meta.remote.sessionName}` : ""}.`] : []),
|
|
418
|
+
...(meta.remote?.bootstrapMessage ? [`Remote setup: ${meta.remote.bootstrapMessage}`] : []),
|
|
419
|
+
...(meta.remote?.warning ? [`Warning: ${meta.remote.warning}`] : []),
|
|
420
|
+
];
|
|
421
|
+
return assembleBackgroundContent({
|
|
422
|
+
surface: "status",
|
|
423
|
+
sections: {
|
|
424
|
+
identity: `Started background ${meta.kind} ${label}. Status: ${meta.status}.`,
|
|
425
|
+
failure: incidentSection(meta.id, {}),
|
|
426
|
+
decision: formatDecision(meta),
|
|
427
|
+
diagnostics: remoteLines.join("\n") || undefined,
|
|
428
|
+
progress: `Log: ${meta.logPath}`,
|
|
429
|
+
},
|
|
430
|
+
gaps: taskGaps(meta),
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function redactedVerbose(meta: BackgroundTaskMeta): unknown {
|
|
435
|
+
const { env, ...rest } = meta;
|
|
436
|
+
const state = failureStateFor(meta.id);
|
|
437
|
+
const observations = Object.values(state.observations);
|
|
438
|
+
const body = {
|
|
439
|
+
...rest,
|
|
440
|
+
...(env ? { env: { omitted: true, keyCount: Object.keys(env).length } } : {}),
|
|
441
|
+
};
|
|
442
|
+
if (!observations.length) return body;
|
|
443
|
+
return {
|
|
444
|
+
failureSummary: formatFailureSummary(state),
|
|
445
|
+
failureJournal: failurePath(meta.id),
|
|
446
|
+
failureObservations: observations.map((observation) => ({
|
|
447
|
+
...observation,
|
|
448
|
+
attentionDeliveredAt: state.delivered[observation.id],
|
|
449
|
+
})),
|
|
450
|
+
...body,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Verbose metadata is explicit evidence under the raw-page budget. When the
|
|
456
|
+
* whole document fits it is returned as plain JSON; otherwise it is paged
|
|
457
|
+
* with a caller cursor like any other retained evidence.
|
|
458
|
+
*/
|
|
459
|
+
function formatVerbose(meta: BackgroundTaskMeta, options: OutputOptions): string {
|
|
460
|
+
const json = JSON.stringify(redactedVerbose(meta), null, 2);
|
|
461
|
+
const resource = `verbose:${scopeKey(options)}:${meta.id}`;
|
|
462
|
+
if (!options.cursor && utf8ByteLength(json) <= backgroundBudget("rawPage", options.maxBytes)) return json;
|
|
463
|
+
return assembleBackgroundContent({
|
|
464
|
+
surface: "rawPage",
|
|
465
|
+
maxBytes: options.maxBytes,
|
|
466
|
+
sections: {
|
|
467
|
+
identity: `Background task ${meta.id} metadata (environment values omitted).`,
|
|
468
|
+
},
|
|
469
|
+
verbatim: (budget) => pageVerbatimText(json, { cursor: options.cursor, maxBytes: budget, resource }),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Retained raw log pages for this task in this scope. */
|
|
474
|
+
function rawLogPage(meta: BackgroundTaskMeta, options: OutputOptions, cursor: string | undefined, budget: number): VerbatimPage {
|
|
475
|
+
return pageTaskLog(meta, { cursor, maxBytes: budget, resource: `log:${scopeKey(options)}:${meta.id}` });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* A compact newest-rows excerpt. When earlier rows, older bytes, or a long
|
|
480
|
+
* line's prefix are not shown, the page says so and its cursor starts a raw
|
|
481
|
+
* page at the oldest retained byte (bg_task_log), so nothing is hidden.
|
|
482
|
+
*/
|
|
483
|
+
function excerptPage(meta: BackgroundTaskMeta, options: OutputOptions, log: LogRead, budget: number): VerbatimPage {
|
|
484
|
+
const text = log.text || "(log is empty)";
|
|
485
|
+
const encoded = utf8ByteLength(text);
|
|
486
|
+
let body = text;
|
|
487
|
+
if (encoded > budget) {
|
|
488
|
+
const slice = sliceUtf8Bytes(text, encoded - budget, budget, false);
|
|
489
|
+
body = slice.bytes <= budget ? slice.text : "";
|
|
490
|
+
if (slice.startByte > 0) {
|
|
491
|
+
const newline = body.indexOf("\n");
|
|
492
|
+
if (newline >= 0 && newline < body.length - 1) body = body.slice(newline + 1);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const shownBytes = log.text ? utf8ByteLength(body) : 0;
|
|
496
|
+
const omittedSomething = Boolean(log.truncated) || shownBytes < utf8ByteLength(log.text);
|
|
497
|
+
if (!omittedSomething) return { text: body, hasMore: false, omittedBytes: 0 };
|
|
498
|
+
const start = rawLogPage(meta, options, undefined, 0);
|
|
499
|
+
const rows = log.omittedRows ? `; ${log.omittedRows} earlier display row(s)` : "";
|
|
500
|
+
return {
|
|
501
|
+
text: body,
|
|
502
|
+
hasMore: true,
|
|
503
|
+
omittedBytes: Math.max(0, (log.totalBytes ?? 0) - shownBytes),
|
|
504
|
+
nextCursor: start.nextCursor,
|
|
505
|
+
via: `pass to bg_task_log id=${meta.id}: raw pages from the oldest retained byte${rows}`,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function formatStatus(
|
|
510
|
+
inspection: MetaInspection | BackgroundTaskMeta | undefined,
|
|
511
|
+
idOrOptions?: string | OutputOptions,
|
|
512
|
+
maybeOptions?: OutputOptions,
|
|
513
|
+
): string {
|
|
514
|
+
const inspectionValue = asInspection(inspection, idOrOptions);
|
|
515
|
+
const options = (typeof idOrOptions === "string" ? maybeOptions : idOrOptions) ?? {};
|
|
516
|
+
if (!inspectionValue.meta) return formatMissingTask(inspectionValue, options);
|
|
517
|
+
const meta = inspectionValue.meta;
|
|
518
|
+
const ownership = classifyOwnership(meta, options);
|
|
519
|
+
if (ownership !== "allow") return formatOwnershipGap(meta.id, ownership, options);
|
|
520
|
+
if (isIncidentCursor(options.cursor)) return assembleIncidentPage(meta, options);
|
|
521
|
+
if (options.verbose) return formatVerbose(meta, options);
|
|
522
|
+
const state = failureStateFor(meta.id);
|
|
523
|
+
const resource = `status:${scopeKey(options)}:${meta.id}`;
|
|
524
|
+
const revision = inspectStatusRevision({
|
|
525
|
+
resource,
|
|
526
|
+
contentRevision: contentRevision(meta),
|
|
527
|
+
failureRevision: failureRevision(state),
|
|
528
|
+
cursor: options.cursor,
|
|
529
|
+
});
|
|
530
|
+
if (options.cursor && revision.change === "none") {
|
|
531
|
+
return assembleBackgroundContent({
|
|
532
|
+
surface: "status",
|
|
533
|
+
maxBytes: options.maxBytes,
|
|
534
|
+
sections: {
|
|
535
|
+
identity: `${identityLine(meta)} · unchanged`,
|
|
536
|
+
failure: activeFailures(state).length
|
|
537
|
+
? `${activeFailures(state).length} active failure observation(s), unchanged.`
|
|
538
|
+
: undefined,
|
|
539
|
+
decision: formatUnchangedEvidence(options.cursor),
|
|
540
|
+
},
|
|
541
|
+
statusCursor: revision.nextCursor,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
const log = readLog(meta.logPath, STATUS_EXCERPT_ROWS);
|
|
545
|
+
// Change/reset and read-gap facts are short and decision-relevant: they are
|
|
546
|
+
// budgeted with the decision section, ahead of long incident rows.
|
|
547
|
+
const changeFacts = [
|
|
548
|
+
...(revision.change === "failure" ? ["change=failure"] : []),
|
|
549
|
+
...(revision.reset ? [`reset=${revision.reset}`] : []),
|
|
550
|
+
...(log.error ? [`log unreadable: ${oneLine(log.error, 200)}; cannot treat this as an empty healthy log.`] : []),
|
|
551
|
+
];
|
|
552
|
+
return assembleBackgroundContent({
|
|
553
|
+
surface: "status",
|
|
554
|
+
maxBytes: options.maxBytes,
|
|
555
|
+
sections: {
|
|
556
|
+
identity: identityLine(meta),
|
|
557
|
+
failure: incidentSection(meta.id, options, state),
|
|
558
|
+
decision: [...changeFacts, formatDecision(meta)].filter(Boolean).join("\n") || undefined,
|
|
559
|
+
diagnostics: formatDiagnostics(meta),
|
|
560
|
+
progress: formatProgress(meta),
|
|
561
|
+
},
|
|
562
|
+
verbatim: log.error ? undefined : (budget) => excerptPage(meta, options, log, budget),
|
|
563
|
+
gaps: [
|
|
564
|
+
...taskGaps(meta),
|
|
565
|
+
...(log.error ? [{ kind: "read" as const, detail: log.error }] : []),
|
|
566
|
+
],
|
|
567
|
+
statusCursor: revision.nextCursor,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function formatLog(id: string, options: OutputOptions = {}): string {
|
|
572
|
+
const inspection = inspectMeta(id);
|
|
573
|
+
if (!inspection.meta) return formatMissingTask(inspection, options);
|
|
574
|
+
const meta = inspection.meta;
|
|
575
|
+
const ownership = classifyOwnership(meta, options);
|
|
576
|
+
if (ownership !== "allow") return formatOwnershipGap(meta.id, ownership, options);
|
|
577
|
+
const fileCursor = cursorKind(options.cursor) === "f";
|
|
578
|
+
const raw = options.raw === true || options.tailLines === 0 || fileCursor;
|
|
579
|
+
const failure = incidentSection(id, options);
|
|
580
|
+
if (raw) {
|
|
581
|
+
return assembleBackgroundContent({
|
|
582
|
+
surface: "rawPage",
|
|
583
|
+
maxBytes: options.maxBytes,
|
|
584
|
+
sections: {
|
|
585
|
+
identity: `${meta.id} raw log (${meta.status})`,
|
|
586
|
+
failure,
|
|
587
|
+
decision: formatDecision(meta),
|
|
588
|
+
diagnostics: formatDiagnostics(meta, [
|
|
589
|
+
"Raw retained bytes; capture/retention loss is not recoverable as full history.",
|
|
590
|
+
]),
|
|
591
|
+
},
|
|
592
|
+
verbatim: (budget) => rawLogPage(meta, options, options.cursor, budget),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
const tailLines = options.tailLines && options.tailLines > 0 ? Math.floor(options.tailLines) : DEFAULT_LOG_TAIL_ROWS;
|
|
596
|
+
const log = readLog(meta.logPath, tailLines);
|
|
597
|
+
const staleCursor = options.cursor ? ["reset=stale-cursor (compact tails have no cursor; pass a raw nextCursor or tail_lines:0)"] : [];
|
|
598
|
+
if (log.error) {
|
|
599
|
+
return assembleBackgroundContent({
|
|
600
|
+
surface: "log",
|
|
601
|
+
maxBytes: options.maxBytes,
|
|
602
|
+
sections: {
|
|
603
|
+
identity: `${meta.id} log (${meta.status})`,
|
|
604
|
+
failure,
|
|
605
|
+
diagnostics: [...staleCursor, `log unreadable: ${log.error}`, "Cannot treat this as an empty healthy log."].join("\n"),
|
|
606
|
+
},
|
|
607
|
+
gaps: [{ kind: "read", detail: log.error }, ...taskGaps(meta)],
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return assembleBackgroundContent({
|
|
611
|
+
surface: "log",
|
|
612
|
+
maxBytes: options.maxBytes,
|
|
613
|
+
sections: {
|
|
614
|
+
identity: `${meta.id} log (${meta.status}) · newest ${tailLines} display row${tailLines === 1 ? "" : "s"}`,
|
|
615
|
+
failure,
|
|
616
|
+
decision: formatDecision(meta),
|
|
617
|
+
diagnostics: formatDiagnostics(meta, staleCursor),
|
|
618
|
+
},
|
|
619
|
+
verbatim: (budget) => excerptPage(meta, options, log, budget),
|
|
620
|
+
gaps: taskGaps(meta),
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function compactRow(meta: BackgroundTaskMeta, incidents: number): string {
|
|
625
|
+
const age = formatDuration((meta.endedAt ?? Date.now()) - meta.startedAt);
|
|
626
|
+
const remote = meta.ssh ? ` ${oneLine(meta.ssh.target, 60)}${meta.remote?.session ? ` ${meta.remote.session}` : ""}` : "";
|
|
627
|
+
const incident = incidents > 0 ? ` · ${incidents} incident${incidents === 1 ? "" : "s"}` : "";
|
|
628
|
+
const label = meta.name ? ` ${oneLine(meta.name, 60)}` : "";
|
|
629
|
+
return `${meta.id} ${meta.status} ${meta.kind} ${age}${incident}${remote}${label}`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function formatList(options: OutputOptions = {}): string {
|
|
633
|
+
const index = listTaskRecords();
|
|
634
|
+
if (index.indexError) {
|
|
635
|
+
return assembleBackgroundContent({
|
|
636
|
+
surface: "list",
|
|
637
|
+
maxBytes: options.maxBytes,
|
|
638
|
+
sections: {
|
|
639
|
+
identity: "Cannot list background tasks.",
|
|
640
|
+
diagnostics: `Task index is unreadable: ${index.indexError}. Cannot treat the registry as empty.`,
|
|
641
|
+
},
|
|
642
|
+
gaps: [{ kind: "read", detail: index.indexError }],
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
if (!options.all && !options.origin) {
|
|
646
|
+
return assembleBackgroundContent({
|
|
647
|
+
surface: "list",
|
|
648
|
+
maxBytes: options.maxBytes,
|
|
649
|
+
sections: {
|
|
650
|
+
identity: "Current session is unavailable.",
|
|
651
|
+
diagnostics: "Pass all:true to list tasks across sessions. Cannot treat the registry as empty.",
|
|
652
|
+
},
|
|
653
|
+
gaps: [{ kind: "read", detail: "session scope unavailable" }],
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
const wanted = options.statuses && options.statuses.length > 0 ? new Set(options.statuses) : undefined;
|
|
657
|
+
let unreadable = 0;
|
|
658
|
+
let unknown = 0;
|
|
659
|
+
const allowed: BackgroundTaskMeta[] = [];
|
|
660
|
+
for (const record of index.records) {
|
|
661
|
+
if (!record.meta) {
|
|
662
|
+
if (record.found) unreadable += 1;
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const ownership = classifyOwnership(record.meta, options);
|
|
666
|
+
if (ownership === "allow") {
|
|
667
|
+
if (!wanted || wanted.has(record.meta.status)) allowed.push(record.meta);
|
|
668
|
+
} else if (ownership === "unknown") {
|
|
669
|
+
unknown += 1;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const scope = scopeKey(options);
|
|
673
|
+
const statusesKey = [...(options.statuses ?? [])].sort().join(",");
|
|
674
|
+
const resource = `list:${scope}:${statusesKey}`;
|
|
675
|
+
const limit = Math.max(1, Math.min(Math.floor(options.limit ?? DEFAULT_LIST_ENTRIES), MAX_LIST_ENTRIES));
|
|
676
|
+
const states = new Map(allowed.map((meta) => [meta.id, failureStateFor(meta.id)] as const));
|
|
677
|
+
const incidentsOf = (id: string) => activeFailures(states.get(id)!).length;
|
|
678
|
+
const revision = inspectStatusRevision({
|
|
679
|
+
resource,
|
|
680
|
+
contentRevision: revisionOf(allowed.map((meta) => [meta.id, meta.status, meta.endedAt ?? null])),
|
|
681
|
+
failureRevision: revisionOf(allowed.map((meta) => [meta.id, failureRevision(states.get(meta.id)!)])),
|
|
682
|
+
cursor: cursorKind(options.cursor) === "s" ? options.cursor : undefined,
|
|
683
|
+
});
|
|
684
|
+
const scopeLabel = options.all ? "all sessions" : "current session";
|
|
685
|
+
if (options.cursor && cursorKind(options.cursor) === "s" && revision.change === "none") {
|
|
686
|
+
return assembleBackgroundContent({
|
|
687
|
+
surface: "list",
|
|
688
|
+
maxBytes: options.maxBytes,
|
|
689
|
+
sections: {
|
|
690
|
+
identity: `${allowed.length} background task${allowed.length === 1 ? "" : "s"} (${scopeLabel}) · unchanged`,
|
|
691
|
+
decision: formatUnchangedEvidence(options.cursor),
|
|
692
|
+
},
|
|
693
|
+
statusCursor: revision.nextCursor,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
const failing = allowed.filter((meta) => incidentsOf(meta.id) > 0);
|
|
697
|
+
const withIncidents = failing.length;
|
|
698
|
+
// One leading incident (the newest failing task's top row), then a count:
|
|
699
|
+
// failures lead the surface without repeating a paragraph per task.
|
|
700
|
+
const listFailure = (budget: number): string | undefined => {
|
|
701
|
+
const newest = failing[0];
|
|
702
|
+
if (!newest) return undefined;
|
|
703
|
+
const count = `${withIncidents} task${withIncidents === 1 ? "" : "s"} with unresolved incidents; newest ${newest.id} has ${incidentsOf(newest.id)}. Full incidents: bg_task_status id=${newest.id}.`;
|
|
704
|
+
const top = formatFailureLines(states.get(newest.id)!)[0] ?? "";
|
|
705
|
+
// The lead-in is a pointer, not the incident page: at most a short preview.
|
|
706
|
+
const room = Math.min(LIST_LEAD_PREVIEW_BYTES, budget - utf8ByteLength(count) - 1);
|
|
707
|
+
if (room < 48 || !top) return count;
|
|
708
|
+
const shown = utf8ByteLength(top) <= room ? top : `${sliceUtf8Bytes(top, 0, room - 12, false).text} (clipped)`;
|
|
709
|
+
return `${shown}\n${count}`;
|
|
710
|
+
};
|
|
711
|
+
const notes = [
|
|
712
|
+
...(unreadable ? [`${unreadable} task record(s) with unreadable metadata; cannot treat as empty or healthy`] : []),
|
|
713
|
+
...(unknown ? [`${unknown} task(s) with unavailable ownership hidden; pass all:true to inspect`] : []),
|
|
714
|
+
...(revision.change === "failure" ? ["change=failure"] : []),
|
|
715
|
+
];
|
|
716
|
+
const gaps: EvidenceGap[] = [
|
|
717
|
+
...(unreadable ? [{ kind: "read" as const, detail: `${unreadable} unreadable metadata file(s)` }] : []),
|
|
718
|
+
...(unknown ? [{ kind: "read" as const, detail: `${unknown} task(s) with unverifiable ownership` }] : []),
|
|
719
|
+
];
|
|
720
|
+
const pageCursor = options.cursor && cursorKind(options.cursor) !== "s" ? options.cursor : undefined;
|
|
721
|
+
return assembleBackgroundContent({
|
|
722
|
+
surface: "list",
|
|
723
|
+
maxBytes: options.maxBytes,
|
|
724
|
+
sections: {
|
|
725
|
+
identity: options.sessionUnavailable && !options.all
|
|
726
|
+
? "Current session identity is unavailable; task ownership cannot be verified."
|
|
727
|
+
: allowed.length === 0
|
|
728
|
+
? `No background tasks found (${scopeLabel}).`
|
|
729
|
+
: `${allowed.length} background task${allowed.length === 1 ? "" : "s"} (${scopeLabel}), newest first`,
|
|
730
|
+
failure: withIncidents ? listFailure : undefined,
|
|
731
|
+
diagnostics: notes.join("\n") || undefined,
|
|
732
|
+
},
|
|
733
|
+
verbatim: allowed.length === 0 && !pageCursor
|
|
734
|
+
? undefined
|
|
735
|
+
: (budget) => pageRows(allowed, {
|
|
736
|
+
cursor: pageCursor,
|
|
737
|
+
resource,
|
|
738
|
+
limit,
|
|
739
|
+
maxBytes: budget,
|
|
740
|
+
keyOf: (meta) => ({ time: meta.startedAt, id: meta.id }),
|
|
741
|
+
render: (meta) => compactRow(meta, incidentsOf(meta.id)),
|
|
742
|
+
}),
|
|
743
|
+
gaps,
|
|
744
|
+
statusCursor: revision.nextCursor,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export function formatStopResult(inspection: MetaInspection, options: OutputOptions = {}): string {
|
|
749
|
+
if (!inspection.meta) return formatMissingTask(inspection, options);
|
|
750
|
+
return formatStatus(inspection, options);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export { inspectMeta, utf8ByteLength };
|