killeros 1.5.8 → 2.0.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.
@@ -1,572 +0,0 @@
1
- import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
- import type { SubagentTaskResult } from "./subagents.ts";
3
- import type { SubagentThread } from "./subagent-lifecycle.ts";
4
-
5
- export const SUBAGENT_PERSISTENCE_TYPE = "killeros-subagent-v1";
6
-
7
- const RECORD_VERSION = 1;
8
- const MAX_PROMPT_CHARS = 20_000;
9
- const MAX_RESULT_BYTES = 256 * 1024;
10
- const MAX_TRACE_BYTES = 64 * 1024;
11
- const MAX_TOOLS = 32;
12
- const MAX_TOOL_CHARS = 64;
13
- const MAX_ARTIFACTS = 32;
14
- const MAX_ARTIFACT_CHARS = 512;
15
- const CLOSED_PROMPT = "[closed thread prompt evicted]";
16
-
17
- export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
18
-
19
- export interface PersistedThreadSession {
20
- id: string;
21
- directory: string;
22
- }
23
-
24
- export interface PersistedCapabilityBoundary {
25
- filesystem: "none" | "read" | "write";
26
- network: "none" | "read" | "full";
27
- process: "none" | "limited" | "full";
28
- childThreads: boolean;
29
- }
30
-
31
- export interface PersistedHandoff {
32
- summary: string;
33
- nextAction?: string;
34
- artifacts?: string[];
35
- }
36
-
37
- export interface PersistedFailure {
38
- message: string;
39
- code?: string;
40
- }
41
-
42
- export interface PersistedTimestamps {
43
- createdAt: number;
44
- updatedAt: number;
45
- startedAt?: number;
46
- endedAt?: number;
47
- closedAt?: number;
48
- }
49
-
50
- export interface PersistedThread {
51
- id: string;
52
- parentId: string;
53
- displayName: string;
54
- attempt: number;
55
- role: string;
56
- prompt: string;
57
- model: string;
58
- tools: string[];
59
- capabilityBoundary: PersistedCapabilityBoundary;
60
- session: PersistedThreadSession;
61
- state: "queued" | "active" | "done" | "failed" | "stopped" | "orphaned" | "closed";
62
- usage: Record<string, unknown>;
63
- handoff: PersistedHandoff | undefined;
64
- result: string | undefined;
65
- failure: PersistedFailure | undefined;
66
- stopReason: string | undefined;
67
- evicted: boolean;
68
- timestamps: PersistedTimestamps;
69
- version: number;
70
- trace: unknown[];
71
- steering: unknown[];
72
- }
73
-
74
- export interface PersistedResult {
75
- id: string;
76
- name: string;
77
- agent: string;
78
- task: string;
79
- status: string;
80
- output: string;
81
- outputBytes: number;
82
- outputTruncatedBytes: number;
83
- usage: Record<string, unknown>;
84
- terminationReason: string | undefined;
85
- errorMessage: string | undefined;
86
- durationMs: number;
87
- exitCode: number | null;
88
- exitConfirmed: boolean;
89
- }
90
-
91
- export type SubagentPersistenceRecord =
92
- | { version: 1; event: "spawn"; parentId: string; thread: PersistedThread }
93
- | { version: 1; event: "snapshot"; parentId: string; id: string; thread: PersistedThread; result?: PersistedResult }
94
- | { version: 1; event: "close"; parentId: string; id: string; closedAt: number };
95
-
96
- export interface SubagentPersistence {
97
- restore(entries: readonly SessionEntry[], parentId: string): readonly SubagentThread[];
98
- recordSpawn(thread: SubagentThread): void;
99
- recordSnapshot(thread: SubagentThread, result?: SubagentTaskResult): void;
100
- recordClose(thread: SubagentThread): void;
101
- }
102
-
103
- const INVALID = Symbol("invalid");
104
- type SourceRecord = Record<string, unknown>;
105
-
106
- function isRecord(value: unknown): value is SourceRecord {
107
- return value !== null && typeof value === "object" && !Array.isArray(value);
108
- }
109
-
110
- function cloneJson(value: unknown, seen = new Set<object>()): unknown | typeof INVALID {
111
- if (value === undefined || value === null || typeof value === "string" || typeof value === "boolean") return value;
112
- if (typeof value === "number") return Number.isFinite(value) ? value : INVALID;
113
- if (typeof value !== "object") return INVALID;
114
- if (seen.has(value)) return INVALID;
115
- seen.add(value);
116
- let copy: unknown;
117
- if (Array.isArray(value)) {
118
- const array: unknown[] = [];
119
- for (const item of value) {
120
- const cloned = cloneJson(item, seen);
121
- if (cloned === INVALID) {
122
- seen.delete(value);
123
- return INVALID;
124
- }
125
- array.push(cloned);
126
- }
127
- copy = array;
128
- } else {
129
- const object: SourceRecord = {};
130
- for (const [key, item] of Object.entries(value)) {
131
- const cloned = cloneJson(item, seen);
132
- if (cloned === INVALID) {
133
- seen.delete(value);
134
- return INVALID;
135
- }
136
- object[key] = cloned;
137
- }
138
- copy = object;
139
- }
140
- seen.delete(value);
141
- return copy;
142
- }
143
-
144
- function cloneRecord(value: unknown): SourceRecord | undefined {
145
- const cloned = cloneJson(value);
146
- return isRecord(cloned) ? cloned : undefined;
147
- }
148
-
149
- function cloneArray(value: unknown): unknown[] | undefined {
150
- const cloned = cloneJson(value);
151
- return Array.isArray(cloned) ? cloned : undefined;
152
- }
153
-
154
- function text(value: unknown): string | undefined {
155
- return typeof value === "string" && value.trim() ? value : undefined;
156
- }
157
-
158
- function finiteNonNegative(value: unknown): value is number {
159
- return typeof value === "number" && Number.isFinite(value) && value >= 0;
160
- }
161
-
162
- function positiveInteger(value: unknown): value is number {
163
- return Number.isSafeInteger(value) && (value as number) > 0;
164
- }
165
-
166
- const THREAD_USAGE_FIELDS = [
167
- "inputTokens",
168
- "outputTokens",
169
- "cacheReadTokens",
170
- "cacheWriteTokens",
171
- "totalTokens",
172
- "costUsd",
173
- "turns",
174
- ] as const;
175
- const RESULT_USAGE_FIELDS = ["input", "output", "cacheRead", "cacheWrite", "totalTokens", "turns"] as const;
176
- const RESULT_COST_FIELDS = ["input", "output", "cacheRead", "cacheWrite", "total"] as const;
177
-
178
- function normalizeThreadUsage(value: unknown): Record<string, unknown> | undefined {
179
- const usage = cloneRecord(value);
180
- if (!usage) return undefined;
181
- for (const field of THREAD_USAGE_FIELDS) {
182
- if (!finiteNonNegative(usage[field])) return undefined;
183
- }
184
- return usage;
185
- }
186
-
187
- function normalizeResultUsage(value: unknown, fallback: unknown): Record<string, unknown> | undefined {
188
- const fallbackRecord = cloneRecord(fallback);
189
- const resultFallback = fallbackRecord && "inputTokens" in fallbackRecord
190
- ? {
191
- input: fallbackRecord.inputTokens,
192
- output: fallbackRecord.outputTokens,
193
- cacheRead: fallbackRecord.cacheReadTokens,
194
- cacheWrite: fallbackRecord.cacheWriteTokens,
195
- totalTokens: fallbackRecord.totalTokens,
196
- turns: fallbackRecord.turns,
197
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: fallbackRecord.costUsd },
198
- }
199
- : fallback;
200
- const usage = cloneRecord(value ?? resultFallback);
201
- if (!usage) return undefined;
202
- if (RESULT_USAGE_FIELDS.some((field) => !finiteNonNegative(usage[field]))) return undefined;
203
- const cost = cloneRecord(usage.cost);
204
- if (!cost || RESULT_COST_FIELDS.some((field) => !finiteNonNegative(cost[field]))) return undefined;
205
- return { ...usage, cost };
206
- }
207
-
208
- function normalizeTrace(value: unknown[]): unknown[] | typeof INVALID {
209
- for (const event of value) {
210
- if (!isRecord(event) || !finiteNonNegative(event.at) || !text(event.kind)) return INVALID;
211
- if (event.message !== undefined && !text(event.message)) return INVALID;
212
- if (event.details !== undefined) {
213
- if (!isRecord(event.details)) return INVALID;
214
- for (const detail of Object.values(event.details)) {
215
- if (detail !== null && !["string", "number", "boolean"].includes(typeof detail)) return INVALID;
216
- }
217
- }
218
- }
219
- return boundedTrace(value);
220
- }
221
-
222
- function normalizeSteering(value: unknown[]): unknown[] | typeof INVALID {
223
- for (const message of value) {
224
- if (!isRecord(message) || !positiveInteger(message.id) || typeof message.at !== "number" || !Number.isFinite(message.at) || !text(message.message)) return INVALID;
225
- }
226
- return value;
227
- }
228
-
229
- function boundedChars(value: string, maxChars: number): string {
230
- return [...value].slice(0, maxChars).join("");
231
- }
232
-
233
- function boundedUtf8(value: string, maxBytes: number): { text: string; omittedBytes: number } {
234
- const bytes = Buffer.from(value, "utf8");
235
- if (bytes.length <= maxBytes) return { text: value, omittedBytes: 0 };
236
- let end = maxBytes;
237
- while (end > 0 && Buffer.byteLength(bytes.subarray(0, end).toString("utf8"), "utf8") > end) end -= 1;
238
- const result = bytes.subarray(0, end).toString("utf8");
239
- return { text: result, omittedBytes: bytes.length - Buffer.byteLength(result, "utf8") };
240
- }
241
-
242
- function boundedTrace(value: unknown[]): unknown[] {
243
- const trace: unknown[] = [];
244
- for (const event of value) {
245
- const candidate = [...trace, event];
246
- if (Buffer.byteLength(JSON.stringify(candidate), "utf8") > MAX_TRACE_BYTES) break;
247
- trace.push(event);
248
- }
249
- return trace;
250
- }
251
-
252
- function normalizeTools(value: unknown): string[] | typeof INVALID {
253
- if (!Array.isArray(value)) return INVALID;
254
- const tools: string[] = [];
255
- for (const item of value.slice(0, MAX_TOOLS)) {
256
- if (typeof item !== "string" || !item.trim()) return INVALID;
257
- tools.push(boundedChars(item, MAX_TOOL_CHARS));
258
- }
259
- return tools;
260
- }
261
-
262
- function normalizeArtifacts(value: unknown): string[] | undefined | typeof INVALID {
263
- if (value === undefined) return undefined;
264
- if (!Array.isArray(value)) return INVALID;
265
- const artifacts: string[] = [];
266
- for (const item of value.slice(0, MAX_ARTIFACTS)) {
267
- if (typeof item !== "string" || !item.trim()) return INVALID;
268
- artifacts.push(boundedChars(item, MAX_ARTIFACT_CHARS));
269
- }
270
- return artifacts;
271
- }
272
-
273
- function normalizeBoundary(value: unknown): PersistedCapabilityBoundary | undefined {
274
- if (!isRecord(value)) return undefined;
275
- if (!(value.filesystem === "none" || value.filesystem === "read" || value.filesystem === "write")) return undefined;
276
- if (!(value.network === "none" || value.network === "read" || value.network === "full")) return undefined;
277
- if (!(value.process === "none" || value.process === "limited" || value.process === "full")) return undefined;
278
- if (typeof value.childThreads !== "boolean") return undefined;
279
- return {
280
- filesystem: value.filesystem,
281
- network: value.network,
282
- process: value.process,
283
- childThreads: value.childThreads,
284
- };
285
- }
286
-
287
- function normalizeHandoff(value: unknown): PersistedHandoff | undefined | typeof INVALID {
288
- if (value === undefined) return undefined;
289
- if (!isRecord(value)) return INVALID;
290
- const summary = text(value.summary);
291
- if (!summary) return INVALID;
292
- const nextAction = value.nextAction === undefined ? undefined : text(value.nextAction);
293
- if (value.nextAction !== undefined && !nextAction) return INVALID;
294
- const artifacts = normalizeArtifacts(value.artifacts);
295
- if (artifacts === INVALID) return INVALID;
296
- return {
297
- summary: boundedChars(summary, 256 * 1024),
298
- nextAction: nextAction === undefined ? undefined : boundedChars(nextAction, 4_000),
299
- artifacts,
300
- };
301
- }
302
-
303
- function normalizeFailure(value: unknown): PersistedFailure | undefined | typeof INVALID {
304
- if (value === undefined) return undefined;
305
- if (!isRecord(value)) return INVALID;
306
- const message = text(value.message);
307
- if (!message) return INVALID;
308
- const code = value.code === undefined ? undefined : text(value.code);
309
- if (value.code !== undefined && !code) return INVALID;
310
- return { message: boundedChars(message, 512), code: code === undefined ? undefined : boundedChars(code, 256) };
311
- }
312
-
313
- function normalizeTimestamps(value: unknown): PersistedTimestamps | undefined {
314
- if (!isRecord(value) || !finiteNonNegative(value.createdAt) || !finiteNonNegative(value.updatedAt)) return undefined;
315
- const timestamps: PersistedTimestamps = { createdAt: value.createdAt, updatedAt: value.updatedAt };
316
- for (const name of ["startedAt", "endedAt", "closedAt"] as const) {
317
- if (value[name] !== undefined) {
318
- if (!finiteNonNegative(value[name])) return undefined;
319
- timestamps[name] = value[name];
320
- }
321
- }
322
- return timestamps;
323
- }
324
-
325
- function normalizeResult(value: unknown, thread: PersistedThread): PersistedResult | undefined | typeof INVALID {
326
- if (!isRecord(value)) return INVALID;
327
- if (value.id !== undefined && value.id !== thread.id) return INVALID;
328
- const id = thread.id;
329
- const name = boundedChars(text(value.name) ?? thread.displayName, 48);
330
- const agent = boundedChars(text(value.agent) ?? thread.role, 64);
331
- const task = typeof value.task === "string" ? value.task : thread.prompt;
332
- const status = text(value.status) ?? (thread.state === "done" ? "complete" : thread.state === "failed" ? "failed" : "cancelled");
333
- const rawOutput = value.output === undefined ? thread.result ?? "" : value.output;
334
- if (!id || !name || !agent || !task || !status || !["queued", "running", "complete", "failed", "cancelled", "limited"].includes(status) || typeof rawOutput !== "string") return INVALID;
335
- const output = boundedUtf8(rawOutput, MAX_RESULT_BYTES);
336
- const usage = normalizeResultUsage(value.usage, thread.usage);
337
- if (!usage) return INVALID;
338
- const outputBytes = value.outputBytes === undefined ? Buffer.byteLength(rawOutput, "utf8") : value.outputBytes;
339
- const priorTruncated = value.outputTruncatedBytes === undefined ? 0 : value.outputTruncatedBytes;
340
- if (!finiteNonNegative(outputBytes) || !finiteNonNegative(priorTruncated)) return INVALID;
341
- const normalizedOutputBytes = outputBytes as number;
342
- const normalizedPriorTruncated = priorTruncated as number;
343
- const durationMs = value.durationMs === undefined ? 0 : value.durationMs;
344
- if (!finiteNonNegative(durationMs)) return INVALID;
345
- const normalizedDurationMs = durationMs as number;
346
- const exitCodeValue = value.exitCode === undefined || value.exitCode === null ? null : value.exitCode;
347
- if (exitCodeValue !== null && (typeof exitCodeValue !== "number" || !Number.isSafeInteger(exitCodeValue) || exitCodeValue < 0)) return INVALID;
348
- const exitCode = exitCodeValue as number | null;
349
- const terminationReason = value.terminationReason === undefined ? undefined : text(value.terminationReason);
350
- const errorMessageValue = value.errorMessage === undefined ? undefined : typeof value.errorMessage === "string" ? value.errorMessage : INVALID;
351
- if (value.terminationReason !== undefined && !terminationReason) return INVALID;
352
- if (errorMessageValue === INVALID) return INVALID;
353
- const errorMessage = errorMessageValue === undefined ? undefined : boundedChars(errorMessageValue as string, 8_000);
354
- const exitConfirmedValue = value.exitConfirmed === undefined ? false : value.exitConfirmed;
355
- if (typeof exitConfirmedValue !== "boolean") return INVALID;
356
- return {
357
- id,
358
- name,
359
- agent,
360
- task: boundedChars(task, MAX_PROMPT_CHARS),
361
- status,
362
- output: output.text,
363
- outputBytes: normalizedOutputBytes,
364
- outputTruncatedBytes: normalizedPriorTruncated + output.omittedBytes,
365
- usage,
366
- terminationReason: terminationReason === undefined ? undefined : boundedChars(terminationReason, 256),
367
- errorMessage,
368
- durationMs: normalizedDurationMs,
369
- exitCode,
370
- exitConfirmed: exitConfirmedValue,
371
- };
372
- }
373
-
374
- function normalizeThread(value: unknown, expectedParentId?: string): PersistedThread | undefined {
375
- if (!isRecord(value)) return undefined;
376
- const id = text(value.id);
377
- const parentId = text(value.parentId);
378
- const displayName = text(value.displayName);
379
- const role = text(value.role);
380
- const prompt = text(value.prompt);
381
- const model = text(value.model);
382
- if (!id || !parentId || expectedParentId !== undefined && parentId !== expectedParentId || !displayName || !/^[A-Za-z0-9][A-Za-z0-9._ -]{0,47}$/u.test(displayName) || !role || !prompt || !model) return undefined;
383
- const tools = normalizeTools(value.tools);
384
- const capabilityBoundary = normalizeBoundary(value.capabilityBoundary);
385
- const sessionId = isRecord(value.session) ? text(value.session.id) : undefined;
386
- const sessionDirectory = isRecord(value.session) && typeof value.session.directory === "string" ? value.session.directory : undefined;
387
- const session = sessionId && sessionDirectory
388
- ? { id: sessionId, directory: sessionDirectory }
389
- : undefined;
390
- const usage = normalizeThreadUsage(value.usage);
391
- const timestamps = normalizeTimestamps(value.timestamps);
392
- const states = new Set(["queued", "active", "done", "failed", "stopped", "orphaned", "closed"]);
393
- const state = value.state;
394
- const attempt = value.attempt;
395
- const version = value.version;
396
- const evicted = value.evicted;
397
- if (tools === INVALID || !capabilityBoundary || !session || !usage || !timestamps || typeof state !== "string" || !states.has(state)
398
- || !positiveInteger(attempt) || !positiveInteger(version) || typeof evicted !== "boolean") return undefined;
399
- const rawResult = value.result === undefined ? value.output : value.result;
400
- if (rawResult !== undefined && typeof rawResult !== "string") return undefined;
401
- const normalizedResult = rawResult as string | undefined;
402
- const handoff = normalizeHandoff(value.handoff);
403
- const failure = normalizeFailure(value.failure);
404
- if (handoff === INVALID || failure === INVALID) return undefined;
405
- const traceSource = value.trace === undefined ? [] : cloneArray(value.trace);
406
- const steering = value.steering === undefined ? [] : cloneArray(value.steering);
407
- if (!traceSource || !steering) return undefined;
408
- const trace = normalizeTrace(traceSource);
409
- const clonedSteering = normalizeSteering(steering);
410
- if (trace === INVALID || clonedSteering === INVALID) return undefined;
411
- const stopReason = value.stopReason === undefined ? undefined : text(value.stopReason);
412
- if (value.stopReason !== undefined && !stopReason) return undefined;
413
- return {
414
- id,
415
- parentId,
416
- displayName,
417
- attempt,
418
- role,
419
- prompt: boundedChars(prompt, MAX_PROMPT_CHARS),
420
- model,
421
- tools,
422
- capabilityBoundary,
423
- session,
424
- state: state as PersistedThread["state"],
425
- usage,
426
- handoff: handoff as PersistedHandoff | undefined,
427
- result: normalizedResult === undefined ? undefined : boundedUtf8(normalizedResult, MAX_RESULT_BYTES).text,
428
- failure: failure as PersistedFailure | undefined,
429
- stopReason,
430
- evicted,
431
- timestamps,
432
- version,
433
- trace,
434
- steering: clonedSteering,
435
- };
436
- }
437
-
438
- function persistedThread(thread: SubagentThread): PersistedThread {
439
- const source = thread as unknown as SourceRecord;
440
- const role = text(source.role);
441
- const parentId = text(source.parentId);
442
- if (!role || !parentId) throw new Error("Subagent persistence requires a child thread with a parentId and role");
443
- const normalized = normalizeThread({
444
- id: source.id,
445
- parentId,
446
- displayName: source.displayName ?? role,
447
- attempt: source.attempt ?? 1,
448
- role,
449
- prompt: source.prompt,
450
- model: source.model,
451
- tools: source.tools,
452
- capabilityBoundary: source.capabilityBoundary,
453
- session: source.session,
454
- state: source.state,
455
- usage: source.usage,
456
- handoff: source.handoff,
457
- result: source.result ?? source.output,
458
- failure: source.failure,
459
- stopReason: source.stopReason,
460
- evicted: source.evicted ?? false,
461
- timestamps: source.timestamps,
462
- version: source.version ?? 1,
463
- trace: source.trace ?? [],
464
- steering: source.steering ?? [],
465
- }, parentId);
466
- if (!normalized) throw new Error("Subagent thread snapshot is not persistable");
467
- return normalized;
468
- }
469
-
470
- function persistedResult(result: SubagentTaskResult, thread: PersistedThread): PersistedResult {
471
- const normalized = normalizeResult(result, thread);
472
- if (!normalized || normalized === INVALID) throw new Error("Subagent task result is not persistable");
473
- return normalized;
474
- }
475
-
476
- function closeTombstone(thread: PersistedThread, closedAt: number): PersistedThread {
477
- return {
478
- ...thread,
479
- state: "closed",
480
- prompt: CLOSED_PROMPT,
481
- handoff: undefined,
482
- trace: [],
483
- steering: [],
484
- result: undefined,
485
- failure: thread.failure ? { message: boundedChars(thread.failure.message, 512), code: thread.failure.code } : undefined,
486
- evicted: true,
487
- timestamps: { ...thread.timestamps, closedAt },
488
- };
489
- }
490
-
491
- function parentIdOf(thread: SubagentThread): string {
492
- const parentId = text((thread as unknown as SourceRecord).parentId);
493
- if (!parentId) throw new Error("Subagent persistence requires parentId");
494
- return parentId;
495
- }
496
-
497
- export function createSubagentPersistence(appendEntry: AppendEntryHandler, now: () => number = Date.now): SubagentPersistence {
498
- const append = (record: SubagentPersistenceRecord): void => appendEntry(SUBAGENT_PERSISTENCE_TYPE, record);
499
-
500
- return {
501
- restore(entries, parentId) {
502
- if (!text(parentId)) return [];
503
- const latest = new Map<string, PersistedThread>();
504
- for (const entry of entries) {
505
- try {
506
- if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== SUBAGENT_PERSISTENCE_TYPE) continue;
507
- const data = entry.data;
508
- if (!isRecord(data) || data.version !== RECORD_VERSION || data.parentId !== parentId) continue;
509
- if (data.event === "spawn") {
510
- const thread = normalizeThread(data.thread, parentId);
511
- if (thread) latest.set(thread.id, thread);
512
- } else if (data.event === "snapshot") {
513
- if (!text(data.id)) continue;
514
- const thread = normalizeThread(data.thread, parentId);
515
- if (!thread || thread.id !== data.id) continue;
516
- if (data.result !== undefined) {
517
- const result = normalizeResult(data.result, thread);
518
- if (!result || result === INVALID) continue;
519
- if (result.output) thread.result = result.output;
520
- }
521
- latest.set(thread.id, thread);
522
- } else if (data.event === "close") {
523
- const closedId = text(data.id);
524
- const closedAt = data.closedAt;
525
- if (!closedId || !finiteNonNegative(closedAt)) continue;
526
- const thread = latest.get(closedId);
527
- if (thread) latest.set(closedId, closeTombstone(thread, closedAt));
528
- }
529
- } catch {
530
- // A bad custom entry must not prevent the parent session from starting.
531
- }
532
- }
533
- return [...latest.values()].map((thread) => {
534
- const restored = { ...thread, tools: [...thread.tools], trace: [...thread.trace], steering: [...thread.steering], timestamps: { ...thread.timestamps } };
535
- if (restored.state === "queued" || restored.state === "active") {
536
- restored.state = "orphaned";
537
- restored.stopReason = "parent_restarted";
538
- }
539
- return restored as unknown as SubagentThread;
540
- });
541
- },
542
-
543
- recordSpawn(thread) {
544
- const snapshot = persistedThread(thread);
545
- append({ version: RECORD_VERSION, event: "spawn", parentId: snapshot.parentId, thread: snapshot });
546
- },
547
-
548
- recordSnapshot(thread, result) {
549
- const snapshot = persistedThread(thread);
550
- const record: Extract<SubagentPersistenceRecord, { event: "snapshot" }> = {
551
- version: RECORD_VERSION,
552
- event: "snapshot",
553
- parentId: snapshot.parentId,
554
- id: snapshot.id,
555
- thread: snapshot,
556
- };
557
- if (result !== undefined) record.result = persistedResult(result, snapshot);
558
- append(record);
559
- },
560
-
561
- recordClose(thread) {
562
- const parentId = parentIdOf(thread);
563
- const source = thread as unknown as SourceRecord;
564
- const timestamps = source.timestamps;
565
- const closedAt = isRecord(timestamps) && finiteNonNegative(timestamps.closedAt) ? timestamps.closedAt : now();
566
- if (!finiteNonNegative(closedAt)) throw new Error("Subagent close time must be a non-negative finite number");
567
- const id = text(source.id);
568
- if (!id) throw new Error("Subagent persistence requires a thread id");
569
- append({ version: RECORD_VERSION, event: "close", parentId, id, closedAt });
570
- },
571
- };
572
- }