@omercnet/paseo-omp 0.3.0 → 0.4.0-next.119.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +19 -5
  2. package/client/omp-config-surface.tsx +243 -24
  3. package/client/omp-config-views.ts +24 -0
  4. package/client/omp-model-picker-state.ts +145 -0
  5. package/client/omp-model-picker.tsx +282 -0
  6. package/client/omp-routing-editor.tsx +307 -0
  7. package/client/support-diagnostics-state.ts +45 -0
  8. package/index.server.ts +27 -2
  9. package/package.json +4 -8
  10. package/paseo-plugin.json +1 -1
  11. package/server/omp-models.ts +59 -0
  12. package/server/omp-settings.ts +30 -20
  13. package/server/operational-failure-diagnostics.ts +76 -0
  14. package/server/package-version.ts +2 -0
  15. package/server/protocol-violation-diagnostics.ts +169 -0
  16. package/server/provider/catalog.ts +39 -10
  17. package/server/provider/connection.ts +88 -9
  18. package/server/provider/host-tools.ts +284 -34
  19. package/server/provider/mcp-transport.ts +2 -1
  20. package/server/provider/omp-rpc.ts +1785 -267
  21. package/server/provider/profile-providers.ts +7 -2
  22. package/server/provider/registration.ts +12 -2
  23. package/server/provider/security.ts +8 -10
  24. package/server/provider/session-descriptors.ts +97 -29
  25. package/server/provider/session.ts +212 -59
  26. package/server/provider/subsessions.ts +334 -74
  27. package/server/provider/timeline-projector.ts +125 -32
  28. package/server/support-diagnostics.ts +284 -0
  29. package/shared/omp-models.ts +49 -0
  30. package/shared/omp-settings.ts +227 -3
  31. package/shared/support-diagnostics.ts +32 -0
  32. package/CHANGELOG.md +0 -113
  33. package/SUPPORT.md +0 -44
  34. package/TESTING.md +0 -150
  35. package/docs/alpha-release-checklist.md +0 -68
  36. package/docs/configuration.md +0 -126
  37. package/docs/core-provider-issue-audit.md +0 -109
  38. package/docs/images/mcp-authorization-compact.png +0 -0
  39. package/docs/images/mcp-controls-wide.png +0 -0
  40. package/docs/images/plugin-manager.png +0 -0
  41. package/docs/images/workspace-settings.png +0 -0
  42. package/docs/installation.md +0 -89
  43. package/tsconfig.json +0 -16
@@ -4,7 +4,13 @@ import { isAbsolute, join } from "node:path";
4
4
  import { z } from "zod";
5
5
  import { ompDataDir } from "../paths";
6
6
  import { isValidImagePayload } from "./image";
