@omercnet/paseo-omp 0.3.0 → 0.4.0-next.119.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.
Files changed (43) hide show
  1. package/README.md +19 -5
  2. package/client/omp-config-surface.tsx +243 -24
  3. package/client/omp-config-views.ts +24 -0
  4. package/client/omp-model-picker-state.ts +145 -0
  5. package/client/omp-model-picker.tsx +282 -0
  6. package/client/omp-routing-editor.tsx +307 -0
  7. package/client/support-diagnostics-state.ts +45 -0
  8. package/index.server.ts +27 -2
  9. package/package.json +4 -8
  10. package/paseo-plugin.json +1 -1
  11. package/server/omp-models.ts +59 -0
  12. package/server/omp-settings.ts +30 -20
  13. package/server/operational-failure-diagnostics.ts +76 -0
  14. package/server/package-version.ts +2 -0
  15. package/server/protocol-violation-diagnostics.ts +169 -0
  16. package/server/provider/catalog.ts +39 -10
  17. package/server/provider/connection.ts +88 -9
  18. package/server/provider/host-tools.ts +284 -34
  19. package/server/provider/mcp-transport.ts +2 -1
  20. package/server/provider/omp-rpc.ts +1785 -267
  21. package/server/provider/profile-providers.ts +7 -2
  22. package/server/provider/registration.ts +12 -2
  23. package/server/provider/security.ts +8 -10
  24. package/server/provider/session-descriptors.ts +97 -29
  25. package/server/provider/session.ts +212 -59
  26. package/server/provider/subsessions.ts +334 -74
  27. package/server/provider/timeline-projector.ts +125 -32
  28. package/server/support-diagnostics.ts +284 -0
  29. package/shared/omp-models.ts +49 -0
  30. package/shared/omp-settings.ts +227 -3
  31. package/shared/support-diagnostics.ts +32 -0
  32. package/CHANGELOG.md +0 -113
  33. package/SUPPORT.md +0 -44
  34. package/TESTING.md +0 -150
  35. package/docs/alpha-release-checklist.md +0 -68
  36. package/docs/configuration.md +0 -126
  37. package/docs/core-provider-issue-audit.md +0 -109
  38. package/docs/images/mcp-authorization-compact.png +0 -0
  39. package/docs/images/mcp-controls-wide.png +0 -0
  40. package/docs/images/plugin-manager.png +0 -0
  41. package/docs/images/workspace-settings.png +0 -0
  42. package/docs/installation.md +0 -89
  43. package/tsconfig.json +0 -16
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { basename, extname } from "node:path";
2
+ import { basename, extname, isAbsolute } from "node:path";
3
3
  import type { ProviderEvent } from "@getpaseo/plugin/server/provider";
4
4
  import { z } from "zod";
5
5
  import type {
@@ -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(),
@@ -94,6 +98,18 @@ const TaskResultDetailsSchema = z.object({
94
98
  exitCode: z.number().optional(),
95
99
  error: z.unknown().optional(),
96
100
  aborted: z.boolean().optional(),
101
+ status: z
102
+ .enum([
103
+ "pending",
104
+ "running",
105
+ "completed",
106
+ "failed",
107
+ "error",
108
+ "aborted",
109
+ "canceled",
110
+ "cancelled",
111
+ ])
112
+ .optional(),
97
113
  }),
98
114
  )
99
115
  .max(MAX_CHILDREN),
