pi-better-subagents 0.4.2 → 0.5.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 +1 -1
- package/callback-fields.ts +92 -0
- package/child-incidents.ts +147 -0
- package/docs/failure-observations.md +50 -3
- package/failures.ts +42 -53
- package/finalization.ts +16 -25
- package/incident-model.ts +320 -0
- package/index.ts +31 -26
- package/lifecycle.ts +9 -8
- package/list.mjs +49 -26
- package/list.ts +1 -0
- package/output-payload.ts +842 -0
- package/package.json +3 -3
- package/parse.ts +14 -5
- package/registry.ts +86 -3
- package/shared-callback-batcher.ts +327 -29
- package/shared-failure-observations.ts +501 -22
- package/shared-log-utils.ts +1025 -2
- package/shared-task-sandbox.ts +3 -1
- package/task-guard.ts +15 -3
- package/tools.ts +144 -79
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing subagent payloads (#312).
|
|
3
|
+
*
|
|
4
|
+
* One assembler path for list / output / result / raw evidence. Budgets,
|
|
5
|
+
* cursors, and envelopes come from shared log-utils; incident rendering and
|
|
6
|
+
* paging come from shared failure-observations; lifecycle stays with its owner
|
|
7
|
+
* (ADR 0006). Ordinary tool-name sequences and usage/cost boilerplate are
|
|
8
|
+
* omitted. Every cursor is bound to the selected session scope.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { statSync } from "node:fs";
|
|
12
|
+
import {
|
|
13
|
+
assemblePriorityEnvelope,
|
|
14
|
+
budgetFor,
|
|
15
|
+
cursorKind,
|
|
16
|
+
formatUnchangedEvidence,
|
|
17
|
+
inspectStatusRevision,
|
|
18
|
+
OUTPUT_PAGE_DEFAULTS,
|
|
19
|
+
pageRetainedFile,
|
|
20
|
+
pageRows,
|
|
21
|
+
pageVerbatimText,
|
|
22
|
+
revisionOf,
|
|
23
|
+
type EnvelopeFailure,
|
|
24
|
+
type EvidenceGap,
|
|
25
|
+
type VerbatimPage,
|
|
26
|
+
} from "./shared-log-utils.ts";
|
|
27
|
+
import { collectRunFailures } from "./failures.ts";
|
|
28
|
+
import {
|
|
29
|
+
formatLifecycleDiagnostics,
|
|
30
|
+
formatLostResult,
|
|
31
|
+
formatOrphanedResult,
|
|
32
|
+
resolveLifecycle,
|
|
33
|
+
type LifecycleValidation,
|
|
34
|
+
} from "./lifecycle.ts";
|
|
35
|
+
import { parseRun, parseRunForLifecycle, type ParsedRun } from "./parse.ts";
|
|
36
|
+
import {
|
|
37
|
+
belongsToOrigin,
|
|
38
|
+
effectiveStatus,
|
|
39
|
+
inspectRunMeta,
|
|
40
|
+
logPathFor,
|
|
41
|
+
originKey,
|
|
42
|
+
originOf,
|
|
43
|
+
type RunCallbackOrigin,
|
|
44
|
+
type RunMeta,
|
|
45
|
+
} from "./registry.ts";
|
|
46
|
+
import {
|
|
47
|
+
activeFailures,
|
|
48
|
+
failureRevision,
|
|
49
|
+
formatIncidentSummary,
|
|
50
|
+
formatTerminalIncidentSummary,
|
|
51
|
+
isIncidentCursor,
|
|
52
|
+
requiresAction,
|
|
53
|
+
pageFailureIncidents,
|
|
54
|
+
type FailureState,
|
|
55
|
+
} from "./shared-failure-observations.ts";
|
|
56
|
+
import { fmtElapsed } from "./widget.ts";
|
|
57
|
+
|
|
58
|
+
export interface PayloadRequest {
|
|
59
|
+
cursor?: string;
|
|
60
|
+
maxBytes?: unknown;
|
|
61
|
+
mode?: unknown;
|
|
62
|
+
all?: unknown;
|
|
63
|
+
lines?: unknown;
|
|
64
|
+
tail_lines?: unknown;
|
|
65
|
+
limit?: unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type LoadedRun =
|
|
69
|
+
| { kind: "ok"; meta: RunMeta }
|
|
70
|
+
| { kind: "missing" }
|
|
71
|
+
| { kind: "unreadable"; detail: string };
|
|
72
|
+
|
|
73
|
+
export type SubagentToolSession = {
|
|
74
|
+
/** Current session origin; `ctx` is the tool call's host context when available. */
|
|
75
|
+
getActiveOrigin?: (ctx?: unknown) => RunCallbackOrigin | undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Bytes of a continuation answer page left for the incident count line. */
|
|
79
|
+
const CONTINUATION_FAILURE_BYTES = 320;
|
|
80
|
+
|
|
81
|
+
function positiveInt(value: unknown): number | undefined {
|
|
82
|
+
const n = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
|
|
83
|
+
if (!Number.isFinite(n) || n <= 0) return undefined;
|
|
84
|
+
return Math.max(1, Math.floor(n));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isAll(value: unknown): boolean {
|
|
88
|
+
return value === true || String(value ?? "").trim().toLowerCase() === "true";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Distinguish a missing run from unreadable, corrupt, or absent metadata
|
|
93
|
+
* beside retained run evidence (AC5). Only a run with neither metadata nor a
|
|
94
|
+
* run directory is missing.
|
|
95
|
+
*/
|
|
96
|
+
export function loadRunRecord(id: string): LoadedRun {
|
|
97
|
+
return inspectRunMeta(id);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function isRawMode(mode: unknown): boolean {
|
|
101
|
+
return String(mode ?? "").trim().toLowerCase() === "raw";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function listScopeKey(
|
|
105
|
+
all: boolean,
|
|
106
|
+
origin: RunCallbackOrigin | undefined,
|
|
107
|
+
parentPid: number,
|
|
108
|
+
sessionAvailable = true,
|
|
109
|
+
): string {
|
|
110
|
+
if (all) return "all";
|
|
111
|
+
if (origin) return `session:${originKey(origin)}`;
|
|
112
|
+
if (!sessionAvailable) return "session:unavailable";
|
|
113
|
+
return `parent:${parentPid}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveActiveOrigin(session: SubagentToolSession | undefined): {
|
|
117
|
+
origin?: RunCallbackOrigin;
|
|
118
|
+
wired: boolean;
|
|
119
|
+
available: boolean;
|
|
120
|
+
} {
|
|
121
|
+
const wired = typeof session?.getActiveOrigin === "function";
|
|
122
|
+
if (!wired) return { wired: false, available: false };
|
|
123
|
+
const origin = session.getActiveOrigin?.();
|
|
124
|
+
if (!origin) return { wired: true, available: false };
|
|
125
|
+
return { origin, wired: true, available: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Scope key for a tool call, shared by list and direct reads. */
|
|
129
|
+
export function requestScopeKey(request: { all?: unknown }, session: SubagentToolSession | undefined, parentPid = process.pid): string {
|
|
130
|
+
const resolved = resolveActiveOrigin(session);
|
|
131
|
+
return listScopeKey(isAll(request.all), resolved.origin, parentPid, !(resolved.wired && !resolved.available));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Current-session list scope. When session identity is wired but unavailable,
|
|
136
|
+
* no run's ownership can be verified, so none is in the default scope.
|
|
137
|
+
*/
|
|
138
|
+
export function runInListScope(
|
|
139
|
+
meta: RunMeta,
|
|
140
|
+
options: { all?: boolean },
|
|
141
|
+
origin: RunCallbackOrigin | undefined,
|
|
142
|
+
parentPid: number,
|
|
143
|
+
sessionAvailable = true,
|
|
144
|
+
): boolean {
|
|
145
|
+
if (options.all) return true;
|
|
146
|
+
if (origin) return ownership(meta, origin, parentPid) === "own";
|
|
147
|
+
if (!sessionAvailable) return false;
|
|
148
|
+
return meta.spawnPid === parentPid;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Current-session ownership. Without a current session id, ownership is only
|
|
153
|
+
* verified for a run this process launched with the same sessionless origin;
|
|
154
|
+
* a legacy run with no recorded origin is never assumed to be ours.
|
|
155
|
+
*/
|
|
156
|
+
export function ownership(meta: RunMeta, origin: RunCallbackOrigin, parentPid = process.pid): "own" | "foreign" | "unavailable" {
|
|
157
|
+
const recorded = meta.callbackOrigin;
|
|
158
|
+
if (!origin.sessionId) {
|
|
159
|
+
if (!recorded) return "unavailable";
|
|
160
|
+
if (recorded.cwd === origin.cwd && !recorded.sessionId && meta.spawnPid === parentPid) return "own";
|
|
161
|
+
return recorded.sessionId ? "foreign" : "unavailable";
|
|
162
|
+
}
|
|
163
|
+
if (belongsToOrigin(meta, origin)) return "own";
|
|
164
|
+
return recorded ? "foreign" : "unavailable";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function logFacts(id: string): unknown {
|
|
168
|
+
try {
|
|
169
|
+
const stats = statSync(logPathFor(id));
|
|
170
|
+
return [stats.dev, stats.ino, stats.size, Math.trunc(stats.mtimeMs)];
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return ["unreadable", (error as NodeJS.ErrnoException).code ?? String(error)];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Lifecycle metadata and log identity. A metadata-only transition is a change. */
|
|
177
|
+
function contentRevisionFor(id: string, meta: RunMeta): string {
|
|
178
|
+
return revisionOf([
|
|
179
|
+
meta.status,
|
|
180
|
+
String(effectiveStatus(meta)),
|
|
181
|
+
meta.exitCode ?? null,
|
|
182
|
+
meta.endedAt ?? null,
|
|
183
|
+
meta.lifecycleClassification ?? null,
|
|
184
|
+
meta.failureReason ?? null,
|
|
185
|
+
logFacts(id),
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function elapsedFor(meta: RunMeta, now = Date.now()): string {
|
|
190
|
+
return fmtElapsed((meta.endedAt ?? now) - meta.startedAt);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function joinSections(parts: Array<string | undefined>): string | undefined {
|
|
194
|
+
const text = parts.filter((part): part is string => Boolean(part && part.length > 0)).join("\n");
|
|
195
|
+
return text.length > 0 ? text : undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function resultIdentity(id: string, status: string, exitCode: string, elapsed: string, classification: string): string {
|
|
199
|
+
return `[${id} · ${status} · exit ${exitCode} · ${elapsed} · lifecycle ${classification}]`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function outputIdentity(id: string, status: string, elapsed: string): string {
|
|
203
|
+
return `[${id} · ${status} · ${elapsed}]`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function logGaps(id: string): EvidenceGap[] {
|
|
207
|
+
try {
|
|
208
|
+
statSync(logPathFor(id));
|
|
209
|
+
return [];
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return [{ kind: "read", detail: error instanceof Error ? error.message : String(error) }];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parserDiagnostics(run: ParsedRun): string | undefined {
|
|
216
|
+
if (!run.diagnostics.length) return undefined;
|
|
217
|
+
return `[parser: ${run.diagnostics.join("; ")}]`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function retrievalHint(id: string): string {
|
|
221
|
+
return `Retrieve retained evidence with subagent_output id="${id}" mode="raw" (16 KiB default, up to 64 KiB, caller cursor).`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function bestParsed(run: ParsedRun): string {
|
|
225
|
+
return run.finalText || run.lastActivity || "";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isRoutineComplete(lifecycle: LifecycleValidation): boolean {
|
|
229
|
+
return lifecycle.classification === "complete" && !lifecycle.incomplete;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function streamEvidence(run: ParsedRun): string {
|
|
233
|
+
const parts = [
|
|
234
|
+
!run.sawEnd ? "no agent_end or agent_settled event" : "terminal event observed",
|
|
235
|
+
run.unmatchedToolCalls.length
|
|
236
|
+
? `unmatched tools: ${run.unmatchedToolCalls.map((call) => call.id ? `${call.toolName} (${call.id})` : call.toolName).join(", ")}`
|
|
237
|
+
: "no unmatched tools",
|
|
238
|
+
];
|
|
239
|
+
return `Stream evidence: ${parts.join("; ")}.`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Per-call context: one failure-state read, scope-bound resources. */
|
|
243
|
+
interface CallContext {
|
|
244
|
+
id: string;
|
|
245
|
+
meta: RunMeta;
|
|
246
|
+
request: PayloadRequest;
|
|
247
|
+
scopeKey: string;
|
|
248
|
+
state: FailureState;
|
|
249
|
+
statusResource: string;
|
|
250
|
+
statusCursor: string;
|
|
251
|
+
/** Terminal runs present lifecycle-independent facts; running runs show prioritized rows (#315). */
|
|
252
|
+
terminal: boolean;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function callContext(id: string, meta: RunMeta, request: PayloadRequest, scopeKey: string, surface: string, terminal: boolean): CallContext {
|
|
256
|
+
const state = collectRunFailures(id, meta.cwd, terminal);
|
|
257
|
+
const statusResource = `${surface}:${scopeKey}:${id}`;
|
|
258
|
+
const statusCursor = inspectStatusRevision({
|
|
259
|
+
resource: statusResource,
|
|
260
|
+
contentRevision: contentRevisionFor(id, meta),
|
|
261
|
+
failureRevision: failureRevision(state),
|
|
262
|
+
}).nextCursor;
|
|
263
|
+
return { id, meta, request, scopeKey, state, statusResource, statusCursor, terminal };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function incidentResource(ctx: CallContext): string {
|
|
267
|
+
return `incidents:${ctx.scopeKey}:${ctx.id}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Shared failure section: whole rows when they fit, otherwise exact counts plus a resuming cursor.
|
|
272
|
+
* Terminal runs list only actionable/incomplete rows and count unclassified history (#315).
|
|
273
|
+
*/
|
|
274
|
+
function failureSection(ctx: CallContext, cap?: number): EnvelopeFailure | undefined {
|
|
275
|
+
if (activeFailures(ctx.state).length === 0 && !ctx.terminal) return undefined;
|
|
276
|
+
const summarize = ctx.terminal ? formatTerminalIncidentSummary : formatIncidentSummary;
|
|
277
|
+
if (ctx.terminal && !summarize(ctx.state, { maxBytes: Number.MAX_SAFE_INTEGER }).text) return undefined;
|
|
278
|
+
return (budget) => summarize(ctx.state, {
|
|
279
|
+
maxBytes: cap === undefined ? budget : Math.min(budget, cap),
|
|
280
|
+
resource: incidentResource(ctx),
|
|
281
|
+
retrieval: `pass as cursor to subagent_result/subagent_output id="${ctx.id}"`,
|
|
282
|
+
}).text || undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function envelopeText(input: {
|
|
286
|
+
maxBytes: number;
|
|
287
|
+
identity?: string;
|
|
288
|
+
failure?: EnvelopeFailure;
|
|
289
|
+
decision?: string;
|
|
290
|
+
diagnostics?: string;
|
|
291
|
+
progress?: string;
|
|
292
|
+
gaps?: EvidenceGap[];
|
|
293
|
+
verbatim?: (remaining: number) => VerbatimPage;
|
|
294
|
+
verbatimReserve?: number;
|
|
295
|
+
statusCursor?: string;
|
|
296
|
+
}): string {
|
|
297
|
+
return assemblePriorityEnvelope({
|
|
298
|
+
maxBytes: input.maxBytes,
|
|
299
|
+
sections: {
|
|
300
|
+
identity: input.identity,
|
|
301
|
+
failure: input.failure,
|
|
302
|
+
decision: input.decision,
|
|
303
|
+
diagnostics: input.diagnostics,
|
|
304
|
+
progress: input.progress,
|
|
305
|
+
},
|
|
306
|
+
gaps: input.gaps,
|
|
307
|
+
verbatim: input.verbatim,
|
|
308
|
+
verbatimReserve: input.verbatimReserve,
|
|
309
|
+
statusCursor: input.statusCursor,
|
|
310
|
+
}).text;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function assembleIncidentPage(ctx: CallContext, identity: string): string {
|
|
314
|
+
const maxBytes = budgetFor("answer", ctx.request.maxBytes);
|
|
315
|
+
const total = activeFailures(ctx.state).length;
|
|
316
|
+
return envelopeText({
|
|
317
|
+
maxBytes,
|
|
318
|
+
identity: `${identity} incident page of ${total} active failure observation${total === 1 ? "" : "s"}`,
|
|
319
|
+
verbatim: (budget) => {
|
|
320
|
+
const page = pageFailureIncidents(ctx.state, { cursor: ctx.request.cursor, maxBytes: budget, resource: incidentResource(ctx) });
|
|
321
|
+
return {
|
|
322
|
+
text: page.text || (page.total === 0 ? "No active failure observations." : ""),
|
|
323
|
+
hasMore: page.hasMore,
|
|
324
|
+
cursor: page.cursor,
|
|
325
|
+
nextCursor: page.nextCursor,
|
|
326
|
+
omittedBytes: 0,
|
|
327
|
+
omittedRows: page.omitted,
|
|
328
|
+
reset: page.reset,
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
statusCursor: ctx.statusCursor,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Caller-owned status cursor: unchanged → a small no-change response. */
|
|
336
|
+
function maybeUnchanged(ctx: CallContext, identity: string): string | undefined {
|
|
337
|
+
const cursor = ctx.request.cursor;
|
|
338
|
+
if (!cursor || cursorKind(cursor) !== "s") return undefined;
|
|
339
|
+
const inspected = inspectStatusRevision({
|
|
340
|
+
cursor,
|
|
341
|
+
resource: ctx.statusResource,
|
|
342
|
+
contentRevision: contentRevisionFor(ctx.id, ctx.meta),
|
|
343
|
+
failureRevision: failureRevision(ctx.state),
|
|
344
|
+
});
|
|
345
|
+
if (inspected.change !== "none") return undefined;
|
|
346
|
+
const active = activeFailures(ctx.state).length;
|
|
347
|
+
return envelopeText({
|
|
348
|
+
maxBytes: budgetFor("status", ctx.request.maxBytes),
|
|
349
|
+
identity: `${identity} · unchanged`,
|
|
350
|
+
failure: active ? `${active} active failure observation(s), unchanged.` : undefined,
|
|
351
|
+
decision: formatUnchangedEvidence(cursor),
|
|
352
|
+
statusCursor: inspected.nextCursor,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Status-change facts for a status cursor that did not match. */
|
|
357
|
+
function changeDiagnostics(ctx: CallContext): string | undefined {
|
|
358
|
+
const cursor = ctx.request.cursor;
|
|
359
|
+
if (!cursor || cursorKind(cursor) !== "s") return undefined;
|
|
360
|
+
const inspected = inspectStatusRevision({
|
|
361
|
+
cursor,
|
|
362
|
+
resource: ctx.statusResource,
|
|
363
|
+
contentRevision: contentRevisionFor(ctx.id, ctx.meta),
|
|
364
|
+
failureRevision: failureRevision(ctx.state),
|
|
365
|
+
});
|
|
366
|
+
if (inspected.change === "failure") return "change=failure";
|
|
367
|
+
if (inspected.change === "reset") return "reset=stale-cursor (status cursor from another run, surface, or scope)";
|
|
368
|
+
return "change=content";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Page cursor for answer/raw pagers; status cursors start a fresh page. */
|
|
372
|
+
function pageCursorFor(cursor: string | undefined): string | undefined {
|
|
373
|
+
if (!cursor) return undefined;
|
|
374
|
+
return cursorKind(cursor) === "s" ? undefined : cursor;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function isContinuationPage(cursor: string | undefined): boolean {
|
|
378
|
+
return cursorKind(cursor) === "t";
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function rawPage(ctx: CallContext, cursor: string | undefined, remaining: number): VerbatimPage {
|
|
382
|
+
return pageRetainedFile(logPathFor(ctx.id), {
|
|
383
|
+
cursor,
|
|
384
|
+
maxBytes: remaining,
|
|
385
|
+
resource: `raw:${ctx.scopeKey}:${ctx.id}`,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function answerPage(ctx: CallContext, text: string, cursor: string | undefined, remaining: number, maxLines?: number): VerbatimPage {
|
|
390
|
+
return pageVerbatimText(text, {
|
|
391
|
+
cursor,
|
|
392
|
+
maxBytes: remaining,
|
|
393
|
+
maxLines,
|
|
394
|
+
resource: `answer:${ctx.scopeKey}:${ctx.id}`,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Answer pages keep half the budget on first pages and nearly all on continuations. */
|
|
399
|
+
function answerReserve(maxBytes: number, cursor: string | undefined): number {
|
|
400
|
+
return isContinuationPage(cursor) ? maxBytes - CONTINUATION_FAILURE_BYTES : Math.floor(maxBytes / 2);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function outputLineCap(request: PayloadRequest): number {
|
|
404
|
+
return positiveInt(request.lines) ?? positiveInt(request.tail_lines) ?? OUTPUT_PAGE_DEFAULTS.logLines;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function assembleUnreadableMetadata(id: string, detail: string, request: PayloadRequest = {}): string {
|
|
408
|
+
return envelopeText({
|
|
409
|
+
maxBytes: budgetFor("status", request.maxBytes),
|
|
410
|
+
identity: `[${id} · metadata unreadable]`,
|
|
411
|
+
diagnostics: joinSections([
|
|
412
|
+
"Run metadata is missing, unreadable, or corrupt. This is not proof the run is absent or healthy.",
|
|
413
|
+
`detail=${detail.replace(/[\r\n\x00-\x1f\x7f]/g, " ")}`,
|
|
414
|
+
`Retained log bytes may still be read with subagent_output id="${id}" mode="raw" all:true once ownership is confirmed.`,
|
|
415
|
+
]),
|
|
416
|
+
gaps: [{ kind: "read", detail }],
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function assembleOwnershipGap(
|
|
421
|
+
id: string,
|
|
422
|
+
kind: "foreign" | "unavailable",
|
|
423
|
+
request: PayloadRequest = {},
|
|
424
|
+
): string {
|
|
425
|
+
const identity = kind === "foreign"
|
|
426
|
+
? `[${id} · foreign session]`
|
|
427
|
+
: `[${id} · ownership unavailable]`;
|
|
428
|
+
const decision = kind === "foreign"
|
|
429
|
+
? "This run belongs to another session. Pass all:true to read it. Evidence is not included."
|
|
430
|
+
: "Current session identity could not be read, so this run's ownership cannot be verified. This is not proof the run is absent, and evidence is not included. Pass all:true if you intend a cross-session read.";
|
|
431
|
+
return envelopeText({
|
|
432
|
+
maxBytes: budgetFor("status", request.maxBytes),
|
|
433
|
+
identity,
|
|
434
|
+
decision,
|
|
435
|
+
gaps: [{ kind: "read", detail: kind === "foreign" ? "foreign-session" : "ownership-unavailable" }],
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function resolveRunAccess(
|
|
440
|
+
id: string,
|
|
441
|
+
request: PayloadRequest,
|
|
442
|
+
session: SubagentToolSession | undefined,
|
|
443
|
+
): { kind: "missing" } | { kind: "unreadable"; detail: string } | { kind: "denied"; payload: string } | { kind: "ok"; meta: RunMeta } {
|
|
444
|
+
const loaded = loadRunRecord(id);
|
|
445
|
+
if (loaded.kind === "missing") return loaded;
|
|
446
|
+
if (loaded.kind === "unreadable") return loaded;
|
|
447
|
+
if (isAll(request.all)) return { kind: "ok", meta: loaded.meta };
|
|
448
|
+
const resolved = resolveActiveOrigin(session);
|
|
449
|
+
// Unwired factories (no session provider) exist only for unit tests that
|
|
450
|
+
// build a tool without the extension. index.ts always passes a provider,
|
|
451
|
+
// so registered tools never take this branch; see the
|
|
452
|
+
// "registered tools fail closed" test in subagent_output_budget.test.mjs.
|
|
453
|
+
if (!resolved.wired) return { kind: "ok", meta: loaded.meta };
|
|
454
|
+
if (!resolved.available || !resolved.origin) {
|
|
455
|
+
return { kind: "denied", payload: assembleOwnershipGap(id, "unavailable", request) };
|
|
456
|
+
}
|
|
457
|
+
const owner = ownership(loaded.meta, resolved.origin);
|
|
458
|
+
if (owner !== "own") {
|
|
459
|
+
return { kind: "denied", payload: assembleOwnershipGap(id, owner, request) };
|
|
460
|
+
}
|
|
461
|
+
return { kind: "ok", meta: loaded.meta };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function defaultScope(): string {
|
|
465
|
+
return listScopeKey(false, undefined, process.pid);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function assembleRunningResult(
|
|
469
|
+
id: string,
|
|
470
|
+
meta: RunMeta,
|
|
471
|
+
request: PayloadRequest = {},
|
|
472
|
+
scopeKey = defaultScope(),
|
|
473
|
+
): string {
|
|
474
|
+
const st = String(effectiveStatus(meta));
|
|
475
|
+
const terminal = st !== "running" && st !== "orphaned";
|
|
476
|
+
const ctx = callContext(id, meta, request, scopeKey, "subagent_result", terminal);
|
|
477
|
+
const identity = `[${id} · ${st} · ${elapsedFor(meta)}]`;
|
|
478
|
+
if (isIncidentCursor(request.cursor)) return assembleIncidentPage(ctx, identity);
|
|
479
|
+
const unchanged = maybeUnchanged(ctx, identity);
|
|
480
|
+
if (unchanged) return unchanged;
|
|
481
|
+
return envelopeText({
|
|
482
|
+
maxBytes: budgetFor("answer", request.maxBytes),
|
|
483
|
+
identity,
|
|
484
|
+
failure: failureSection(ctx),
|
|
485
|
+
decision: `Run ${id} is still running — no result yet. You'll be notified when it finishes; don't poll.`,
|
|
486
|
+
diagnostics: joinSections([changeDiagnostics(ctx), parserDiagnostics(parseRun(id))]),
|
|
487
|
+
gaps: logGaps(id),
|
|
488
|
+
statusCursor: ctx.statusCursor,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function assembleRaw(
|
|
493
|
+
id: string,
|
|
494
|
+
meta: RunMeta,
|
|
495
|
+
status: string,
|
|
496
|
+
request: PayloadRequest,
|
|
497
|
+
surface: "output" | "result",
|
|
498
|
+
scopeKey: string,
|
|
499
|
+
): string {
|
|
500
|
+
const terminal = status !== "running" && status !== "orphaned";
|
|
501
|
+
const ctx = callContext(id, meta, request, scopeKey, surface === "result" ? "subagent_result" : "subagent_output", terminal);
|
|
502
|
+
const identity = outputIdentity(id, status, elapsedFor(meta));
|
|
503
|
+
const unchanged = maybeUnchanged(ctx, identity);
|
|
504
|
+
if (unchanged) return unchanged;
|
|
505
|
+
const run = parseRun(id);
|
|
506
|
+
const maxBytes = budgetFor("rawPage", request.maxBytes);
|
|
507
|
+
const continuing = cursorKind(request.cursor) === "f";
|
|
508
|
+
return envelopeText({
|
|
509
|
+
maxBytes,
|
|
510
|
+
identity: `${identity} raw retained log`,
|
|
511
|
+
failure: failureSection(ctx, continuing ? CONTINUATION_FAILURE_BYTES : 1024),
|
|
512
|
+
diagnostics: joinSections([changeDiagnostics(ctx), parserDiagnostics(run)]),
|
|
513
|
+
gaps: logGaps(id),
|
|
514
|
+
verbatim: (remaining) => rawPage(ctx, pageCursorFor(request.cursor), remaining),
|
|
515
|
+
verbatimReserve: Math.floor(maxBytes / 2),
|
|
516
|
+
statusCursor: ctx.statusCursor,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function assembleSubagentOutput(
|
|
521
|
+
id: string,
|
|
522
|
+
meta: RunMeta,
|
|
523
|
+
request: PayloadRequest = {},
|
|
524
|
+
healthLine = "",
|
|
525
|
+
scopeKey = defaultScope(),
|
|
526
|
+
): string {
|
|
527
|
+
const st = String(effectiveStatus(meta));
|
|
528
|
+
if (isRawMode(request.mode) || cursorKind(request.cursor) === "f") return assembleRaw(id, meta, st, request, "output", scopeKey);
|
|
529
|
+
const terminal = st !== "running" && st !== "orphaned";
|
|
530
|
+
const ctx = callContext(id, meta, request, scopeKey, "subagent_output", terminal);
|
|
531
|
+
const identity = outputIdentity(id, st, elapsedFor(meta));
|
|
532
|
+
if (isIncidentCursor(request.cursor)) return assembleIncidentPage(ctx, identity);
|
|
533
|
+
const unchanged = maybeUnchanged(ctx, identity);
|
|
534
|
+
if (unchanged) return unchanged;
|
|
535
|
+
const pageCursor = pageCursorFor(request.cursor);
|
|
536
|
+
const run = parseRun(id);
|
|
537
|
+
const parsed = bestParsed(run);
|
|
538
|
+
const gaps = logGaps(id);
|
|
539
|
+
const emptyLog = gaps.length === 0 && !parsed && !run.diagnostics.length;
|
|
540
|
+
const decision = gaps.length
|
|
541
|
+
? "Child log is missing or unreadable; this is not an empty healthy result."
|
|
542
|
+
: undefined;
|
|
543
|
+
const diagnostics = joinSections([
|
|
544
|
+
changeDiagnostics(ctx),
|
|
545
|
+
healthLine || undefined,
|
|
546
|
+
parserDiagnostics(run),
|
|
547
|
+
emptyLog && !decision ? "(no output yet)" : undefined,
|
|
548
|
+
!parsed && !emptyLog && gaps.length === 0 ? "(no parsed output yet)" : undefined,
|
|
549
|
+
gaps.length || run.diagnostics.some((line) => /unreadable|truncated/i.test(line))
|
|
550
|
+
? retrievalHint(id)
|
|
551
|
+
: undefined,
|
|
552
|
+
]);
|
|
553
|
+
const maxBytes = budgetFor("log", request.maxBytes);
|
|
554
|
+
const maxLines = outputLineCap(request);
|
|
555
|
+
return envelopeText({
|
|
556
|
+
maxBytes,
|
|
557
|
+
identity,
|
|
558
|
+
failure: failureSection(ctx, isContinuationPage(pageCursor) ? CONTINUATION_FAILURE_BYTES : undefined),
|
|
559
|
+
decision,
|
|
560
|
+
diagnostics,
|
|
561
|
+
gaps,
|
|
562
|
+
verbatim: parsed
|
|
563
|
+
? (remaining) => answerPage(ctx, parsed, pageCursor, remaining, maxLines)
|
|
564
|
+
: gaps.length === 0
|
|
565
|
+
? (remaining) => rawPage(ctx, pageCursor, remaining)
|
|
566
|
+
: undefined,
|
|
567
|
+
verbatimReserve: answerReserve(maxBytes, pageCursor),
|
|
568
|
+
statusCursor: ctx.statusCursor,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function assembleOrphanedResult(
|
|
573
|
+
id: string,
|
|
574
|
+
meta: RunMeta,
|
|
575
|
+
healthLine: string,
|
|
576
|
+
request: PayloadRequest = {},
|
|
577
|
+
scopeKey = defaultScope(),
|
|
578
|
+
): string {
|
|
579
|
+
if (isRawMode(request.mode) || cursorKind(request.cursor) === "f") return assembleRaw(id, meta, "orphaned", request, "result", scopeKey);
|
|
580
|
+
const ctx = callContext(id, meta, request, scopeKey, "subagent_result", false);
|
|
581
|
+
const identity = `[${id} · orphaned · ${elapsedFor(meta)}]`;
|
|
582
|
+
if (isIncidentCursor(request.cursor)) return assembleIncidentPage(ctx, identity);
|
|
583
|
+
const unchanged = maybeUnchanged(ctx, identity);
|
|
584
|
+
if (unchanged) return unchanged;
|
|
585
|
+
const pageCursor = pageCursorFor(request.cursor);
|
|
586
|
+
const run = parseRun(id);
|
|
587
|
+
const gaps = logGaps(id);
|
|
588
|
+
const parsed = bestParsed(run);
|
|
589
|
+
const maxBytes = budgetFor("answer", request.maxBytes);
|
|
590
|
+
return envelopeText({
|
|
591
|
+
maxBytes,
|
|
592
|
+
identity,
|
|
593
|
+
failure: failureSection(ctx, isContinuationPage(pageCursor) ? CONTINUATION_FAILURE_BYTES : undefined),
|
|
594
|
+
decision: "Run is orphaned — non-final. Supervision was lost; related processes may still be alive. There is no final result yet.",
|
|
595
|
+
diagnostics: joinSections([
|
|
596
|
+
changeDiagnostics(ctx),
|
|
597
|
+
healthLine || undefined,
|
|
598
|
+
parserDiagnostics(run),
|
|
599
|
+
gaps.length ? "Child log is missing or unreadable; this is not an empty healthy result." : undefined,
|
|
600
|
+
formatOrphanedResult(run, "").split("\n").slice(0, 2).join("\n"),
|
|
601
|
+
"--- best-current parsed output ---",
|
|
602
|
+
retrievalHint(id),
|
|
603
|
+
]),
|
|
604
|
+
gaps,
|
|
605
|
+
verbatim: parsed
|
|
606
|
+
? (remaining) => answerPage(ctx, parsed, pageCursor, remaining)
|
|
607
|
+
: gaps.length === 0
|
|
608
|
+
? (remaining) => rawPage(ctx, pageCursor, remaining)
|
|
609
|
+
: undefined,
|
|
610
|
+
verbatimReserve: answerReserve(maxBytes, pageCursor),
|
|
611
|
+
statusCursor: ctx.statusCursor,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function assembleSubagentResult(
|
|
616
|
+
id: string,
|
|
617
|
+
meta: RunMeta,
|
|
618
|
+
request: PayloadRequest = {},
|
|
619
|
+
healthLine = "",
|
|
620
|
+
scopeKey = defaultScope(),
|
|
621
|
+
): string {
|
|
622
|
+
const st = effectiveStatus(meta);
|
|
623
|
+
if (isRawMode(request.mode) || cursorKind(request.cursor) === "f") return assembleRaw(id, meta, String(st), request, "result", scopeKey);
|
|
624
|
+
const ctx = callContext(id, meta, request, scopeKey, "subagent_result", true);
|
|
625
|
+
const exit = meta.exitCode === undefined ? "?" : String(meta.exitCode);
|
|
626
|
+
const run = parseRunForLifecycle(id);
|
|
627
|
+
const lifecycle = resolveLifecycle(meta, run);
|
|
628
|
+
const identity = resultIdentity(id, String(st), exit, elapsedFor(meta), lifecycle.classification);
|
|
629
|
+
if (isIncidentCursor(request.cursor)) return assembleIncidentPage(ctx, identity);
|
|
630
|
+
const unchanged = maybeUnchanged(ctx, identity);
|
|
631
|
+
if (unchanged) return unchanged;
|
|
632
|
+
const pageCursor = pageCursorFor(request.cursor);
|
|
633
|
+
const gaps = logGaps(id);
|
|
634
|
+
const maxBytes = budgetFor("answer", request.maxBytes);
|
|
635
|
+
const failure = failureSection(ctx, isContinuationPage(pageCursor) ? CONTINUATION_FAILURE_BYTES : undefined);
|
|
636
|
+
const reserve = answerReserve(maxBytes, pageCursor);
|
|
637
|
+
|
|
638
|
+
if (lifecycle.incomplete) {
|
|
639
|
+
const parsed = bestParsed(run);
|
|
640
|
+
return envelopeText({
|
|
641
|
+
maxBytes,
|
|
642
|
+
identity,
|
|
643
|
+
failure,
|
|
644
|
+
decision: "Run ended unexpectedly before producing a coherent final result.",
|
|
645
|
+
diagnostics: joinSections([
|
|
646
|
+
changeDiagnostics(ctx),
|
|
647
|
+
healthLine || undefined,
|
|
648
|
+
formatLifecycleDiagnostics(lifecycle),
|
|
649
|
+
streamEvidence(run),
|
|
650
|
+
parserDiagnostics(run),
|
|
651
|
+
gaps.length ? "Child log is missing or unreadable; this is not an empty healthy result." : undefined,
|
|
652
|
+
"--- best available parsed output ---",
|
|
653
|
+
retrievalHint(id),
|
|
654
|
+
]),
|
|
655
|
+
gaps,
|
|
656
|
+
verbatim: parsed ? (remaining) => answerPage(ctx, parsed, pageCursor, remaining) : undefined,
|
|
657
|
+
verbatimReserve: reserve,
|
|
658
|
+
statusCursor: ctx.statusCursor,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (st === "lost" || lifecycle.classification === "lost") {
|
|
663
|
+
const parsed = bestParsed(run);
|
|
664
|
+
return envelopeText({
|
|
665
|
+
maxBytes,
|
|
666
|
+
identity,
|
|
667
|
+
failure,
|
|
668
|
+
decision: "Run is lost: no related process remains and no coherent terminal completion was observed. This is a terminal unknown outcome, not a normal failure.",
|
|
669
|
+
diagnostics: joinSections([
|
|
670
|
+
changeDiagnostics(ctx),
|
|
671
|
+
healthLine || undefined,
|
|
672
|
+
formatLifecycleDiagnostics(lifecycle),
|
|
673
|
+
parserDiagnostics(run),
|
|
674
|
+
formatLostResult(run, "").split("\n").slice(0, 2).join("\n"),
|
|
675
|
+
"--- best-available parsed output ---",
|
|
676
|
+
retrievalHint(id),
|
|
677
|
+
]),
|
|
678
|
+
gaps,
|
|
679
|
+
verbatim: parsed
|
|
680
|
+
? (remaining) => answerPage(ctx, parsed, pageCursor, remaining)
|
|
681
|
+
: gaps.length === 0
|
|
682
|
+
? (remaining) => rawPage(ctx, pageCursor, remaining)
|
|
683
|
+
: undefined,
|
|
684
|
+
verbatimReserve: reserve,
|
|
685
|
+
statusCursor: ctx.statusCursor,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const exceptional = isRoutineComplete(lifecycle) ? undefined : formatLifecycleDiagnostics(lifecycle);
|
|
690
|
+
const answer = run.finalText || "";
|
|
691
|
+
const fallback = !answer
|
|
692
|
+
? (run.lastActivity
|
|
693
|
+
? `(no final answer parsed; latest activity)\n${run.lastActivity}`
|
|
694
|
+
: undefined)
|
|
695
|
+
: undefined;
|
|
696
|
+
return envelopeText({
|
|
697
|
+
maxBytes,
|
|
698
|
+
identity,
|
|
699
|
+
failure,
|
|
700
|
+
diagnostics: joinSections([
|
|
701
|
+
changeDiagnostics(ctx),
|
|
702
|
+
healthLine || undefined,
|
|
703
|
+
exceptional,
|
|
704
|
+
parserDiagnostics(run),
|
|
705
|
+
!answer && gaps.length ? "Child log is missing or unreadable; this is not an empty healthy result." : undefined,
|
|
706
|
+
!answer ? retrievalHint(id) : undefined,
|
|
707
|
+
]),
|
|
708
|
+
gaps,
|
|
709
|
+
verbatim: (remaining) => answerPage(ctx, answer || fallback || "(no final answer parsed)", pageCursor, remaining),
|
|
710
|
+
verbatimReserve: reserve,
|
|
711
|
+
statusCursor: ctx.statusCursor,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ---- list ----------------------------------------------------------------
|
|
716
|
+
|
|
717
|
+
export interface SubagentListItem {
|
|
718
|
+
meta: RunMeta;
|
|
719
|
+
status: string;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function listIncidentCount(id: string, cwd: string, terminal: boolean): { count: number; actionRequired: number; revision: string } {
|
|
723
|
+
const state = collectRunFailures(id, cwd, terminal);
|
|
724
|
+
const active = activeFailures(state);
|
|
725
|
+
return { count: active.length, actionRequired: active.filter(requiresAction).length, revision: failureRevision(state) };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export function formatIncidentLabel(count: number, actionRequired = 0): string {
|
|
729
|
+
if (count <= 0) return "";
|
|
730
|
+
return `${count} incident${count === 1 ? "" : "s"}${actionRequired ? ` · ${actionRequired} action required` : ""}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export function listIncidentLabel(id: string, cwd: string, terminal: boolean): string {
|
|
734
|
+
const { count, actionRequired } = listIncidentCount(id, cwd, terminal);
|
|
735
|
+
return formatIncidentLabel(count, actionRequired);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Status revisions for a list: every row's lifecycle facts (content) and
|
|
740
|
+
* every row's full failure signature (failure-only changes).
|
|
741
|
+
*/
|
|
742
|
+
export function listRevisions(
|
|
743
|
+
items: Array<{ id: string; status: string; meta?: RunMeta }>,
|
|
744
|
+
failureSignatures: string[],
|
|
745
|
+
): { contentRevision: string; failureRevision: string } {
|
|
746
|
+
return {
|
|
747
|
+
contentRevision: revisionOf(items.map((item) => [
|
|
748
|
+
item.id,
|
|
749
|
+
item.status,
|
|
750
|
+
item.meta?.status ?? null,
|
|
751
|
+
item.meta?.exitCode ?? null,
|
|
752
|
+
item.meta?.endedAt ?? null,
|
|
753
|
+
])),
|
|
754
|
+
failureRevision: revisionOf(items.map((item, index) => [item.id, failureSignatures[index] ?? ""])),
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export function assembleSubagentListPayload(input: {
|
|
759
|
+
warnings: string[];
|
|
760
|
+
items: SubagentListItem[];
|
|
761
|
+
render: (item: SubagentListItem) => string;
|
|
762
|
+
limit: number;
|
|
763
|
+
cursor?: string;
|
|
764
|
+
maxBytes?: unknown;
|
|
765
|
+
contentRevision: string;
|
|
766
|
+
failureRevision: string;
|
|
767
|
+
scopeKey: string;
|
|
768
|
+
statusesKey?: string;
|
|
769
|
+
sessionWarning?: string;
|
|
770
|
+
unreadable?: Array<{ id: string; detail: string }>;
|
|
771
|
+
indexError?: string;
|
|
772
|
+
incidentRuns?: number;
|
|
773
|
+
}): string {
|
|
774
|
+
const maxBytes = budgetFor("list", input.maxBytes);
|
|
775
|
+
const resource = `subagent_list:${input.scopeKey}:${input.statusesKey ?? ""}`;
|
|
776
|
+
const statusCursorInput = cursorKind(input.cursor) === "s" ? input.cursor : undefined;
|
|
777
|
+
const status = inspectStatusRevision({
|
|
778
|
+
cursor: statusCursorInput,
|
|
779
|
+
resource,
|
|
780
|
+
contentRevision: input.contentRevision,
|
|
781
|
+
failureRevision: input.failureRevision,
|
|
782
|
+
});
|
|
783
|
+
const matching = input.items.length;
|
|
784
|
+
const scopeLabel = input.scopeKey === "all" ? "all" : input.scopeKey;
|
|
785
|
+
if (statusCursorInput && status.change === "none") {
|
|
786
|
+
return envelopeText({
|
|
787
|
+
maxBytes,
|
|
788
|
+
identity: `subagent_list · ${matching} matching · scope ${scopeLabel} · unchanged`,
|
|
789
|
+
decision: formatUnchangedEvidence(statusCursorInput),
|
|
790
|
+
statusCursor: status.nextCursor,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
const unreadable = input.unreadable ?? [];
|
|
794
|
+
const pageCursor = input.cursor && !statusCursorInput ? input.cursor : undefined;
|
|
795
|
+
const identity = input.indexError
|
|
796
|
+
? "subagent_list · run index unreadable"
|
|
797
|
+
: matching === 0
|
|
798
|
+
? "No subagent runs match filters."
|
|
799
|
+
: `subagent_list · ${matching} matching · scope ${scopeLabel} · newest first`;
|
|
800
|
+
const diagnostics = joinSections([
|
|
801
|
+
input.sessionWarning,
|
|
802
|
+
...input.warnings,
|
|
803
|
+
input.indexError ? `Run index could not be read: ${input.indexError}. Cannot treat the registry as empty.` : undefined,
|
|
804
|
+
unreadable.length
|
|
805
|
+
? `${unreadable.length} run record(s) with missing or unreadable metadata (ownership unknown; not counted as absent or healthy): ${unreadable.slice(0, 3).map((item) => item.id).join(", ")}${unreadable.length > 3 ? ", …" : ""}`
|
|
806
|
+
: undefined,
|
|
807
|
+
statusCursorInput && status.change === "failure" ? "change=failure" : undefined,
|
|
808
|
+
statusCursorInput && status.change === "reset" ? "reset=stale-cursor (status cursor from another scope or filter)" : undefined,
|
|
809
|
+
]);
|
|
810
|
+
const gaps: EvidenceGap[] = [
|
|
811
|
+
...(input.indexError ? [{ kind: "read" as const, detail: input.indexError }] : []),
|
|
812
|
+
...(unreadable.length ? [{ kind: "read" as const, detail: `${unreadable.length} unreadable run metadata record(s)` }] : []),
|
|
813
|
+
];
|
|
814
|
+
return envelopeText({
|
|
815
|
+
maxBytes,
|
|
816
|
+
identity,
|
|
817
|
+
failure: input.incidentRuns
|
|
818
|
+
? `${input.incidentRuns} listed run${input.incidentRuns === 1 ? "" : "s"} with active failure observations; inspect with subagent_result`
|
|
819
|
+
: undefined,
|
|
820
|
+
diagnostics,
|
|
821
|
+
gaps,
|
|
822
|
+
verbatim: matching === 0 && !pageCursor
|
|
823
|
+
? undefined
|
|
824
|
+
: (remaining) => pageRows(input.items, {
|
|
825
|
+
cursor: pageCursor,
|
|
826
|
+
resource,
|
|
827
|
+
limit: input.limit,
|
|
828
|
+
maxBytes: remaining,
|
|
829
|
+
keyOf: (item) => ({ time: item.meta.startedAt, id: item.meta.id }),
|
|
830
|
+
render: input.render,
|
|
831
|
+
}),
|
|
832
|
+
// List rows always get half the page; the lead-in is a pointer.
|
|
833
|
+
verbatimReserve: Math.floor(maxBytes / 2),
|
|
834
|
+
statusCursor: status.nextCursor,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export function unknownRunError(id: string): Error {
|
|
839
|
+
return new Error(`Unknown run id: ${id}`);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export { originOf };
|