7
- import { boundedJsonBytes, OmpCleanupFailure, OmpPublicError, utf8Bytes } from "./security";
7
+ import {
8
+ boundedJsonBytes,
9
+ boundedJsonMetrics,
10
+ OmpCleanupFailure,
11
+ OmpPublicError,
12
+ utf8Bytes,
13
+ } from "./security";
8
14
  import {
9
15
  listOmpSessionDescriptors,
10
16
  type OmpSessionDescriptor,
@@ -56,6 +62,15 @@ const MAX_PENDING_ONE_WAY_WRITES = 256;
56
62
  const MAX_PENDING_WRITE_BYTES = 8 * 1024 * 1024;
57
63
  const MAX_LINE_PARTS = 4_096;
58
64
  const MAX_ARRAY_ITEMS = 512;
65
+ // OMP read metadata can contain one source entry per displayed line. Bound this optional,
66
+ // opaque field separately so over-budget metadata can be omitted without losing completion events.
67
+ const MAX_OPTIONAL_METADATA_BYTES = MAX_TOOL_PAYLOAD_LENGTH;
68
+ const MAX_OPTIONAL_METADATA_ITEMS = 2_048;
69
+ const MAX_OPTIONAL_METADATA_NODES = 4_096;
70
+ const MAX_TASK_CORRELATION_BYTES = 256 * 1024;
71
+ const MAX_TASK_CORRELATION_ITEMS = 1_024;
72
+ const MAX_TASK_CORRELATION_NODES = 4_096;
73
+ const MAX_MODEL_CATALOG_ITEMS = 4_096;
59
74
  // Tool-intensive OMP turns legitimately exceed 64 blocks; transport byte/node budgets remain the
60
75
  // primary resource bounds.
61
76
  export const OMP_MAX_CONTENT_PARTS = 4_096;
@@ -70,6 +85,407 @@ const MAX_COST_USD = 1_000_000_000;
70
85
  const MAX_RPC_ERROR_BYTES = 4_096;
71
86
  const MAX_RPC_ERROR_CODE_BYTES = 256;
72
87
  const PROMPT_SCHEDULING_FAILURE = "OMP prompt scheduling failed";
88
+ const PROTOCOL_VIOLATION_COALESCE_MS = 10_000;
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;
188
+ export const OMP_PROTOCOL_VIOLATION_CATEGORIES = [
189
+ "duplicate-ready",
190
+ "frame-limit",
191
+ "incomplete-frame",
192
+ "interleaved-chunk",
193
+ "invalid-chunk",
194
+ "invalid-envelope",
195
+ "invalid-event",
196
+ "invalid-event-state",
197
+ "invalid-json",
198
+ "invalid-ready",
199
+ "invalid-response",
200
+ "remote-frame-error",
201
+ ] as const;
202
+ export type OmpProtocolViolationCategory = (typeof OMP_PROTOCOL_VIOLATION_CATEGORIES)[number];
203
+ export const OMP_PROTOCOL_VIOLATION_REASONS = [
204
+ "output-ended-mid-frame",
205
+ "physical-frame-limit",
206
+ "semantic-frame-limit",
207
+ "json-decode",
208
+ "chunk-json-decode",
209
+ "object-envelope",
210
+ "missing-frame-type",
211
+ "response-schema",
212
+ "chunk-schema",
213
+ "chunk-metadata",
214
+ "chunk-size",
215
+ "chunk-start-index",
216
+ "chunk-sequence",
217
+ "chunk-byte-count",
218
+ "chunk-timeout",
219
+ "frame-interleaved-with-chunk",
220
+ "remote-frame-error",
221
+ "ready-already-received",
222
+ "ready-schema",
223
+ "notice-level-type",
224
+ "notice-message-type",
225
+ "notice-schema",
226
+ "event-schema",
227
+ "message-event-schema",
228
+ "tool-event-schema",
229
+ "lifecycle-event-schema",
230
+ "subagent-event-schema",
231
+ "configuration-event-schema",
232
+ "extension-ui-schema",
233
+ "unknown-event-type",
234
+ "event-state-transition",
235
+ ] as const;
236
+ export type OmpProtocolViolationReason = (typeof OMP_PROTOCOL_VIOLATION_REASONS)[number];
237
+ export const OMP_PROTOCOL_DIAGNOSTIC_PHASES = [
238
+ "startup",
239
+ "negotiation",
240
+ "idle",
241
+ "active-turn",
242
+ "closing",
243
+ ] as const;
244
+ export type OmpProtocolDiagnosticPhase = (typeof OMP_PROTOCOL_DIAGNOSTIC_PHASES)[number];
245
+
246
+ export const OMP_PROTOCOL_EVENT_TYPES = [
247
+ "agent_start",
248
+ "agent_end",
249
+ "turn_start",
250
+ "turn_end",
251
+ "message_start",
252
+ "message_update",
253
+ "message_end",
254
+ "tool_execution_start",
255
+ "tool_execution_update",
256
+ "tool_execution_end",
257
+ "tool_stream_update",
258
+ "compaction_start",
259
+ "compaction_end",
260
+ "subagent_lifecycle",
261
+ "subagent_progress",
262
+ "subagent_event",
263
+ "todo_reminder",
264
+ "model_changed",
265
+ "config_warnings_changed",
266
+ "advisor_cost_changed",
267
+ "ttsr_triggered",
268
+ "irc_message",
269
+ "thinking_level_changed",
270
+ "goal_updated",
271
+ "auto_retry_start",
272
+ "auto_retry_end",
273
+ "retry_fallback_applied",
274
+ "retry_fallback_succeeded",
275
+ "todo_auto_clear",
276
+ "auto_compaction_start",
277
+ "auto_compaction_end",
278
+ "available_commands_update",
279
+ "notice",
280
+ "command_output",
281
+ "extension_ui_request",
282
+ "prompt_result",
283
+ "host_tool_call",
284
+ "host_tool_cancel",
285
+ "tool_approval_request",
286
+ "tool_approval_cancel",
287
+ "advisor_yielded",
288
+ ] as const;
289
+ export type OmpProtocolEventType = (typeof OMP_PROTOCOL_EVENT_TYPES)[number];
290
+
291
+ export const OMP_PROTOCOL_DIAGNOSTIC_FIELDS = [
292
+ "frame",
293
+ "frame.byteLength",
294
+ "frame.type",
295
+ "response",
296
+ "ready",
297
+ "event",
298
+ "event.sequence",
299
+ "notice.level",
300
+ "notice.message",
301
+ "chunk",
302
+ "chunk.data",
303
+ "chunk.index",
304
+ "chunk.sequence",
305
+ "chunk.byteLength",
306
+ ] as const;
307
+ export type OmpProtocolDiagnosticField = (typeof OMP_PROTOCOL_DIAGNOSTIC_FIELDS)[number];
308
+
309
+ export const OMP_PROTOCOL_DIAGNOSTIC_EXPECTATIONS = [
310
+ "complete-json-line",
311
+ "within-byte-limit",
312
+ "valid-json",
313
+ "object-envelope",
314
+ "bounded-frame-type",
315
+ "valid-response-frame",
316
+ "valid-ready-frame",
317
+ "single-ready-frame",
318
+ "valid-event-frame",
319
+ "valid-event-state-transition",
320
+ "valid-message-event",
321
+ "valid-tool-event",
322
+ "valid-lifecycle-event",
323
+ "valid-subagent-event",
324
+ "valid-configuration-event",
325
+ "valid-extension-ui-event",
326
+ "known-event-type",
327
+ "notice-level-enum",
328
+ "notice-message-string",
329
+ "valid-chunk-frame",
330
+ "valid-base64-chunk",
331
+ "first-chunk-index-zero",
332
+ "contiguous-chunk-sequence",
333
+ "declared-chunk-byte-count",
334
+ "chunk-before-deadline",
335
+ "no-interleaved-frame",
336
+ "no-remote-frame-error",
337
+ ] as const;
338
+ export type OmpProtocolDiagnosticExpectation =
339
+ (typeof OMP_PROTOCOL_DIAGNOSTIC_EXPECTATIONS)[number];
340
+
341
+ export const OMP_PROTOCOL_DIAGNOSTIC_ACTUAL_TYPES = [
342
+ "missing",
343
+ "null",
344
+ "array",
345
+ "object",
346
+ "string",
347
+ "number",
348
+ "boolean",
349
+ "invalid-json",
350
+ "partial-frame",
351
+ "oversized",
352
+ "duplicate",
353
+ "interleaved",
354
+ "out-of-order",
355
+ "timeout",
356
+ "remote-error",
357
+ "mismatched",
358
+ ] as const;
359
+ export type OmpProtocolDiagnosticActualType = (typeof OMP_PROTOCOL_DIAGNOSTIC_ACTUAL_TYPES)[number];
360
+
361
+ export interface OmpProtocolViolationDiagnostic {
362
+ category: OmpProtocolViolationCategory;
363
+ reason: OmpProtocolViolationReason;
364
+ phase: OmpProtocolDiagnosticPhase;
365
+ occurrenceCount: number;
366
+ eventType?: OmpProtocolEventType;
367
+ frameType?: "ready" | "response" | "rpc_chunk" | "rpc_frame_error" | "notice";
368
+ field?: OmpProtocolDiagnosticField;
369
+ expected?: OmpProtocolDiagnosticExpectation;
370
+ actualType?: OmpProtocolDiagnosticActualType;
371
+ maxByteSize?: number;
372
+ limitBytes?: number;
373
+ }
374
+
375
+ function protocolDiagnosticActualType(value: unknown): OmpProtocolDiagnosticActualType {
376
+ if (value === undefined) return "missing";
377
+ if (value === null) return "null";
378
+ if (Array.isArray(value)) return "array";
379
+ if (typeof value === "object") return "object";
380
+ if (typeof value === "string") return "string";
381
+ if (typeof value === "number") return "number";
382
+ if (typeof value === "boolean") return "boolean";
383
+ return "object";
384
+ }
385
+
386
+ function isProtocolEventType(value: string): value is OmpProtocolEventType {
387
+ return (OMP_PROTOCOL_EVENT_TYPES as readonly string[]).includes(value);
388
+ }
389
+
390
+ function eventSchemaClassification(
391
+ eventType: OmpProtocolEventType,
392
+ ): Pick<OmpProtocolViolationDiagnostic, "reason" | "expected"> {
393
+ switch (eventType) {
394
+ case "message_start":
395
+ case "message_update":
396
+ case "message_end":
397
+ return { reason: "message-event-schema", expected: "valid-message-event" };
398
+ case "irc_message":
399
+ return { reason: "message-event-schema", expected: "valid-message-event" };
400
+ case "tool_execution_start":
401
+ case "tool_execution_update":
402
+ case "tool_stream_update":
403
+ case "tool_execution_end":
404
+ case "host_tool_call":
405
+ case "host_tool_cancel":
406
+ case "tool_approval_request":
407
+ case "tool_approval_cancel":
408
+ return { reason: "tool-event-schema", expected: "valid-tool-event" };
409
+ case "subagent_lifecycle":
410
+ case "subagent_progress":
411
+ case "subagent_event":
412
+ return { reason: "subagent-event-schema", expected: "valid-subagent-event" };
413
+ case "model_changed":
414
+ case "thinking_level_changed":
415
+ case "goal_updated":
416
+ case "todo_reminder":
417
+ case "todo_auto_clear":
418
+ case "available_commands_update":
419
+ case "retry_fallback_applied":
420
+ case "retry_fallback_succeeded":
421
+ case "config_warnings_changed":
422
+ case "advisor_cost_changed":
423
+ case "ttsr_triggered":
424
+ return { reason: "configuration-event-schema", expected: "valid-configuration-event" };
425
+ case "extension_ui_request":
426
+ return { reason: "extension-ui-schema", expected: "valid-extension-ui-event" };
427
+ default:
428
+ return { reason: "lifecycle-event-schema", expected: "valid-lifecycle-event" };
429
+ }
430
+ }
431
+
432
+ function invalidEventDiagnosticMetadata(
433
+ frame: Record<string, unknown>,
434
+ type: string,
435
+ ): Pick<
436
+ OmpProtocolViolationDiagnostic,
437
+ "reason" | "eventType" | "frameType" | "field" | "expected" | "actualType"
438
+ > {
439
+ if (!isProtocolEventType(type)) {
440
+ return {
441
+ reason: "unknown-event-type",
442
+ field: "frame.type",
443
+ expected: "known-event-type",
444
+ actualType: "string",
445
+ };
446
+ }
447
+ if (type !== "notice") {
448
+ return {
449
+ ...eventSchemaClassification(type),
450
+ eventType: type,
451
+ field: "event",
452
+ actualType: "object",
453
+ };
454
+ }
455
+ if (typeof frame.level !== "string") {
456
+ return {
457
+ reason: "notice-level-type",
458
+ eventType: "notice",
459
+ frameType: "notice",
460
+ field: "notice.level",
461
+ expected: "notice-level-enum",
462
+ actualType: protocolDiagnosticActualType(frame.level),
463
+ };
464
+ }
465
+ if (typeof frame.message !== "string") {
466
+ return {
467
+ reason: "notice-message-type",
468
+ eventType: "notice",
469
+ frameType: "notice",
470
+ field: "notice.message",
471
+ expected: "notice-message-string",
472
+ actualType: protocolDiagnosticActualType(frame.message),
473
+ };
474
+ }
475
+ return {
476
+ reason: "notice-schema",
477
+ eventType: "notice",
478
+ frameType: "notice",
479
+ field: "event",
480
+ expected: "valid-event-frame",
481
+ actualType: "object",
482
+ };
483
+ }
484
+
485
+ type PendingProtocolViolation = Omit<OmpProtocolViolationDiagnostic, "occurrenceCount"> & {
486
+ occurrenceCount: number;
487
+ };
488
+ type ProtocolViolationKey = `${OmpProtocolViolationCategory}:${OmpProtocolViolationReason}`;
73
489
  const MAX_CONTEXT_PERCENT = 1_000_000;
74
490
  function boundedJsonString(maxBytes: number, minBytes = 0) {
75
491
  return z.string().refine((value) => {
@@ -101,12 +517,101 @@ function boundedString(maxBytes: number, minBytes = 0) {
101
517
  return bytes >= minBytes && bytes <= maxBytes;
102
518
  });
103
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
+ }
527
+ function boundedOptionalStringArray(maxItems: number, maxBytes: number) {
528
+ return z.unknown().transform((value): string[] | undefined => {
529
+ if (!Array.isArray(value)) return undefined;
530
+ return value
531
+ .filter(
532
+ (item): item is string =>
533
+ typeof item === "string" && item.length > 0 && utf8Bytes(item) <= maxBytes,
534
+ )
535
+ .slice(0, maxItems);
536
+ });
537
+ }
104
538
 
105
539
  const IDENTIFIER = boundedString(MAX_ID_LENGTH, 1);
106
540
  const NAME = boundedString(MAX_NAME_LENGTH, 1);
107
541
  const OMP_PROVIDER_NAME = NAME.refine((provider) => !provider.includes("/"));
108
542
  const TEXT = boundedString(MAX_TEXT_LENGTH);
543
+ const RAW_DISPLAY_TEXT = boundedString(MAX_IMAGE_DATA_LENGTH);
544
+ const DISPLAY_TRUNCATION_MARKER = "<truncated>";
545
+
546
+ function boundRawDisplayContent(value: unknown): unknown {
547
+ if (typeof value === "string") {
548
+ return utf8Bytes(value) <= MAX_IMAGE_DATA_LENGTH ? value : DISPLAY_TRUNCATION_MARKER;
549
+ }
550
+ if (!Array.isArray(value)) return value;
551
+ let totalBytes = 0;
552
+ for (const part of value) {
553
+ if (!part || typeof part !== "object" || Array.isArray(part)) continue;
554
+ const record = part as Record<string, unknown>;
555
+ if (typeof record.text === "string") totalBytes += utf8Bytes(record.text);
556
+ if (typeof record.thinking === "string") totalBytes += utf8Bytes(record.thinking);
557
+ }
558
+ if (totalBytes <= MAX_IMAGE_DATA_LENGTH) return value;
559
+
560
+ let retainedMarker = false;
561
+ return value.map((part) => {
562
+ if (!part || typeof part !== "object" || Array.isArray(part)) return part;
563
+ const copy = { ...(part as Record<string, unknown>) };
564
+ for (const key of ["text", "thinking"] as const) {
565
+ if (typeof copy[key] !== "string") continue;
566
+ if (retainedMarker) delete copy[key];
567
+ else {
568
+ copy[key] = DISPLAY_TRUNCATION_MARKER;
569
+ retainedMarker = true;
570
+ }
571
+ }
572
+ return copy;
573
+ });
574
+ }
575
+
576
+ function sanitizeLiveMessageDisplay(value: unknown): unknown {
577
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
578
+ const message = value as Record<string, unknown>;
579
+ if (message.role === "assistant") {
580
+ const content = boundRawDisplayContent(message.content);
581
+ return content === message.content ? value : { ...message, content };
582
+ }
583
+ if (
584
+ (message.role === "bashExecution" || message.role === "pythonExecution") &&
585
+ typeof message.output === "string" &&
586
+ utf8Bytes(message.output) > MAX_IMAGE_DATA_LENGTH
587
+ ) {
588
+ return { ...message, output: DISPLAY_TRUNCATION_MARKER };
589
+ }
590
+ return value;
591
+ }
592
+
593
+ function sanitizeLiveDisplayFrame(value: unknown): unknown {
594
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
595
+ const frame = value as Record<string, unknown>;
596
+ if (
597
+ frame.type === "message_start" ||
598
+ frame.type === "message_update" ||
599
+ frame.type === "message_end"
600
+ ) {
601
+ const message = sanitizeLiveMessageDisplay(frame.message);
602
+ return message === frame.message ? value : { ...frame, message };
603
+ }
604
+ if (frame.type !== "agent_end" || !Array.isArray(frame.messages)) return value;
605
+ let changed = false;
606
+ const messages = frame.messages.map((message) => {
607
+ const sanitized = sanitizeLiveMessageDisplay(message);
608
+ if (sanitized !== message) changed = true;
609
+ return sanitized;
610
+ });
611
+ return changed ? { ...frame, messages } : value;
612
+ }
109
613
  const OmpThinkingLevelSchema = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
614
+ const OmpCurrentThinkingLevelSchema = boundedString(32).optional().catch(undefined);
110
615
 
111
616
  function isBoundedJson(
112
617
  value: unknown,
@@ -118,6 +623,320 @@ function isBoundedJson(
118
623
  boundedJsonBytes(value, maxBytes, maxItems, maxBytes, maxNodes) !== Number.POSITIVE_INFINITY
119
624
  );
120
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
+ }
798
+ function optionalMetadataMetrics(value: unknown) {
799
+ return boundedJsonMetrics(
800
+ value,
801
+ MAX_OPTIONAL_METADATA_BYTES,
802
+ MAX_OPTIONAL_METADATA_ITEMS,
803
+ MAX_OPTIONAL_METADATA_BYTES,
804
+ MAX_OPTIONAL_METADATA_NODES,
805
+ );
806
+ }
807
+
808
+ function optionalMetadataIsBounded(value: unknown): boolean {
809
+ return optionalMetadataMetrics(value) !== undefined;
810
+ }
811
+
812
+ function omitUnsafeOptionalDetails(value: unknown): unknown {
813
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
814
+ const record = value as Record<string, unknown>;
815
+ if (!Object.hasOwn(record, "details") || optionalMetadataIsBounded(record.details)) return value;
816
+ const { details: _details, ...safe } = record;
817
+ return safe;
818
+ }
819
+
820
+ const TASK_RESULT_STATUSES: Readonly<Record<string, true>> = {
821
+ pending: true,
822
+ running: true,
823
+ completed: true,
824
+ failed: true,
825
+ error: true,
826
+ aborted: true,
827
+ canceled: true,
828
+ cancelled: true,
829
+ };
830
+ const TASK_PROGRESS_STATUSES: Readonly<Record<string, true>> = {
831
+ pending: true,
832
+ running: true,
833
+ completed: true,
834
+ failed: true,
835
+ aborted: true,
836
+ };
837
+
838
+ function boundedTaskId(value: unknown): value is string {
839
+ return typeof value === "string" && value.length > 0 && utf8Bytes(value) <= MAX_ID_LENGTH;
840
+ }
841
+
842
+ function taskCorrelationDetails(value: unknown): unknown {
843
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
844
+ const details = value as Record<string, unknown>;
845
+ if (!Array.isArray(details.results) || details.results.length > MAX_TASK_CORRELATION_ITEMS)
846
+ return;
847
+ const results: Record<string, unknown>[] = [];
848
+ for (const value of details.results) {
849
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
850
+ const result = value as Record<string, unknown>;
851
+ if (!boundedTaskId(result.id)) return;
852
+ const safe: Record<string, unknown> = { id: result.id };
853
+ if (typeof result.status === "string" && Object.hasOwn(TASK_RESULT_STATUSES, result.status)) {
854
+ safe.status = result.status;
855
+ }
856
+ if (typeof result.aborted === "boolean") safe.aborted = result.aborted;
857
+ if (typeof result.exitCode === "number" && Number.isFinite(result.exitCode)) {
858
+ safe.exitCode = result.exitCode;
859
+ }
860
+ if (
861
+ result.error !== undefined &&
862
+ boundedJsonMetrics(result.error, 4_096, 32, 4_096, 64) !== undefined
863
+ ) {
864
+ safe.error = result.error;
865
+ }
866
+ results.push(safe);
867
+ }
868
+ let progress: Record<string, unknown>[] | undefined;
869
+ if (details.progress !== undefined) {
870
+ if (!Array.isArray(details.progress) || details.progress.length > MAX_TASK_CORRELATION_ITEMS) {
871
+ return;
872
+ }
873
+ progress = [];
874
+ for (const value of details.progress) {
875
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
876
+ const item = value as Record<string, unknown>;
877
+ if (
878
+ !boundedTaskId(item.id) ||
879
+ typeof item.index !== "number" ||
880
+ !Number.isInteger(item.index) ||
881
+ item.index < 0 ||
882
+ item.index >= MAX_TASK_CORRELATION_ITEMS ||
883
+ typeof item.status !== "string" ||
884
+ !Object.hasOwn(TASK_PROGRESS_STATUSES, item.status)
885
+ ) {
886
+ return;
887
+ }
888
+ progress.push({ id: item.id, index: item.index, status: item.status });
889
+ }
890
+ }
891
+ const correlation = { results, ...(progress ? { progress } : {}) };
892
+ return boundedJsonMetrics(
893
+ correlation,
894
+ MAX_TASK_CORRELATION_BYTES,
895
+ MAX_TASK_CORRELATION_ITEMS,
896
+ MAX_TASK_CORRELATION_BYTES,
897
+ MAX_TASK_CORRELATION_NODES,
898
+ )
899
+ ? correlation
900
+ : undefined;
901
+ }
902
+
903
+ function omitOptionalDetails(value: unknown, preserveTaskCorrelation = false): unknown {
904
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
905
+ const record = value as Record<string, unknown>;
906
+ if (!Object.hasOwn(record, "details")) return value;
907
+ const { details: _details, ...structural } = record;
908
+ if (!preserveTaskCorrelation) return structural;
909
+ const correlation = taskCorrelationDetails(record.details);
910
+ return correlation === undefined ? structural : { ...structural, details: correlation };
911
+ }
912
+
913
+ function createOptionalMetadataSanitizer(): (value: unknown, taskResult?: boolean) => unknown {
914
+ let retainedBytes = 0;
915
+ let retainedNodes = 0;
916
+ return (value, taskResult = false) => {
917
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
918
+ const record = value as Record<string, unknown>;
919
+ if (!Object.hasOwn(record, "details")) return value;
920
+ const metrics = optionalMetadataMetrics(record.details);
921
+ if (
922
+ metrics &&
923
+ retainedBytes + metrics.bytes <= MAX_OPTIONAL_METADATA_BYTES &&
924
+ retainedNodes + metrics.nodes <= MAX_OPTIONAL_METADATA_NODES
925
+ ) {
926
+ retainedBytes += metrics.bytes;
927
+ retainedNodes += metrics.nodes;
928
+ return value;
929
+ }
930
+ const correlation = taskResult ? taskCorrelationDetails(record.details) : undefined;
931
+ const { details: _details, ...safe } = record;
932
+ return correlation === undefined ? safe : { ...safe, details: correlation };
933
+ };
934
+ }
935
+
936
+ const OmpOptionalMetadataSchema = z.preprocess(
937
+ (value) => (optionalMetadataIsBounded(value) ? value : undefined),
938
+ z.unknown().optional(),
939
+ );
121
940
 
122
941
  const OmpContentPartSchema = z
123
942
  .object({
@@ -147,6 +966,14 @@ const OmpContentPartSchema = z
147
966
  context.addIssue({ code: "custom", message: "invalid image payload" });
148
967
  }
149
968
  });
969
+ const OmpAssistantContentPartSchema = OmpContentPartSchema.safeExtend({
970
+ text: RAW_DISPLAY_TEXT.optional(),
971
+ thinking: RAW_DISPLAY_TEXT.optional(),
972
+ });
973
+ const OmpAssistantDisplayContentSchema = z.preprocess(
974
+ boundRawDisplayContent,
975
+ z.union([RAW_DISPLAY_TEXT, z.array(OmpAssistantContentPartSchema).max(OMP_MAX_CONTENT_PARTS)]),
976
+ );
150
977
  const OmpDisplayContentSchema = z.union([
151
978
  TEXT,
152
979
  z.array(OmpContentPartSchema).max(OMP_MAX_CONTENT_PARTS),
@@ -165,60 +992,51 @@ const OmpMessageIdentityShape = {
165
992
  responseId: IDENTIFIER.optional(),
166
993
  images: OmpImageArraySchema.optional(),
167
994
  timestamp: z.number().finite().optional(),
168
- details: z
169
- .unknown()
170
- .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096))
171
- .optional(),
995
+ details: OmpOptionalMetadataSchema,
172
996
  };
173
- type OmpContentPart = z.infer<typeof OmpContentPartSchema>;
174
- type OmpMessageIdentity = {
175
- id?: string;
176
- entryId?: string;
177
- responseId?: string;
178
- images?: OmpContentPart[];
179
- timestamp?: number;
180
- details?: unknown;
181
- display?: boolean;
182
- customType?: string;
183
- content?: unknown;
184
- command?: string;
185
- output?: string;
186
- exitCode?: number | null;
187
- cancelled?: boolean;
188
- 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,
189
1014
  };
190
- export type OmpMessage = OmpMessageIdentity &
191
- (
192
- | {
193
- role: "assistant";
194
- content?: string | OmpContentPart[];
195
- errorMessage?: string | null;
196
- stopReason?: string;
197
- }
198
- | { role: "user"; content: string | OmpContentPart[] }
199
- | {
200
- role: "toolResult";
201
- toolCallId: string;
202
- toolName: string;
203
- content: unknown;
204
- details?: unknown;
205
- isError?: boolean;
206
- }
207
- | {
208
- role: "bashExecution";
209
- command: string;
210
- output?: string;
211
- exitCode?: number | null;
212
- cancelled?: boolean;
213
- truncated?: boolean;
214
- }
215
- | { role: "custom"; customType?: string; content?: unknown; display?: boolean }
216
- );
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
+ });
217
1035
 