@@ -192,8 +208,15 @@ function replayChildren(messages: readonly OmpMessage[]): ReplayChildRef[] {
192
208
  const details = taskResultDetails(message);
193
209
  const results = details?.results ?? [];
194
210
  for (const result of results) {
211
+ const canceled =
212
+ result.aborted === true ||
213
+ result.status === "aborted" ||
214
+ result.status === "canceled" ||
215
+ result.status === "cancelled";
195
216
  const failed =
196
217
  message.isError === true ||
218
+ result.status === "failed" ||
219
+ result.status === "error" ||
197
220
  Boolean(result.error) ||
198
221
  (typeof result.exitCode === "number" && result.exitCode !== 0);
199
222
  children.push({
@@ -201,7 +224,7 @@ function replayChildren(messages: readonly OmpMessage[]): ReplayChildRef[] {
201
224
  agent: result.agent ?? call?.title,
202
225
  description: call?.description,
203
226
  parentToolCallId: message.toolCallId,
204
- status: result.aborted === true ? "canceled" : failed ? "failed" : "completed",
227
+ status: canceled ? "canceled" : failed ? "failed" : "completed",
205
228
  });
206
229
  }
207
230
  const resultIds = new Set(results.map((result) => result.id));
@@ -283,12 +306,18 @@ function terminalStatus(status: string): ChildTerminalStatus | undefined {
283
306
  return;
284
307
  }
285
308
 
309
+ function bufferedNativeId(event: OmpSubagentEvent): string {
310
+ return event.type === "subagent_progress" ? event.payload.progress.id : event.payload.id;
311
+ }
312
+
286
313
  export class OmpSubsessionProjector {
287
314
  private readonly children = new Map<string, ChildState>();
288
315
  private readonly sessionIdByNativeId = new Map<string, string>();
289
316
  private readonly toolOwners = new Map<string, string>();
290
317
  private readonly dispatches = new Map<string, TaskDispatch>();
291
- 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();
292
321
  private bufferedBytes = 0;
293
322
  private replaying = false;
294
323
  private closed = false;
@@ -355,15 +384,7 @@ export class OmpSubsessionProjector {
355
384
  handle(event: OmpSubagentEvent): void {
356
385
  if (this.closed) return;
357
386
  if (this.replaying) {
358
- if (this.bufferedEvents.length >= MAX_BUFFERED_EVENTS) {
359
- throw new OmpPublicError("OMP subagent replay event limit reached");
360
- }
361
- const bytes = boundedJsonBytes(event, MAX_BUFFERED_BYTES, 1_024, MAX_BUFFERED_BYTES, 4_096);
362
- if (bytes === Number.POSITIVE_INFINITY || this.bufferedBytes + bytes > MAX_BUFFERED_BYTES) {
363
- throw new OmpPublicError("OMP subagent replay event limit reached");
364
- }
365
- this.bufferedEvents.push(event);
366
- this.bufferedBytes += bytes;
387
+ this.bufferEvent(event);
367
388
  return;
368
389
  }
369
390
  this.apply(event);
@@ -390,23 +411,48 @@ export class OmpSubsessionProjector {
390
411
  signal,
391
412
  0,
392
413
  );
393
- 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
+ }
394
420
  await this.replaySnapshots(snapshots, runtimeSession, runtime, visited, budget, signal);
395
421
  signal.throwIfAborted();
396
422
  this.reconcileSnapshots(snapshots);
397
423
  completed = true;
424
+ } catch (error) {
425
+ if (signal.aborted) throw error;
426
+ this.terminalize("failed");
427
+ completed = true;
398
428
  } finally {
399
429
  this.replaying = false;
400
430
  const buffered = this.bufferedEvents.splice(0);
401
431
  this.bufferedBytes = 0;
402
432
  if (completed && !signal.aborted) {
403
- 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
+ }
404
448
  } else {
449
+ this.terminalize("failed");
405
450
  this.closed = true;
406
451
  for (const child of this.children.values()) child.projector.close();
407
452
  this.dispatches.clear();
408
453
  this.toolOwners.clear();
409
454
  }
455
+ this.omittedBufferedChildren = new Set();
410
456
  }
411
457
  }
412
458
 
@@ -441,6 +487,126 @@ export class OmpSubsessionProjector {
441
487
  }
442
488
  this.bufferedEvents.length = 0;
443
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);
444
610
  }
445
611
 
