@omercnet/paseo-omp 0.3.0 → 0.4.0-next.114.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 +2 -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 +60 -8
  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 +1011 -107
  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 +45 -11
  25. package/server/provider/session.ts +200 -58
  26. package/server/provider/subsessions.ts +311 -73
  27. package/server/provider/timeline-projector.ts +34 -11
  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
@@ -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,75 @@ 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
+ 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) {
707
912
  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
- );
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
+ }
725
945
  }
726
946
  const activeToolCallIds = new Set(
727
947
  snapshots.flatMap((snapshot) =>
@@ -744,46 +964,64 @@ export class OmpSubsessionProjector {
744
964
  budget: ReplayBudget,
745
965
  signal: AbortSignal,
746
966
  depth: number,
967
+ snapshotIds?: ReadonlySet<string>,
747
968
  ): Promise<void> {
748
969
  signal.throwIfAborted();
749
970
  if (depth > MAX_REPLAY_DEPTH) throw new OmpPublicError("OMP subagent history is too deep");
750
971
  this.indexTaskCalls(parentSessionId, messages);
751
972
  for (const ref of replayChildren(messages)) {
752
973
  signal.throwIfAborted();
753
- if (!parentSessionFile) {
754
- throw new OmpPublicError("OMP parent transcript identity is unavailable");
755
- }
974
+ if (!parentSessionFile || snapshotIds?.has(ref.id)) continue;
756
975
  const visitKey = `${parentSessionFile}\0${ref.id}`;
757
976
  if (visited.has(visitKey)) continue;
758
977
  visited.add(visitKey);
759
- const history = await waitForReplay(
760
- runtime.readPersistedSubagentTranscript({
761
- parentSessionFile,
762
- childTranscriptId: ref.id,
763
- 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
+ }),
764
988
  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
- );
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
+ }
787
1025
  }
788
1026
  }
789
1027
 
@@ -12,11 +12,14 @@ import {
12
12
  type JsonValue,
13
13
  OmpPublicDataSerializer,
14
14
  OmpPublicError,
15
+ truncateUtf8,
15
16
  utf8Bytes,
16
17
  } from "./security";
17
18
 
18
19
  const STREAM_FRAME_MS = 32;
19
20
  const MAX_STREAM_TEXT_LENGTH = 4 * 1024 * 1024;
21
+ const MAX_RAW_STREAM_TEXT_LENGTH = 8 * 1024 * 1024;
22
+ const MAX_REDACTED_STREAM_TEXT_LENGTH = MAX_RAW_STREAM_TEXT_LENGTH * 3;
20
23
  const MAX_ACTIVE_TOOLS = 64;
21
24
  const MAX_TODOS = 256;
22
25
  const MAX_TURN_NATIVE_IDENTITIES = 1_024;
@@ -908,10 +911,7 @@ export class OmpTimelineProjector {
908
911
  return;
909
912
  }
910
913
  if (message.role === "bashExecution") {
911
- const text = message.output
912
- ? `$ ${message.command}\n${message.output}`
913
- : `$ ${message.command}`;
914
- this.project({ type: "command_output", text }, this.replayTurnId);
914
+ this.publishCustomMessage(message);
915
915
  this.finishTurn(this.replayTurnId);
916
916
  this.replayTurnId = null;
917
917
  }
@@ -1007,13 +1007,28 @@ export class OmpTimelineProjector {
1007
1007
  if (!this.stream || this.closed || this.stream.dirtyBlocks.size === 0) return;
1008
1008
  const stream = this.stream;
1009
1009
  if (!stream.nativeIdentity && !finalizeFallback) return;
1010
- const indexes = [...stream.dirtyBlocks].sort((left, right) => left - right);
1010
+ const indexes = [...stream.blocks.keys()].sort((left, right) => left - right);
1011
+ let remainingTextBytes = MAX_STREAM_TEXT_LENGTH;
1011
1012
  stream.dirtyBlocks.clear();
1012
1013
  for (const contentIndex of indexes) {
1013
1014
  const block = stream.blocks.get(contentIndex);
1014
- if (!block?.text) continue;
1015
- const publicText = block.kind === "image" ? block.text : this.dataFilter.text(block.text);
1016
- if (!publicText || block.publishedText === publicText) continue;
1015
+ if (!block) continue;
1016
+ const publicText =
1017
+ block.kind === "image"
1018
+ ? block.text
1019
+ : truncateUtf8(
1020
+ this.dataFilter.text(block.text, MAX_REDACTED_STREAM_TEXT_LENGTH),
1021
+ remainingTextBytes,
1022
+ );
1023
+ if (block.kind !== "image") {
1024
+ remainingTextBytes = Math.max(0, remainingTextBytes - utf8Bytes(publicText));
1025
+ }
1026
+ if (
1027
+ block.publishedText === publicText ||
1028
+ (!publicText && block.publishedText === undefined)
1029
+ ) {
1030
+ continue;
1031
+ }
1017
1032
  const nextPublishedBytes = utf8Bytes(publicText);
1018
1033
  if (
1019
1034
  stream.retainedBytes + stream.publishedBytes + nextPublishedBytes >
@@ -1267,7 +1282,7 @@ export class OmpTimelineProjector {
1267
1282
  (snapshot.kind === "image" ? 0 : snapshotBytes);
1268
1283
  if (
1269
1284
  retainedBytes + stream.publishedBytes > MAX_STREAM_TOTAL_BYTES ||
1270
- textBytes > MAX_STREAM_TEXT_LENGTH
1285
+ textBytes > MAX_RAW_STREAM_TEXT_LENGTH
1271
1286
  ) {
1272
1287
  return;
1273
1288
  }
@@ -1291,7 +1306,7 @@ export class OmpTimelineProjector {
1291
1306
 
1292
1307
  private publishCommand(turnId: string): void {
1293
1308
  if (!this.commandText) return;
1294
- const publicText = this.dataFilter.text(this.commandText);
1309
+ const publicText = this.dataFilter.text(this.commandText, MAX_STREAM_TEXT_LENGTH);
1295
1310
  if (!publicText || publicText === this.commandPublishedText) return;
1296
1311
  this.commandPublishedText = publicText;
1297
1312
  this.publish({
@@ -1377,13 +1392,21 @@ export class OmpTimelineProjector {
1377
1392
  type: "shell",
1378
1393
  command,
1379
1394
  ...(firstString(details, "cwd") ? { cwd: firstString(details, "cwd") } : {}),
1380
- ...(output ? { output: this.dataFilter.text(output) } : {}),
1395
+ ...(output ? { output: this.dataFilter.text(output, MAX_STREAM_TEXT_LENGTH) } : {}),
1381
1396
  ...(typeof message.exitCode === "number" || message.exitCode === null
1382
1397
  ? { exitCode: message.exitCode }
1383
1398
  : typeof details?.exitCode === "number"
1384
1399
  ? { exitCode: details.exitCode }
1385
1400
  : {}),
1386
1401
  },
1402
+ ...(message.cancelled !== undefined || message.truncated !== undefined
1403
+ ? {
1404
+ metadata: {
1405
+ ...(message.cancelled !== undefined ? { cancelled: message.cancelled } : {}),
1406
+ ...(message.truncated !== undefined ? { truncated: message.truncated } : {}),
1407
+ },
1408
+ }
1409
+ : {}),
1387
1410
  status: message.cancelled ? "canceled" : "completed",
1388
1411
  error: null,
1389
1412
  });