@omercnet/paseo-omp 0.3.0-next.100.1 → 0.3.0-next.102.1
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/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { type FileHandle, lstat, open, opendir, realpath } from "node:fs/promise
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
6
6
|
import { ompSessionDir } from "../paths";
|
|
7
|
+
import { isValidImagePayload } from "./image";
|
|
7
8
|
|
|
8
9
|
const MAX_DESCRIPTOR_PREFIX_BYTES = 64 * 1024;
|
|
9
10
|
const MAX_DESCRIPTOR_SUFFIX_BYTES = 64 * 1024;
|
|
@@ -48,6 +49,7 @@ export interface OmpPersistedSessionTranscript {
|
|
|
48
49
|
nativeSessionId: string;
|
|
49
50
|
byteLength: number;
|
|
50
51
|
messages: unknown[];
|
|
52
|
+
imageReplayWarning?: true;
|
|
51
53
|
}
|
|
52
54
|
export interface OmpSessionListOptions {
|
|
53
55
|
cwd?: string;
|
|
@@ -66,6 +68,11 @@ interface ScanBudget {
|
|
|
66
68
|
exhausted: boolean;
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
interface BlobReplayBudget {
|
|
72
|
+
bytes: number;
|
|
73
|
+
imageReplayWarning?: true;
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
export function validateNativeSessionId(value: unknown): string {
|
|
70
77
|
if (typeof value !== "string" || !NATIVE_SESSION_ID.test(value)) {
|
|
71
78
|
throw new Error("Invalid OMP session identifier");
|
|
@@ -156,6 +163,7 @@ async function yieldToEventLoop(): Promise<void> {
|
|
|
156
163
|
setImmediate(result.resolve);
|
|
157
164
|
await result.promise;
|
|
158
165
|
}
|
|
166
|
+
const UNAVAILABLE_IMAGE_MARKER = "[Image unavailable during session replay]";
|
|
159
167
|
async function readStableFile(
|
|
160
168
|
handle: FileHandle,
|
|
161
169
|
byteLength: number,
|
|
@@ -213,6 +221,7 @@ async function hydrateBlobImageData(
|
|
|
213
221
|
) {
|
|
214
222
|
throw new Error("OMP transcript image blob failed ownership or size validation");
|
|
215
223
|
}
|
|
224
|
+
budget.bytes += stat.size;
|
|
216
225
|
const bytes = await readStableFile(
|
|
217
226
|
handle,
|
|
218
227
|
stat.size,
|
|
@@ -222,7 +231,6 @@ async function hydrateBlobImageData(
|
|
|
222
231
|
if (createHash("sha256").update(bytes).digest("hex") !== hash) {
|
|
223
232
|
throw new Error("OMP transcript image blob failed integrity validation");
|
|
224
233
|
}
|
|
225
|
-
budget.bytes += bytes.byteLength;
|
|
226
234
|
return bytes.toString("base64");
|
|
227
235
|
} finally {
|
|
228
236
|
await handle.close().catch(() => undefined);
|
|
@@ -232,11 +240,13 @@ async function hydrateBlobImageData(
|
|
|
232
240
|
async function hydrateImageParts(
|
|
233
241
|
value: unknown,
|
|
234
242
|
blobDirectory: string,
|
|
235
|
-
budget:
|
|
243
|
+
budget: BlobReplayBudget,
|
|
244
|
+
failureMode: "marker" | "omit",
|
|
236
245
|
signal?: AbortSignal,
|
|
237
246
|
): Promise<unknown> {
|
|
238
247
|
if (!Array.isArray(value)) return value;
|
|
239
248
|
let hydrated: unknown[] | undefined;
|
|
249
|
+
let omitted: boolean[] | undefined;
|
|
240
250
|
for (let index = 0; index < value.length; index += 1) {
|
|
241
251
|
const part = value[index];
|
|
242
252
|
if (
|
|
@@ -251,30 +261,53 @@ async function hydrateImageParts(
|
|
|
251
261
|
) {
|
|
252
262
|
continue;
|
|
253
263
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
264
|
+
try {
|
|
265
|
+
const data = await hydrateBlobImageData(part.data, blobDirectory, budget, signal);
|
|
266
|
+
const mimeType = "mimeType" in part ? part.mimeType : undefined;
|
|
267
|
+
if (typeof mimeType !== "string" || !isValidImagePayload(data, mimeType, data.length)) {
|
|
268
|
+
throw new Error("OMP transcript image blob failed MIME validation");
|
|
269
|
+
}
|
|
270
|
+
hydrated ??= [...value];
|
|
271
|
+
hydrated[index] = { ...part, data };
|
|
272
|
+
} catch {
|
|
273
|
+
signal?.throwIfAborted();
|
|
274
|
+
budget.imageReplayWarning = true;
|
|
275
|
+
hydrated ??= [...value];
|
|
276
|
+
if (failureMode === "marker") {
|
|
277
|
+
hydrated[index] = { type: "text", text: UNAVAILABLE_IMAGE_MARKER };
|
|
278
|
+
} else {
|
|
279
|
+
omitted ??= [];
|
|
280
|
+
omitted[index] = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
257
283
|
}
|
|
258
|
-
|
|
284
|
+
if (!hydrated) return value;
|
|
285
|
+
return omitted ? hydrated.filter((_, index) => !omitted[index]) : hydrated;
|
|
259
286
|
}
|
|
260
287
|
|
|
261
288
|
async function hydratePersistedMessageImages(
|
|
262
289
|
message: unknown,
|
|
263
290
|
blobDirectory: string | undefined,
|
|
264
|
-
budget:
|
|
291
|
+
budget: BlobReplayBudget,
|
|
265
292
|
signal?: AbortSignal,
|
|
266
293
|
): Promise<unknown> {
|
|
267
294
|
if (!blobDirectory || !message || typeof message !== "object" || Array.isArray(message)) {
|
|
268
295
|
return message;
|
|
269
296
|
}
|
|
270
297
|
const record = message as Record<string, unknown>;
|
|
271
|
-
let content = await hydrateImageParts(record.content, blobDirectory, budget, signal);
|
|
298
|
+
let content = await hydrateImageParts(record.content, blobDirectory, budget, "marker", signal);
|
|
272
299
|
if (content && typeof content === "object" && !Array.isArray(content)) {
|
|
273
300
|
const contentRecord = content as Record<string, unknown>;
|
|
274
|
-
const nested = await hydrateImageParts(
|
|
301
|
+
const nested = await hydrateImageParts(
|
|
302
|
+
contentRecord.content,
|
|
303
|
+
blobDirectory,
|
|
304
|
+
budget,
|
|
305
|
+
"marker",
|
|
306
|
+
signal,
|
|
307
|
+
);
|
|
275
308
|
if (nested !== contentRecord.content) content = { ...contentRecord, content: nested };
|
|
276
309
|
}
|
|
277
|
-
const images = await hydrateImageParts(record.images, blobDirectory, budget, signal);
|
|
310
|
+
const images = await hydrateImageParts(record.images, blobDirectory, budget, "omit", signal);
|
|
278
311
|
if (content === record.content && images === record.images) return message;
|
|
279
312
|
return { ...record, content, images };
|
|
280
313
|
}
|
|
@@ -620,7 +653,7 @@ export async function readOmpPersistedSessionTranscript(
|
|
|
620
653
|
throw new Error("OMP session transcript exceeds message limits");
|
|
621
654
|
}
|
|
622
655
|
const hydratedMessages: unknown[] = [];
|
|
623
|
-
const blobBudget = { bytes: 0 };
|
|
656
|
+
const blobBudget: BlobReplayBudget = { bytes: 0 };
|
|
624
657
|
for (const message of messages) {
|
|
625
658
|
signal?.throwIfAborted();
|
|
626
659
|
hydratedMessages.push(
|
|
@@ -632,6 +665,7 @@ export async function readOmpPersistedSessionTranscript(
|
|
|
632
665
|
nativeSessionId,
|
|
633
666
|
byteLength: bytes.byteLength,
|
|
634
667
|
messages: hydratedMessages,
|
|
668
|
+
...(blobBudget.imageReplayWarning ? { imageReplayWarning: true as const } : {}),
|
|
635
669
|
};
|
|
636
670
|
} catch (error) {
|
|
637
671
|
if (error instanceof Error) throw error;
|
|
@@ -1592,6 +1592,18 @@ export class OmpProviderSession {
|
|
|
1592
1592
|
replay.signal,
|
|
1593
1593
|
);
|
|
1594
1594
|
messages = transcript.messages;
|
|
1595
|
+
if (transcript.imageReplayWarning) {
|
|
1596
|
+
this.emit({
|
|
1597
|
+
type: "timeline.item",
|
|
1598
|
+
sessionId: this.id,
|
|
1599
|
+
item: {
|
|
1600
|
+
id: "omp:replay-image-unavailable",
|
|
1601
|
+
type: "notification",
|
|
1602
|
+
level: "warning",
|
|
1603
|
+
message: "OMP skipped one or more unavailable images while replaying this session.",
|
|
1604
|
+
},
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1595
1607
|
} catch (error) {
|
|
1596
1608
|
if (replay.signal.aborted) throw error;
|
|
1597
1609
|
this.emit({
|
|
@@ -4542,7 +4554,8 @@ export class OmpProviderSession {
|
|
|
4542
4554
|
turn.deferredAgentEnd = candidate;
|
|
4543
4555
|
void this.subsessions.reconcile(this.runtime).catch(() => {
|
|
4544
4556
|
if (!turn.terminal && this.activeTurn === turn) {
|
|
4545
|
-
this.
|
|
4557
|
+
this.subsessions?.terminalize("failed");
|
|
4558
|
+
this.resumeDeferredAgentEnd();
|
|
4546
4559
|
}
|
|
4547
4560
|
});
|
|
4548
4561
|
return true;
|
|
@@ -21,13 +21,15 @@ import { OmpTimelineProjector, type OmpTimelineScheduler } from "./timeline-proj
|
|
|
21
21
|
|
|
22
22
|
const MAX_CHILDREN = 1_024;
|
|
23
23
|
const MAX_TASK_DISPATCHES = 4_096;
|
|
24
|
-
const MAX_BUFFERED_EVENTS =
|
|
24
|
+
const MAX_BUFFERED_EVENTS = MAX_CHILDREN * 3;
|
|
25
25
|
const MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
26
26
|
const MAX_CHILD_MESSAGE_IDENTITIES = 2_048;
|
|
27
27
|
const MAX_REPLAY_MESSAGES = 100_000;
|
|
28
28
|
const MAX_REPLAY_BYTES = 64 * 1024 * 1024;
|
|
29
29
|
const MAX_REPLAY_NODES = 400_000;
|
|
30
30
|
const MAX_REPLAY_DEPTH = 16;
|
|
31
|
+
const CHILD_REPLAY_UNAVAILABLE = "OMP subagent history is unavailable or incomplete";
|
|
32
|
+
type BufferedSubagentEvent = { event: OmpSubagentEvent; bytes: number };
|
|
31
33
|
|
|
32
34
|
type Emit = (event: ProviderEvent) => void;
|
|
33
35
|
type ChildTerminalStatus = "completed" | "failed" | "canceled";
|
|
@@ -53,6 +55,7 @@ type ChildState = {
|
|
|
53
55
|
status: ChildStatus;
|
|
54
56
|
terminalRequested?: ChildTerminalStatus;
|
|
55
57
|
sessionClosed: boolean;
|
|
58
|
+
replayUnavailable?: boolean;
|
|
56
59
|
seenInSnapshot: boolean;
|
|
57
60
|
seenAssistantIdentities: BoundedStringSet;
|
|
58
61
|
projector: OmpTimelineProjector;
|
|
@@ -63,6 +66,7 @@ type TaskDispatch = {
|
|
|
63
66
|
childSessionIds: Set<string>;
|
|
64
67
|
acknowledged: boolean;
|
|
65
68
|
};
|
|
69
|
+
type ReplayHistory = { sessionFile: string; messages: OmpMessage[] };
|
|
66
70
|
type ReplayBudget = { messages: number; bytes: number; nodes: number };
|
|
67
71
|
const TaskArgsSchema = z.object({
|
|
68
72
|
tasks: z.array(z.unknown()).max(MAX_CHILDREN).optional(),
|
|
@@ -302,12 +306,18 @@ function terminalStatus(status: string): ChildTerminalStatus | undefined {
|
|
|
302
306
|
return;
|
|
303
307
|
}
|
|
304
308
|
|
|
309
|
+
function bufferedNativeId(event: OmpSubagentEvent): string {
|
|
310
|
+
return event.type === "subagent_progress" ? event.payload.progress.id : event.payload.id;
|
|
311
|
+
}
|
|
312
|
+
|
|
305
313
|
export class OmpSubsessionProjector {
|
|
306
314
|
private readonly children = new Map<string, ChildState>();
|
|
307
315
|
private readonly sessionIdByNativeId = new Map<string, string>();
|
|
308
316
|
private readonly toolOwners = new Map<string, string>();
|
|
309
317
|
private readonly dispatches = new Map<string, TaskDispatch>();
|
|
310
|
-
private readonly bufferedEvents:
|
|
318
|
+
private readonly bufferedEvents: BufferedSubagentEvent[] = [];
|
|
319
|
+
// null means every child observed during this replay is omitted after tombstone saturation.
|
|
320
|
+
private omittedBufferedChildren: Set<string> | null = new Set();
|
|
311
321
|
private bufferedBytes = 0;
|
|
312
322
|
private replaying = false;
|
|
313
323
|
private closed = false;
|
|
@@ -374,15 +384,7 @@ export class OmpSubsessionProjector {
|
|
|
374
384
|
handle(event: OmpSubagentEvent): void {
|
|
375
385
|
if (this.closed) return;
|
|
376
386
|
if (this.replaying) {
|
|
377
|
-
|
|
378
|
-
throw new OmpPublicError("OMP subagent replay event limit reached");
|
|
379
|
-
}
|
|
380
|
-
const bytes = boundedJsonBytes(event, MAX_BUFFERED_BYTES, 1_024, MAX_BUFFERED_BYTES, 4_096);
|
|
381
|
-
if (bytes === Number.POSITIVE_INFINITY || this.bufferedBytes + bytes > MAX_BUFFERED_BYTES) {
|
|
382
|
-
throw new OmpPublicError("OMP subagent replay event limit reached");
|
|
383
|
-
}
|
|
384
|
-
this.bufferedEvents.push(event);
|
|
385
|
-
this.bufferedBytes += bytes;
|
|
387
|
+
this.bufferEvent(event);
|
|
386
388
|
return;
|
|
387
389
|
}
|
|
388
390
|
this.apply(event);
|
|
@@ -409,23 +411,48 @@ export class OmpSubsessionProjector {
|
|
|
409
411
|
signal,
|
|
410
412
|
0,
|
|
411
413
|
);
|
|
412
|
-
|
|
414
|
+
let snapshots: OmpSubagentSnapshot[] = [];
|
|
415
|
+
try {
|
|
416
|
+
snapshots = await waitForReplay(runtimeSession.getSubagents(), signal);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (signal.aborted) throw error;
|
|
419
|
+
}
|
|
413
420
|
await this.replaySnapshots(snapshots, runtimeSession, runtime, visited, budget, signal);
|
|
414
421
|
signal.throwIfAborted();
|
|
415
422
|
this.reconcileSnapshots(snapshots);
|
|
416
423
|
completed = true;
|
|
424
|
+
} catch (error) {
|
|
425
|
+
if (signal.aborted) throw error;
|
|
426
|
+
this.terminalize("failed");
|
|
427
|
+
completed = true;
|
|
417
428
|
} finally {
|
|
418
429
|
this.replaying = false;
|
|
419
430
|
const buffered = this.bufferedEvents.splice(0);
|
|
420
431
|
this.bufferedBytes = 0;
|
|
421
432
|
if (completed && !signal.aborted) {
|
|
422
|
-
for (const event of buffered)
|
|
433
|
+
for (const { event } of buffered) {
|
|
434
|
+
if (
|
|
435
|
+
event.type === "subagent_progress" &&
|
|
436
|
+
!terminalStatus(event.payload.progress.status)
|
|
437
|
+
) {
|
|
438
|
+
const sessionId = this.sessionIdByNativeId.get(event.payload.progress.id);
|
|
439
|
+
const child = sessionId ? this.children.get(sessionId) : undefined;
|
|
440
|
+
if (child && child.status !== "running") continue;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
this.apply(event);
|
|
444
|
+
} catch {
|
|
445
|
+
this.terminalize("failed");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
423
448
|
} else {
|
|
449
|
+
this.terminalize("failed");
|
|
424
450
|
this.closed = true;
|
|
425
451
|
for (const child of this.children.values()) child.projector.close();
|
|
426
452
|
this.dispatches.clear();
|
|
427
453
|
this.toolOwners.clear();
|
|
428
454
|
}
|
|
455
|
+
this.omittedBufferedChildren = new Set();
|
|
429
456
|
}
|
|
430
457
|
}
|
|
431
458
|
|
|
@@ -460,6 +487,126 @@ export class OmpSubsessionProjector {
|
|
|
460
487
|
}
|
|
461
488
|
this.bufferedEvents.length = 0;
|
|
462
489
|
this.bufferedBytes = 0;
|
|
490
|
+
this.omittedBufferedChildren = new Set();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private bufferEvent(event: OmpSubagentEvent): void {
|
|
494
|
+
const isProgress = event.type === "subagent_progress";
|
|
495
|
+
const progressTerminal = isProgress ? terminalStatus(event.payload.progress.status) : undefined;
|
|
496
|
+
const isAdvisoryProgress = isProgress && !progressTerminal;
|
|
497
|
+
const bufferedEvent: OmpSubagentEvent =
|
|
498
|
+
isProgress && progressTerminal
|
|
499
|
+
? {
|
|
500
|
+
type: "subagent_lifecycle",
|
|
501
|
+
payload: {
|
|
502
|
+
id: event.payload.progress.id,
|
|
503
|
+
agent: event.payload.agent,
|
|
504
|
+
status: event.payload.progress.status,
|
|
505
|
+
index: event.payload.index,
|
|
506
|
+
...(event.payload.agentSource ? { agentSource: event.payload.agentSource } : {}),
|
|
507
|
+
...(event.payload.parentToolCallId
|
|
508
|
+
? { parentToolCallId: event.payload.parentToolCallId }
|
|
509
|
+
: {}),
|
|
510
|
+
...(event.payload.detached !== undefined ? { detached: event.payload.detached } : {}),
|
|
511
|
+
},
|
|
512
|
+
}
|
|
513
|
+
: event;
|
|
514
|
+
const incomingId = bufferedNativeId(bufferedEvent);
|
|
515
|
+
if (this.isBufferedChildOmitted(incomingId)) return;
|
|
516
|
+
const bytes = boundedJsonBytes(
|
|
517
|
+
bufferedEvent,
|
|
518
|
+
MAX_BUFFERED_BYTES,
|
|
519
|
+
1_024,
|
|
520
|
+
MAX_BUFFERED_BYTES,
|
|
521
|
+
4_096,
|
|
522
|
+
);
|
|
523
|
+
if (bytes === Number.POSITIVE_INFINITY) {
|
|
524
|
+
if (!isAdvisoryProgress) this.omitBufferedChild(incomingId);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (isAdvisoryProgress) {
|
|
528
|
+
for (let index = this.bufferedEvents.length - 1; index >= 0; index -= 1) {
|
|
529
|
+
const queued = this.bufferedEvents[index]?.event;
|
|
530
|
+
if (!queued) continue;
|
|
531
|
+
if (
|
|
532
|
+
queued.type === "subagent_lifecycle" &&
|
|
533
|
+
queued.payload.id === incomingId &&
|
|
534
|
+
terminalStatus(queued.payload.status)
|
|
535
|
+
) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (queued.type !== "subagent_progress" || queued.payload.progress.id !== incomingId) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (terminalStatus(queued.payload.progress.status)) return;
|
|
542
|
+
const [removed] = this.bufferedEvents.splice(index, 1);
|
|
543
|
+
this.bufferedBytes -= removed?.bytes ?? 0;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const bufferedIds = new Set<string>();
|
|
549
|
+
for (const child of this.children.values()) bufferedIds.add(child.nativeId);
|
|
550
|
+
for (const { event: queued } of this.bufferedEvents) bufferedIds.add(bufferedNativeId(queued));
|
|
551
|
+
if (!bufferedIds.has(incomingId) && bufferedIds.size >= MAX_CHILDREN) {
|
|
552
|
+
if (isAdvisoryProgress) return;
|
|
553
|
+
const advisory = this.bufferedEvents.find(
|
|
554
|
+
({ event: queued }) =>
|
|
555
|
+
queued.type === "subagent_progress" && !terminalStatus(queued.payload.progress.status),
|
|
556
|
+
);
|
|
557
|
+
if (!advisory) {
|
|
558
|
+
this.omitBufferedChild(incomingId);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const evictedId = bufferedNativeId(advisory.event);
|
|
562
|
+
this.omitBufferedChild(evictedId);
|
|
563
|
+
if (this.isBufferedChildOmitted(incomingId)) return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
while (
|
|
567
|
+
this.bufferedEvents.length >= MAX_BUFFERED_EVENTS ||
|
|
568
|
+
this.bufferedBytes + bytes > MAX_BUFFERED_BYTES
|
|
569
|
+
) {
|
|
570
|
+
const advisoryIndex = this.bufferedEvents.findIndex(
|
|
571
|
+
({ event: queued }) =>
|
|
572
|
+
queued.type === "subagent_progress" && !terminalStatus(queued.payload.progress.status),
|
|
573
|
+
);
|
|
574
|
+
if (advisoryIndex < 0) {
|
|
575
|
+
if (!isAdvisoryProgress) this.omitBufferedChild(incomingId);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const [removed] = this.bufferedEvents.splice(advisoryIndex, 1);
|
|
579
|
+
this.bufferedBytes -= removed?.bytes ?? 0;
|
|
580
|
+
}
|
|
581
|
+
this.bufferedEvents.push({ event: bufferedEvent, bytes });
|
|
582
|
+
this.bufferedBytes += bytes;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private isBufferedChildOmitted(nativeId: string): boolean {
|
|
586
|
+
return this.omittedBufferedChildren === null || this.omittedBufferedChildren.has(nativeId);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private omitBufferedChild(nativeId: string): void {
|
|
590
|
+
const omitted = this.omittedBufferedChildren;
|
|
591
|
+
if (!omitted || omitted.has(nativeId)) return;
|
|
592
|
+
if (omitted.size >= MAX_CHILDREN) {
|
|
593
|
+
this.omittedBufferedChildren = null;
|
|
594
|
+
this.bufferedEvents.length = 0;
|
|
595
|
+
this.bufferedBytes = 0;
|
|
596
|
+
for (const child of this.children.values()) child.replayUnavailable = true;
|
|
597
|
+
this.terminalize("failed");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
omitted.add(nativeId);
|
|
601
|
+
for (let index = this.bufferedEvents.length - 1; index >= 0; index -= 1) {
|
|
602
|
+
const queued = this.bufferedEvents[index];
|
|
603
|
+
if (!queued || bufferedNativeId(queued.event) !== nativeId) continue;
|
|
604
|
+
const [removed] = this.bufferedEvents.splice(index, 1);
|
|
605
|
+
this.bufferedBytes -= removed?.bytes ?? 0;
|
|
606
|
+
}
|
|
607
|
+
const sessionId = this.sessionIdByNativeId.get(nativeId);
|
|
608
|
+
const child = sessionId ? this.children.get(sessionId) : undefined;
|
|
609
|
+
if (child?.status === "running") this.failReplayChild(child);
|
|
463
610
|
}
|
|
464
611
|
|
|
465
612
|
private apply(event: OmpSubagentEvent): void {
|
|
@@ -587,6 +734,7 @@ export class OmpSubsessionProjector {
|
|
|
587
734
|
}
|
|
588
735
|
|
|
589
736
|
private restartChild(child: ChildState): void {
|
|
737
|
+
child.replayUnavailable = false;
|
|
590
738
|
if (child.status === "running") return;
|
|
591
739
|
child.status = "running";
|
|
592
740
|
child.terminalRequested = undefined;
|
|
@@ -607,7 +755,12 @@ export class OmpSubsessionProjector {
|
|
|
607
755
|
if (!this.hasDirectActivity(child.sessionId)) this.finishChild(child, status);
|
|
608
756
|
}
|
|
609
757
|
|
|
610
|
-
private finishChild(
|
|
758
|
+
private finishChild(
|
|
759
|
+
child: ChildState,
|
|
760
|
+
status: ChildTerminalStatus,
|
|
761
|
+
force = false,
|
|
762
|
+
errorMessage = "OMP subagent failed",
|
|
763
|
+
): void {
|
|
611
764
|
if (child.status !== "running") return;
|
|
612
765
|
if (!force && this.hasDirectActivity(child.sessionId)) {
|
|
613
766
|
child.terminalRequested = status;
|
|
@@ -620,7 +773,7 @@ export class OmpSubsessionProjector {
|
|
|
620
773
|
sessionId: child.sessionId,
|
|
621
774
|
turnId: child.turnId,
|
|
622
775
|
state: status,
|
|
623
|
-
...(status === "failed" ? { error: { message:
|
|
776
|
+
...(status === "failed" ? { error: { message: errorMessage } } : {}),
|
|
624
777
|
});
|
|
625
778
|
for (const [toolCallId, dispatch] of this.dispatches) {
|
|
626
779
|
if (dispatch.childSessionIds.has(child.sessionId)) this.settleDispatch(toolCallId, dispatch);
|
|
@@ -631,6 +784,10 @@ export class OmpSubsessionProjector {
|
|
|
631
784
|
}
|
|
632
785
|
this.onActivityChange();
|
|
633
786
|
}
|
|
787
|
+
private failReplayChild(child: ChildState): void {
|
|
788
|
+
child.replayUnavailable = true;
|
|
789
|
+
this.finishChild(child, "failed", true, CHILD_REPLAY_UNAVAILABLE);
|
|
790
|
+
}
|
|
634
791
|
|
|
635
792
|
private hasDirectActivity(ownerSessionId: string): boolean {
|
|
636
793
|
for (const child of this.children.values()) {
|
|
@@ -691,6 +848,10 @@ export class OmpSubsessionProjector {
|
|
|
691
848
|
this.resolveParent(snapshot.parentToolCallId, snapshot.sessionFile),
|
|
692
849
|
);
|
|
693
850
|
present.add(child.nativeId);
|
|
851
|
+
if (child.replayUnavailable) {
|
|
852
|
+
child.seenInSnapshot = true;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
694
855
|
const terminal = terminalStatus(snapshot.status);
|
|
695
856
|
if (terminal) this.requestTerminal(child, terminal);
|
|
696
857
|
else this.restartChild(child);
|
|
@@ -712,36 +873,75 @@ export class OmpSubsessionProjector {
|
|
|
712
873
|
budget: ReplayBudget,
|
|
713
874
|
signal: AbortSignal,
|
|
714
875
|
): Promise<void> {
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
876
|
+
const collected: Array<{
|
|
877
|
+
snapshot: OmpSubagentSnapshot;
|
|
878
|
+
sessionFile?: string;
|
|
879
|
+
messages?: OmpMessage[];
|
|
880
|
+
}> = [];
|
|
881
|
+
for (const snapshot of snapshots) {
|
|
720
882
|
signal.throwIfAborted();
|
|
721
883
|
if (this.sessionIdByNativeId.has(snapshot.id)) continue;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
884
|
+
try {
|
|
885
|
+
const history = await waitForReplay(
|
|
886
|
+
runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
|
|
887
|
+
signal,
|
|
888
|
+
);
|
|
889
|
+
try {
|
|
890
|
+
this.accountReplay(history.messages, budget, signal);
|
|
891
|
+
collected.push({
|
|
892
|
+
snapshot,
|
|
893
|
+
sessionFile: history.sessionFile,
|
|
894
|
+
messages: history.messages,
|
|
895
|
+
});
|
|
896
|
+
} catch (error) {
|
|
897
|
+
if (signal.aborted) throw error;
|
|
898
|
+
collected.push({ snapshot, sessionFile: history.sessionFile });
|
|
899
|
+
}
|
|
900
|
+
} catch (error) {
|
|
901
|
+
if (signal.aborted) throw error;
|
|
902
|
+
collected.push({ snapshot });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
collected.sort((left, right) => {
|
|
906
|
+
const leftDepth = left.sessionFile?.split("/").length ?? Number.MAX_SAFE_INTEGER;
|
|
907
|
+
const rightDepth = right.sessionFile?.split("/").length ?? Number.MAX_SAFE_INTEGER;
|
|
908
|
+
return leftDepth - rightDepth;
|
|
909
|
+
});
|
|
910
|
+
const snapshotIds = new Set(collected.map(({ snapshot }) => snapshot.id));
|
|
911
|
+
for (const { snapshot, sessionFile, messages } of collected) {
|
|
727
912
|
signal.throwIfAborted();
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
913
|
+
if (this.sessionIdByNativeId.has(snapshot.id)) continue;
|
|
914
|
+
let child: ChildState | undefined;
|
|
915
|
+
try {
|
|
916
|
+
child = this.ensureChild(
|
|
917
|
+
{ ...snapshot, sessionFile },
|
|
918
|
+
this.resolveParent(snapshot.parentToolCallId, sessionFile),
|
|
919
|
+
);
|
|
920
|
+
if (!messages || this.isBufferedChildOmitted(snapshot.id)) {
|
|
921
|
+
this.failReplayChild(child);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
this.projectReplay(child, messages, signal);
|
|
925
|
+
if (!sessionFile) {
|
|
926
|
+
this.failReplayChild(child);
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
visited.add(`${sessionFile}\0${snapshot.id}`);
|
|
930
|
+
await this.replayChildren(
|
|
931
|
+
child.sessionId,
|
|
932
|
+
sessionFile,
|
|
933
|
+
messages,
|
|
934
|
+
runtime,
|
|
935
|
+
visited,
|
|
936
|
+
budget,
|
|
937
|
+
signal,
|
|
938
|
+
1,
|
|
939
|
+
snapshotIds,
|
|
940
|
+
);
|
|
941
|
+
} catch (error) {
|
|
942
|
+
if (signal.aborted) throw error;
|
|
943
|
+
if (child) this.failReplayChild(child);
|
|
944
|
+
}
|
|
745
945
|
}
|
|
746
946
|
const activeToolCallIds = new Set(
|
|
747
947
|
snapshots.flatMap((snapshot) =>
|
|
@@ -764,46 +964,64 @@ export class OmpSubsessionProjector {
|
|
|
764
964
|
budget: ReplayBudget,
|
|
765
965
|
signal: AbortSignal,
|
|
766
966
|
depth: number,
|
|
967
|
+
snapshotIds?: ReadonlySet<string>,
|
|
767
968
|
): Promise<void> {
|
|
768
969
|
signal.throwIfAborted();
|
|
769
970
|
if (depth > MAX_REPLAY_DEPTH) throw new OmpPublicError("OMP subagent history is too deep");
|
|
770
971
|
this.indexTaskCalls(parentSessionId, messages);
|
|
771
972
|
for (const ref of replayChildren(messages)) {
|
|
772
973
|
signal.throwIfAborted();
|
|
773
|
-
if (!parentSessionFile)
|
|
774
|
-
throw new OmpPublicError("OMP parent transcript identity is unavailable");
|
|
775
|
-
}
|
|
974
|
+
if (!parentSessionFile || snapshotIds?.has(ref.id)) continue;
|
|
776
975
|
const visitKey = `${parentSessionFile}\0${ref.id}`;
|
|
777
976
|
if (visited.has(visitKey)) continue;
|
|
778
977
|
visited.add(visitKey);
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
978
|
+
let child: ChildState | undefined;
|
|
979
|
+
let history: ReplayHistory | undefined;
|
|
980
|
+
try {
|
|
981
|
+
const loaded = await waitForReplay(
|
|
982
|
+
runtime.readPersistedSubagentTranscript({
|
|
983
|
+
parentSessionFile,
|
|
984
|
+
childTranscriptId: ref.id,
|
|
985
|
+
cwd: this.cwd,
|
|
986
|
+
signal,
|
|
987
|
+
}),
|
|
784
988
|
signal,
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
989
|
+
);
|
|
990
|
+
history = loaded;
|
|
991
|
+
child = this.ensureChild({ ...ref, sessionFile: loaded.sessionFile }, parentSessionId);
|
|
992
|
+
this.accountReplay(loaded.messages, budget, signal);
|
|
993
|
+
signal.throwIfAborted();
|
|
994
|
+
this.projectReplay(child, loaded.messages, signal);
|
|
995
|
+
await this.replayChildren(
|
|
996
|
+
child.sessionId,
|
|
997
|
+
history.sessionFile,
|
|
998
|
+
history.messages,
|
|
999
|
+
runtime,
|
|
1000
|
+
visited,
|
|
1001
|
+
budget,
|
|
1002
|
+
signal,
|
|
1003
|
+
depth + 1,
|
|
1004
|
+
snapshotIds,
|
|
1005
|
+
);
|
|
1006
|
+
signal.throwIfAborted();
|
|
1007
|
+
this.requestTerminal(
|
|
1008
|
+
child,
|
|
1009
|
+
ref.status === "derive" ? replayTerminalStatus(history.messages) : ref.status,
|
|
1010
|
+
);
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
if (signal.aborted) throw error;
|
|
1013
|
+
if (!child) {
|
|
1014
|
+
try {
|
|
1015
|
+
child = this.ensureChild(
|
|
1016
|
+
{ ...ref, sessionFile: history?.sessionFile },
|
|
1017
|
+
parentSessionId,
|
|
1018
|
+
);
|
|
1019
|
+
} catch {
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
this.failReplayChild(child);
|
|
1024
|
+
}
|
|
807
1025
|
}
|
|
808
1026
|
}
|
|
809
1027
|
|