@omercnet/paseo-omp 0.3.0-next.101.1 → 0.3.0-next.103.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 +1 -1
- package/server/provider/omp-rpc.ts +10 -2
- package/server/provider/session.ts +15 -10
- package/server/provider/subsessions.ts +290 -72
package/package.json
CHANGED
|
@@ -1262,6 +1262,7 @@ const ProtocolNegotiationResultSchema = z.object({
|
|
|
1262
1262
|
});
|
|
1263
1263
|
|
|
1264
1264
|
export type OmpModel = z.infer<typeof OmpModelSchema>;
|
|
1265
|
+
export type OmpBranchResult = z.infer<typeof OmpBranchResultSchema>;
|
|
1265
1266
|
export type OmpSessionState = z.infer<typeof OmpSessionStateSchema>;
|
|
1266
1267
|
export type OmpSessionStats = z.infer<typeof OmpSessionStatsSchema>;
|
|
1267
1268
|
export type OmpCompactionResult = z.infer<typeof OmpCompactionResultSchema>;
|
|
@@ -1369,7 +1370,7 @@ export interface OmpRuntimeSession {
|
|
|
1369
1370
|
respondToExtensionUi(response: OmpExtensionUiResponse): Promise<void>;
|
|
1370
1371
|
respondToToolApproval(response: OmpToolApprovalResponse): Promise<void>;
|
|
1371
1372
|
getBranchMessages(): Promise<Array<{ entryId: string; text: string }>>;
|
|
1372
|
-
branch(entryId: string): Promise<
|
|
1373
|
+
branch(entryId: string): Promise<OmpBranchResult>;
|
|
1373
1374
|
readonly canReplayHistory: boolean;
|
|
1374
1375
|
getMessages(): Promise<OmpMessage[]>;
|
|
1375
1376
|
abort(): Promise<void>;
|
|
@@ -1416,6 +1417,13 @@ export interface OmpRpcRuntimeOptions {
|
|
|
1416
1417
|
) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
|
|
1417
1418
|
}
|
|
1418
1419
|
|
|
1420
|
+
export class OmpRpcRequestRejectedError extends Error {
|
|
1421
|
+
constructor() {
|
|
1422
|
+
super("OMP RPC request failed");
|
|
1423
|
+
this.name = "OmpRpcRequestRejectedError";
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1419
1427
|
type PendingRequest = {
|
|
1420
1428
|
resolve(value: unknown): void;
|
|
1421
1429
|
reject(error: Error): void;
|
|
@@ -2517,7 +2525,7 @@ class OmpRpcProcess {
|
|
|
2517
2525
|
settled.reject(new Error("OMP RPC response is invalid"));
|
|
2518
2526
|
}
|
|
2519
2527
|
} else {
|
|
2520
|
-
settled.reject(new
|
|
2528
|
+
settled.reject(new OmpRpcRequestRejectedError());
|
|
2521
2529
|
}
|
|
2522
2530
|
}
|
|
2523
2531
|
|
|
@@ -21,6 +21,7 @@ import { OmpHostToolsBridge, type OmpMcpConnector, validateOmpHostToolConfig } f
|
|
|
21
21
|
import { isOmpImageMimeType, isValidImagePayload, OmpImageMaterializer } from "./image";
|
|
22
22
|
import type {
|
|
23
23
|
OmpAvailableCommand,
|
|
24
|
+
OmpBranchResult,
|
|
24
25
|
OmpCompactionResult,
|
|
25
26
|
OmpExtensionUiResponse,
|
|
26
27
|
OmpImage,
|
|
@@ -35,7 +36,7 @@ import type {
|
|
|
35
36
|
OmpToolApprovalCancel,
|
|
36
37
|
OmpToolApprovalRequest,
|
|
37
38
|
} from "./omp-rpc";
|
|
38
|
-
import { buildOmpSpawnRequest } from "./omp-rpc";
|
|
39
|
+
import { buildOmpSpawnRequest, OmpRpcRequestRejectedError } from "./omp-rpc";
|
|
39
40
|
import {
|
|
40
41
|
BoundedStringSet,
|
|
41
42
|
boundedJsonBytes,
|
|
@@ -1679,19 +1680,22 @@ export class OmpProviderSession {
|
|
|
1679
1680
|
throw new OmpPublicError("OMP conversation rewind requires negotiated RPC protocol v2");
|
|
1680
1681
|
}
|
|
1681
1682
|
const entryId = this.projector.resolveRevertToken(input.token);
|
|
1682
|
-
const
|
|
1683
|
-
runtime.getBranchMessages(),
|
|
1684
|
-
runtime.getState(),
|
|
1685
|
-
]);
|
|
1683
|
+
const beforeState = await runtime.getState();
|
|
1686
1684
|
this.requireCurrentRuntime(runtime, generation);
|
|
1687
1685
|
if (this.activeTurn || beforeState.isStreaming || beforeState.isCompacting) {
|
|
1688
1686
|
throw new OmpPublicError("Cannot rewind the OMP conversation while a turn is active");
|
|
1689
1687
|
}
|
|
1690
|
-
if (!branchMessages.some((message) => message.entryId === entryId)) {
|
|
1691
|
-
throw new OmpPublicError("OMP conversation rewind token is stale");
|
|
1692
|
-
}
|
|
1693
1688
|
branchMutationPossible = true;
|
|
1694
|
-
|
|
1689
|
+
let result: OmpBranchResult;
|
|
1690
|
+
try {
|
|
1691
|
+
result = await runtime.branch(entryId);
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
if (error instanceof OmpRpcRequestRejectedError) {
|
|
1694
|
+
branchMutationPossible = false;
|
|
1695
|
+
throw new OmpPublicError("OMP conversation rewind token is stale");
|
|
1696
|
+
}
|
|
1697
|
+
throw error;
|
|
1698
|
+
}
|
|
1695
1699
|
this.requireCurrentRuntime(runtime, generation);
|
|
1696
1700
|
if (result.cancelled) {
|
|
1697
1701
|
branchMutationPossible = false;
|
|
@@ -4554,7 +4558,8 @@ export class OmpProviderSession {
|
|
|
4554
4558
|
turn.deferredAgentEnd = candidate;
|
|
4555
4559
|
void this.subsessions.reconcile(this.runtime).catch(() => {
|
|
4556
4560
|
if (!turn.terminal && this.activeTurn === turn) {
|
|
4557
|
-
this.
|
|
4561
|
+
this.subsessions?.terminalize("failed");
|
|
4562
|
+
this.resumeDeferredAgentEnd();
|
|
4558
4563
|
}
|
|
4559
4564
|
});
|
|
4560
4565
|
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
|
|