218
- const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
1036
+ const OmpMessageSchema = z.discriminatedUnion("role", [
219
1037
  z.object({
220
1038
  role: z.literal("assistant"),
221
- content: OmpDisplayContentSchema.optional(),
1039
+ content: OmpAssistantDisplayContentSchema.optional(),
222
1040
  ...OmpMessageIdentityShape,
223
1041
  errorMessage: boundedString(4_096).nullable().optional(),
224
1042
  stopReason: boundedString(64).optional(),
@@ -228,36 +1046,70 @@ const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
228
1046
  content: OmpDisplayContentSchema,
229
1047
  ...OmpMessageIdentityShape,
230
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
+ }),
231
1058
  z.object({
232
1059
  role: z.literal("toolResult"),
233
1060
  toolCallId: IDENTIFIER,
234
1061
  toolName: NAME,
235
- content: z
236
- .unknown()
237
- .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 8_192)),
1062
+ content: OmpOpaqueMessagePayloadSchema,
238
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(),
239
1068
  ...OmpMessageIdentityShape,
240
1069
  }),
241
1070
  z.object({
242
1071
  role: z.literal("bashExecution"),
243
1072
  command: TEXT,
244
- output: TEXT.optional(),
245
- exitCode: z.number().int().nullable().optional(),
246
- cancelled: z.boolean().optional(),
247
- truncated: z.boolean().optional(),
1073
+ ...OmpExecutionMessageShape,
248
1074
  ...OmpMessageIdentityShape,
249
1075
  }),
250
1076
  z.object({
251
- role: z.literal("custom"),
252
- customType: NAME.optional(),
253
- content: z
254
- .unknown()
255
- .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 8_192))
256
- .optional(),
257
- 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),
258
1109
  ...OmpMessageIdentityShape,
259
1110
  }),
260
1111
  ]);
1112
+ export type OmpMessage = z.infer<typeof OmpMessageSchema>;
261
1113
 
262
1114
  const OmpAssistantMessageEventSchema = z
