@omercnet/paseo-omp 0.3.0-next.101.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
@@ -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.102.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -4554,7 +4554,8 @@ export class OmpProviderSession {
4554
4554
  turn.deferredAgentEnd = candidate;
4555
4555
  void this.subsessions.reconcile(this.runtime).catch(() => {
4556
4556
  if (!turn.terminal && this.activeTurn === turn) {
4557
- this.handleRuntimeFailure("OMP subagent reconciliation failed");
4557
+ this.subsessions?.terminalize("failed");
4558
+ this.resumeDeferredAgentEnd();
4558
4559
  }
4559
4560
  });
4560
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 = 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