@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "0.3.0-next.101.1",
3
+ "version": "0.3.0-next.103.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -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<{ text: string; cancelled: boolean }>;
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 Error("OMP RPC request failed"));
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 [branchMessages, beforeState] = await Promise.all([
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
- const result = await runtime.branch(entryId);
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.handleRuntimeFailure("OMP subagent reconciliation failed");
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 = 1_024;
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: OmpSubagentEvent[] = [];
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
- if (this.bufferedEvents.length >= MAX_BUFFERED_EVENTS) {
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
- const snapshots = await waitForReplay(runtimeSession.getSubagents(), signal);
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) this.apply(event);
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(child: ChildState, status: ChildTerminalStatus, force = false): void {
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: "OMP subagent failed" } } : {}),
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 ordered = [...snapshots].sort(
716
- (left, right) =>
717
- (left.sessionFile?.split("/").length ?? 0) - (right.sessionFile?.split("/").length ?? 0),
718
- );
719
- for (const snapshot of ordered) {
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
- const history = await waitForReplay(
723
- runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
724
- signal,
725
- );
726
- this.accountReplay(history.messages, budget, signal);
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
- const parentSessionId = this.resolveParent(snapshot.parentToolCallId, history.sessionFile);
729
- const child = this.ensureChild(
730
- { ...snapshot, sessionFile: history.sessionFile },
731
- parentSessionId,
732
- );
733
- this.projectReplay(child, history.messages, signal);
734
- visited.add(`${history.sessionFile}\0${snapshot.id}`);
735
- await this.replayChildren(
736
- child.sessionId,
737
- history.sessionFile,
738
- history.messages,
739
- runtime,
740
- visited,
741
- budget,
742
- signal,
743
- 1,
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
- const history = await waitForReplay(
780
- runtime.readPersistedSubagentTranscript({
781
- parentSessionFile,
782
- childTranscriptId: ref.id,
783
- cwd: this.cwd,
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
- signal,
787
- );
788
- this.accountReplay(history.messages, budget, signal);
789
- signal.throwIfAborted();
790
- const child = this.ensureChild({ ...ref, sessionFile: history.sessionFile }, parentSessionId);
791
- this.projectReplay(child, history.messages, signal);
792
- await this.replayChildren(
793
- child.sessionId,
794
- history.sessionFile,
795
- history.messages,
796
- runtime,
797
- visited,
798
- budget,
799
- signal,
800
- depth + 1,
801
- );
802
- signal.throwIfAborted();
803
- this.requestTerminal(
804
- child,
805
- ref.status === "derive" ? replayTerminalStatus(history.messages) : ref.status,
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