@omercnet/paseo-omp 0.4.0-next.114.1 → 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.
@@ -86,7 +86,105 @@ const MAX_RPC_ERROR_BYTES = 4_096;
86
86
  const MAX_RPC_ERROR_CODE_BYTES = 256;
87
87
  const PROMPT_SCHEDULING_FAILURE = "OMP prompt scheduling failed";
88
88
  const PROTOCOL_VIOLATION_COALESCE_MS = 10_000;
89
-
89
+ const MAX_REPLAY_MESSAGES = 100_000;
90
+ const MAX_REPLAY_BYTES = 64 * 1024 * 1024;
91
+ const MAX_REPLAY_NODES = 400_000;
92
+ const OMP_MESSAGE_PAGE_LIMIT = 256;
93
+ const MAX_MESSAGE_PAGES = 512;
94
+ const MAX_MESSAGE_PAGE_BUSY_RETRIES = 4;
95
+ const MAX_MESSAGE_PAGE_STALE_RESTARTS = 2;
96
+ const MESSAGE_PAGE_RETRY_BASE_MS = 50;
97
+
98
+ export const OMP_MESSAGE_REPLAY_POLICIES = {
99
+ assistant: "render",
100
+ user: "render",
101
+ developer: "ignore",
102
+ toolResult: "render",
103
+ bashExecution: "normalize",
104
+ pythonExecution: "normalize",
105
+ custom: "normalize",
106
+ hookMessage: "normalize",
107
+ branchSummary: "ignore",
108
+ compactionSummary: "ignore",
109
+ fileMention: "ignore",
110
+ } as const;
111
+
112
+ export const OMP_SESSION_EVENT_POLICIES = {
113
+ agent_start: "lifecycle",
114
+ agent_end: "lifecycle",
115
+ turn_start: "lifecycle",
116
+ turn_end: "lifecycle",
117
+ message_start: "normalize",
118
+ message_update: "normalize",
119
+ message_end: "normalize",
120
+ tool_execution_start: "normalize",
121
+ tool_execution_update: "normalize",
122
+ tool_stream_update: "normalize",
123
+ tool_execution_end: "normalize",
124
+ auto_compaction_start: "normalize",
125
+ auto_compaction_end: "normalize",
126
+ auto_retry_start: "normalize",
127
+ auto_retry_end: "normalize",
128
+ retry_fallback_applied: "normalize",
129
+ retry_fallback_succeeded: "normalize",
130
+ model_changed: "normalize",
131
+ config_warnings_changed: "ignore",
132
+ advisor_cost_changed: "ignore",
133
+ advisor_yielded: "normalize",
134
+ ttsr_triggered: "ignore",
135
+ todo_reminder: "normalize",
136
+ todo_auto_clear: "normalize",
137
+ irc_message: "normalize",
138
+ notice: "normalize",
139
+ thinking_level_changed: "normalize",
140
+ goal_updated: "normalize",
141
+ } as const;
142
+
143
+ export const OMP_RPC_COMMAND_POLICIES = {
144
+ negotiate_protocol: "implemented",
145
+ prompt: "implemented",
146
+ steer: "implemented",
147
+ follow_up: "implemented",
148
+ abort: "implemented",
149
+ abort_and_prompt: "unsupported",
150
+ new_session: "unsupported",
151
+ get_state: "implemented",
152
+ set_fast_mode: "unsupported",
153
+ get_available_commands: "implemented",
154
+ set_todos: "unsupported",
155
+ set_host_tools: "implemented",
156
+ set_host_uri_schemes: "unsupported",
157
+ set_subagent_subscription: "implemented",
158
+ get_subagents: "implemented",
159
+ get_subagent_messages: "implemented",
160
+ set_model: "implemented",
161
+ cycle_model: "unsupported",
162
+ get_available_models: "implemented",
163
+ set_thinking_level: "implemented",
164
+ cycle_thinking_level: "unsupported",
165
+ set_steering_mode: "unsupported",
166
+ set_follow_up_mode: "unsupported",
167
+ set_interrupt_mode: "unsupported",
168
+ compact: "implemented",
169
+ set_auto_compaction: "implemented",
170
+ set_auto_retry: "unsupported",
171
+ abort_retry: "unsupported",
172
+ bash: "unsupported",
173
+ abort_bash: "unsupported",
174
+ get_session_stats: "implemented",
175
+ export_html: "unsupported",
176
+ switch_session: "unsupported",
177
+ branch: "implemented",
178
+ get_branch_messages: "implemented",
179
+ get_last_assistant_text: "unsupported",
180
+ set_session_name: "unsupported",
181
+ handoff: "implemented",
182
+ get_messages: "implemented",
183
+ get_messages_page: "implemented",
184
+ get_login_providers: "unsupported",
185
+ login: "unsupported",
186
+ } as const;
187
+ export const OMP_RPC_RESPONSE_POLICIES = OMP_RPC_COMMAND_POLICIES;
90
188
  export const OMP_PROTOCOL_VIOLATION_CATEGORIES = [
91
189
  "duplicate-ready",
92
190
  "frame-limit",
@@ -156,6 +254,7 @@ export const OMP_PROTOCOL_EVENT_TYPES = [
156
254
  "tool_execution_start",
157
255
  "tool_execution_update",
158
256
  "tool_execution_end",
257
+ "tool_stream_update",
159
258
  "compaction_start",
160
259
  "compaction_end",
161
260
  "subagent_lifecycle",
@@ -163,6 +262,10 @@ export const OMP_PROTOCOL_EVENT_TYPES = [
163
262
  "subagent_event",
164
263
  "todo_reminder",
165
264
  "model_changed",
265
+ "config_warnings_changed",
266
+ "advisor_cost_changed",
267
+ "ttsr_triggered",
268
+ "irc_message",
166
269
  "thinking_level_changed",
167
270
  "goal_updated",
168
271
  "auto_retry_start",
@@ -292,8 +395,11 @@ function eventSchemaClassification(
292
395
  case "message_update":
293
396
  case "message_end":
294
397
  return { reason: "message-event-schema", expected: "valid-message-event" };
398
+ case "irc_message":
399
+ return { reason: "message-event-schema", expected: "valid-message-event" };
295
400
  case "tool_execution_start":
296
401
  case "tool_execution_update":
402
+ case "tool_stream_update":
297
403
  case "tool_execution_end":
298
404
  case "host_tool_call":
299
405
  case "host_tool_cancel":
@@ -312,6 +418,9 @@ function eventSchemaClassification(
312
418
  case "available_commands_update":
313
419
  case "retry_fallback_applied":
314
420
  case "retry_fallback_succeeded":
421
+ case "config_warnings_changed":
422
+ case "advisor_cost_changed":
423
+ case "ttsr_triggered":
315
424
  return { reason: "configuration-event-schema", expected: "valid-configuration-event" };
316
425
  case "extension_ui_request":
317
426
  return { reason: "extension-ui-schema", expected: "valid-extension-ui-event" };
@@ -408,6 +517,13 @@ function boundedString(maxBytes: number, minBytes = 0) {
408
517
  return bytes >= minBytes && bytes <= maxBytes;
409
518
  });
410
519
  }
520
+ function isSafeOpaqueCursor(value: string): boolean {
521
+ for (let index = 0; index < value.length; index += 1) {
522
+ const code = value.charCodeAt(index);
523
+ if (code <= 0x1f || code === 0x7f) return false;
524
+ }
525
+ return true;
526
+ }
411
527
  function boundedOptionalStringArray(maxItems: number, maxBytes: number) {
412
528
  return z.unknown().transform((value): string[] | undefined => {
413
529
  if (!Array.isArray(value)) return undefined;
@@ -465,7 +581,7 @@ function sanitizeLiveMessageDisplay(value: unknown): unknown {
465
581
  return content === message.content ? value : { ...message, content };
466
582
  }
467
583
  if (
468
- message.role === "bashExecution" &&
584
+ (message.role === "bashExecution" || message.role === "pythonExecution") &&
469
585
  typeof message.output === "string" &&
470
586
  utf8Bytes(message.output) > MAX_IMAGE_DATA_LENGTH
471
587
  ) {
@@ -507,6 +623,178 @@ function isBoundedJson(
507
623
  boundedJsonBytes(value, maxBytes, maxItems, maxBytes, maxNodes) !== Number.POSITIVE_INFINITY
508
624
  );
509
625
  }
626
+ const MAX_RESPONSE_DEPTH = 16;
627
+ export const OMP_RPC_BOUND_DIMENSIONS = [
628
+ "bytes",
629
+ "items",
630
+ "nodes",
631
+ "depth",
632
+ "string-bytes",
633
+ ] as const;
634
+ export type OmpRpcBoundDimension = (typeof OMP_RPC_BOUND_DIMENSIONS)[number];
635
+ export const OMP_RPC_DIAGNOSTIC_COMMANDS = [
636
+ "negotiate_protocol",
637
+ "get_state",
638
+ "get_session_stats",
639
+ "get_available_models",
640
+ "get_available_commands",
641
+ "set_subagent_subscription",
642
+ "get_subagents",
643
+ "get_subagent_messages",
644
+ "prompt",
645
+ "compact",
646
+ "set_auto_compaction",
647
+ "set_model",
648
+ "set_thinking_level",
649
+ "handoff",
650
+ "get_branch_messages",
651
+ "branch",
652
+ "get_messages_page",
653
+ "get_messages",
654
+ "abort",
655
+ "set_host_tools",
656
+ ] as const;
657
+ export type OmpRpcDiagnosticCommand = (typeof OMP_RPC_DIAGNOSTIC_COMMANDS)[number];
658
+ type JsonBoundViolation = {
659
+ dimension: OmpRpcBoundDimension;
660
+ actual: number;
661
+ limit: number;
662
+ };
663
+ type JsonBoundInspection = { bytes: number; nodes: number; violation?: JsonBoundViolation };
664
+
665
+ function rpcDiagnosticCommand(value: string): OmpRpcDiagnosticCommand | undefined {
666
+ return (OMP_RPC_DIAGNOSTIC_COMMANDS as readonly string[]).includes(value)
667
+ ? (value as OmpRpcDiagnosticCommand)
668
+ : undefined;
669
+ }
670
+
671
+ function encodedJsonStringBytes(value: string): number {
672
+ let bytes = 2;
673
+ for (let index = 0; index < value.length; index += 1) {
674
+ const code = value.charCodeAt(index);
675
+ if (code === 0x22 || code === 0x5c) {
676
+ bytes += 2;
677
+ } else if (code <= 0x1f) {
678
+ bytes +=
679
+ code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d ? 2 : 6;
680
+ } else if (code >= 0xd800 && code <= 0xdbff) {
681
+ const low = value.charCodeAt(index + 1);
682
+ if (low >= 0xdc00 && low <= 0xdfff) {
683
+ bytes += 4;
684
+ index += 1;
685
+ } else {
686
+ bytes += 6;
687
+ }
688
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
689
+ bytes += 6;
690
+ } else {
691
+ bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : 3;
692
+ }
693
+ }
694
+ return bytes;
695
+ }
696
+
697
+ function inspectJsonBounds(
698
+ value: unknown,
699
+ maxBytes: number,
700
+ maxItems: number,
701
+ maxStringBytes: number,
702
+ maxNodes: number,
703
+ ): JsonBoundInspection {
704
+ const stack: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }];
705
+ let nodes = 0;
706
+ let bytes = 0;
707
+ const addBytes = (count: number): JsonBoundViolation | undefined => {
708
+ bytes += count;
709
+ return bytes > maxBytes ? { dimension: "bytes", actual: bytes, limit: maxBytes } : undefined;
710
+ };
711
+ while (stack.length > 0) {
712
+ const current = stack.pop();
713
+ if (!current) break;
714
+ nodes += 1;
715
+ if (nodes > maxNodes)
716
+ return { bytes, nodes, violation: { dimension: "nodes", actual: nodes, limit: maxNodes } };
717
+ if (current.depth > MAX_RESPONSE_DEPTH) {
718
+ return {
719
+ bytes,
720
+ nodes,
721
+ violation: { dimension: "depth", actual: current.depth, limit: MAX_RESPONSE_DEPTH },
722
+ };
723
+ }
724
+ const item = current.value;
725
+ if (item === null) {
726
+ const violation = addBytes(4);
727
+ if (violation) return { bytes, nodes, violation };
728
+ continue;
729
+ }
730
+ if (typeof item === "boolean") {
731
+ const violation = addBytes(item ? 4 : 5);
732
+ if (violation) return { bytes, nodes, violation };
733
+ continue;
734
+ }
735
+ if (typeof item === "number") {
736
+ const violation = addBytes((JSON.stringify(item) ?? "null").length);
737
+ if (violation) return { bytes, nodes, violation };
738
+ continue;
739
+ }
740
+ if (typeof item === "string") {
741
+ const itemBytes = utf8Bytes(item);
742
+ if (itemBytes > maxStringBytes) {
743
+ return {
744
+ bytes,
745
+ nodes,
746
+ violation: { dimension: "string-bytes", actual: itemBytes, limit: maxStringBytes },
747
+ };
748
+ }
749
+ const violation = addBytes(encodedJsonStringBytes(item));
750
+ if (violation) return { bytes, nodes, violation };
751
+ continue;
752
+ }
753
+ if (typeof item !== "object") continue;
754
+ if (Array.isArray(item)) {
755
+ if (item.length > maxItems) {
756
+ return {
757
+ bytes,
758
+ nodes,
759
+ violation: { dimension: "items", actual: item.length, limit: maxItems },
760
+ };
761
+ }
762
+ const violation = addBytes(2 + Math.max(0, item.length - 1));
763
+ if (violation) return { bytes, nodes, violation };
764
+ for (let index = item.length - 1; index >= 0; index -= 1) {
765
+ stack.push({ value: item[index], depth: current.depth + 1 });
766
+ }
767
+ continue;
768
+ }
769
+ const entries = Object.entries(item).filter(([, child]) => child !== undefined);
770
+ if (entries.length > maxItems) {
771
+ return {
772
+ bytes,
773
+ nodes,
774
+ violation: { dimension: "items", actual: entries.length, limit: maxItems },
775
+ };
776
+ }
777
+ let violation = addBytes(2 + Math.max(0, entries.length - 1) + entries.length);
778
+ if (violation) return { bytes, nodes, violation };
779
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
780
+ const [key, child] = entries[index] as [string, unknown];
781
+ violation = addBytes(encodedJsonStringBytes(key));
782
+ if (violation) return { bytes, nodes, violation };
783
+ stack.push({ value: child, depth: current.depth + 1 });
784
+ }
785
+ }
786
+ return { bytes, nodes };
787
+ }
788
+
789
+ function jsonBoundViolation(
790
+ value: unknown,
791
+ maxBytes: number,
792
+ maxItems: number,
793
+ maxStringBytes: number,
794
+ maxNodes: number,
795
+ ): JsonBoundViolation | undefined {
796
+ return inspectJsonBounds(value, maxBytes, maxItems, maxStringBytes, maxNodes).violation;
797
+ }
510
798
  function optionalMetadataMetrics(value: unknown) {
511
799
  return boundedJsonMetrics(
512
800
  value,
@@ -706,52 +994,46 @@ const OmpMessageIdentityShape = {
706
994
  timestamp: z.number().finite().optional(),
707
995
  details: OmpOptionalMetadataSchema,
708
996
  };
709
- type OmpContentPart = z.infer<typeof OmpContentPartSchema>;
710
- type OmpMessageIdentity = {
711
- id?: string;
712
- entryId?: string;
713
- responseId?: string;
714
- images?: OmpContentPart[];
715
- timestamp?: number;
716
- details?: unknown;
717
- display?: boolean;
718
- customType?: string;
719
- content?: unknown;
720
- command?: string;
721
- output?: string;
722
- exitCode?: number | null;
723
- cancelled?: boolean;
724
- truncated?: boolean;
997
+ const OmpOpaqueMessagePayloadSchema = z
998
+ .unknown()
999
+ .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 8_192));
1000
+ const OmpExecutionOutputSchema = z.preprocess(
1001
+ (value) =>
1002
+ typeof value === "string" && utf8Bytes(value) > MAX_IMAGE_DATA_LENGTH
1003
+ ? DISPLAY_TRUNCATION_MARKER
1004
+ : value,
1005
+ RAW_DISPLAY_TEXT.optional(),
1006
+ );
1007
+ const OmpExecutionMessageShape = {
1008
+ output: OmpExecutionOutputSchema,
1009
+ exitCode: z.number().int().nullable().optional(),
1010
+ cancelled: z.boolean().optional(),
1011
+ truncated: z.boolean().optional(),
1012
+ excludeFromContext: z.boolean().optional(),
1013
+ meta: OmpOptionalMetadataSchema,
725
1014
  };
726
- export type OmpMessage = OmpMessageIdentity &
727
- (
728
- | {
729
- role: "assistant";
730
- content?: string | OmpContentPart[];
731
- errorMessage?: string | null;
732
- stopReason?: string;
733
- }
734
- | { role: "user"; content: string | OmpContentPart[] }
735
- | {
736
- role: "toolResult";
737
- toolCallId: string;
738
- toolName: string;
739
- content: unknown;
740
- details?: unknown;
741
- isError?: boolean;
742
- }
743
- | {
744
- role: "bashExecution";
745
- command: string;
746
- output?: string;
747
- exitCode?: number | null;
748
- cancelled?: boolean;
749
- truncated?: boolean;
750
- }
751
- | { role: "custom"; customType?: string; content?: unknown; display?: boolean }
752
- );
1015
+ const OmpCustomMessageShape = {
1016
+ customType: NAME.optional(),
1017
+ content: OmpDisplayContentSchema.optional(),
1018
+ display: z.boolean().optional(),
1019
+ attribution: OmpOpaqueMessagePayloadSchema.optional(),
1020
+ };
1021
+ const OmpFileMentionSchema = z
1022
+ .object({
1023
+ path: boundedString(MAX_PATH_LENGTH),
1024
+ content: TEXT,
1025
+ lineCount: z.number().int().nonnegative().safe().optional(),
1026
+ byteSize: z.number().int().nonnegative().safe().optional(),
1027
+ skippedReason: z.enum(["tooLarge", "binary"]).optional(),
1028
+ image: OmpContentPartSchema.optional(),
1029
+ })
1030
+ .superRefine((file, context) => {
1031
+ if (file.image && file.image.type !== "image") {
1032
+ context.addIssue({ code: "custom", message: "invalid file mention image" });
1033
+ }
1034
+ });
753
1035
 
754
- const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
1036
+ const OmpMessageSchema = z.discriminatedUnion("role", [
755
1037
  z.object({
756
1038
  role: z.literal("assistant"),
757
1039
  content: OmpAssistantDisplayContentSchema.optional(),
@@ -764,42 +1046,70 @@ const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
764
1046
  content: OmpDisplayContentSchema,
765
1047
  ...OmpMessageIdentityShape,
766
1048
  }),
1049
+ z.object({
1050
+ role: z.literal("developer"),
1051
+ content: OmpDisplayContentSchema,
1052
+ attribution: OmpOpaqueMessagePayloadSchema.optional(),
1053
+ synthetic: z.boolean().optional(),
1054
+ userInitiated: z.boolean().optional(),
1055
+ providerPayload: OmpOpaqueMessagePayloadSchema.optional(),
1056
+ ...OmpMessageIdentityShape,
1057
+ }),
767
1058
  z.object({
768
1059
  role: z.literal("toolResult"),
769
1060
  toolCallId: IDENTIFIER,
770
1061
  toolName: NAME,
771
- content: z
772
- .unknown()
773
- .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 8_192)),
1062
+ content: OmpOpaqueMessagePayloadSchema,
774
1063
  isError: z.boolean().optional(),
1064
+ attribution: OmpOpaqueMessagePayloadSchema.optional(),
1065
+ prunedAt: z.number().finite().nonnegative().optional(),
1066
+ providerMetadata: OmpOpaqueMessagePayloadSchema.optional(),
1067
+ useless: z.boolean().optional(),
775
1068
  ...OmpMessageIdentityShape,
776
1069
  }),
777
1070
  z.object({
778
1071
  role: z.literal("bashExecution"),
779
1072
  command: TEXT,
780
- output: z.preprocess(
781
- (value) =>
782
- typeof value === "string" && utf8Bytes(value) > MAX_IMAGE_DATA_LENGTH
783
- ? DISPLAY_TRUNCATION_MARKER
784
- : value,
785
- RAW_DISPLAY_TEXT.optional(),
786
- ),
787
- exitCode: z.number().int().nullable().optional(),
788
- cancelled: z.boolean().optional(),
789
- truncated: z.boolean().optional(),
1073
+ ...OmpExecutionMessageShape,
790
1074
  ...OmpMessageIdentityShape,
791
1075
  }),
792
1076
  z.object({
793
- role: z.literal("custom"),
794
- customType: NAME.optional(),
795
- content: z
796
- .unknown()
797
- .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 8_192))
798
- .optional(),
799
- display: z.boolean().optional(),
1077
+ role: z.literal("pythonExecution"),
1078
+ code: TEXT,
1079
+ ...OmpExecutionMessageShape,
1080
+ ...OmpMessageIdentityShape,
1081
+ }),
1082
+ z.object({ role: z.literal("custom"), ...OmpCustomMessageShape, ...OmpMessageIdentityShape }),
1083
+ z.object({
1084
+ role: z.literal("hookMessage"),
1085
+ ...OmpCustomMessageShape,
1086
+ ...OmpMessageIdentityShape,
1087
+ }),
1088
+ z.object({
1089
+ role: z.literal("branchSummary"),
1090
+ summary: TEXT,
1091
+ fromId: IDENTIFIER,
1092
+ ...OmpMessageIdentityShape,
1093
+ }),
1094
+ z.object({
1095
+ role: z.literal("compactionSummary"),
1096
+ summary: TEXT,
1097
+ shortSummary: TEXT.optional(),
1098
+ tokensBefore: z.number().finite().nonnegative(),
1099
+ tokensAfter: z.number().finite().nonnegative().optional(),
1100
+ method: NAME.optional(),
1101
+ providerPayload: OmpOpaqueMessagePayloadSchema.optional(),
1102
+ blocks: z.array(OmpContentPartSchema).max(OMP_MAX_CONTENT_PARTS).optional(),
1103
+ warning: boundedString(64 * 1024).optional(),
1104
+ ...OmpMessageIdentityShape,
1105
+ }),
1106
+ z.object({
1107
+ role: z.literal("fileMention"),
1108
+ files: z.array(OmpFileMentionSchema).max(MAX_ARRAY_ITEMS),
800
1109
  ...OmpMessageIdentityShape,
801
1110
  }),
802
1111
  ]);