263
1115
  .object({
@@ -309,19 +1161,33 @@ const OmpAvailableCommandSchema = z.object({
309
1161
  .optional(),
310
1162
  source: boundedString(64).optional(),
311
1163
  });
1164
+ const OmpThinkingMetadataSchema = z
1165
+ .unknown()
1166
+ .transform((value): { efforts?: string[]; defaultLevel?: string } | undefined => {
1167
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
1168
+ const record = value as Record<string, unknown>;
1169
+ const efforts = boundedOptionalStringArray(16, 32).parse(record.efforts);
1170
+ const defaultLevel = boundedString(32).optional().catch(undefined).parse(record.defaultLevel);
1171
+ return {
1172
+ ...(efforts !== undefined ? { efforts } : {}),
1173
+ ...(defaultLevel !== undefined ? { defaultLevel } : {}),
1174
+ };
1175
+ });
312
1176
  const OmpModelSchema = z.object({
313
1177
  provider: OMP_PROVIDER_NAME,
314
1178
  id: NAME,
315
- name: boundedString(MAX_NAME_LENGTH).optional(),
316
- reasoning: z.boolean().optional(),
317
- thinking: z
318
- .object({
319
- efforts: z.array(boundedString(32)).max(16).optional(),
320
- defaultLevel: boundedString(32).optional(),
321
- })
322
- .optional(),
323
- input: z.array(NAME).max(16).optional(),
324
- contextWindow: z.number().int().nonnegative().max(100_000_000).nullable().optional(),
1179
+ name: boundedString(MAX_NAME_LENGTH).optional().catch(undefined),
1180
+ reasoning: z.boolean().optional().catch(undefined),
1181
+ thinking: OmpThinkingMetadataSchema.optional(),
1182
+ input: boundedOptionalStringArray(16, MAX_NAME_LENGTH).optional(),
1183
+ contextWindow: z
1184
+ .number()
1185
+ .int()
1186
+ .nonnegative()
1187
+ .max(100_000_000)
1188
+ .nullable()
1189
+ .optional()
1190
+ .catch(undefined),
325
1191
  });
326
1192
  const TokenCountSchema = z.number().int().nonnegative().max(MAX_TOKEN_COUNT);
327
1193
  const OptionalTokenCountSchema = TokenCountSchema.nullable().optional();
@@ -373,13 +1239,13 @@ const OmpCompactionResultSchema = z.object({
373
1239
  });
374
1240
  const OmpSessionStateSchema = z.object({
375
1241
  model: OmpModelSchema.nullable().optional(),
376
- thinkingLevel: OmpThinkingLevelSchema.optional(),
1242
+ thinkingLevel: OmpCurrentThinkingLevelSchema,
377
1243
  isStreaming: z.boolean(),
378
1244
  isCompacting: z.boolean(),
379
1245
  sessionId: IDENTIFIER,
380
1246
  autoCompactionEnabled: z.boolean().optional(),
381
- contextUsage: OmpContextUsageSchema.nullable().optional(),
382
- sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
1247
+ contextUsage: OmpContextUsageSchema.nullable().optional().catch(undefined),
1248
+ sessionFile: boundedString(MAX_PATH_LENGTH).optional().catch(undefined),
383
1249
  });
384
1250
  const OmpReadyFrameSchema = z.object({
385
1251
  type: z.literal("ready"),
@@ -392,6 +1258,7 @@ const OmpReadyFrameSchema = z.object({
392
1258
  const OmpResponseFrameSchema = z.object({
393
1259
  type: z.literal("response"),
394
1260
  id: IDENTIFIER,
1261
+ command: boundedString(64, 1).optional(),
395
1262
  success: z.boolean(),
396
1263
  data: z.unknown().optional(),
397
1264
  error: boundedString(MAX_RPC_ERROR_BYTES).optional(),
@@ -409,6 +1276,16 @@ const JsonObjectSchema = z.record(z.string(), z.unknown());
409
1276
  const BoundedToolPayloadSchema = z
410
1277
  .unknown()
411
1278
  .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096));
1279
+ const OmpToolResultPayloadSchema = z.preprocess(
1280
+ omitUnsafeOptionalDetails,
1281
+ z
1282
+ .unknown()
1283
+ .refine(
1284
+ (value) =>
1285
+ isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, MAX_OPTIONAL_METADATA_ITEMS, 8_192) &&
1286
+ isBoundedJson(omitOptionalDetails(value), MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096),
1287
+ ),
1288
+ );
412
1289
  const OmpHostToolDefinitionSchema = z.object({
413
1290
  name: NAME,
414
1291
  label: NAME.optional(),
@@ -587,7 +1464,7 @@ const OmpCompactionEndSchema = z.object({
587
1464
  errorMessage: boundedString(4_096).optional(),
588
1465
  skipped: z.boolean().optional(),
589
1466
  });
590
- const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
1467
+ const OmpCoreAgentEventSchema = z.discriminatedUnion("type", [
591
1468
  z.object({ type: z.literal("agent_start") }),
592
1469
  z.object({
593
1470
  type: z.literal("agent_end"),
@@ -616,13 +1493,19 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
616
1493
  toolCallId: IDENTIFIER,
617
1494
  toolName: NAME,
618
1495
  args: BoundedToolPayloadSchema.optional(),
619
- partialResult: BoundedToolPayloadSchema,
1496
+ partialResult: OmpToolResultPayloadSchema,
1497
+ }),
1498
+ z.object({
1499
+ type: z.literal("tool_stream_update"),
1500
+ toolCallId: IDENTIFIER,
1501
+ toolName: NAME,
1502
+ update: BoundedToolPayloadSchema,
620
1503
  }),
621
1504
  z.object({
622
1505
  type: z.literal("tool_execution_end"),
623
1506
  toolCallId: IDENTIFIER,
624
1507
  toolName: NAME,
625
- result: BoundedToolPayloadSchema,
1508
+ result: OmpToolResultPayloadSchema,
626
1509
  isError: z.boolean().optional(),
627
1510
  }),
628
1511
  OmpCompactionStartSchema,
@@ -644,6 +1527,99 @@ const OmpGoalModeStateSchema = z.object({
644
1527
  reason: boundedString(4_096).optional(),
645
1528
  goal: OmpGoalSchema.optional(),
646
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
+ ]);
647
1623
  const OmpSubagentStatusSchema = z.enum([
648
1624
  "pending",
649
1625
  "running",
@@ -786,80 +1762,10 @@ const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
786
1762
  type: z.literal("subagent_event"),
787
1763
  payload: z.object({ id: IDENTIFIER, event: OmpAgentSessionEventSchema }),
788
1764
  }),
789
- z.object({
790
- type: z.literal("todo_reminder"),
791
- todos: z
792
- .array(
793
- z.object({
794
- id: IDENTIFIER.optional(),
795
- content: boundedString(16_384),
796
- status: z.enum(["pending", "in_progress", "blocked", "completed", "abandoned"]),
797
- }),
798
- )
799
- .max(MAX_TODOS),
800
- }),
801
- z.object({ type: z.literal("model_changed") }),
802
- z.object({
803
- type: z.literal("thinking_level_changed"),
804
- thinkingLevel: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
805
- }),
806
- z.object({
807
- type: z.literal("goal_updated"),
808
- goal: OmpGoalSchema.nullable().optional(),
809
- state: OmpGoalModeStateSchema.optional(),
810
- }),
811
- z.object({
812
- type: z.literal("auto_retry_start"),
813
- attempt: z.number().int().nonnegative().safe(),
814
- maxAttempts: z.number().int().positive().safe(),
815
- delayMs: z.number().int().nonnegative().safe(),
816
- errorMessage: boundedString(64 * 1024),
817
- errorId: z.number().int().safe().optional(),
818
- }),
819
- z.object({
820
- type: z.literal("auto_retry_end"),
821
- success: z.boolean(),
822
- attempt: z.number().int().nonnegative().safe(),
823
- finalError: boundedString(64 * 1024).optional(),
824
- recoveredErrors: BoundedToolPayloadSchema.optional(),
825
- }),
826
- z.object({
827
- type: z.literal("retry_fallback_applied"),
828
- from: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
829
- to: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
830
- role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
831
- }),
832
- z.object({
833
- type: z.literal("retry_fallback_succeeded"),
834
- model: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
835
- role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
836
- }),
837
- z.object({ type: z.literal("todo_auto_clear") }),
838
- z.object({
839
- type: z.literal("auto_compaction_start"),
840
- reason: boundedString(4_096),
841
- action: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
842
- }),
843
- z.object({
844
- type: z.literal("auto_compaction_end"),
845
- action: NAME.optional(),
846
- result: OmpCompactionResultSchema.nullable().optional(),
847
- aborted: z.boolean().optional(),
848
- willRetry: z.boolean().optional(),
849
- errorMessage: boundedString(64 * 1024).optional(),
850
- skipped: z.boolean().optional(),
851
- }),
852
1765
  z.object({
853
1766
  type: z.literal("available_commands_update"),
854
1767
  commands: z.array(OmpAvailableCommandSchema).max(MAX_ARRAY_ITEMS),
855
1768
  }),
856
- z.object({
857
- type: z.literal("notice"),
858
- id: IDENTIFIER.optional(),
859
- level: z.enum(["info", "warning", "error"]),
860
- message: boundedString(64 * 1024),
861
- source: boundedString(MAX_NAME_LENGTH).optional(),
862
- }),
863
1769
  z.object({ type: z.literal("command_output"), text: TEXT.optional() }),
864
1770
  OmpExtensionUiRequestSchema,
865
1771
  z.object({
@@ -871,10 +1777,126 @@ const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
871
1777
  OmpHostToolCancelSchema,
872
1778
  OmpToolApprovalRequestSchema,
873
1779
  OmpToolApprovalCancelSchema,
874
- z.object({ type: z.literal("advisor_yielded") }),
875
1780
  ]);
1781
+ type OptionalDetailsMapper = (value: unknown, taskResult?: boolean) => unknown;
1782
+
1783
+ function mapRecordField(
1784
+ record: Record<string, unknown>,
1785
+ key: string,
1786
+ map: OptionalDetailsMapper,
1787
+ taskResult = false,
1788
+ ): Record<string, unknown> {
1789
+ if (!Object.hasOwn(record, key)) return record;
1790
+ const next = map(record[key], taskResult);
1791
+ return next === record[key] ? record : { ...record, [key]: next };
1792
+ }
1793
+
1794
+ function mapMessageList(value: unknown, map: OptionalDetailsMapper): unknown {
1795
+ if (!Array.isArray(value)) return value;
1796
+ let changed = false;
1797
+ const messages = value.map((message) => {
1798
+ const taskResult =
1799
+ message !== null &&
1800
+ typeof message === "object" &&
1801
+ !Array.isArray(message) &&
1802
+ (message as Record<string, unknown>).role === "toolResult" &&
1803
+ (message as Record<string, unknown>).toolName === "task";
1804
+ const next = map(message, taskResult);
1805
+ changed ||= next !== message;
1806
+ return next;
1807
+ });
1808
+ return changed ? messages : value;
1809
+ }
1810
+
1811
+ function mapAgentEventDetails(
1812
+ frame: Record<string, unknown>,
1813
+ map: OptionalDetailsMapper,
1814
+ ): Record<string, unknown> {
1815
+ switch (frame.type) {
1816
+ case "message_start":
1817
+ case "message_update":
1818
+ case "message_end": {
1819
+ const message = frame.message;
1820
+ const taskResult =
1821
+ message !== null &&
1822
+ typeof message === "object" &&
1823
+ !Array.isArray(message) &&
1824
+ (message as Record<string, unknown>).role === "toolResult" &&
1825
+ (message as Record<string, unknown>).toolName === "task";
1826
+ return mapRecordField(frame, "message", map, taskResult);
1827
+ }
1828
+ case "tool_execution_update":
1829
+ return mapRecordField(frame, "partialResult", map, frame.toolName === "task");
1830
+ case "tool_execution_end":
1831
+ return mapRecordField(frame, "result", map, frame.toolName === "task");
1832
+ case "agent_end":
1833
+ return mapRecordField(frame, "messages", (messages) => mapMessageList(messages, map));
1834
+ case "irc_message":
1835
+ return mapRecordField(frame, "message", map);
1836
+ default:
1837
+ return frame;
1838
+ }
1839
+ }
1840
+
1841
+ function mapRuntimeFrameDetails(
1842
+ frame: Record<string, unknown>,
1843
+ map: OptionalDetailsMapper,
1844
+ ): Record<string, unknown> {
1845
+ if (frame.type !== "subagent_event") return mapAgentEventDetails(frame, map);
1846
+ if (frame.payload === null || typeof frame.payload !== "object" || Array.isArray(frame.payload)) {
1847
+ return frame;
1848
+ }
1849
+ const payload = frame.payload as Record<string, unknown>;
1850
+ if (payload.event === null || typeof payload.event !== "object" || Array.isArray(payload.event)) {
1851
+ return frame;
1852
+ }
1853
+ const event = mapAgentEventDetails(payload.event as Record<string, unknown>, map);
1854
+ return event === payload.event ? frame : { ...frame, payload: { ...payload, event } };
1855
+ }
1856
+
1857
+ function sanitizeMessageListMetadata(value: unknown): unknown {
1858
+ return mapMessageList(value, createOptionalMetadataSanitizer());
1859
+ }
1860
+
1861
+ function sanitizeHistoryResponseData(value: unknown): unknown {
1862
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
1863
+ return mapRecordField(value as Record<string, unknown>, "messages", sanitizeMessageListMetadata);
1864
+ }
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
+
1885
+ function runtimeFrameCollectionLimit(frame: Record<string, unknown>): number {
1886
+ const type = frame.type;
1887
+ if (
1888
+ type === "message_start" ||
1889
+ type === "message_update" ||
1890
+ type === "message_end" ||
1891
+ type === "agent_end" ||
1892
+ type === "subagent_event"
1893
+ ) {
1894
+ return OMP_MAX_CONTENT_PARTS;
1895
+ }
1896
+ return 1_024;
1897
+ }
876
1898
  const OmpModelsResultSchema = z.object({
877
- models: z.array(OmpModelSchema).min(1).max(256),
1899
+ models: z.array(OmpModelSchema).min(1).max(MAX_MODEL_CATALOG_ITEMS),
878
1900
  });
879
1901
  const OmpPromptAckSchema = z.object({ agentInvoked: z.boolean().optional() }).optional();
880
1902
  const OmpAvailableCommandsResultSchema = z.object({
@@ -885,7 +1907,12 @@ const OmpBranchMessagesResultSchema = z.object({
885
1907
  });
886
1908
  const OmpBranchResultSchema = z.object({ text: TEXT, cancelled: z.boolean() });
887
1909
  const OmpMessagesResultSchema = z.object({
888
- 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),
889
1916
  });
890
1917
  const OmpSubagentsResultSchema = z.object({
891
1918
  subagents: z
@@ -911,7 +1938,7 @@ const OmpSubagentMessagesResultSchema = z.object({
911
1938
  fromByte: z.number().int().nonnegative(),
912
1939
  nextByte: z.number().int().nonnegative(),
913
1940
  reset: z.boolean(),
914
- messages: z.array(OmpMessageSchema).max(100_000),
1941
+ messages: z.array(OmpMessageSchema).max(MAX_REPLAY_MESSAGES),
915
1942
  });
916
1943
  const ProtocolNegotiationResultSchema = z.object({
917
1944
  protocolVersion: z.literal(2),
@@ -919,6 +1946,7 @@ const ProtocolNegotiationResultSchema = z.object({
919
1946
  });
920
1947
 
921
1948
  export type OmpModel = z.infer<typeof OmpModelSchema>;
1949
+ export type OmpBranchResult = z.infer<typeof OmpBranchResultSchema>;
922
1950
  export type OmpSessionState = z.infer<typeof OmpSessionStateSchema>;
923
1951
  export type OmpSessionStats = z.infer<typeof OmpSessionStatsSchema>;
924
1952
  export type OmpCompactionResult = z.infer<typeof OmpCompactionResultSchema>;
@@ -960,6 +1988,7 @@ export interface OmpPersistedSessionMessages {
960
1988
  nativeSessionId: string;
961
1989
  byteLength: number;
962
1990
  messages: OmpMessage[];
1991
+ imageReplayWarning?: true;
963
1992
  }
964
1993
 
965
1994
  export interface OmpStartOptions {
@@ -1025,9 +2054,9 @@ export interface OmpRuntimeSession {
1025
2054
  respondToExtensionUi(response: OmpExtensionUiResponse): Promise<void>;
1026
2055
  respondToToolApproval(response: OmpToolApprovalResponse): Promise<void>;
1027
2056
  getBranchMessages(): Promise<Array<{ entryId: string; text: string }>>;
1028
- branch(entryId: string): Promise<{ text: string; cancelled: boolean }>;
2057
+ branch(entryId: string): Promise<OmpBranchResult>;
1029
2058
  readonly canReplayHistory: boolean;
1030
- getMessages(): Promise<OmpMessage[]>;
2059
+ getMessages(signal?: AbortSignal): Promise<OmpMessage[]>;
1031
2060
  abort(): Promise<void>;
1032
2061
  setHostTools(tools: readonly OmpHostToolDefinition[]): Promise<string[]>;
1033
2062
  sendHostToolResult(result: OmpHostToolResult): void;
@@ -1048,6 +2077,7 @@ export interface OmpRuntime {
1048
2077
  readPersistedSubagentTranscript(options: {
1049
2078
  parentSessionFile: string;
1050
2079
  childTranscriptId: string;
2080
+ sessionFile?: string;
1051
2081
  cwd: string;
1052
2082
  signal?: AbortSignal;
1053
2083
  }): Promise<OmpPersistedSubagentMessages>;
@@ -1067,11 +2097,36 @@ export interface OmpRpcRuntimeOptions {
1067
2097
  terminateProcessTree?: (pid: number) => Promise<boolean | "uncertain">;
1068
2098
  environment?: NodeJS.ProcessEnv;
1069
2099
  requestTimeoutMs?: number;
2100
+ reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
1070
2101
  listSessions?: (
1071
2102
  options: OmpSessionListOptions,
1072
2103
  ) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
1073
2104
  }
1074
2105
 
2106
+ export type OmpRpcRequestRejectionCode = "session_busy" | "stale_cursor" | "unsupported_command";
2107
+
2108
+ export class OmpRpcRequestRejectedError extends Error {
2109
+ constructor(
2110
+ readonly command?: OmpRpcDiagnosticCommand,
2111
+ readonly code?: OmpRpcRequestRejectionCode,
2112
+ ) {
2113
+ super("OMP RPC request failed");
2114
+ this.name = "OmpRpcRequestRejectedError";
2115
+ }
2116
+ }
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
+
1075
2130
  type PendingRequest = {
1076
2131
  resolve(value: unknown): void;
1077
2132
  reject(error: Error): void;
@@ -1591,12 +2646,21 @@ class OmpRpcProcess {
1591
2646
  private spawnFailedWithoutProcess = false;
1592
2647
  private readyReceived = false;
1593
2648
  private outputSettled = false;
2649
+ private turnActive = false;
2650
+ private readonly pendingProtocolViolations = new Map<
2651
+ ProtocolViolationKey,
2652
+ PendingProtocolViolation
2653
+ >();
2654
+ private protocolViolationTimer: TimerHandle | null = null;
1594
2655
 
1595
2656
  constructor(
1596
2657
  options: OmpStartOptions,
1597
2658
  spawnProcess?: OmpRpcRuntimeOptions["spawnProcess"],
1598
2659
  terminateProcessTree?: OmpRpcRuntimeOptions["terminateProcessTree"],
1599
2660
  private readonly requestTimeoutMs = REQUEST_TIMEOUT_MS,
2661
+ private readonly reportProtocolViolation: (
2662
+ diagnostic: OmpProtocolViolationDiagnostic,
2663
+ ) => void | Promise<void> = (diagnostic) => console.error("OMP protocol violation", diagnostic),
1600
2664
  ) {
1601
2665
  const ready = Promise.withResolvers<ReadyFrame>();
1602
2666
  this.rejectReady = ready.reject;
@@ -1685,7 +2749,15 @@ class OmpRpcProcess {
1685
2749
  private settleOutput(): void {
1686
2750
  if (this.outputSettled) return;
1687
2751
  this.outputSettled = true;
1688
- if (this.lineBytes > 0 || this.discardingLine) this.recordProtocolViolation();
2752
+ if (this.lineBytes > 0 || this.discardingLine) {
2753
+ this.recordProtocolViolation("incomplete-frame", {
2754
+ reason: "output-ended-mid-frame",
2755
+ field: "frame",
2756
+ expected: "complete-json-line",
2757
+ actualType: "partial-frame",
2758
+ maxByteSize: this.discardingLine ? this.discardedLineBytes : this.lineBytes,
2759
+ });
2760
+ }
1689
2761
  this.lineParts = [];
1690
2762
  this.lineBytes = 0;
1691
2763
  this.discardingLine = false;
@@ -1868,16 +2940,17 @@ class OmpRpcProcess {
1868
2940
 
1869
2941
  private async closeProcess(): Promise<void> {
1870
2942
  this.closed = true;
2943
+ this.flushProtocolViolations();
1871
2944
  this.clearChunk();
1872
2945
  this.failPending(new Error("OMP RPC process was closed"));
2946
+ const cleanupPromise = this.startTreeCleanup();
1873
2947
  if (!this.exited) {
1874
2948
  try {
1875
2949
  this.child.stdin.end();
1876
2950
  } catch {
1877
- // Continue to process-tree cleanup when the input channel is already closed.
2951
+ // Continue waiting for process-tree cleanup when the input channel is already closed.
1878
2952
  }
1879
2953
  }
1880
- const cleanupPromise = this.startTreeCleanup();
1881
2954
  const cleanup = await cleanupPromise;
1882
2955
  if (cleanup !== "verified") throw new Error("OMP RPC process tree cleanup failed");
1883
2956
  if (
@@ -1928,8 +3001,13 @@ class OmpRpcProcess {
1928
3001
  } else {
1929
3002
  this.appendLinePart(part);
1930
3003
  if (this.fatalError) return;
1931
- if (this.discardingLine) this.resetDiscardedLine();
1932
- 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();
1933
3011
  }
1934
3012
  start = index + 1;
1935
3013
  }
@@ -1964,7 +3042,14 @@ class OmpRpcProcess {
1964
3042
  this.lineParts = [];
1965
3043
  this.lineBytes = 0;
1966
3044
  this.discardingLine = true;
1967
- this.recordProtocolViolation();
3045
+ this.recordProtocolViolation("frame-limit", {
3046
+ reason: "physical-frame-limit",
3047
+ field: "frame.byteLength",
3048
+ expected: "within-byte-limit",
3049
+ actualType: "oversized",
3050
+ maxByteSize: nextBytes,
3051
+ limitBytes: this.physicalFrameLimit,
3052
+ });
1968
3053
  return;
1969
3054
  }
1970
3055
  this.lineParts.push(part);
@@ -1977,32 +3062,20 @@ class OmpRpcProcess {
1977
3062
  this.lineParts = [];
1978
3063
  this.lineBytes = 0;
1979
3064
  const payload = line.at(-1) === 13 ? line.subarray(0, -1) : line;
1980
- if (payload.byteLength > MAX_SEMANTIC_FRAME_BYTES) {
1981
- this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
1982
- return;
1983
- }
1984
3065
  let decoded: unknown;
1985
3066
  try {
1986
3067
  decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
1987
3068
  } catch {
1988
- this.recordProtocolViolation();
1989
- return;
1990
- }
1991
- if (this.receiveKnownResponse(decoded)) return;
1992
- if (this.receiveDegradedAgentEnd(decoded, true)) return;
1993
- if (
1994
- boundedJsonBytes(decoded, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) ===
1995
- Number.POSITIVE_INFINITY
1996
- ) {
1997
- this.recordProtocolViolation();
1998
- return;
1999
- }
2000
- const frame = JsonObjectSchema.safeParse(decoded);
2001
- if (!frame.success) {
2002
- this.recordProtocolViolation();
3069
+ this.recordProtocolViolation("invalid-json", {
3070
+ reason: "json-decode",
3071
+ field: "frame",
3072
+ expected: "valid-json",
3073
+ actualType: "invalid-json",
3074
+ maxByteSize: payload.byteLength,
3075
+ });
2003
3076
  return;
2004
3077
  }
2005
- this.receiveFrame(frame.data);
3078
+ this.receiveDecodedFrame(decoded, payload.byteLength);
2006
3079
  }
2007
3080
 
2008
3081
  private receiveChunk(frame: ChunkFrame): void {
@@ -2010,7 +3083,10 @@ class OmpRpcProcess {
2010
3083
  frame.byteLength > MAX_SEMANTIC_FRAME_BYTES &&
2011
3084
  ![...this.pending.values()].some(
2012
3085
  (pending) =>
2013
- 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",
2014
3090
  )
2015
3091
  ) {
2016
3092
  this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
@@ -2022,17 +3098,30 @@ class OmpRpcProcess {
2022
3098
  frame.data.length % 4 !== 0 ||
2023
3099
  !/^[A-Za-z0-9+/]*={0,2}$/u.test(frame.data)
2024
3100
  ) {
2025
- this.rejectChunk();
3101
+ this.rejectChunk("chunk-metadata", {
3102
+ field: "chunk",
3103
+ expected: "valid-chunk-frame",
3104
+ actualType: "object",
3105
+ });
2026
3106
  return;
2027
3107
  }
2028
3108
  const decoded = Buffer.from(frame.data, "base64");
2029
3109
  if (decoded.byteLength > MAX_CHUNK_BYTES) {
2030
- this.rejectChunk();
3110
+ this.rejectChunk("chunk-size", {
3111
+ field: "chunk.data",
3112
+ expected: "within-byte-limit",
3113
+ actualType: "oversized",
3114
+ limitBytes: MAX_CHUNK_BYTES,
3115
+ });
2031
3116
  return;
2032
3117
  }
2033
3118
  if (!this.chunk) {
2034
3119
  if (frame.index !== 0) {
2035
- this.rejectChunk();
3120
+ this.rejectChunk("chunk-start-index", {
3121
+ field: "chunk.index",
3122
+ expected: "first-chunk-index-zero",
3123
+ actualType: "out-of-order",
3124
+ });
2036
3125
  return;
2037
3126
  }
2038
3127
  this.chunk = {
@@ -2041,7 +3130,15 @@ class OmpRpcProcess {
2041
3130
  byteLength: frame.byteLength,
2042
3131
  parts: [],
2043
3132
  receivedBytes: 0,
2044
- timer: setTimeout(() => this.rejectChunk(), CHUNK_STALE_MS),
3133
+ timer: setTimeout(
3134
+ () =>
3135
+ this.rejectChunk("chunk-timeout", {
3136
+ field: "chunk.sequence",
3137
+ expected: "chunk-before-deadline",
3138
+ actualType: "timeout",
3139
+ }),
3140
+ CHUNK_STALE_MS,
3141
+ ),
2045
3142
  };
2046
3143
  }
2047
3144
  const chunk = this.chunk;
@@ -2053,14 +3150,22 @@ class OmpRpcProcess {
2053
3150
  chunk.receivedBytes + decoded.byteLength > chunk.byteLength ||
2054
3151
  chunk.receivedBytes + decoded.byteLength > this.reassembledFrameLimit
2055
3152
  ) {
2056
- this.rejectChunk();
3153
+ this.rejectChunk("chunk-sequence", {
3154
+ field: "chunk.sequence",
3155
+ expected: "contiguous-chunk-sequence",
3156
+ actualType: "out-of-order",
3157
+ });
2057
3158
  return;
2058
3159
  }
2059
3160
  chunk.parts.push(decoded);
2060
3161
  chunk.receivedBytes += decoded.byteLength;
2061
3162
  if (chunk.parts.length !== chunk.count) return;
2062
3163
  if (chunk.receivedBytes !== chunk.byteLength) {
2063
- this.rejectChunk();
3164
+ this.rejectChunk("chunk-byte-count", {
3165
+ field: "chunk.byteLength",
3166
+ expected: "declared-chunk-byte-count",
3167
+ actualType: "mismatched",
3168
+ });
2064
3169
  return;
2065
3170
  }
2066
3171
  const reassembled = Buffer.concat(chunk.parts, chunk.receivedBytes);
@@ -2069,29 +3174,52 @@ class OmpRpcProcess {
2069
3174
  try {
2070
3175
  decodedFrame = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(reassembled));
2071
3176
  } catch {
2072
- this.recordProtocolViolation();
3177
+ this.recordProtocolViolation("invalid-json", {
3178
+ reason: "chunk-json-decode",
3179
+ field: "chunk.data",
3180
+ expected: "valid-json",
3181
+ actualType: "invalid-json",
3182
+ maxByteSize: reassembled.byteLength,
3183
+ });
2073
3184
  return;
2074
3185
  }
2075
- if (this.receiveKnownResponse(decodedFrame)) return;
2076
- if (this.receiveDegradedAgentEnd(decodedFrame, true)) return;
2077
- if (
2078
- boundedJsonBytes(
2079
- decodedFrame,
2080
- MAX_SEMANTIC_FRAME_BYTES,
2081
- 1_024,
2082
- MAX_IMAGE_DATA_LENGTH,
2083
- 4_096,
2084
- ) === Number.POSITIVE_INFINITY
2085
- ) {
2086
- this.recordProtocolViolation();
3186
+ this.receiveDecodedFrame(decodedFrame, reassembled.byteLength);
3187
+ }
3188
+
3189
+ private receiveDecodedFrame(value: unknown, rawByteLength: number): void {
3190
+ if (value && typeof value === "object" && !Array.isArray(value)) {
3191
+ const frame = value as Record<string, unknown>;
3192
+ const pending = typeof frame.id === "string" ? this.pending.get(frame.id) : undefined;
3193
+ if (
3194
+ frame.type === "response" &&
3195
+ (pending?.command === "get_messages" ||
3196
+ pending?.command === "get_messages_page" ||
3197
+ pending?.command === "get_subagent_messages" ||
3198
+ pending?.command === "get_state")
3199
+ ) {
3200
+ this.receiveResponse(frame);
3201
+ return;
3202
+ }
3203
+ }
3204
+ if (this.receiveUnsupportedCommandResponse(value)) return;
3205
+ if (rawByteLength > MAX_SEMANTIC_FRAME_BYTES) {
3206
+ this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
2087
3207
  return;
2088
3208
  }
2089
- const frameObject = JsonObjectSchema.safeParse(decodedFrame);
2090
- if (!frameObject.success) {
2091
- this.recordProtocolViolation();
3209
+ if (this.receiveKnownResponse(value)) return;
3210
+ const sanitized = sanitizeLiveDisplayFrame(value);
3211
+ const frame = JsonObjectSchema.safeParse(sanitized);
3212
+ if (!frame.success) {
3213
+ this.recordProtocolViolation("invalid-envelope", {
3214
+ reason: "object-envelope",
3215
+ field: "frame",
3216
+ expected: "object-envelope",
3217
+ actualType: protocolDiagnosticActualType(sanitized),
3218
+ maxByteSize: rawByteLength,
3219
+ });
2092
3220
  return;
2093
3221
  }
2094
- this.receiveFrame(frameObject.data);
3222
+ this.receiveFrame(frame.data, rawByteLength);
2095
3223
  }
2096
3224
 
2097
3225
  private receiveKnownResponse(value: unknown): boolean {
@@ -2108,15 +3236,50 @@ class OmpRpcProcess {
2108
3236
  return true;
2109
3237
  }
2110
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
+
2111
3263
  private receiveResponse(frame: Record<string, unknown>): void {
2112
3264
  const rawId = typeof frame.id === "string" ? frame.id : undefined;
2113
3265
  const knownPending = rawId ? this.pending.get(rawId) : undefined;
2114
3266
  const response = OmpResponseFrameSchema.safeParse(frame);
2115
3267
  if (!response.success) {
3268
+ const handledAcceptedFailure =
3269
+ rawId !== undefined && knownPending === undefined
3270
+ ? this.emitAcceptedPromptFailure(rawId, frame)
3271
+ : false;
3272
+ if (!handledAcceptedFailure) {
3273
+ this.recordProtocolViolation("invalid-response", {
3274
+ reason: "response-schema",
3275
+ frameType: "response",
3276
+ field: "response",
3277
+ expected: "valid-response-frame",
3278
+ actualType: "object",
3279
+ });
3280
+ }
2116
3281
  if (rawId && knownPending) {
2117
3282
  this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
2118
- } else if (!rawId || !this.emitAcceptedPromptFailure(rawId, frame)) {
2119
- this.recordProtocolViolation();
2120
3283
  }
2121
3284
  return;
2122
3285
  }
@@ -2129,14 +3292,29 @@ class OmpRpcProcess {
2129
3292
  }
2130
3293
  const isBranchHistory = pending.command === "get_branch_messages";
2131
3294
  const isHistory =
2132
- pending.command === "get_messages" || pending.command === "get_subagent_messages";
2133
- const responseItemLimit = isBranchHistory ? 1_024 : isHistory ? 100_000 : MAX_ARRAY_ITEMS;
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;
3303
+ const boundedFrame =
3304
+ responseData === response.data.data ? frame : { ...frame, data: responseData };
3305
+ const responseItemLimit = isBranchHistory
3306
+ ? 1_024
3307
+ : isHistory
3308
+ ? 100_000
3309
+ : pending.command === "get_available_models"
3310
+ ? MAX_MODEL_CATALOG_ITEMS
3311
+ : MAX_ARRAY_ITEMS;
2134
3312
  const responseByteLimit =
2135
3313
  isBranchHistory || isHistory
2136
3314
  ? Math.min(MAX_REASSEMBLED_FRAME_BYTES, this.reassembledFrameLimit)
2137
3315
  : 2 * 1024 * 1024;
2138
- // A model catalog contains up to 256 structured models, so its aggregate
2139
- // node budget must exceed the small state/command-response budget.
3316
+ // Model catalogs are truncated before publication, while the transport still enforces
3317
+ // aggregate byte and node budgets over the complete response.
2140
3318
  const responseNodeLimit = isBranchHistory
2141
3319
  ? 4_096
2142
3320
  : isHistory
@@ -2144,17 +3322,21 @@ class OmpRpcProcess {
2144
3322
  : pending.command === "get_available_models"
2145
3323
  ? 16_384
2146
3324
  : 2_048;
2147
- if (
2148
- boundedJsonBytes(
2149
- frame,
2150
- responseByteLimit,
2151
- responseItemLimit,
2152
- MAX_IMAGE_DATA_LENGTH,
2153
- responseNodeLimit,
2154
- ) === Number.POSITIVE_INFINITY
2155
- ) {
3325
+ const violation = jsonBoundViolation(
3326
+ boundedFrame,
3327
+ responseByteLimit,
3328
+ responseItemLimit,
3329
+ MAX_IMAGE_DATA_LENGTH,
3330
+ responseNodeLimit,
3331
+ );
3332
+ if (violation) {
2156
3333
  this.takePending(response.data.id)?.reject(
2157
- 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
+ ),
2158
3340
  );
2159
3341
  return;
2160
3342
  }
@@ -2162,7 +3344,7 @@ class OmpRpcProcess {
2162
3344
  if (!settled) return;
2163
3345
  if (response.data.success) {
2164
3346
  try {
2165
- settled.beforeResolve?.(response.data.data);
3347
+ settled.beforeResolve?.(responseData);
2166
3348
  if (settled.command === "prompt") {
2167
3349
  if (this.acceptedPromptIds.size >= MAX_PENDING_REQUESTS) {
2168
3350
  const oldest = this.acceptedPromptIds.values().next().value;
@@ -2170,12 +3352,21 @@ class OmpRpcProcess {
2170
3352
  }
2171
3353
  this.acceptedPromptIds.add(response.data.id);
2172
3354
  }
2173
- settled.resolve(response.data.data);
3355
+ settled.resolve(responseData);
2174
3356
  } catch {
2175
3357
  settled.reject(new Error("OMP RPC response is invalid"));
2176
3358
  }
2177
3359
  } else {
2178
- settled.reject(new Error("OMP RPC request failed"));
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));
2179
3370
  }
2180
3371
  }
2181
3372
 
@@ -2219,21 +3410,27 @@ class OmpRpcProcess {
2219
3410
  this.fail(new Error("OMP emitted invalid terminal metadata"));
2220
3411
  return true;
2221
3412
  }
3413
+ const structuralFrame = mapRuntimeFrameDetails(frame, omitOptionalDetails);
2222
3414
  const messagesAreSafe =
2223
3415
  frame.messages === undefined ||
2224
3416
  (Array.isArray(frame.messages) &&
2225
3417
  frame.messages.length <= MAX_ARRAY_ITEMS &&
2226
3418
  boundedJsonBytes(
2227
- frame.messages,
3419
+ structuralFrame.messages as unknown[],
2228
3420
  MAX_SEMANTIC_FRAME_BYTES,
2229
- MAX_ARRAY_ITEMS,
2230
- MAX_TEXT_LENGTH,
3421
+ OMP_MAX_CONTENT_PARTS,
3422
+ MAX_IMAGE_DATA_LENGTH,
2231
3423
  4_096,
2232
3424
  ) !== Number.POSITIVE_INFINITY);
2233
3425
  const payloadIsSafe =
2234
3426
  messagesAreSafe &&
2235
- boundedJsonBytes(frame, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) !==
2236
- Number.POSITIVE_INFINITY;
3427
+ boundedJsonBytes(
3428
+ structuralFrame,
3429
+ MAX_SEMANTIC_FRAME_BYTES,
3430
+ OMP_MAX_CONTENT_PARTS,
3431
+ MAX_IMAGE_DATA_LENGTH,
3432
+ 4_096,
3433
+ ) !== Number.POSITIVE_INFINITY;
2237
3434
  if (onlyUnsafePayload && payloadIsSafe) return false;
2238
3435
  if (envelope.data.isTerminal === false) {
2239
3436
  this.fail(new Error("OMP emitted an invalid nonterminal agent_end payload"));
@@ -2254,42 +3451,89 @@ class OmpRpcProcess {
2254
3451
  return true;
2255
3452
  }
2256
3453
 
2257
- private receiveFrame(frame: Record<string, unknown>): void {
3454
+ private receiveFrame(frame: Record<string, unknown>, rawByteLength: number): void {
2258
3455
  const type = typeof frame.type === "string" && frame.type.length <= 64 ? frame.type : null;
2259
3456
  if (!type) {
2260
- this.recordProtocolViolation();
3457
+ this.recordProtocolViolation("invalid-envelope", {
3458
+ reason: "missing-frame-type",
3459
+ field: "frame.type",
3460
+ expected: "bounded-frame-type",
3461
+ actualType: protocolDiagnosticActualType(frame.type),
3462
+ maxByteSize: rawByteLength,
3463
+ });
2261
3464
  return;
2262
3465
  }
2263
- if (this.receiveDegradedAgentEnd(frame, true)) return;
3466
+ const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
3467
+ if (this.receiveDegradedAgentEnd(safeFrame, true)) return;
2264
3468
  if (
2265
- boundedJsonBytes(frame, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) ===
2266
- Number.POSITIVE_INFINITY
3469
+ boundedJsonBytes(
3470
+ mapRuntimeFrameDetails(safeFrame, omitOptionalDetails),
3471
+ MAX_SEMANTIC_FRAME_BYTES,
3472
+ runtimeFrameCollectionLimit(safeFrame),
3473
+ MAX_IMAGE_DATA_LENGTH,
3474
+ 4_096,
3475
+ ) === Number.POSITIVE_INFINITY
2267
3476
  ) {
2268
- this.recordProtocolViolation();
3477
+ this.recordProtocolViolation("frame-limit", {
3478
+ reason: "semantic-frame-limit",
3479
+ field: "frame.byteLength",
3480
+ expected: "within-byte-limit",
3481
+ actualType: "oversized",
3482
+ maxByteSize: rawByteLength,
3483
+ limitBytes: MAX_SEMANTIC_FRAME_BYTES,
3484
+ });
2269
3485
  return;
2270
3486
  }
2271
3487
  if (type === "rpc_chunk") {
2272
- const chunk = OmpChunkFrameSchema.safeParse(frame);
2273
- if (!chunk.success) this.rejectChunk();
2274
- else this.receiveChunk(chunk.data);
3488
+ const chunk = OmpChunkFrameSchema.safeParse(safeFrame);
3489
+ if (!chunk.success) {
3490
+ this.rejectChunk("chunk-schema", {
3491
+ field: "chunk",
3492
+ expected: "valid-chunk-frame",
3493
+ actualType: "object",
3494
+ });
3495
+ } else this.receiveChunk(chunk.data);
2275
3496
  return;
2276
3497
  }
2277
3498
  if (this.chunk) {
2278
3499
  this.clearChunk();
2279
- this.recordProtocolViolation();
3500
+ this.recordProtocolViolation("interleaved-chunk", {
3501
+ reason: "frame-interleaved-with-chunk",
3502
+ field: "chunk.sequence",
3503
+ expected: "no-interleaved-frame",
3504
+ actualType: "interleaved",
3505
+ });
2280
3506
  }
2281
3507
  if (type === "rpc_frame_error") {
2282
- this.recordProtocolViolation();
3508
+ this.recordProtocolViolation("remote-frame-error", {
3509
+ reason: "remote-frame-error",
3510
+ frameType: "rpc_frame_error",
3511
+ field: "frame",
3512
+ expected: "no-remote-frame-error",
3513
+ actualType: "remote-error",
3514
+ });
2283
3515
  return;
2284
3516
  }
2285
3517
  if (type === "ready") {
2286
3518
  if (this.readyReceived) {
2287
- this.recordProtocolViolation();
3519
+ this.recordProtocolViolation("duplicate-ready", {
3520
+ reason: "ready-already-received",
3521
+ frameType: "ready",
3522
+ field: "ready",
3523
+ expected: "single-ready-frame",
3524
+ actualType: "duplicate",
3525
+ });
2288
3526
  return;
2289
3527
  }
2290
- const ready = OmpReadyFrameSchema.safeParse(frame);
3528
+ const ready = OmpReadyFrameSchema.safeParse(safeFrame);
2291
3529
  if (!ready.success) {
2292
- this.recordProtocolViolation();
3530
+ this.recordProtocolViolation("invalid-ready", {
3531
+ reason: "ready-schema",
3532
+ frameType: "ready",
3533
+ field: "ready",
3534
+ expected: "valid-ready-frame",
3535
+ actualType: "object",
3536
+ });
2293
3537
  } else {
2294
3538
  this.readyReceived = true;
2295
3539
  this.resolveReady(ready.data);
@@ -2297,24 +3541,37 @@ class OmpRpcProcess {
2297
3541
  return;
2298
3542
  }
2299
3543
  if (type === "response") {
2300
- this.receiveResponse(frame);
3544
+ this.receiveResponse(safeFrame);
2301
3545
  return;
2302
3546
  }
2303
- const event = OmpRuntimeEventSchema.safeParse(frame);
3547
+ const event = OmpRuntimeEventSchema.safeParse(safeFrame);
2304
3548
  if (!event.success) {
2305
- this.rejectMatchingToolApproval(frame);
2306
- if (type === "agent_end" && this.receiveDegradedAgentEnd(frame, false)) return;
2307
- this.recordProtocolViolation();
3549
+ this.rejectMatchingToolApproval(safeFrame);
3550
+ if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
3551
+ this.recordProtocolViolation("invalid-event", {
3552
+ ...invalidEventDiagnosticMetadata(safeFrame, type),
3553
+ maxByteSize: rawByteLength,
3554
+ });
2308
3555
  return;
2309
3556
  }
2310
3557
  if (!this.acceptEventState(event.data)) {
2311
- this.recordProtocolViolation();
3558
+ this.recordProtocolViolation("invalid-event-state", {
3559
+ reason: "event-state-transition",
3560
+ eventType: event.data.type,
3561
+ field: "event.sequence",
3562
+ expected: "valid-event-state-transition",
3563
+ actualType: "out-of-order",
3564
+ maxByteSize: rawByteLength,
3565
+ });
2312
3566
  return;
2313
3567
  }
2314
3568
  this.emit(event.data);
2315
3569
  if (event.data.type === "prompt_result" && event.data.id) {
2316
3570
  this.acceptedPromptIds.delete(event.data.id);
2317
3571
  }
3572
+ if (event.data.type === "turn_end" || event.data.type === "agent_end") {
3573
+ this.turnActive = false;
3574
+ }
2318
3575
  if (
2319
3576
  event.data.type === "message_end" ||
2320
3577
  event.data.type === "turn_end" ||
@@ -2342,6 +3599,7 @@ class OmpRpcProcess {
2342
3599
 
2343
3600
  private acceptEventState(event: z.infer<typeof OmpRuntimeEventSchema>): boolean {
2344
3601
  if (event.type === "turn_start") {
3602
+ this.turnActive = true;
2345
3603
  this.streamedBlocks.clear();
2346
3604
  this.commandTextLength = 0;
2347
3605
  this.activeToolCallIds.clear();
@@ -2363,7 +3621,7 @@ class OmpRpcProcess {
2363
3621
  this.activeToolCallIds.add(event.toolCallId);
2364
3622
  return true;
2365
3623
  }
2366
- if (event.type === "tool_execution_update") {
3624
+ if (event.type === "tool_execution_update" || event.type === "tool_stream_update") {
2367
3625
  return this.activeToolCallIds.has(event.toolCallId);
2368
3626
  }
2369
3627
  if (event.type === "tool_execution_end") {
@@ -2411,13 +3669,84 @@ class OmpRpcProcess {
2411
3669
  return true;
2412
3670
  }
2413
3671
 
2414
- private rejectChunk(): void {
3672
+ private protocolDiagnosticPhase(): OmpProtocolDiagnosticPhase {
3673
+ if (this.closed || this.exited || this.outputSettled) return "closing";
3674
+ if (!this.readyReceived) return "startup";
3675
+ for (const pending of this.pending.values()) {
3676
+ if (pending.command === "negotiate_protocol") return "negotiation";
3677
+ if (pending.command === "prompt") return "active-turn";
3678
+ }
3679
+ if (this.turnActive || this.acceptedPromptIds.size > 0) return "active-turn";
3680
+ return "idle";
3681
+ }
3682
+
3683
+ private rejectChunk(
3684
+ reason: OmpProtocolViolationReason,
3685
+ metadata: Omit<
3686
+ OmpProtocolViolationDiagnostic,
3687
+ "category" | "reason" | "occurrenceCount" | "phase" | "frameType"
3688
+ > = {},
3689
+ ): void {
2415
3690
  this.clearChunk();
2416
- this.recordProtocolViolation();
3691
+ this.recordProtocolViolation("invalid-chunk", {
3692
+ reason,
3693
+ frameType: "rpc_chunk",
3694
+ ...metadata,
3695
+ });
3696
+ }
3697
+
3698
+ private recordProtocolViolation(
3699
+ category: OmpProtocolViolationCategory,
3700
+ metadata: Omit<OmpProtocolViolationDiagnostic, "category" | "occurrenceCount" | "phase">,
3701
+ ): void {
3702
+ const phase = this.protocolDiagnosticPhase();
3703
+ if (!this.protocolViolationTimer) {
3704
+ this.emitProtocolViolation({ category, occurrenceCount: 1, phase, ...metadata });
3705
+ this.protocolViolationTimer = setTimeout(
3706
+ () => this.flushProtocolViolations(),
3707
+ PROTOCOL_VIOLATION_COALESCE_MS,
3708
+ );
3709
+ return;
3710
+ }
3711
+ const key = `${category}:${metadata.reason}` as ProtocolViolationKey;
3712
+ const pending = this.pendingProtocolViolations.get(key);
3713
+ if (!pending) {
3714
+ this.pendingProtocolViolations.set(key, {
3715
+ category,
3716
+ occurrenceCount: 1,
3717
+ phase,
3718
+ ...metadata,
3719
+ });
3720
+ return;
3721
+ }
3722
+ pending.occurrenceCount = Math.min(Number.MAX_SAFE_INTEGER, pending.occurrenceCount + 1);
3723
+ pending.maxByteSize =
3724
+ Math.max(pending.maxByteSize ?? 0, metadata.maxByteSize ?? 0) || undefined;
3725
+ pending.reason = metadata.reason;
3726
+ pending.phase = phase;
3727
+ pending.frameType = metadata.frameType;
3728
+ pending.field = metadata.field;
3729
+ pending.expected = metadata.expected;
3730
+ pending.actualType = metadata.actualType;
3731
+ pending.limitBytes = metadata.limitBytes;
2417
3732
  }
2418
3733
 
2419
- private recordProtocolViolation(): void {
2420
- // Malformed bounded frames are isolated so the following frame starts from clean state.
3734
+ private flushProtocolViolations(): void {
3735
+ if (this.protocolViolationTimer) clearTimeout(this.protocolViolationTimer);
3736
+ this.protocolViolationTimer = null;
3737
+ for (const diagnostic of this.pendingProtocolViolations.values()) {
3738
+ this.emitProtocolViolation(diagnostic);
3739
+ }
3740
+ this.pendingProtocolViolations.clear();
3741
+ }
3742
+
3743
+ private emitProtocolViolation(diagnostic: OmpProtocolViolationDiagnostic): void {
3744
+ try {
3745
+ const reporting = this.reportProtocolViolation(diagnostic);
3746
+ if (reporting) void reporting.catch(() => undefined);
3747
+ } catch {
3748
+ // Diagnostics must never alter transport flow or cleanup.
3749
+ }
2421
3750
  }
2422
3751
 
2423
3752
  private fail(error: Error): void {
@@ -2503,6 +3832,7 @@ class OmpRpcSession implements OmpRuntimeSession {
2503
3832
  readonly inheritedRedactionValues: readonly string[],
2504
3833
  readonly supportsTypedToolApprovals: boolean,
2505
3834
  ) {}
3835
+ private messagePagingSupport: "unknown" | "supported" | "unsupported" = "unknown";
2506
3836
 
2507
3837
  onEvent(listener: (event: OmpRpcEvent) => void): () => void {
2508
3838
  return this.process.onEvent(listener);
@@ -2516,6 +3846,195 @@ class OmpRpcSession implements OmpRuntimeSession {
2516
3846
  return OmpSessionStatsSchema.parse(await this.process.request({ type: "get_session_stats" }));
2517
3847
  }
2518
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
+
2519
4038
  async compact(customInstructions?: string): Promise<OmpCompactionResult> {
2520
4039
  const instructions =
2521
4040
  customInstructions === undefined
@@ -2611,16 +4130,6 @@ class OmpRpcSession implements OmpRuntimeSession {
2611
4130
  return result.data;
2612
4131
  }
2613
4132
 
2614
- async getMessages(): Promise<OmpMessage[]> {
2615
- if (!this.canReplayHistory) {
2616
- throw new Error("OMP history replay requires negotiated RPC protocol v2");
2617
- }
2618
- const result = OmpMessagesResultSchema.parse(
2619
- await this.process.request({ type: "get_messages" }),
2620
- );
2621
- return result.messages;
2622
- }
2623
-
2624
4133
  async setHostTools(tools: readonly OmpHostToolDefinition[]): Promise<string[]> {
2625
4134
  const safeTools = z.array(OmpHostToolDefinitionSchema).max(MAX_HOST_TOOLS).parse(tools);
2626
4135
  if (safeTools.length === 0) return [];
@@ -2728,12 +4237,16 @@ export class OmpRpcRuntime implements OmpRuntime {
2728
4237
  );
2729
4238
  return {
2730
4239
  ...transcript,
2731
- messages: z.array(OmpMessageSchema).max(100_000).parse(transcript.messages),
4240
+ messages: z
4241
+ .array(OmpMessageSchema)
4242
+ .max(MAX_REPLAY_MESSAGES)
4243
+ .parse(sanitizeMessageListMetadata(transcript.messages)),
2732
4244
  };
2733
4245
  }
2734
4246
  async readPersistedSubagentTranscript(options: {
2735
4247
  parentSessionFile: string;
2736
4248
  childTranscriptId: string;
4249
+ sessionFile?: string;
2737
4250
  cwd: string;
2738
4251
  signal?: AbortSignal;
2739
4252
  }): Promise<OmpPersistedSubagentMessages> {
@@ -2742,10 +4255,14 @@ export class OmpRpcRuntime implements OmpRuntime {
2742
4255
  options.childTranscriptId,
2743
4256
  options.cwd,
2744
4257
  options.signal,
4258
+ options.sessionFile,
2745
4259
  );
2746
4260
  return {
2747
4261
  ...transcript,
2748
- messages: z.array(OmpMessageSchema).max(100_000).parse(transcript.messages),
4262
+ messages: z
4263
+ .array(OmpMessageSchema)
4264
+ .max(MAX_REPLAY_MESSAGES)
4265
+ .parse(sanitizeMessageListMetadata(transcript.messages)),
2749
4266
  };
2750
4267
  }
2751
4268
 
@@ -2760,6 +4277,7 @@ export class OmpRpcRuntime implements OmpRuntime {
2760
4277
  this.options.spawnProcess,
2761
4278
  this.options.terminateProcessTree,
2762
4279
  options.requestTimeoutMs ?? this.options.requestTimeoutMs,
4280
+ this.options.reportProtocolViolation,
2763
4281
  );
2764
4282
  const abort = () => void process.close().catch(() => undefined);
2765
4283
  options.signal?.addEventListener("abort", abort, { once: true });