446
612
  private apply(event: OmpSubagentEvent): void {
@@ -555,6 +721,7 @@ export class OmpSubsessionProjector {
555
721
  type: "session.opened",
556
722
  sessionId,
557
723
  parentSessionId,
724
+ toolCallId: ref.parentToolCallId,
558
725
  capabilities: ["session.subsession"],
559
726
  restoration: "parent",
560
727
  cwd: this.cwd,
@@ -567,6 +734,7 @@ export class OmpSubsessionProjector {
567
734
  }
568
735
 
569
736
  private restartChild(child: ChildState): void {
737
+ child.replayUnavailable = false;
570
738
  if (child.status === "running") return;
571
739
  child.status = "running";
572
740
  child.terminalRequested = undefined;
@@ -587,7 +755,12 @@ export class OmpSubsessionProjector {
587
755
  if (!this.hasDirectActivity(child.sessionId)) this.finishChild(child, status);
588
756
  }
589
757
 
590
- 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 {
591
764
  if (child.status !== "running") return;
592
765
  if (!force && this.hasDirectActivity(child.sessionId)) {
593
766
  child.terminalRequested = status;
@@ -600,7 +773,7 @@ export class OmpSubsessionProjector {
600
773
  sessionId: child.sessionId,
601
774
  turnId: child.turnId,
602
775
  state: status,
603
- ...(status === "failed" ? { error: { message: "OMP subagent failed" } } : {}),
776
+ ...(status === "failed" ? { error: { message: errorMessage } } : {}),
604
777
  });
605
778
  for (const [toolCallId, dispatch] of this.dispatches) {
606
779
  if (dispatch.childSessionIds.has(child.sessionId)) this.settleDispatch(toolCallId, dispatch);
@@ -611,6 +784,10 @@ export class OmpSubsessionProjector {
611
784
  }
612
785
  this.onActivityChange();
613
786
  }
787
+ private failReplayChild(child: ChildState): void {
788
+ child.replayUnavailable = true;
789
+ this.finishChild(child, "failed", true, CHILD_REPLAY_UNAVAILABLE);
790
+ }
614
791
 
615
792
  private hasDirectActivity(ownerSessionId: string): boolean {
616
793
  for (const child of this.children.values()) {
@@ -671,6 +848,10 @@ export class OmpSubsessionProjector {
671
848
  this.resolveParent(snapshot.parentToolCallId, snapshot.sessionFile),
672
849
  );
673
850
  present.add(child.nativeId);
851
+ if (child.replayUnavailable) {
852
+ child.seenInSnapshot = true;
853
+ continue;
854
+ }
674
855
  const terminal = terminalStatus(snapshot.status);
675
856
  if (terminal) this.requestTerminal(child, terminal);
676
857
  else this.restartChild(child);
@@ -692,36 +873,97 @@ export class OmpSubsessionProjector {
692
873
  budget: ReplayBudget,
693
874
  signal: AbortSignal,
694
875
  ): Promise<void> {
695
- const ordered = [...snapshots].sort(
696
- (left, right) =>
697
- (left.sessionFile?.split("/").length ?? 0) - (right.sessionFile?.split("/").length ?? 0),
698
- );
699
- 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) {
700
882
  signal.throwIfAborted();
701
883
  if (this.sessionIdByNativeId.has(snapshot.id)) continue;
702
- const history = await waitForReplay(
703
- runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
704
- signal,
705
- );
706
- this.accountReplay(history.messages, budget, signal);
884
+ try {
885
+ let history: ReplayHistory;
886
+ if (snapshot.sessionFile && isAbsolute(snapshot.sessionFile) && this.rootSessionFile) {
887
+ try {
888
+ history = await waitForReplay(
889
+ runtime.readPersistedSubagentTranscript({
890
+ parentSessionFile: this.rootSessionFile,
891
+ childTranscriptId: snapshot.id,
892
+ sessionFile: snapshot.sessionFile,
893
+ cwd: this.cwd,
894
+ signal,
895
+ }),
896
+ signal,
897
+ );
898
+ } catch (error) {
899
+ if (signal.aborted) throw error;
900
+ history = await waitForReplay(
901
+ runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
902
+ signal,
903
+ );
904
+ }
905
+ } else {
906
+ history = await waitForReplay(
907
+ runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
908
+ signal,
909
+ );
910
+ }
911
+ try {
912
+ this.accountReplay(history.messages, budget, signal);
913
+ collected.push({
914
+ snapshot,
915
+ sessionFile: history.sessionFile,
916
+ messages: history.messages,
917
+ });
918
+ } catch (error) {
919
+ if (signal.aborted) throw error;
920
+ collected.push({ snapshot, sessionFile: history.sessionFile });
921
+ }
922
+ } catch (error) {
923
+ if (signal.aborted) throw error;
924
+ collected.push({ snapshot, sessionFile: snapshot.sessionFile });
925
+ }
926
+ }
927
+ collected.sort((left, right) => {
928
+ const leftDepth = left.sessionFile?.split("/").length ?? Number.MAX_SAFE_INTEGER;
929
+ const rightDepth = right.sessionFile?.split("/").length ?? Number.MAX_SAFE_INTEGER;
930
+ return leftDepth - rightDepth;
931
+ });
932
+ const snapshotIds = new Set(collected.map(({ snapshot }) => snapshot.id));
933
+ for (const { snapshot, sessionFile, messages } of collected) {
707
934
  signal.throwIfAborted();
708
- const parentSessionId = this.resolveParent(snapshot.parentToolCallId, history.sessionFile);
709
- const child = this.ensureChild(
710
- { ...snapshot, sessionFile: history.sessionFile },
711
- parentSessionId,
712
- );
713
- this.projectReplay(child, history.messages, signal);
714
- visited.add(`${history.sessionFile}\0${snapshot.id}`);
715
- await this.replayChildren(
716
- child.sessionId,
717
- history.sessionFile,
718
- history.messages,
719
- runtime,
720
- visited,
721
- budget,
722
- signal,
723
- 1,
724
- );
935
+ if (this.sessionIdByNativeId.has(snapshot.id)) continue;
936
+ let child: ChildState | undefined;
937
+ try {
938
+ child = this.ensureChild(
939
+ { ...snapshot, sessionFile },
940
+ this.resolveParent(snapshot.parentToolCallId, sessionFile),
941
+ );
942
+ if (!messages || this.isBufferedChildOmitted(snapshot.id)) {
943
+ this.failReplayChild(child);
944
+ continue;
945
+ }
946
+ this.projectReplay(child, messages, signal);
947
+ if (!sessionFile) {
948
+ this.failReplayChild(child);
949
+ continue;
950
+ }
951
+ visited.add(`${sessionFile}\0${snapshot.id}`);
952
+ await this.replayChildren(
953
+ child.sessionId,
954
+ sessionFile,
955
+ messages,
956
+ runtime,
957
+ visited,
958
+ budget,
959
+ signal,
960
+ 1,
961
+ snapshotIds,
962
+ );
963
+ } catch (error) {
964
+ if (signal.aborted) throw error;
965
+ if (child) this.failReplayChild(child);
966
+ }
725
967
  }
726
968
  const activeToolCallIds = new Set(
727
969
  snapshots.flatMap((snapshot) =>
@@ -744,46 +986,64 @@ export class OmpSubsessionProjector {
744
986
  budget: ReplayBudget,
745
987
  signal: AbortSignal,
746
988
  depth: number,
989
+ snapshotIds?: ReadonlySet<string>,
747
990
  ): Promise<void> {
748
991
  signal.throwIfAborted();
749
992
  if (depth > MAX_REPLAY_DEPTH) throw new OmpPublicError("OMP subagent history is too deep");
750
993
  this.indexTaskCalls(parentSessionId, messages);
751
994
  for (const ref of replayChildren(messages)) {
752
995
  signal.throwIfAborted();
753
- if (!parentSessionFile) {
754
- throw new OmpPublicError("OMP parent transcript identity is unavailable");
755
- }
996
+ if (!parentSessionFile || snapshotIds?.has(ref.id)) continue;
756
997
  const visitKey = `${parentSessionFile}\0${ref.id}`;
757
998
  if (visited.has(visitKey)) continue;
758
999
  visited.add(visitKey);
759
- const history = await waitForReplay(
760
- runtime.readPersistedSubagentTranscript({
761
- parentSessionFile,
762
- childTranscriptId: ref.id,
763
- cwd: this.cwd,
1000
+ let child: ChildState | undefined;
1001
+ let history: ReplayHistory | undefined;
1002
+ try {
1003
+ const loaded = await waitForReplay(
1004
+ runtime.readPersistedSubagentTranscript({
1005
+ parentSessionFile,
1006
+ childTranscriptId: ref.id,
1007
+ cwd: this.cwd,
1008
+ signal,
1009
+ }),
764
1010
  signal,
765
- }),
766
- signal,
767
- );
768
- this.accountReplay(history.messages, budget, signal);
769
- signal.throwIfAborted();
770
- const child = this.ensureChild({ ...ref, sessionFile: history.sessionFile }, parentSessionId);
771
- this.projectReplay(child, history.messages, signal);
772
- await this.replayChildren(
773
- child.sessionId,
774
- history.sessionFile,
775
- history.messages,
776
- runtime,
777
- visited,
778
- budget,
779
- signal,
780
- depth + 1,
781
- );
782
- signal.throwIfAborted();
783
- this.requestTerminal(
784
- child,
785
- ref.status === "derive" ? replayTerminalStatus(history.messages) : ref.status,
786
- );
1011
+ );
1012
+ history = loaded;
1013
+ child = this.ensureChild({ ...ref, sessionFile: loaded.sessionFile }, parentSessionId);
1014
+ this.accountReplay(loaded.messages, budget, signal);
1015
+ signal.throwIfAborted();
1016
+ this.projectReplay(child, loaded.messages, signal);
1017
+ await this.replayChildren(
1018
+ child.sessionId,
1019
+ history.sessionFile,
1020
+ history.messages,
1021
+ runtime,
1022
+ visited,
1023
+ budget,
1024
+ signal,
1025
+ depth + 1,
1026
+ snapshotIds,
1027
+ );
1028
+ signal.throwIfAborted();
1029
+ this.requestTerminal(
1030
+ child,
1031
+ ref.status === "derive" ? replayTerminalStatus(history.messages) : ref.status,
1032
+ );
1033
+ } catch (error) {
1034
+ if (signal.aborted) throw error;
1035
+ if (!child) {
1036
+ try {
1037
+ child = this.ensureChild(
1038
+ { ...ref, sessionFile: history?.sessionFile },
1039
+ parentSessionId,
1040
+ );
1041
+ } catch {
1042
+ continue;
1043
+ }
1044
+ }
1045
+ this.failReplayChild(child);
1046
+ }
787
1047
  }
788
1048
  }
789
1049