1112
+ export type OmpMessage = z.infer<typeof OmpMessageSchema>;
803
1113
 
804
1114
  const OmpAssistantMessageEventSchema = z
805
1115
  .object({
@@ -948,6 +1258,7 @@ const OmpReadyFrameSchema = z.object({
948
1258
  const OmpResponseFrameSchema = z.object({
949
1259
  type: z.literal("response"),
950
1260
  id: IDENTIFIER,
1261
+ command: boundedString(64, 1).optional(),
951
1262
  success: z.boolean(),
952
1263
  data: z.unknown().optional(),
953
1264
  error: boundedString(MAX_RPC_ERROR_BYTES).optional(),
@@ -1153,7 +1464,7 @@ const OmpCompactionEndSchema = z.object({
1153
1464
  errorMessage: boundedString(4_096).optional(),
1154
1465
  skipped: z.boolean().optional(),
1155
1466
  });
1156
- const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
1467
+ const OmpCoreAgentEventSchema = z.discriminatedUnion("type", [
1157
1468
  z.object({ type: z.literal("agent_start") }),
1158
1469
  z.object({
1159
1470
  type: z.literal("agent_end"),
@@ -1184,6 +1495,12 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
1184
1495
  args: BoundedToolPayloadSchema.optional(),
1185
1496
  partialResult: OmpToolResultPayloadSchema,
1186
1497
  }),
1498
+ z.object({
1499
+ type: z.literal("tool_stream_update"),
1500
+ toolCallId: IDENTIFIER,
1501
+ toolName: NAME,
1502
+ update: BoundedToolPayloadSchema,
1503
+ }),
1187
1504
  z.object({
1188
1505
  type: z.literal("tool_execution_end"),
1189
1506
  toolCallId: IDENTIFIER,
@@ -1210,6 +1527,99 @@ const OmpGoalModeStateSchema = z.object({
1210
1527
  reason: boundedString(4_096).optional(),
1211
1528
  goal: OmpGoalSchema.optional(),
1212
1529
  });
1530
+ const OmpTodoReminderEventSchema = z.object({
1531
+ type: z.literal("todo_reminder"),
1532
+ todos: z
1533
+ .array(
1534
+ z.object({
1535
+ id: IDENTIFIER.optional(),
1536
+ content: boundedString(16_384),
1537
+ status: z.enum(["pending", "in_progress", "blocked", "completed", "abandoned"]),
1538
+ }),
1539
+ )
1540
+ .max(MAX_TODOS),
1541
+ attempt: z.number().int().nonnegative().safe().optional(),
1542
+ maxAttempts: z.number().int().nonnegative().safe().optional(),
1543
+ });
1544
+ const OmpThinkingLevelChangedEventSchema = z.object({
1545
+ type: z.literal("thinking_level_changed"),
1546
+ thinkingLevel: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
1547
+ configured: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
1548
+ resolved: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
1549
+ });
1550
+ const OmpAutoRetryStartEventSchema = z.object({
1551
+ type: z.literal("auto_retry_start"),
1552
+ attempt: z.number().int().nonnegative().safe(),
1553
+ maxAttempts: z.number().int().positive().safe(),
1554
+ delayMs: z.number().int().nonnegative().safe(),
1555
+ errorMessage: boundedString(64 * 1024),
1556
+ errorId: z.number().int().safe().optional(),
1557
+ });
1558
+ const OmpAutoRetryEndEventSchema = z.object({
1559
+ type: z.literal("auto_retry_end"),
1560
+ success: z.boolean(),
1561
+ attempt: z.number().int().nonnegative().safe(),
1562
+ finalError: boundedString(64 * 1024).optional(),
1563
+ retryErrors: BoundedToolPayloadSchema.optional(),
1564
+ recoveredErrors: BoundedToolPayloadSchema.optional(),
1565
+ });
1566
+ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
1567
+ ...OmpCoreAgentEventSchema.options,
1568
+ z.object({ type: z.literal("config_warnings_changed") }),
1569
+ z.object({ type: z.literal("advisor_cost_changed") }),
1570
+ z.object({ type: z.literal("advisor_yielded") }),
1571
+ z.object({
1572
+ type: z.literal("ttsr_triggered"),
1573
+ rules: z.array(BoundedToolPayloadSchema).max(MAX_ARRAY_ITEMS),
1574
+ }),
1575
+ OmpTodoReminderEventSchema,
1576
+ z.object({ type: z.literal("todo_auto_clear") }),
1577
+ z.object({
1578
+ type: z.literal("irc_message"),
1579
+ message: OmpMessageSchema.refine((message) => message.role === "custom"),
1580
+ }),
1581
+ z.object({ type: z.literal("model_changed") }),
1582
+ OmpThinkingLevelChangedEventSchema,
1583
+ z.object({
1584
+ type: z.literal("goal_updated"),
1585
+ goal: OmpGoalSchema.nullable().optional(),
1586
+ state: OmpGoalModeStateSchema.optional(),
1587
+ }),
1588
+ OmpAutoRetryStartEventSchema,
1589
+ OmpAutoRetryEndEventSchema,
1590
+ z.object({
1591
+ type: z.literal("retry_fallback_applied"),
1592
+ from: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1593
+ to: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1594
+ role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1595
+ }),
1596
+ z.object({
1597
+ type: z.literal("retry_fallback_succeeded"),
1598
+ model: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1599
+ role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1600
+ }),
1601
+ z.object({
1602
+ type: z.literal("auto_compaction_start"),
1603
+ reason: boundedString(4_096),
1604
+ action: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1605
+ }),
1606
+ z.object({
1607
+ type: z.literal("auto_compaction_end"),
1608
+ action: NAME.optional(),
1609
+ result: OmpCompactionResultSchema.nullable().optional(),
1610
+ aborted: z.boolean().optional(),
1611
+ willRetry: z.boolean().optional(),
1612
+ errorMessage: boundedString(64 * 1024).optional(),
1613
+ skipped: z.boolean().optional(),
1614
+ }),
1615
+ z.object({
1616
+ type: z.literal("notice"),
1617
+ id: IDENTIFIER.optional(),
1618
+ level: z.enum(["info", "warning", "error"]),
1619
+ message: boundedString(64 * 1024),
1620
+ source: boundedString(MAX_NAME_LENGTH).optional(),
1621
+ }),
1622
+ ]);
1213
1623
  const OmpSubagentStatusSchema = z.enum([
1214
1624
  "pending",
1215
1625
  "running",
@@ -1352,80 +1762,10 @@ const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
1352
1762
  type: z.literal("subagent_event"),
1353
1763
  payload: z.object({ id: IDENTIFIER, event: OmpAgentSessionEventSchema }),
1354
1764
  }),
1355
- z.object({
1356
- type: z.literal("todo_reminder"),
1357
- todos: z
1358
- .array(
1359
- z.object({
1360
- id: IDENTIFIER.optional(),
1361
- content: boundedString(16_384),
1362
- status: z.enum(["pending", "in_progress", "blocked", "completed", "abandoned"]),
1363
- }),
1364
- )
1365
- .max(MAX_TODOS),
1366
- }),
1367
- z.object({ type: z.literal("model_changed") }),
1368
- z.object({
1369
- type: z.literal("thinking_level_changed"),
1370
- thinkingLevel: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
1371
- }),
1372
- z.object({
1373
- type: z.literal("goal_updated"),
1374
- goal: OmpGoalSchema.nullable().optional(),
1375
- state: OmpGoalModeStateSchema.optional(),
1376
- }),
1377
- z.object({
1378
- type: z.literal("auto_retry_start"),
1379
- attempt: z.number().int().nonnegative().safe(),
1380
- maxAttempts: z.number().int().positive().safe(),
1381
- delayMs: z.number().int().nonnegative().safe(),
1382
- errorMessage: boundedString(64 * 1024),
1383
- errorId: z.number().int().safe().optional(),
1384
- }),
1385
- z.object({
1386
- type: z.literal("auto_retry_end"),
1387
- success: z.boolean(),
1388
- attempt: z.number().int().nonnegative().safe(),
1389
- finalError: boundedString(64 * 1024).optional(),
1390
- recoveredErrors: BoundedToolPayloadSchema.optional(),
1391
- }),
1392
- z.object({
1393
- type: z.literal("retry_fallback_applied"),
1394
- from: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1395
- to: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1396
- role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1397
- }),
1398
- z.object({
1399
- type: z.literal("retry_fallback_succeeded"),
1400
- model: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1401
- role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1402
- }),
1403
- z.object({ type: z.literal("todo_auto_clear") }),
1404
- z.object({
1405
- type: z.literal("auto_compaction_start"),
1406
- reason: boundedString(4_096),
1407
- action: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1408
- }),
1409
- z.object({
1410
- type: z.literal("auto_compaction_end"),
1411
- action: NAME.optional(),
1412
- result: OmpCompactionResultSchema.nullable().optional(),
1413
- aborted: z.boolean().optional(),
1414
- willRetry: z.boolean().optional(),
1415
- errorMessage: boundedString(64 * 1024).optional(),
1416
- skipped: z.boolean().optional(),
1417
- }),
1418
1765
  z.object({
1419
1766
  type: z.literal("available_commands_update"),
1420
1767
  commands: z.array(OmpAvailableCommandSchema).max(MAX_ARRAY_ITEMS),
1421
1768
  }),
1422
- z.object({
1423
- type: z.literal("notice"),
1424
- id: IDENTIFIER.optional(),
1425
- level: z.enum(["info", "warning", "error"]),
1426
- message: boundedString(64 * 1024),
1427
- source: boundedString(MAX_NAME_LENGTH).optional(),
1428
- }),
1429
1769
  z.object({ type: z.literal("command_output"), text: TEXT.optional() }),
1430
1770
  OmpExtensionUiRequestSchema,
1431
1771
  z.object({
@@ -1437,7 +1777,6 @@ const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
1437
1777
  OmpHostToolCancelSchema,
1438
1778
  OmpToolApprovalRequestSchema,
1439
1779
  OmpToolApprovalCancelSchema,
1440
- z.object({ type: z.literal("advisor_yielded") }),
1441
1780
  ]);
1442
1781
  type OptionalDetailsMapper = (value: unknown, taskResult?: boolean) => unknown;
1443
1782
 
@@ -1492,6 +1831,8 @@ function mapAgentEventDetails(
1492
1831
  return mapRecordField(frame, "result", map, frame.toolName === "task");
1493
1832
  case "agent_end":
1494
1833
  return mapRecordField(frame, "messages", (messages) => mapMessageList(messages, map));
1834
+ case "irc_message":
1835
+ return mapRecordField(frame, "message", map);
1495
1836
  default:
1496
1837
  return frame;
1497
1838
  }
@@ -1522,6 +1863,25 @@ function sanitizeHistoryResponseData(value: unknown): unknown {
1522
1863
  return mapRecordField(value as Record<string, unknown>, "messages", sanitizeMessageListMetadata);
1523
1864
  }
1524
1865
 
1866
+ function projectSessionStateResponseData(value: unknown): unknown {
1867
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
1868
+ const source = value as Record<string, unknown>;
1869
+ const projected: Record<string, unknown> = {};
1870
+ for (const key of [
1871
+ "model",
1872
+ "thinkingLevel",
1873
+ "isStreaming",
1874
+ "isCompacting",
1875
+ "sessionId",
1876
+ "autoCompactionEnabled",
1877
+ "contextUsage",
1878
+ "sessionFile",
1879
+ ] as const) {
1880
+ if (Object.hasOwn(source, key)) projected[key] = source[key];
1881
+ }
1882
+ return projected;
1883
+ }
1884
+
1525
1885
  function runtimeFrameCollectionLimit(frame: Record<string, unknown>): number {
1526
1886
  const type = frame.type;
1527
1887
  if (
@@ -1547,7 +1907,12 @@ const OmpBranchMessagesResultSchema = z.object({
1547
1907
  });
1548
1908
  const OmpBranchResultSchema = z.object({ text: TEXT, cancelled: z.boolean() });
1549
1909
  const OmpMessagesResultSchema = z.object({
1550
- messages: z.array(OmpMessageSchema).max(100_000),
1910
+ messages: z.array(OmpMessageSchema).max(MAX_REPLAY_MESSAGES),
1911
+ });
1912
+ const OmpMessagesPageResultSchema = z.object({
1913
+ messages: z.array(OmpMessageSchema).max(OMP_MESSAGE_PAGE_LIMIT),
1914
+ nextCursor: boundedString(2_048, 1).refine(isSafeOpaqueCursor).optional(),
1915
+ totalMessages: z.number().int().nonnegative().max(MAX_REPLAY_MESSAGES),
1551
1916
  });
1552
1917
  const OmpSubagentsResultSchema = z.object({
1553
1918
  subagents: z
@@ -1573,7 +1938,7 @@ const OmpSubagentMessagesResultSchema = z.object({
1573
1938
  fromByte: z.number().int().nonnegative(),
1574
1939
  nextByte: z.number().int().nonnegative(),
1575
1940
  reset: z.boolean(),
1576
- messages: z.array(OmpMessageSchema).max(100_000),
1941
+ messages: z.array(OmpMessageSchema).max(MAX_REPLAY_MESSAGES),
1577
1942
  });
1578
1943
  const ProtocolNegotiationResultSchema = z.object({
1579
1944
  protocolVersion: z.literal(2),
@@ -1691,7 +2056,7 @@ export interface OmpRuntimeSession {
1691
2056
  getBranchMessages(): Promise<Array<{ entryId: string; text: string }>>;
1692
2057
  branch(entryId: string): Promise<OmpBranchResult>;
1693
2058
  readonly canReplayHistory: boolean;
1694
- getMessages(): Promise<OmpMessage[]>;
2059
+ getMessages(signal?: AbortSignal): Promise<OmpMessage[]>;
1695
2060
  abort(): Promise<void>;
1696
2061
  setHostTools(tools: readonly OmpHostToolDefinition[]): Promise<string[]>;
1697
2062
  sendHostToolResult(result: OmpHostToolResult): void;
@@ -1712,6 +2077,7 @@ export interface OmpRuntime {
1712
2077
  readPersistedSubagentTranscript(options: {
1713
2078
  parentSessionFile: string;
1714
2079
  childTranscriptId: string;
2080
+ sessionFile?: string;
1715
2081
  cwd: string;
1716
2082
  signal?: AbortSignal;
1717
2083
  }): Promise<OmpPersistedSubagentMessages>;
@@ -1737,13 +2103,30 @@ export interface OmpRpcRuntimeOptions {
1737
2103
  ) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
1738
2104
  }
1739
2105
 
2106
+ export type OmpRpcRequestRejectionCode = "session_busy" | "stale_cursor" | "unsupported_command";
2107
+
1740
2108
  export class OmpRpcRequestRejectedError extends Error {
1741
- constructor() {
2109
+ constructor(
2110
+ readonly command?: OmpRpcDiagnosticCommand,
2111
+ readonly code?: OmpRpcRequestRejectionCode,
2112
+ ) {
1742
2113
  super("OMP RPC request failed");
1743
2114
  this.name = "OmpRpcRequestRejectedError";
1744
2115
  }
1745
2116
  }
1746
2117
 
2118
+ export class OmpRpcResponseLimitError extends Error {
2119
+ constructor(
2120
+ readonly command: OmpRpcDiagnosticCommand | undefined,
2121
+ readonly bound: OmpRpcBoundDimension,
2122
+ readonly actual: number,
2123
+ readonly limit: number,
2124
+ ) {
2125
+ super("OMP RPC response exceeded command limits");
2126
+ this.name = "OmpRpcResponseLimitError";
2127
+ }
2128
+ }
2129
+
1747
2130
  type PendingRequest = {
1748
2131
  resolve(value: unknown): void;
1749
2132
  reject(error: Error): void;
@@ -2618,8 +3001,13 @@ class OmpRpcProcess {
2618
3001
  } else {
2619
3002
  this.appendLinePart(part);
2620
3003
  if (this.fatalError) return;
2621
- if (this.discardingLine) this.resetDiscardedLine();
2622
- else this.completeLine();
3004
+ if (this.discardingLine) {
3005
+ if (this.discardedLineBytes > MAX_SEMANTIC_FRAME_BYTES) {
3006
+ this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
3007
+ return;
3008
+ }
3009
+ this.resetDiscardedLine();
3010
+ } else this.completeLine();
2623
3011
  }
2624
3012
  start = index + 1;
2625
3013
  }
@@ -2674,10 +3062,6 @@ class OmpRpcProcess {
2674
3062
  this.lineParts = [];
2675
3063
  this.lineBytes = 0;
2676
3064
  const payload = line.at(-1) === 13 ? line.subarray(0, -1) : line;
2677
- if (payload.byteLength > MAX_SEMANTIC_FRAME_BYTES) {
2678
- this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
2679
- return;
2680
- }
2681
3065
  let decoded: unknown;
2682
3066
  try {
2683
3067
  decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
@@ -2699,7 +3083,10 @@ class OmpRpcProcess {
2699
3083
  frame.byteLength > MAX_SEMANTIC_FRAME_BYTES &&
2700
3084
  ![...this.pending.values()].some(
2701
3085
  (pending) =>
2702
- pending.command === "get_messages" || pending.command === "get_subagent_messages",
3086
+ pending.command === "get_messages" ||
3087
+ pending.command === "get_messages_page" ||
3088
+ pending.command === "get_subagent_messages" ||
3089
+ pending.command === "get_state",
2703
3090
  )
2704
3091
  ) {
2705
3092
  this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
@@ -2805,12 +3192,16 @@ class OmpRpcProcess {
2805
3192
  const pending = typeof frame.id === "string" ? this.pending.get(frame.id) : undefined;
2806
3193
  if (
2807
3194
  frame.type === "response" &&
2808
- (pending?.command === "get_messages" || pending?.command === "get_subagent_messages")
3195
+ (pending?.command === "get_messages" ||
3196
+ pending?.command === "get_messages_page" ||
3197
+ pending?.command === "get_subagent_messages" ||
3198
+ pending?.command === "get_state")
2809
3199
  ) {
2810
3200
  this.receiveResponse(frame);
2811
3201
  return;
2812
3202
  }
2813
3203
  }
3204
+ if (this.receiveUnsupportedCommandResponse(value)) return;
2814
3205
  if (rawByteLength > MAX_SEMANTIC_FRAME_BYTES) {
2815
3206
  this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
2816
3207
  return;
@@ -2845,6 +3236,30 @@ class OmpRpcProcess {
2845
3236
  return true;
2846
3237
  }
2847
3238
 
3239
+ private receiveUnsupportedCommandResponse(value: unknown): boolean {
3240
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3241
+ const frame = value as Record<string, unknown>;
3242
+ if (
3243
+ frame.type !== "response" ||
3244
+ frame.id !== undefined ||
3245
+ frame.success !== false ||
3246
+ typeof frame.command !== "string" ||
3247
+ frame.error !== `Unknown command: ${frame.command}`
3248
+ ) {
3249
+ return false;
3250
+ }
3251
+ const matches = [...this.pending.entries()].filter(
3252
+ ([, pending]) => pending.command === frame.command,
3253
+ );
3254
+ if (matches.length === 0) return false;
3255
+ const [id, pending] = matches[0] ?? [];
3256
+ if (!id || !pending) return false;
3257
+ this.takePending(id)?.reject(
3258
+ new OmpRpcRequestRejectedError(rpcDiagnosticCommand(pending.command), "unsupported_command"),
3259
+ );
3260
+ return true;
3261
+ }
3262
+
2848
3263
  private receiveResponse(frame: Record<string, unknown>): void {
2849
3264
  const rawId = typeof frame.id === "string" ? frame.id : undefined;
2850
3265
  const knownPending = rawId ? this.pending.get(rawId) : undefined;
@@ -2877,10 +3292,14 @@ class OmpRpcProcess {
2877
3292
  }
2878
3293
  const isBranchHistory = pending.command === "get_branch_messages";
2879
3294
  const isHistory =
2880
- pending.command === "get_messages" || pending.command === "get_subagent_messages";
2881
- const responseData = isHistory
2882
- ? sanitizeHistoryResponseData(response.data.data)
2883
- : response.data.data;
3295
+ pending.command === "get_messages" ||
3296
+ pending.command === "get_messages_page" ||
3297
+ pending.command === "get_subagent_messages";
3298
+ const rawResponseData =
3299
+ pending.command === "get_state"
3300
+ ? projectSessionStateResponseData(response.data.data)
3301
+ : response.data.data;
3302
+ const responseData = isHistory ? sanitizeHistoryResponseData(rawResponseData) : rawResponseData;
2884
3303
  const boundedFrame =
2885
3304
  responseData === response.data.data ? frame : { ...frame, data: responseData };
2886
3305
  const responseItemLimit = isBranchHistory
@@ -2903,17 +3322,21 @@ class OmpRpcProcess {
2903
3322
  : pending.command === "get_available_models"
2904
3323
  ? 16_384
2905
3324
  : 2_048;
2906
- if (
2907
- boundedJsonBytes(
2908
- boundedFrame,
2909
- responseByteLimit,
2910
- responseItemLimit,
2911
- MAX_IMAGE_DATA_LENGTH,
2912
- responseNodeLimit,
2913
- ) === Number.POSITIVE_INFINITY
2914
- ) {
3325
+ const violation = jsonBoundViolation(
3326
+ boundedFrame,
3327
+ responseByteLimit,
3328
+ responseItemLimit,
3329
+ MAX_IMAGE_DATA_LENGTH,
3330
+ responseNodeLimit,
3331
+ );
3332
+ if (violation) {
2915
3333
  this.takePending(response.data.id)?.reject(
2916
- new Error("OMP RPC response exceeded command limits"),
3334
+ new OmpRpcResponseLimitError(
3335
+ rpcDiagnosticCommand(pending.command),
3336
+ violation.dimension,
3337
+ violation.actual,
3338
+ violation.limit,
3339
+ ),
2917
3340
  );
2918
3341
  return;
2919
3342
  }
@@ -2934,7 +3357,16 @@ class OmpRpcProcess {
2934
3357
  settled.reject(new Error("OMP RPC response is invalid"));
2935
3358
  }
2936
3359
  } else {
2937
- settled.reject(new OmpRpcRequestRejectedError());
3360
+ const responseCode = response.data.code;
3361
+ const code: OmpRpcRequestRejectionCode | undefined =
3362
+ responseCode === "session_busy" ||
3363
+ responseCode === "stale_cursor" ||
3364
+ responseCode === "unsupported_command"
3365
+ ? responseCode
3366
+ : response.data.error === `Unknown command: ${pending.command}`
3367
+ ? "unsupported_command"
3368
+ : undefined;
3369
+ settled.reject(new OmpRpcRequestRejectedError(rpcDiagnosticCommand(pending.command), code));
2938
3370
  }
2939
3371
  }
2940
3372
 
@@ -3189,7 +3621,7 @@ class OmpRpcProcess {
3189
3621
  this.activeToolCallIds.add(event.toolCallId);
3190
3622
  return true;
3191
3623
  }
3192
- if (event.type === "tool_execution_update") {
3624
+ if (event.type === "tool_execution_update" || event.type === "tool_stream_update") {
3193
3625
  return this.activeToolCallIds.has(event.toolCallId);
3194
3626
  }
3195
3627
  if (event.type === "tool_execution_end") {
@@ -3400,6 +3832,7 @@ class OmpRpcSession implements OmpRuntimeSession {
3400
3832
  readonly inheritedRedactionValues: readonly string[],
3401
3833
  readonly supportsTypedToolApprovals: boolean,
3402
3834
  ) {}
3835
+ private messagePagingSupport: "unknown" | "supported" | "unsupported" = "unknown";
3403
3836
 
3404
3837
  onEvent(listener: (event: OmpRpcEvent) => void): () => void {
3405
3838
  return this.process.onEvent(listener);
@@ -3413,6 +3846,195 @@ class OmpRpcSession implements OmpRuntimeSession {
3413
3846
  return OmpSessionStatsSchema.parse(await this.process.request({ type: "get_session_stats" }));
3414
3847
  }
3415
3848
 
3849
+ async getMessages(signal?: AbortSignal): Promise<OmpMessage[]> {
3850
+ if (!this.canReplayHistory) {
3851
+ throw new Error("OMP history replay requires negotiated RPC protocol v2");
3852
+ }
3853
+ if (this.messagePagingSupport === "unsupported") {
3854
+ const legacy = OmpMessagesResultSchema.parse(
3855
+ await this.requestWithSignal({ type: "get_messages" }, signal),
3856
+ );
3857
+ return legacy.messages;
3858
+ }
3859
+ let staleRestarts = 0;
3860
+ let busyRetries = 0;
3861
+ while (true) {
3862
+ const messages: OmpMessage[] = [];
3863
+ const seenCursors = new Set<string>();
3864
+ const seenMessageIdentities = new Set<string>();
3865
+ let pageCount = 0;
3866
+ let cursor: string | undefined;
3867
+ let totalMessages: number | undefined;
3868
+ let aggregateBytes = 0;
3869
+ let aggregateNodes = 0;
3870
+ try {
3871
+ while (true) {
3872
+ signal?.throwIfAborted();
3873
+ let rawPage: unknown;
3874
+ try {
3875
+ rawPage = await this.requestWithSignal(
3876
+ {
3877
+ type: "get_messages_page",
3878
+ ...(cursor ? { cursor } : {}),
3879
+ limit: OMP_MESSAGE_PAGE_LIMIT,
3880
+ },
3881
+ signal,
3882
+ );
3883
+ } catch (error) {
3884
+ if (
3885
+ error instanceof OmpRpcRequestRejectedError &&
3886
+ error.code === "unsupported_command"
3887
+ ) {
3888
+ this.messagePagingSupport = "unsupported";
3889
+ const legacy = OmpMessagesResultSchema.parse(
3890
+ await this.requestWithSignal({ type: "get_messages" }, signal),
3891
+ );
3892
+ return legacy.messages;
3893
+ }
3894
+ if (
3895
+ error instanceof OmpRpcRequestRejectedError &&
3896
+ error.code === "session_busy" &&
3897
+ busyRetries < MAX_MESSAGE_PAGE_BUSY_RETRIES
3898
+ ) {
3899
+ const delayMs = MESSAGE_PAGE_RETRY_BASE_MS * 2 ** busyRetries;
3900
+ busyRetries += 1;
3901
+ await this.waitForReplayRetry(delayMs, signal);
3902
+ continue;
3903
+ }
3904
+ throw error;
3905
+ }
3906
+ pageCount += 1;
3907
+ if (pageCount > MAX_MESSAGE_PAGES) {
3908
+ throw new OmpRpcResponseLimitError(
3909
+ "get_messages_page",
3910
+ "items",
3911
+ pageCount,
3912
+ MAX_MESSAGE_PAGES,
3913
+ );
3914
+ }
3915
+ const page = OmpMessagesPageResultSchema.parse(rawPage);
3916
+ this.messagePagingSupport = "supported";
3917
+ if (totalMessages !== undefined && page.totalMessages !== totalMessages) {
3918
+ throw new Error("OMP message pagination returned an inconsistent total");
3919
+ }
3920
+ totalMessages ??= page.totalMessages;
3921
+ const nextMessageCount = messages.length + page.messages.length;
3922
+ if (nextMessageCount > totalMessages || nextMessageCount > MAX_REPLAY_MESSAGES) {
3923
+ throw new OmpRpcResponseLimitError(
3924
+ "get_messages_page",
3925
+ "items",
3926
+ nextMessageCount,
3927
+ Math.min(totalMessages, MAX_REPLAY_MESSAGES),
3928
+ );
3929
+ }
3930
+ const pageIdentities = new Set<string>();
3931
+ for (const message of page.messages) {
3932
+ const identity = message.entryId ?? message.id ?? message.responseId;
3933
+ if (!identity) continue;
3934
+ if (seenMessageIdentities.has(identity)) {
3935
+ throw new Error("OMP message pagination repeated a message identity");
3936
+ }
3937
+ pageIdentities.add(identity);
3938
+ }
3939
+ const inspection = inspectJsonBounds(
3940
+ page.messages,
3941
+ MAX_REPLAY_BYTES,
3942
+ MAX_REPLAY_MESSAGES,
3943
+ MAX_IMAGE_DATA_LENGTH,
3944
+ MAX_REPLAY_NODES,
3945
+ );
3946
+ if (inspection.violation) {
3947
+ throw new OmpRpcResponseLimitError(
3948
+ "get_messages_page",
3949
+ inspection.violation.dimension,
3950
+ inspection.violation.actual,
3951
+ inspection.violation.limit,
3952
+ );
3953
+ }
3954
+ aggregateBytes += inspection.bytes;
3955
+ aggregateNodes += inspection.nodes;
3956
+ if (aggregateBytes > MAX_REPLAY_BYTES) {
3957
+ throw new OmpRpcResponseLimitError(
3958
+ "get_messages_page",
3959
+ "bytes",
3960
+ aggregateBytes,
3961
+ MAX_REPLAY_BYTES,
3962
+ );
3963
+ }
3964
+ if (aggregateNodes > MAX_REPLAY_NODES) {
3965
+ throw new OmpRpcResponseLimitError(
3966
+ "get_messages_page",
3967
+ "nodes",
3968
+ aggregateNodes,
3969
+ MAX_REPLAY_NODES,
3970
+ );
3971
+ }
3972
+ for (const identity of pageIdentities) seenMessageIdentities.add(identity);
3973
+ messages.push(...page.messages);
3974
+ const nextCursor = page.nextCursor;
3975
+ if (!nextCursor) {
3976
+ if (messages.length !== totalMessages) {
3977
+ throw new Error("OMP message pagination ended before the advertised total");
3978
+ }
3979
+ return messages;
3980
+ }
3981
+ if (page.messages.length === 0 || messages.length >= totalMessages) {
3982
+ throw new Error("OMP message pagination did not make progress");
3983
+ }
3984
+ if (seenCursors.has(nextCursor)) {
3985
+ throw new Error("OMP message pagination repeated a cursor");
3986
+ }
3987
+ seenCursors.add(nextCursor);
3988
+ cursor = nextCursor;
3989
+ }
3990
+ } catch (error) {
3991
+ if (
3992
+ error instanceof OmpRpcRequestRejectedError &&
3993
+ error.code === "stale_cursor" &&
3994
+ staleRestarts < MAX_MESSAGE_PAGE_STALE_RESTARTS
3995
+ ) {
3996
+ staleRestarts += 1;
3997
+ continue;
3998
+ }
3999
+ throw error;
4000
+ }
4001
+ }
4002
+ }
4003
+
4004
+ private async requestWithSignal(
4005
+ command: Record<string, unknown>,
4006
+ signal?: AbortSignal,
4007
+ ): Promise<unknown> {
4008
+ signal?.throwIfAborted();
4009
+ const request = this.process.request(command);
4010
+ if (!signal) return request;
4011
+ const aborted = Promise.withResolvers<never>();
4012
+ const onAbort = () => aborted.reject(signal.reason);
4013
+ signal.addEventListener("abort", onAbort, { once: true });
4014
+ try {
4015
+ return await Promise.race([request, aborted.promise]);
4016
+ } finally {
4017
+ signal.removeEventListener("abort", onAbort);
4018
+ void request.catch(() => undefined);
4019
+ }
4020
+ }
4021
+
4022
+ private async waitForReplayRetry(delayMs: number, signal?: AbortSignal): Promise<void> {
4023
+ signal?.throwIfAborted();
4024
+ if (!signal) {
4025
+ await waitMs(delayMs);
4026
+ return;
4027
+ }
4028
+ const aborted = Promise.withResolvers<never>();
4029
+ const onAbort = () => aborted.reject(signal.reason);
4030
+ signal.addEventListener("abort", onAbort, { once: true });
4031
+ try {
4032
+ await Promise.race([waitMs(delayMs), aborted.promise]);
4033
+ } finally {
4034
+ signal.removeEventListener("abort", onAbort);
4035
+ }
4036
+ }
4037
+
3416
4038
  async compact(customInstructions?: string): Promise<OmpCompactionResult> {
3417
4039
  const instructions =
3418
4040
  customInstructions === undefined
@@ -3508,16 +4130,6 @@ class OmpRpcSession implements OmpRuntimeSession {
3508
4130
  return result.data;
3509
4131
  }
3510
4132
 
3511
- async getMessages(): Promise<OmpMessage[]> {
3512
- if (!this.canReplayHistory) {
3513
- throw new Error("OMP history replay requires negotiated RPC protocol v2");
3514
- }
3515
- const result = OmpMessagesResultSchema.parse(
3516
- await this.process.request({ type: "get_messages" }),
3517
- );
3518
- return result.messages;
3519
- }
3520
-
3521
4133
  async setHostTools(tools: readonly OmpHostToolDefinition[]): Promise<string[]> {
3522
4134
  const safeTools = z.array(OmpHostToolDefinitionSchema).max(MAX_HOST_TOOLS).parse(tools);
3523
4135
  if (safeTools.length === 0) return [];
@@ -3627,13 +4239,14 @@ export class OmpRpcRuntime implements OmpRuntime {
3627
4239
  ...transcript,
3628
4240
  messages: z
3629
4241
  .array(OmpMessageSchema)
3630
- .max(100_000)
4242
+ .max(MAX_REPLAY_MESSAGES)
3631
4243
  .parse(sanitizeMessageListMetadata(transcript.messages)),
3632
4244
  };
3633
4245
  }
3634
4246
  async readPersistedSubagentTranscript(options: {
3635
4247
  parentSessionFile: string;
3636
4248
  childTranscriptId: string;
4249
+ sessionFile?: string;
3637
4250
  cwd: string;
3638
4251
  signal?: AbortSignal;
3639
4252
  }): Promise<OmpPersistedSubagentMessages> {
@@ -3642,12 +4255,13 @@ export class OmpRpcRuntime implements OmpRuntime {
3642
4255
  options.childTranscriptId,
3643
4256
  options.cwd,
3644
4257
  options.signal,
4258
+ options.sessionFile,
3645
4259
  );
3646
4260
  return {
3647
4261
  ...transcript,
3648
4262
  messages: z
3649
4263
  .array(OmpMessageSchema)
3650
- .max(100_000)
4264
+ .max(MAX_REPLAY_MESSAGES)
3651
4265
  .parse(sanitizeMessageListMetadata(transcript.messages)),
3652
4266
  };
3653
4267
  }