@omercnet/paseo-omp 0.4.0-next.120.1 → 0.4.0-next.123.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.
@@ -1,22 +1,60 @@
1
- import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
1
+ import type { ChildProcessWithoutNullStreams } from "node:child_process";
3
2
  import { join } from "node:path";
4
3
  import { z } from "zod";
5
4
  import { ompDataDir } from "../paths";
6
- import { isValidImagePayload } from "./image";
5
+ import type { OmpSpawnRequest, OmpStartOptions } from "./omp-rpc-environment";
6
+ import { waitMs } from "./omp-rpc-process";
7
7
  import {
8
- buildOmpSpawnRequest,
9
- type OmpSpawnRequest,
10
- type OmpStartOptions,
11
- } from "./omp-rpc-environment";
8
+ inspectJsonBounds,
9
+ MAX_IMAGE_DATA_LENGTH,
10
+ MAX_REPLAY_MESSAGES,
11
+ NAME,
12
+ OMP_MESSAGE_PAGE_LIMIT,
13
+ type OmpAvailableCommand,
14
+ OmpAvailableCommandsResultSchema,
15
+ OmpBranchMessagesResultSchema,
16
+ type OmpBranchResult,
17
+ OmpBranchResultSchema,
18
+ type OmpCompactionResult,
19
+ OmpCompactionResultSchema,
20
+ type OmpExtensionUiResponse,
21
+ type OmpHostToolDefinition,
22
+ OmpHostToolDefinitionSchema,
23
+ type OmpHostToolResult,
24
+ type OmpHostToolUpdate,
25
+ type OmpImage,
26
+ type OmpMessage,
27
+ OmpMessageSchema,
28
+ OmpMessagesPageResultSchema,
29
+ OmpMessagesResultSchema,
30
+ type OmpModel,
31
+ OmpModelSchema,
32
+ OmpModelsResultSchema,
33
+ type OmpPersistedSessionMessages,
34
+ type OmpPersistedSubagentMessages,
35
+ OmpPromptAckSchema,
36
+ type OmpProtocolViolationDiagnostic,
37
+ type OmpRpcEvent,
38
+ type OmpSessionState,
39
+ OmpSessionStateSchema,
40
+ type OmpSessionStats,
41
+ OmpSessionStatsSchema,
42
+ type OmpSubagentMessagesResult,
43
+ OmpSubagentMessagesResultSchema,
44
+ type OmpSubagentSnapshot,
45
+ OmpSubagentsResultSchema,
46
+ type OmpToolApprovalResponse,
47
+ OmpToolApprovalResponseSchema,
48
+ ProtocolNegotiationResultSchema,
49
+ sanitizeMessageListMetadata,
50
+ } from "./omp-rpc-protocol";
12
51
  import {
13
- isConfirmedNoProcessSpawnFailure,
14
- PROCESS_STOP_TIMEOUT_MS,
15
- type ProcessTreeCleanup,
16
- stopWindowsTree,
17
- terminatePosixProcessTree,
18
- waitMs,
19
- } from "./omp-rpc-process";
52
+ OmpRpcProcess,
53
+ OmpRpcRequestRejectedError,
54
+ OmpRpcResponseLimitError,
55
+ validateReadyMetadata,
56
+ waitWithTimeout,
57
+ } from "./omp-rpc-transport";
20
58
  import {
21
59
  MAX_ID_LENGTH,
22
60
  MAX_NAME_LENGTH,
@@ -25,13 +63,7 @@ import {
25
63
  OmpThinkingLevelSchema,
26
64
  validateBoundedText,
27
65
  } from "./omp-rpc-values";
28
- import {
29
- boundedJsonBytes,
30
- boundedJsonMetrics,
31
- OmpCleanupFailure,
32
- OmpPublicError,
33
- utf8Bytes,
34
- } from "./security";
66
+ import { OmpCleanupFailure, OmpPublicError } from "./security";
35
67
  import {
36
68
  listOmpSessionDescriptors,
37
69
  type OmpSessionDescriptor,
@@ -41,1970 +73,18 @@ import {
41
73
  } from "./session-descriptors";
42
74
 
43
75
  const READY_TIMEOUT_MS = 20_000;
44
- const REQUEST_TIMEOUT_MS = 60_000;
45
- const CHUNK_STALE_MS = 30_000;
46
- const MAX_PHYSICAL_FRAME_BYTES = 1024 * 1024;
47
- const MAX_CHUNK_BYTES = 256 * 1024;
48
- const MAX_ENCODED_CHUNK_BYTES = Math.ceil(MAX_CHUNK_BYTES / 3) * 4;
49
- const MAX_REASSEMBLED_FRAME_BYTES = 64 * 1024 * 1024;
50
- const MAX_SEMANTIC_FRAME_BYTES = 12 * 1024 * 1024;
51
- const MAX_CHUNK_COUNT = MAX_REASSEMBLED_FRAME_BYTES / MAX_CHUNK_BYTES;
52
- const MAX_CONFIG_EVENT_TEXT_BYTES = 64 * 1024;
53
- const MAX_STREAM_TEXT_LENGTH = 4 * 1024 * 1024;
54
- const MAX_IMAGE_DATA_LENGTH = 8 * 1024 * 1024;
55
- const MAX_TOOL_PAYLOAD_LENGTH = 256 * 1024;
56
- const MAX_ACTIVE_TOOLS = 64;
57
76
  const MAX_HOST_TOOLS = 256;
58
- const MAX_TOOL_APPROVAL_FRAME_BYTES = 64 * 1024;
59
- const MAX_TOOL_APPROVAL_STRING_BYTES = 8 * 1024;
60
- const MAX_TOOL_APPROVAL_COLLECTION_ITEMS = 32;
61
- const MAX_TOOL_APPROVAL_INPUT_NODES = 256;
62
- const MAX_TOOL_APPROVAL_DEPTH = 4;
63
- const MAX_TOOL_APPROVAL_ID_BYTES = 512;
64
- const MAX_TOOL_APPROVAL_NAME_BYTES = 256;
65
- const MAX_TOOL_APPROVAL_DETAIL_LINES = 16;
66
- const MAX_TOOL_APPROVAL_DETAIL_BYTES = 2 * 1024;
67
- const MAX_TOOL_APPROVAL_METADATA_FIELDS = 33;
68
- const MAX_TOOL_APPROVAL_METADATA_FIELD_BYTES = 64;
69
- const MAX_TOOL_APPROVAL_TIMEOUT_MS = 24 * 60 * 60 * 1000;
70
- const MAX_TOOL_APPROVAL_PATH_BYTES = 4 * 1024;
71
- const MAX_TOOL_APPROVAL_CONTENT_BYTES = 20 * 1024;
72
- type TimerHandle = ReturnType<typeof setTimeout>;
73
- const MAX_PENDING_REQUESTS = 256;
74
- const MAX_PENDING_ONE_WAY_WRITES = 256;
75
- const MAX_PENDING_WRITE_BYTES = 8 * 1024 * 1024;
76
- const MAX_LINE_PARTS = 4_096;
77
- const MAX_ARRAY_ITEMS = 512;
78
77
  // OMP read metadata can contain one source entry per displayed line. Bound this optional,
79
78
  // opaque field separately so over-budget metadata can be omitted without losing completion events.
80
- const MAX_OPTIONAL_METADATA_BYTES = MAX_TOOL_PAYLOAD_LENGTH;
81
- const MAX_OPTIONAL_METADATA_ITEMS = 2_048;
82
- const MAX_OPTIONAL_METADATA_NODES = 4_096;
83
- const MAX_TASK_CORRELATION_BYTES = 256 * 1024;
84
- const MAX_TASK_CORRELATION_ITEMS = 1_024;
85
- const MAX_TASK_CORRELATION_NODES = 4_096;
86
- const MAX_MODEL_CATALOG_ITEMS = 4_096;
87
79
  // Tool-intensive OMP turns legitimately exceed 64 blocks; transport byte/node budgets remain the
88
80
  // primary resource bounds.
89
- export const OMP_MAX_CONTENT_PARTS = 4_096;
90
- const MAX_TODOS = 256;
91
- const MAX_TOKEN_COUNT = Number.MAX_SAFE_INTEGER;
92
- const MAX_COST_USD = 1_000_000_000;
93
- const MAX_RPC_ERROR_BYTES = 4_096;
94
- const MAX_RPC_ERROR_CODE_BYTES = 256;
95
- const PROMPT_SCHEDULING_FAILURE = "OMP prompt scheduling failed";
96
- const PROTOCOL_VIOLATION_COALESCE_MS = 10_000;
97
- const MAX_REPLAY_MESSAGES = 100_000;
98
81
  const MAX_REPLAY_BYTES = 64 * 1024 * 1024;
99
82
  const MAX_REPLAY_NODES = 400_000;
100
- const OMP_MESSAGE_PAGE_LIMIT = 256;
101
83
  const MAX_MESSAGE_PAGES = 512;
102
84
  const MAX_MESSAGE_PAGE_BUSY_RETRIES = 4;
103
85
  const MAX_MESSAGE_PAGE_STALE_RESTARTS = 2;
104
86
  const MESSAGE_PAGE_RETRY_BASE_MS = 50;
105
87
 
106
- export const OMP_MESSAGE_REPLAY_POLICIES = {
107
- assistant: "render",
108
- user: "render",
109
- developer: "ignore",
110
- toolResult: "render",
111
- bashExecution: "normalize",
112
- pythonExecution: "normalize",
113
- custom: "normalize",
114
- hookMessage: "normalize",
115
- branchSummary: "ignore",
116
- compactionSummary: "ignore",
117
- fileMention: "ignore",
118
- } as const;
119
-
120
- export const OMP_SESSION_EVENT_POLICIES = {
121
- agent_start: "lifecycle",
122
- agent_end: "lifecycle",
123
- turn_start: "lifecycle",
124
- turn_end: "lifecycle",
125
- message_start: "normalize",
126
- message_update: "normalize",
127
- message_end: "normalize",
128
- tool_execution_start: "normalize",
129
- tool_execution_update: "normalize",
130
- tool_stream_update: "normalize",
131
- tool_execution_end: "normalize",
132
- auto_compaction_start: "normalize",
133
- auto_compaction_end: "normalize",
134
- auto_retry_start: "normalize",
135
- auto_retry_end: "normalize",
136
- retry_fallback_applied: "normalize",
137
- retry_fallback_succeeded: "normalize",
138
- model_changed: "normalize",
139
- config_warnings_changed: "ignore",
140
- advisor_cost_changed: "ignore",
141
- advisor_yielded: "normalize",
142
- ttsr_triggered: "ignore",
143
- todo_reminder: "normalize",
144
- todo_auto_clear: "normalize",
145
- irc_message: "normalize",
146
- notice: "normalize",
147
- thinking_level_changed: "normalize",
148
- goal_updated: "normalize",
149
- } as const;
150
-
151
- export const OMP_RPC_COMMAND_POLICIES = {
152
- negotiate_protocol: "implemented",
153
- prompt: "implemented",
154
- steer: "implemented",
155
- follow_up: "implemented",
156
- abort: "implemented",
157
- abort_and_prompt: "unsupported",
158
- new_session: "unsupported",
159
- get_state: "implemented",
160
- set_fast_mode: "unsupported",
161
- get_available_commands: "implemented",
162
- set_todos: "unsupported",
163
- set_host_tools: "implemented",
164
- set_host_uri_schemes: "unsupported",
165
- set_subagent_subscription: "implemented",
166
- get_subagents: "implemented",
167
- get_subagent_messages: "implemented",
168
- set_model: "implemented",
169
- cycle_model: "unsupported",
170
- get_available_models: "implemented",
171
- set_thinking_level: "implemented",
172
- cycle_thinking_level: "unsupported",
173
- set_steering_mode: "unsupported",
174
- set_follow_up_mode: "unsupported",
175
- set_interrupt_mode: "unsupported",
176
- compact: "implemented",
177
- set_auto_compaction: "implemented",
178
- set_auto_retry: "unsupported",
179
- abort_retry: "unsupported",
180
- bash: "unsupported",
181
- abort_bash: "unsupported",
182
- get_session_stats: "implemented",
183
- export_html: "unsupported",
184
- switch_session: "unsupported",
185
- branch: "implemented",
186
- get_branch_messages: "implemented",
187
- get_last_assistant_text: "unsupported",
188
- set_session_name: "unsupported",
189
- handoff: "implemented",
190
- get_messages: "implemented",
191
- get_messages_page: "implemented",
192
- get_login_providers: "unsupported",
193
- login: "unsupported",
194
- } as const;
195
- export const OMP_RPC_RESPONSE_POLICIES = OMP_RPC_COMMAND_POLICIES;
196
- export const OMP_PROTOCOL_VIOLATION_CATEGORIES = [
197
- "duplicate-ready",
198
- "frame-limit",
199
- "incomplete-frame",
200
- "interleaved-chunk",
201
- "invalid-chunk",
202
- "invalid-envelope",
203
- "invalid-event",
204
- "invalid-event-state",
205
- "invalid-json",
206
- "invalid-ready",
207
- "invalid-response",
208
- "remote-frame-error",
209
- ] as const;
210
- export type OmpProtocolViolationCategory = (typeof OMP_PROTOCOL_VIOLATION_CATEGORIES)[number];
211
- export const OMP_PROTOCOL_VIOLATION_REASONS = [
212
- "output-ended-mid-frame",
213
- "physical-frame-limit",
214
- "semantic-frame-limit",
215
- "json-decode",
216
- "chunk-json-decode",
217
- "object-envelope",
218
- "missing-frame-type",
219
- "response-schema",
220
- "chunk-schema",
221
- "chunk-metadata",
222
- "chunk-size",
223
- "chunk-start-index",
224
- "chunk-sequence",
225
- "chunk-byte-count",
226
- "chunk-timeout",
227
- "frame-interleaved-with-chunk",
228
- "remote-frame-error",
229
- "ready-already-received",
230
- "ready-schema",
231
- "notice-level-type",
232
- "notice-message-type",
233
- "notice-schema",
234
- "event-schema",
235
- "message-event-schema",
236
- "tool-event-schema",
237
- "lifecycle-event-schema",
238
- "subagent-event-schema",
239
- "configuration-event-schema",
240
- "extension-ui-schema",
241
- "unknown-event-type",
242
- "event-state-transition",
243
- ] as const;
244
- export type OmpProtocolViolationReason = (typeof OMP_PROTOCOL_VIOLATION_REASONS)[number];
245
- export const OMP_PROTOCOL_DIAGNOSTIC_PHASES = [
246
- "startup",
247
- "negotiation",
248
- "idle",
249
- "active-turn",
250
- "closing",
251
- ] as const;
252
- export type OmpProtocolDiagnosticPhase = (typeof OMP_PROTOCOL_DIAGNOSTIC_PHASES)[number];
253
-
254
- export const OMP_PROTOCOL_EVENT_TYPES = [
255
- "agent_start",
256
- "agent_end",
257
- "turn_start",
258
- "turn_end",
259
- "message_start",
260
- "message_update",
261
- "message_end",
262
- "tool_execution_start",
263
- "tool_execution_update",
264
- "tool_execution_end",
265
- "tool_stream_update",
266
- "compaction_start",
267
- "compaction_end",
268
- "subagent_lifecycle",
269
- "subagent_progress",
270
- "subagent_event",
271
- "todo_reminder",
272
- "model_changed",
273
- "config_warnings_changed",
274
- "advisor_cost_changed",
275
- "ttsr_triggered",
276
- "irc_message",
277
- "thinking_level_changed",
278
- "goal_updated",
279
- "auto_retry_start",
280
- "auto_retry_end",
281
- "retry_fallback_applied",
282
- "retry_fallback_succeeded",
283
- "todo_auto_clear",
284
- "auto_compaction_start",
285
- "auto_compaction_end",
286
- "available_commands_update",
287
- "notice",
288
- "command_output",
289
- "extension_ui_request",
290
- "prompt_result",
291
- "host_tool_call",
292
- "host_tool_cancel",
293
- "tool_approval_request",
294
- "tool_approval_cancel",
295
- "advisor_yielded",
296
- ] as const;
297
- export type OmpProtocolEventType = (typeof OMP_PROTOCOL_EVENT_TYPES)[number];
298
-
299
- export const OMP_PROTOCOL_DIAGNOSTIC_FIELDS = [
300
- "frame",
301
- "frame.byteLength",
302
- "frame.type",
303
- "response",
304
- "ready",
305
- "event",
306
- "event.sequence",
307
- "notice.level",
308
- "notice.message",
309
- "chunk",
310
- "chunk.data",
311
- "chunk.index",
312
- "chunk.sequence",
313
- "chunk.byteLength",
314
- ] as const;
315
- export type OmpProtocolDiagnosticField = (typeof OMP_PROTOCOL_DIAGNOSTIC_FIELDS)[number];
316
-
317
- export const OMP_PROTOCOL_DIAGNOSTIC_EXPECTATIONS = [
318
- "complete-json-line",
319
- "within-byte-limit",
320
- "valid-json",
321
- "object-envelope",
322
- "bounded-frame-type",
323
- "valid-response-frame",
324
- "valid-ready-frame",
325
- "single-ready-frame",
326
- "valid-event-frame",
327
- "valid-event-state-transition",
328
- "valid-message-event",
329
- "valid-tool-event",
330
- "valid-lifecycle-event",
331
- "valid-subagent-event",
332
- "valid-configuration-event",
333
- "valid-extension-ui-event",
334
- "known-event-type",
335
- "notice-level-enum",
336
- "notice-message-string",
337
- "valid-chunk-frame",
338
- "valid-base64-chunk",
339
- "first-chunk-index-zero",
340
- "contiguous-chunk-sequence",
341
- "declared-chunk-byte-count",
342
- "chunk-before-deadline",
343
- "no-interleaved-frame",
344
- "no-remote-frame-error",
345
- ] as const;
346
- export type OmpProtocolDiagnosticExpectation =
347
- (typeof OMP_PROTOCOL_DIAGNOSTIC_EXPECTATIONS)[number];
348
-
349
- export const OMP_PROTOCOL_DIAGNOSTIC_ACTUAL_TYPES = [
350
- "missing",
351
- "null",
352
- "array",
353
- "object",
354
- "string",
355
- "number",
356
- "boolean",
357
- "invalid-json",
358
- "partial-frame",
359
- "oversized",
360
- "duplicate",
361
- "interleaved",
362
- "out-of-order",
363
- "timeout",
364
- "remote-error",
365
- "mismatched",
366
- ] as const;
367
- export type OmpProtocolDiagnosticActualType = (typeof OMP_PROTOCOL_DIAGNOSTIC_ACTUAL_TYPES)[number];
368
-
369
- export interface OmpProtocolViolationDiagnostic {
370
- category: OmpProtocolViolationCategory;
371
- reason: OmpProtocolViolationReason;
372
- phase: OmpProtocolDiagnosticPhase;
373
- occurrenceCount: number;
374
- eventType?: OmpProtocolEventType;
375
- frameType?: "ready" | "response" | "rpc_chunk" | "rpc_frame_error" | "notice";
376
- field?: OmpProtocolDiagnosticField;
377
- expected?: OmpProtocolDiagnosticExpectation;
378
- actualType?: OmpProtocolDiagnosticActualType;
379
- maxByteSize?: number;
380
- limitBytes?: number;
381
- }
382
-
383
- function protocolDiagnosticActualType(value: unknown): OmpProtocolDiagnosticActualType {
384
- if (value === undefined) return "missing";
385
- if (value === null) return "null";
386
- if (Array.isArray(value)) return "array";
387
- if (typeof value === "object") return "object";
388
- if (typeof value === "string") return "string";
389
- if (typeof value === "number") return "number";
390
- if (typeof value === "boolean") return "boolean";
391
- return "object";
392
- }
393
-
394
- function isProtocolEventType(value: string): value is OmpProtocolEventType {
395
- return (OMP_PROTOCOL_EVENT_TYPES as readonly string[]).includes(value);
396
- }
397
-
398
- function eventSchemaClassification(
399
- eventType: OmpProtocolEventType,
400
- ): Pick<OmpProtocolViolationDiagnostic, "reason" | "expected"> {
401
- switch (eventType) {
402
- case "message_start":
403
- case "message_update":
404
- case "message_end":
405
- return { reason: "message-event-schema", expected: "valid-message-event" };
406
- case "irc_message":
407
- return { reason: "message-event-schema", expected: "valid-message-event" };
408
- case "tool_execution_start":
409
- case "tool_execution_update":
410
- case "tool_stream_update":
411
- case "tool_execution_end":
412
- case "host_tool_call":
413
- case "host_tool_cancel":
414
- case "tool_approval_request":
415
- case "tool_approval_cancel":
416
- return { reason: "tool-event-schema", expected: "valid-tool-event" };
417
- case "subagent_lifecycle":
418
- case "subagent_progress":
419
- case "subagent_event":
420
- return { reason: "subagent-event-schema", expected: "valid-subagent-event" };
421
- case "model_changed":
422
- case "thinking_level_changed":
423
- case "goal_updated":
424
- case "todo_reminder":
425
- case "todo_auto_clear":
426
- case "available_commands_update":
427
- case "retry_fallback_applied":
428
- case "retry_fallback_succeeded":
429
- case "config_warnings_changed":
430
- case "advisor_cost_changed":
431
- case "ttsr_triggered":
432
- return { reason: "configuration-event-schema", expected: "valid-configuration-event" };
433
- case "extension_ui_request":
434
- return { reason: "extension-ui-schema", expected: "valid-extension-ui-event" };
435
- default:
436
- return { reason: "lifecycle-event-schema", expected: "valid-lifecycle-event" };
437
- }
438
- }
439
-
440
- function invalidEventDiagnosticMetadata(
441
- frame: Record<string, unknown>,
442
- type: string,
443
- ): Pick<
444
- OmpProtocolViolationDiagnostic,
445
- "reason" | "eventType" | "frameType" | "field" | "expected" | "actualType"
446
- > {
447
- if (!isProtocolEventType(type)) {
448
- return {
449
- reason: "unknown-event-type",
450
- field: "frame.type",
451
- expected: "known-event-type",
452
- actualType: "string",
453
- };
454
- }
455
- if (type !== "notice") {
456
- return {
457
- ...eventSchemaClassification(type),
458
- eventType: type,
459
- field: "event",
460
- actualType: "object",
461
- };
462
- }
463
- if (typeof frame.level !== "string") {
464
- return {
465
- reason: "notice-level-type",
466
- eventType: "notice",
467
- frameType: "notice",
468
- field: "notice.level",
469
- expected: "notice-level-enum",
470
- actualType: protocolDiagnosticActualType(frame.level),
471
- };
472
- }
473
- if (typeof frame.message !== "string") {
474
- return {
475
- reason: "notice-message-type",
476
- eventType: "notice",
477
- frameType: "notice",
478
- field: "notice.message",
479
- expected: "notice-message-string",
480
- actualType: protocolDiagnosticActualType(frame.message),
481
- };
482
- }
483
- return {
484
- reason: "notice-schema",
485
- eventType: "notice",
486
- frameType: "notice",
487
- field: "event",
488
- expected: "valid-event-frame",
489
- actualType: "object",
490
- };
491
- }
492
-
493
- type PendingProtocolViolation = Omit<OmpProtocolViolationDiagnostic, "occurrenceCount"> & {
494
- occurrenceCount: number;
495
- };
496
- type ProtocolViolationKey = `${OmpProtocolViolationCategory}:${OmpProtocolViolationReason}`;
497
- const MAX_CONTEXT_PERCENT = 1_000_000;
498
- function boundedJsonString(maxBytes: number, minBytes = 0) {
499
- return z.string().refine((value) => {
500
- const bytes = Buffer.byteLength(JSON.stringify(value), "utf8") - 2;
501
- return bytes >= minBytes && bytes <= maxBytes;
502
- });
503
- }
504
- function isBoundedToolApprovalId(value: unknown): value is string {
505
- return typeof value === "string" && value.length > 0 && Buffer.byteLength(value, "utf8") <= 512;
506
- }
507
-
508
- export const OMP_HOST_TOOL_FRAME_LIMIT_ERROR =
509
- "MCP host tool result exceeds the OMP RPC frame limit";
510
- const MIN_HOST_TOOL_RESULT_FRAME_BYTES = Buffer.byteLength(
511
- `${JSON.stringify({
512
- type: "host_tool_result",
513
- id: "\0".repeat(MAX_ID_LENGTH),
514
- result: {
515
- content: [{ type: "text", text: OMP_HOST_TOOL_FRAME_LIMIT_ERROR }],
516
- details: {},
517
- isError: true,
518
- },
519
- isError: true,
520
- })}\n`,
521
- );
522
- function boundedString(maxBytes: number, minBytes = 0) {
523
- return z.string().refine((value) => {
524
- const bytes = utf8Bytes(value);
525
- return bytes >= minBytes && bytes <= maxBytes;
526
- });
527
- }
528
- function isSafeOpaqueCursor(value: string): boolean {
529
- for (let index = 0; index < value.length; index += 1) {
530
- const code = value.charCodeAt(index);
531
- if (code <= 0x1f || code === 0x7f) return false;
532
- }
533
- return true;
534
- }
535
- function boundedOptionalStringArray(maxItems: number, maxBytes: number) {
536
- return z.unknown().transform((value): string[] | undefined => {
537
- if (!Array.isArray(value)) return undefined;
538
- return value
539
- .filter(
540
- (item): item is string =>
541
- typeof item === "string" && item.length > 0 && utf8Bytes(item) <= maxBytes,
542
- )
543
- .slice(0, maxItems);
544
- });
545
- }
546
-
547
- const IDENTIFIER = boundedString(MAX_ID_LENGTH, 1);
548
- const NAME = boundedString(MAX_NAME_LENGTH, 1);
549
- const OMP_PROVIDER_NAME = NAME.refine((provider) => !provider.includes("/"));
550
- const TEXT = boundedString(MAX_TEXT_LENGTH);
551
- const RAW_DISPLAY_TEXT = boundedString(MAX_IMAGE_DATA_LENGTH);
552
- const DISPLAY_TRUNCATION_MARKER = "<truncated>";
553
-
554
- function boundRawDisplayContent(value: unknown): unknown {
555
- if (typeof value === "string") {
556
- return utf8Bytes(value) <= MAX_IMAGE_DATA_LENGTH ? value : DISPLAY_TRUNCATION_MARKER;
557
- }
558
- if (!Array.isArray(value)) return value;
559
- let totalBytes = 0;
560
- for (const part of value) {
561
- if (!part || typeof part !== "object" || Array.isArray(part)) continue;
562
- const record = part as Record<string, unknown>;
563
- if (typeof record.text === "string") totalBytes += utf8Bytes(record.text);
564
- if (typeof record.thinking === "string") totalBytes += utf8Bytes(record.thinking);
565
- }
566
- if (totalBytes <= MAX_IMAGE_DATA_LENGTH) return value;
567
-
568
- let retainedMarker = false;
569
- return value.map((part) => {
570
- if (!part || typeof part !== "object" || Array.isArray(part)) return part;
571
- const copy = { ...(part as Record<string, unknown>) };
572
- for (const key of ["text", "thinking"] as const) {
573
- if (typeof copy[key] !== "string") continue;
574
- if (retainedMarker) delete copy[key];
575
- else {
576
- copy[key] = DISPLAY_TRUNCATION_MARKER;
577
- retainedMarker = true;
578
- }
579
- }
580
- return copy;
581
- });
582
- }
583
-
584
- function sanitizeLiveMessageDisplay(value: unknown): unknown {
585
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
586
- const message = value as Record<string, unknown>;
587
- if (message.role === "assistant") {
588
- const content = boundRawDisplayContent(message.content);
589
- return content === message.content ? value : { ...message, content };
590
- }
591
- if (
592
- (message.role === "bashExecution" || message.role === "pythonExecution") &&
593
- typeof message.output === "string" &&
594
- utf8Bytes(message.output) > MAX_IMAGE_DATA_LENGTH
595
- ) {
596
- return { ...message, output: DISPLAY_TRUNCATION_MARKER };
597
- }
598
- return value;
599
- }
600
-
601
- function sanitizeLiveDisplayFrame(value: unknown): unknown {
602
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
603
- const frame = value as Record<string, unknown>;
604
- if (
605
- frame.type === "message_start" ||
606
- frame.type === "message_update" ||
607
- frame.type === "message_end"
608
- ) {
609
- const message = sanitizeLiveMessageDisplay(frame.message);
610
- return message === frame.message ? value : { ...frame, message };
611
- }
612
- if (frame.type !== "agent_end" || !Array.isArray(frame.messages)) return value;
613
- let changed = false;
614
- const messages = frame.messages.map((message) => {
615
- const sanitized = sanitizeLiveMessageDisplay(message);
616
- if (sanitized !== message) changed = true;
617
- return sanitized;
618
- });
619
- return changed ? { ...frame, messages } : value;
620
- }
621
- const OmpCurrentThinkingLevelSchema = boundedString(32).optional().catch(undefined);
622
-
623
- function isBoundedJson(
624
- value: unknown,
625
- maxBytes = MAX_TOOL_PAYLOAD_LENGTH,
626
- maxItems = MAX_ARRAY_ITEMS,
627
- maxNodes = 2_048,
628
- ): boolean {
629
- return (
630
- boundedJsonBytes(value, maxBytes, maxItems, maxBytes, maxNodes) !== Number.POSITIVE_INFINITY
631
- );
632
- }
633
- const MAX_RESPONSE_DEPTH = 16;
634
- export const OMP_RPC_BOUND_DIMENSIONS = [
635
- "bytes",
636
- "items",
637
- "nodes",
638
- "depth",
639
- "string-bytes",
640
- ] as const;
641
- export type OmpRpcBoundDimension = (typeof OMP_RPC_BOUND_DIMENSIONS)[number];
642
- export const OMP_RPC_DIAGNOSTIC_COMMANDS = [
643
- "negotiate_protocol",
644
- "get_state",
645
- "get_session_stats",
646
- "get_available_models",
647
- "get_available_commands",
648
- "set_subagent_subscription",
649
- "get_subagents",
650
- "get_subagent_messages",
651
- "prompt",
652
- "compact",
653
- "set_auto_compaction",
654
- "set_model",
655
- "set_thinking_level",
656
- "handoff",
657
- "get_branch_messages",
658
- "branch",
659
- "get_messages_page",
660
- "get_messages",
661
- "abort",
662
- "set_host_tools",
663
- ] as const;
664
- export type OmpRpcDiagnosticCommand = (typeof OMP_RPC_DIAGNOSTIC_COMMANDS)[number];
665
- type JsonBoundViolation = {
666
- dimension: OmpRpcBoundDimension;
667
- actual: number;
668
- limit: number;
669
- };
670
- type JsonBoundInspection = { bytes: number; nodes: number; violation?: JsonBoundViolation };
671
-
672
- function rpcDiagnosticCommand(value: string): OmpRpcDiagnosticCommand | undefined {
673
- return (OMP_RPC_DIAGNOSTIC_COMMANDS as readonly string[]).includes(value)
674
- ? (value as OmpRpcDiagnosticCommand)
675
- : undefined;
676
- }
677
-
678
- function encodedJsonStringBytes(value: string): number {
679
- let bytes = 2;
680
- for (let index = 0; index < value.length; index += 1) {
681
- const code = value.charCodeAt(index);
682
- if (code === 0x22 || code === 0x5c) {
683
- bytes += 2;
684
- } else if (code <= 0x1f) {
685
- bytes +=
686
- code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d ? 2 : 6;
687
- } else if (code >= 0xd800 && code <= 0xdbff) {
688
- const low = value.charCodeAt(index + 1);
689
- if (low >= 0xdc00 && low <= 0xdfff) {
690
- bytes += 4;
691
- index += 1;
692
- } else {
693
- bytes += 6;
694
- }
695
- } else if (code >= 0xdc00 && code <= 0xdfff) {
696
- bytes += 6;
697
- } else {
698
- bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : 3;
699
- }
700
- }
701
- return bytes;
702
- }
703
-
704
- function inspectJsonBounds(
705
- value: unknown,
706
- maxBytes: number,
707
- maxItems: number,
708
- maxStringBytes: number,
709
- maxNodes: number,
710
- ): JsonBoundInspection {
711
- const stack: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }];
712
- let nodes = 0;
713
- let bytes = 0;
714
- const addBytes = (count: number): JsonBoundViolation | undefined => {
715
- bytes += count;
716
- return bytes > maxBytes ? { dimension: "bytes", actual: bytes, limit: maxBytes } : undefined;
717
- };
718
- while (stack.length > 0) {
719
- const current = stack.pop();
720
- if (!current) break;
721
- nodes += 1;
722
- if (nodes > maxNodes)
723
- return { bytes, nodes, violation: { dimension: "nodes", actual: nodes, limit: maxNodes } };
724
- if (current.depth > MAX_RESPONSE_DEPTH) {
725
- return {
726
- bytes,
727
- nodes,
728
- violation: { dimension: "depth", actual: current.depth, limit: MAX_RESPONSE_DEPTH },
729
- };
730
- }
731
- const item = current.value;
732
- if (item === null) {
733
- const violation = addBytes(4);
734
- if (violation) return { bytes, nodes, violation };
735
- continue;
736
- }
737
- if (typeof item === "boolean") {
738
- const violation = addBytes(item ? 4 : 5);
739
- if (violation) return { bytes, nodes, violation };
740
- continue;
741
- }
742
- if (typeof item === "number") {
743
- const violation = addBytes((JSON.stringify(item) ?? "null").length);
744
- if (violation) return { bytes, nodes, violation };
745
- continue;
746
- }
747
- if (typeof item === "string") {
748
- const itemBytes = utf8Bytes(item);
749
- if (itemBytes > maxStringBytes) {
750
- return {
751
- bytes,
752
- nodes,
753
- violation: { dimension: "string-bytes", actual: itemBytes, limit: maxStringBytes },
754
- };
755
- }
756
- const violation = addBytes(encodedJsonStringBytes(item));
757
- if (violation) return { bytes, nodes, violation };
758
- continue;
759
- }
760
- if (typeof item !== "object") continue;
761
- if (Array.isArray(item)) {
762
- if (item.length > maxItems) {
763
- return {
764
- bytes,
765
- nodes,
766
- violation: { dimension: "items", actual: item.length, limit: maxItems },
767
- };
768
- }
769
- const violation = addBytes(2 + Math.max(0, item.length - 1));
770
- if (violation) return { bytes, nodes, violation };
771
- for (let index = item.length - 1; index >= 0; index -= 1) {
772
- stack.push({ value: item[index], depth: current.depth + 1 });
773
- }
774
- continue;
775
- }
776
- const entries = Object.entries(item).filter(([, child]) => child !== undefined);
777
- if (entries.length > maxItems) {
778
- return {
779
- bytes,
780
- nodes,
781
- violation: { dimension: "items", actual: entries.length, limit: maxItems },
782
- };
783
- }
784
- let violation = addBytes(2 + Math.max(0, entries.length - 1) + entries.length);
785
- if (violation) return { bytes, nodes, violation };
786
- for (let index = entries.length - 1; index >= 0; index -= 1) {
787
- const [key, child] = entries[index] as [string, unknown];
788
- violation = addBytes(encodedJsonStringBytes(key));
789
- if (violation) return { bytes, nodes, violation };
790
- stack.push({ value: child, depth: current.depth + 1 });
791
- }
792
- }
793
- return { bytes, nodes };
794
- }
795
-
796
- function jsonBoundViolation(
797
- value: unknown,
798
- maxBytes: number,
799
- maxItems: number,
800
- maxStringBytes: number,
801
- maxNodes: number,
802
- ): JsonBoundViolation | undefined {
803
- return inspectJsonBounds(value, maxBytes, maxItems, maxStringBytes, maxNodes).violation;
804
- }
805
- function optionalMetadataMetrics(value: unknown) {
806
- return boundedJsonMetrics(
807
- value,
808
- MAX_OPTIONAL_METADATA_BYTES,
809
- MAX_OPTIONAL_METADATA_ITEMS,
810
- MAX_OPTIONAL_METADATA_BYTES,
811
- MAX_OPTIONAL_METADATA_NODES,
812
- );
813
- }
814
-
815
- function optionalMetadataIsBounded(value: unknown): boolean {
816
- return optionalMetadataMetrics(value) !== undefined;
817
- }
818
-
819
- function omitUnsafeOptionalDetails(value: unknown): unknown {
820
- if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
821
- const record = value as Record<string, unknown>;
822
- if (!Object.hasOwn(record, "details") || optionalMetadataIsBounded(record.details)) return value;
823
- const { details: _details, ...safe } = record;
824
- return safe;
825
- }
826
-
827
- const TASK_RESULT_STATUSES: Readonly<Record<string, true>> = {
828
- pending: true,
829
- running: true,
830
- completed: true,
831
- failed: true,
832
- error: true,
833
- aborted: true,
834
- canceled: true,
835
- cancelled: true,
836
- };
837
- const TASK_PROGRESS_STATUSES: Readonly<Record<string, true>> = {
838
- pending: true,
839
- running: true,
840
- completed: true,
841
- failed: true,
842
- aborted: true,
843
- };
844
-
845
- function boundedTaskId(value: unknown): value is string {
846
- return typeof value === "string" && value.length > 0 && utf8Bytes(value) <= MAX_ID_LENGTH;
847
- }
848
-
849
- function taskCorrelationDetails(value: unknown): unknown {
850
- if (value === null || typeof value !== "object" || Array.isArray(value)) return;
851
- const details = value as Record<string, unknown>;
852
- if (!Array.isArray(details.results) || details.results.length > MAX_TASK_CORRELATION_ITEMS)
853
- return;
854
- const results: Record<string, unknown>[] = [];
855
- for (const value of details.results) {
856
- if (value === null || typeof value !== "object" || Array.isArray(value)) return;
857
- const result = value as Record<string, unknown>;
858
- if (!boundedTaskId(result.id)) return;
859
- const safe: Record<string, unknown> = { id: result.id };
860
- if (typeof result.status === "string" && Object.hasOwn(TASK_RESULT_STATUSES, result.status)) {
861
- safe.status = result.status;
862
- }
863
- if (typeof result.aborted === "boolean") safe.aborted = result.aborted;
864
- if (typeof result.exitCode === "number" && Number.isFinite(result.exitCode)) {
865
- safe.exitCode = result.exitCode;
866
- }
867
- if (
868
- result.error !== undefined &&
869
- boundedJsonMetrics(result.error, 4_096, 32, 4_096, 64) !== undefined
870
- ) {
871
- safe.error = result.error;
872
- }
873
- results.push(safe);
874
- }
875
- let progress: Record<string, unknown>[] | undefined;
876
- if (details.progress !== undefined) {
877
- if (!Array.isArray(details.progress) || details.progress.length > MAX_TASK_CORRELATION_ITEMS) {
878
- return;
879
- }
880
- progress = [];
881
- for (const value of details.progress) {
882
- if (value === null || typeof value !== "object" || Array.isArray(value)) return;
883
- const item = value as Record<string, unknown>;
884
- if (
885
- !boundedTaskId(item.id) ||
886
- typeof item.index !== "number" ||
887
- !Number.isInteger(item.index) ||
888
- item.index < 0 ||
889
- item.index >= MAX_TASK_CORRELATION_ITEMS ||
890
- typeof item.status !== "string" ||
891
- !Object.hasOwn(TASK_PROGRESS_STATUSES, item.status)
892
- ) {
893
- return;
894
- }
895
- progress.push({ id: item.id, index: item.index, status: item.status });
896
- }
897
- }
898
- const correlation = { results, ...(progress ? { progress } : {}) };
899
- return boundedJsonMetrics(
900
- correlation,
901
- MAX_TASK_CORRELATION_BYTES,
902
- MAX_TASK_CORRELATION_ITEMS,
903
- MAX_TASK_CORRELATION_BYTES,
904
- MAX_TASK_CORRELATION_NODES,
905
- )
906
- ? correlation
907
- : undefined;
908
- }
909
-
910
- function omitOptionalDetails(value: unknown, preserveTaskCorrelation = false): unknown {
911
- if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
912
- const record = value as Record<string, unknown>;
913
- if (!Object.hasOwn(record, "details")) return value;
914
- const { details: _details, ...structural } = record;
915
- if (!preserveTaskCorrelation) return structural;
916
- const correlation = taskCorrelationDetails(record.details);
917
- return correlation === undefined ? structural : { ...structural, details: correlation };
918
- }
919
-
920
- function createOptionalMetadataSanitizer(): (value: unknown, taskResult?: boolean) => unknown {
921
- let retainedBytes = 0;
922
- let retainedNodes = 0;
923
- return (value, taskResult = false) => {
924
- if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
925
- const record = value as Record<string, unknown>;
926
- if (!Object.hasOwn(record, "details")) return value;
927
- const metrics = optionalMetadataMetrics(record.details);
928
- if (
929
- metrics &&
930
- retainedBytes + metrics.bytes <= MAX_OPTIONAL_METADATA_BYTES &&
931
- retainedNodes + metrics.nodes <= MAX_OPTIONAL_METADATA_NODES
932
- ) {
933
- retainedBytes += metrics.bytes;
934
- retainedNodes += metrics.nodes;
935
- return value;
936
- }
937
- const correlation = taskResult ? taskCorrelationDetails(record.details) : undefined;
938
- const { details: _details, ...safe } = record;
939
- return correlation === undefined ? safe : { ...safe, details: correlation };
940
- };
941
- }
942
-
943
- const OmpOptionalMetadataSchema = z.preprocess(
944
- (value) => (optionalMetadataIsBounded(value) ? value : undefined),
945
- z.unknown().optional(),
946
- );
947
-
948
- const OmpContentPartSchema = z
949
- .object({
950
- type: NAME,
951
- text: TEXT.optional(),
952
- thinking: TEXT.optional(),
953
- data: boundedString(MAX_IMAGE_DATA_LENGTH).optional(),
954
- mimeType: boundedString(128).optional(),
955
- id: IDENTIFIER.optional(),
956
- name: NAME.optional(),
957
- arguments: z
958
- .unknown()
959
- .refine((value) => isBoundedJson(value, MAX_TOOL_PAYLOAD_LENGTH, 1_024, 4_096))
960
- .optional(),
961
- })
962
- .superRefine((part, context) => {
963
- if (part.type === "toolCall" && (!part.id || !part.name || part.arguments === undefined)) {
964
- context.addIssue({ code: "custom", message: "invalid tool call payload" });
965
- return;
966
- }
967
- if (part.type !== "image") return;
968
- if (
969
- part.data === undefined ||
970
- part.mimeType === undefined ||
971
- !isValidImagePayload(part.data, part.mimeType, MAX_IMAGE_DATA_LENGTH)
972
- ) {
973
- context.addIssue({ code: "custom", message: "invalid image payload" });
974
- }
975
- });
976
- const OmpAssistantContentPartSchema = OmpContentPartSchema.safeExtend({
977
- text: RAW_DISPLAY_TEXT.optional(),
978
- thinking: RAW_DISPLAY_TEXT.optional(),
979
- });
980
- const OmpAssistantDisplayContentSchema = z.preprocess(
981
- boundRawDisplayContent,
982
- z.union([RAW_DISPLAY_TEXT, z.array(OmpAssistantContentPartSchema).max(OMP_MAX_CONTENT_PARTS)]),
983
- );
984
- const OmpDisplayContentSchema = z.union([
985
- TEXT,
986
- z.array(OmpContentPartSchema).max(OMP_MAX_CONTENT_PARTS),
987
- ]);
988
- const OmpImageArraySchema = z
989
- .array(OmpContentPartSchema)
990
- .max(OMP_MAX_CONTENT_PARTS)
991
- .superRefine((parts, context) => {
992
- if (parts.some((part) => part.type !== "image")) {
993
- context.addIssue({ code: "custom", message: "invalid image collection" });
994
- }
995
- });
996
- const OmpMessageIdentityShape = {
997
- id: IDENTIFIER.optional(),
998
- entryId: IDENTIFIER.optional(),
999
- responseId: IDENTIFIER.optional(),
1000
- images: OmpImageArraySchema.optional(),
1001
- timestamp: z.number().finite().optional(),
1002
- details: OmpOptionalMetadataSchema,
1003
- };
1004
- const OmpOpaqueMessagePayloadSchema = z
1005
- .unknown()
1006
- .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 8_192));
1007
- const OmpExecutionOutputSchema = z.preprocess(
1008
- (value) =>
1009
- typeof value === "string" && utf8Bytes(value) > MAX_IMAGE_DATA_LENGTH
1010
- ? DISPLAY_TRUNCATION_MARKER
1011
- : value,
1012
- RAW_DISPLAY_TEXT.optional(),
1013
- );
1014
- const OmpExecutionMessageShape = {
1015
- output: OmpExecutionOutputSchema,
1016
- exitCode: z.number().int().nullable().optional(),
1017
- cancelled: z.boolean().optional(),
1018
- truncated: z.boolean().optional(),
1019
- excludeFromContext: z.boolean().optional(),
1020
- meta: OmpOptionalMetadataSchema,
1021
- };
1022
- const OmpCustomMessageShape = {
1023
- customType: NAME.optional(),
1024
- content: OmpDisplayContentSchema.optional(),
1025
- display: z.boolean().optional(),
1026
- attribution: OmpOpaqueMessagePayloadSchema.optional(),
1027
- };
1028
- const OmpFileMentionSchema = z
1029
- .object({
1030
- path: boundedString(MAX_PATH_LENGTH),
1031
- content: TEXT,
1032
- lineCount: z.number().int().nonnegative().safe().optional(),
1033
- byteSize: z.number().int().nonnegative().safe().optional(),
1034
- skippedReason: z.enum(["tooLarge", "binary"]).optional(),
1035
- image: OmpContentPartSchema.optional(),
1036
- })
1037
- .superRefine((file, context) => {
1038
- if (file.image && file.image.type !== "image") {
1039
- context.addIssue({ code: "custom", message: "invalid file mention image" });
1040
- }
1041
- });
1042
-
1043
- const OmpMessageSchema = z.discriminatedUnion("role", [
1044
- z.object({
1045
- role: z.literal("assistant"),
1046
- content: OmpAssistantDisplayContentSchema.optional(),
1047
- ...OmpMessageIdentityShape,
1048
- errorMessage: boundedString(4_096).nullable().optional(),
1049
- stopReason: boundedString(64).optional(),
1050
- }),
1051
- z.object({
1052
- role: z.literal("user"),
1053
- content: OmpDisplayContentSchema,
1054
- ...OmpMessageIdentityShape,
1055
- }),
1056
- z.object({
1057
- role: z.literal("developer"),
1058
- content: OmpDisplayContentSchema,
1059
- attribution: OmpOpaqueMessagePayloadSchema.optional(),
1060
- synthetic: z.boolean().optional(),
1061
- userInitiated: z.boolean().optional(),
1062
- providerPayload: OmpOpaqueMessagePayloadSchema.optional(),
1063
- ...OmpMessageIdentityShape,
1064
- }),
1065
- z.object({
1066
- role: z.literal("toolResult"),
1067
- toolCallId: IDENTIFIER,
1068
- toolName: NAME,
1069
- content: OmpOpaqueMessagePayloadSchema,
1070
- isError: z.boolean().optional(),
1071
- attribution: OmpOpaqueMessagePayloadSchema.optional(),
1072
- prunedAt: z.number().finite().nonnegative().optional(),
1073
- providerMetadata: OmpOpaqueMessagePayloadSchema.optional(),
1074
- useless: z.boolean().optional(),
1075
- ...OmpMessageIdentityShape,
1076
- }),
1077
- z.object({
1078
- role: z.literal("bashExecution"),
1079
- command: TEXT,
1080
- ...OmpExecutionMessageShape,
1081
- ...OmpMessageIdentityShape,
1082
- }),
1083
- z.object({
1084
- role: z.literal("pythonExecution"),
1085
- code: TEXT,
1086
- ...OmpExecutionMessageShape,
1087
- ...OmpMessageIdentityShape,
1088
- }),
1089
- z.object({ role: z.literal("custom"), ...OmpCustomMessageShape, ...OmpMessageIdentityShape }),
1090
- z.object({
1091
- role: z.literal("hookMessage"),
1092
- ...OmpCustomMessageShape,
1093
- ...OmpMessageIdentityShape,
1094
- }),
1095
- z.object({
1096
- role: z.literal("branchSummary"),
1097
- summary: TEXT,
1098
- fromId: IDENTIFIER,
1099
- ...OmpMessageIdentityShape,
1100
- }),
1101
- z.object({
1102
- role: z.literal("compactionSummary"),
1103
- summary: TEXT,
1104
- shortSummary: TEXT.optional(),
1105
- tokensBefore: z.number().finite().nonnegative(),
1106
- tokensAfter: z.number().finite().nonnegative().optional(),
1107
- method: NAME.optional(),
1108
- providerPayload: OmpOpaqueMessagePayloadSchema.optional(),
1109
- blocks: z.array(OmpContentPartSchema).max(OMP_MAX_CONTENT_PARTS).optional(),
1110
- warning: boundedString(64 * 1024).optional(),
1111
- ...OmpMessageIdentityShape,
1112
- }),
1113
- z.object({
1114
- role: z.literal("fileMention"),
1115
- files: z.array(OmpFileMentionSchema).max(MAX_ARRAY_ITEMS),
1116
- ...OmpMessageIdentityShape,
1117
- }),
1118
- ]);
1119
- export type OmpMessage = z.infer<typeof OmpMessageSchema>;
1120
-
1121
- const OmpAssistantMessageEventSchema = z
1122
- .object({
1123
- type: NAME,
1124
- contentIndex: z
1125
- .number()
1126
- .int()
1127
- .nonnegative()
1128
- .max(OMP_MAX_CONTENT_PARTS - 1)
1129
- .optional(),
1130
- delta: TEXT.optional(),
1131
- content: z
1132
- .unknown()
1133
- .refine((value) => isBoundedJson(value, MAX_IMAGE_DATA_LENGTH + 1_024))
1134
- .optional(),
1135
- })
1136
- .superRefine((event, context) => {
1137
- const content = event.content;
1138
- const imageLike =
1139
- content !== null &&
1140
- typeof content === "object" &&
1141
- !Array.isArray(content) &&
1142
- "type" in content &&
1143
- content.type === "image";
1144
- if (!event.type.startsWith("image_") && !imageLike) return;
1145
- const image = OmpContentPartSchema.safeParse(content);
1146
- if (!image.success || image.data.type !== "image") {
1147
- context.addIssue({ code: "custom", message: "invalid image event" });
1148
- }
1149
- });
1150
-
1151
- const OmpAvailableCommandSchema = z.object({
1152
- name: NAME,
1153
- aliases: z.array(NAME).max(32).optional(),
1154
- description: boundedString(4_096).optional(),
1155
- input: z
1156
- .object({ hint: boundedString(1_024).optional() })
1157
- .nullable()
1158
- .optional(),
1159
- subcommands: z
1160
- .array(
1161
- z.object({
1162
- name: NAME,
1163
- description: boundedString(4_096).optional(),
1164
- usage: boundedString(1_024).optional(),
1165
- }),
1166
- )
1167
- .max(128)
1168
- .optional(),
1169
- source: boundedString(64).optional(),
1170
- });
1171
- const OmpThinkingMetadataSchema = z
1172
- .unknown()
1173
- .transform((value): { efforts?: string[]; defaultLevel?: string } | undefined => {
1174
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
1175
- const record = value as Record<string, unknown>;
1176
- const efforts = boundedOptionalStringArray(16, 32).parse(record.efforts);
1177
- const defaultLevel = boundedString(32).optional().catch(undefined).parse(record.defaultLevel);
1178
- return {
1179
- ...(efforts !== undefined ? { efforts } : {}),
1180
- ...(defaultLevel !== undefined ? { defaultLevel } : {}),
1181
- };
1182
- });
1183
- const OmpModelSchema = z.object({
1184
- provider: OMP_PROVIDER_NAME,
1185
- id: NAME,
1186
- name: boundedString(MAX_NAME_LENGTH).optional().catch(undefined),
1187
- reasoning: z.boolean().optional().catch(undefined),
1188
- thinking: OmpThinkingMetadataSchema.optional(),
1189
- input: boundedOptionalStringArray(16, MAX_NAME_LENGTH).optional(),
1190
- contextWindow: z
1191
- .number()
1192
- .int()
1193
- .nonnegative()
1194
- .max(100_000_000)
1195
- .nullable()
1196
- .optional()
1197
- .catch(undefined),
1198
- });
1199
- const TokenCountSchema = z.number().int().nonnegative().max(MAX_TOKEN_COUNT);
1200
- const OptionalTokenCountSchema = TokenCountSchema.nullable().optional();
1201
- const OptionalCostSchema = z
1202
- .number()
1203
- .finite()
1204
- .nonnegative()
1205
- .max(MAX_COST_USD)
1206
- .nullable()
1207
- .optional();
1208
- const OmpContextUsageSchema = z.object({
1209
- tokens: OptionalTokenCountSchema,
1210
- contextWindow: TokenCountSchema.max(100_000_000).nullable().optional(),
1211
- percent: z.number().finite().nonnegative().max(MAX_CONTEXT_PERCENT).nullable().optional(),
1212
- });
1213
- const OmpSessionStatsSchema = z.object({
1214
- userMessages: OptionalTokenCountSchema,
1215
- assistantMessages: OptionalTokenCountSchema,
1216
- toolCalls: OptionalTokenCountSchema,
1217
- toolResults: OptionalTokenCountSchema,
1218
- totalMessages: OptionalTokenCountSchema,
1219
- tokens: z
1220
- .object({
1221
- input: OptionalTokenCountSchema,
1222
- output: OptionalTokenCountSchema,
1223
- reasoning: OptionalTokenCountSchema,
1224
- cacheRead: OptionalTokenCountSchema,
1225
- cacheWrite: OptionalTokenCountSchema,
1226
- total: OptionalTokenCountSchema,
1227
- })
1228
- .nullable()
1229
- .optional(),
1230
- cost: OptionalCostSchema,
1231
- premiumRequests: OptionalTokenCountSchema,
1232
- credits: z
1233
- .object({
1234
- cost: OptionalCostSchema,
1235
- committedCost: OptionalCostSchema,
1236
- acuCost: OptionalCostSchema,
1237
- })
1238
- .nullable()
1239
- .optional(),
1240
- routedModels: z.record(NAME, OptionalTokenCountSchema).nullable().optional(),
1241
- contextUsage: OmpContextUsageSchema.nullable().optional(),
1242
- });
1243
- const OmpCompactionResultSchema = z.object({
1244
- tokensBefore: OptionalTokenCountSchema,
1245
- preTokens: OptionalTokenCountSchema,
1246
- });
1247
- const OmpSessionStateSchema = z.object({
1248
- model: OmpModelSchema.nullable().optional(),
1249
- thinkingLevel: OmpCurrentThinkingLevelSchema,
1250
- isStreaming: z.boolean(),
1251
- isCompacting: z.boolean(),
1252
- sessionId: IDENTIFIER,
1253
- autoCompactionEnabled: z.boolean().optional(),
1254
- contextUsage: OmpContextUsageSchema.nullable().optional().catch(undefined),
1255
- sessionFile: boundedString(MAX_PATH_LENGTH).optional().catch(undefined),
1256
- });
1257
- const OmpReadyFrameSchema = z.object({
1258
- type: z.literal("ready"),
1259
- protocolVersion: z.number().int().positive().max(16).optional(),
1260
- supportedProtocolVersions: z.array(z.number().int().positive().max(16)).max(8).optional(),
1261
- maxFrameBytes: z.number().int().positive().optional(),
1262
- maxReassembledFrameBytes: z.number().int().positive().optional(),
1263
- features: z.record(z.string(), z.unknown()).optional(),
1264
- });
1265
- const OmpResponseFrameSchema = z.object({
1266
- type: z.literal("response"),
1267
- id: IDENTIFIER,
1268
- command: boundedString(64, 1).optional(),
1269
- success: z.boolean(),
1270
- data: z.unknown().optional(),
1271
- error: boundedString(MAX_RPC_ERROR_BYTES).optional(),
1272
- code: boundedString(MAX_RPC_ERROR_CODE_BYTES, 1).optional(),
1273
- });
1274
- const OmpChunkFrameSchema = z.object({
1275
- type: z.literal("rpc_chunk"),
1276
- chunkId: IDENTIFIER,
1277
- index: z.number().int().nonnegative(),
1278
- count: z.number().int().positive().max(MAX_CHUNK_COUNT),
1279
- byteLength: z.number().int().nonnegative().max(MAX_REASSEMBLED_FRAME_BYTES),
1280
- data: boundedString(MAX_ENCODED_CHUNK_BYTES),
1281
- });
1282
- const JsonObjectSchema = z.record(z.string(), z.unknown());
1283
- const BoundedToolPayloadSchema = z
1284
- .unknown()
1285
- .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096));
1286
- const OmpToolResultPayloadSchema = z.preprocess(
1287
- omitUnsafeOptionalDetails,
1288
- z
1289
- .unknown()
1290
- .refine(
1291
- (value) =>
1292
- isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, MAX_OPTIONAL_METADATA_ITEMS, 8_192) &&
1293
- isBoundedJson(omitOptionalDetails(value), MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096),
1294
- ),
1295
- );
1296
- const OmpHostToolDefinitionSchema = z.object({
1297
- name: NAME,
1298
- label: NAME.optional(),
1299
- description: boundedString(MAX_TEXT_LENGTH),
1300
- loadMode: z.enum(["essential", "discoverable"]).optional(),
1301
- parameters: JsonObjectSchema,
1302
- });
1303
- const OmpHostToolCallSchema = z.object({
1304
- type: z.literal("host_tool_call"),
1305
- id: IDENTIFIER,
1306
- toolCallId: IDENTIFIER,
1307
- toolName: NAME,
1308
- arguments: JsonObjectSchema,
1309
- });
1310
- const OmpHostToolCancelSchema = z.object({
1311
- type: z.literal("host_tool_cancel"),
1312
- id: IDENTIFIER,
1313
- targetId: IDENTIFIER,
1314
- });
1315
- const OmpHostToolContentSchema = z.object({ type: NAME, text: TEXT.optional() }).passthrough();
1316
- const OmpHostToolAgentResultSchema = z.object({
1317
- content: z.array(OmpHostToolContentSchema).max(MAX_ARRAY_ITEMS),
1318
- details: BoundedToolPayloadSchema.optional(),
1319
- isError: z.boolean().optional(),
1320
- });
1321
- const OmpHostToolResultSchema = z.object({
1322
- type: z.literal("host_tool_result"),
1323
- id: IDENTIFIER,
1324
- result: OmpHostToolAgentResultSchema,
1325
- isError: z.boolean().optional(),
1326
- });
1327
- const OmpHostToolUpdateSchema = z.object({
1328
- type: z.literal("host_tool_update"),
1329
- id: IDENTIFIER,
1330
- partialResult: OmpHostToolAgentResultSchema,
1331
- });
1332
- const OmpToolApprovalIdentitySchema = z.discriminatedUnion("kind", [
1333
- z.object({ kind: z.literal("shell"), command: boundedJsonString(24 * 1024, 1) }).strict(),
1334
- z
1335
- .object({
1336
- kind: z.literal("edit"),
1337
- paths: z.array(boundedJsonString(MAX_TOOL_APPROVAL_PATH_BYTES, 1)).min(1).max(16),
1338
- content: boundedJsonString(MAX_TOOL_APPROVAL_CONTENT_BYTES),
1339
- })
1340
- .strict(),
1341
- z
1342
- .object({
1343
- kind: z.literal("write"),
1344
- path: boundedJsonString(MAX_TOOL_APPROVAL_PATH_BYTES, 1),
1345
- content: boundedJsonString(MAX_TOOL_APPROVAL_CONTENT_BYTES),
1346
- })
1347
- .strict(),
1348
- z.object({ kind: z.literal("other") }).strict(),
1349
- ]);
1350
- type OmpToolApprovalValue =
1351
- | string
1352
- | number
1353
- | boolean
1354
- | null
1355
- | OmpToolApprovalValue[]
1356
- | { [key: string]: OmpToolApprovalValue };
1357
- const OmpToolApprovalValueSchema: z.ZodType<OmpToolApprovalValue> = z.lazy(() =>
1358
- z.union([
1359
- boundedString(MAX_TOOL_APPROVAL_STRING_BYTES),
1360
- z.number().finite(),
1361
- z.boolean(),
1362
- z.null(),
1363
- z.array(OmpToolApprovalValueSchema).max(MAX_TOOL_APPROVAL_COLLECTION_ITEMS),
1364
- z.record(boundedString(128, 1), OmpToolApprovalValueSchema),
1365
- ]),
1366
- );
1367
- function approvalInputWithinBounds(value: unknown): boolean {
1368
- const pending: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }];
1369
- let nodes = 0;
1370
- while (pending.length > 0) {
1371
- const current = pending.pop();
1372
- if (!current) break;
1373
- nodes += 1;
1374
- if (nodes > MAX_TOOL_APPROVAL_INPUT_NODES) return false;
1375
- if (current.value === null || typeof current.value !== "object") continue;
1376
- if (current.depth >= MAX_TOOL_APPROVAL_DEPTH) return false;
1377
- for (const child of Array.isArray(current.value)
1378
- ? current.value
1379
- : Object.values(current.value as Record<string, unknown>)) {
1380
- pending.push({ value: child, depth: current.depth + 1 });
1381
- }
1382
- }
1383
- return true;
1384
- }
1385
- const OmpToolApprovalRequestSchema = z
1386
- .object({
1387
- type: z.literal("tool_approval_request"),
1388
- id: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
1389
- toolCallId: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
1390
- toolKind: z.enum(["shell", "edit", "write", "other"]),
1391
- toolName: boundedString(MAX_TOOL_APPROVAL_NAME_BYTES, 1),
1392
- tier: z.enum(["read", "write", "exec"]),
1393
- identity: OmpToolApprovalIdentitySchema,
1394
- input: z
1395
- .record(boundedString(128, 1), OmpToolApprovalValueSchema)
1396
- .refine(approvalInputWithinBounds),
1397
- detail: z
1398
- .object({
1399
- lines: z
1400
- .array(boundedString(MAX_TOOL_APPROVAL_DETAIL_BYTES))
1401
- .max(MAX_TOOL_APPROVAL_DETAIL_LINES),
1402
- truncated: z.boolean(),
1403
- truncatedFields: z
1404
- .array(boundedString(MAX_TOOL_APPROVAL_METADATA_FIELD_BYTES))
1405
- .max(MAX_TOOL_APPROVAL_METADATA_FIELDS),
1406
- redacted: z.boolean(),
1407
- redactedFields: z
1408
- .array(boundedString(MAX_TOOL_APPROVAL_METADATA_FIELD_BYTES))
1409
- .max(MAX_TOOL_APPROVAL_METADATA_FIELDS),
1410
- reason: boundedString(MAX_TOOL_APPROVAL_DETAIL_BYTES).optional(),
1411
- providerSafetyChecks: z
1412
- .array(boundedString(MAX_TOOL_APPROVAL_DETAIL_BYTES))
1413
- .max(MAX_TOOL_APPROVAL_DETAIL_LINES)
1414
- .optional(),
1415
- })
1416
- .strict(),
1417
- timeout: z.number().finite().nonnegative().max(MAX_TOOL_APPROVAL_TIMEOUT_MS).optional(),
1418
- })
1419
- .strict()
1420
- .superRefine((request, context) => {
1421
- if (request.toolKind !== request.identity.kind) {
1422
- context.addIssue({ code: "custom", message: "tool approval identity kind mismatch" });
1423
- }
1424
- if (Buffer.byteLength(JSON.stringify(request), "utf8") + 1 > MAX_TOOL_APPROVAL_FRAME_BYTES) {
1425
- context.addIssue({ code: "custom", message: "tool approval request exceeds bounds" });
1426
- }
1427
- });
1428
- const OmpToolApprovalCancelSchema = z
1429
- .object({
1430
- type: z.literal("tool_approval_cancel"),
1431
- id: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
1432
- targetId: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
1433
- toolCallId: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
1434
- })
1435
- .strict();
1436
- const OmpToolApprovalResponseSchema = z.union([
1437
- z
1438
- .object({
1439
- type: z.literal("tool_approval_response"),
1440
- id: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
1441
- toolCallId: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
1442
- approved: z.boolean(),
1443
- })
1444
- .strict(),
1445
- z
1446
- .object({
1447
- type: z.literal("tool_approval_response"),
1448
- id: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
1449
- toolCallId: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
1450
- cancelled: z.literal(true),
1451
- timedOut: z.boolean().optional(),
1452
- })
1453
- .strict(),
1454
- ]);
1455
- const OmpAgentEndEnvelopeSchema = z.object({
1456
- type: z.literal("agent_end"),
1457
- requestId: IDENTIFIER.optional(),
1458
- messageCount: z.number().int().nonnegative().optional(),
1459
- isTerminal: z.boolean().optional(),
1460
- });
1461
- const OmpCompactionStartSchema = z.object({
1462
- type: z.literal("compaction_start"),
1463
- reason: boundedString(4_096).optional(),
1464
- });
1465
- const OmpCompactionEndSchema = z.object({
1466
- type: z.literal("compaction_end"),
1467
- reason: boundedString(4_096).optional(),
1468
- result: BoundedToolPayloadSchema.optional(),
1469
- aborted: z.boolean().optional(),
1470
- willRetry: z.boolean().optional(),
1471
- errorMessage: boundedString(4_096).optional(),
1472
- skipped: z.boolean().optional(),
1473
- });
1474
- const OmpCoreAgentEventSchema = z.discriminatedUnion("type", [
1475
- z.object({ type: z.literal("agent_start") }),
1476
- z.object({
1477
- type: z.literal("agent_end"),
1478
- requestId: IDENTIFIER.optional(),
1479
- messages: z.array(OmpMessageSchema).max(MAX_ARRAY_ITEMS).optional(),
1480
- messageCount: z.number().int().nonnegative().optional(),
1481
- isTerminal: z.boolean().optional(),
1482
- }),
1483
- z.object({ type: z.literal("turn_start") }),
1484
- z.object({ type: z.literal("turn_end") }),
1485
- z.object({ type: z.literal("message_start"), message: OmpMessageSchema }),
1486
- z.object({
1487
- type: z.literal("message_update"),
1488
- message: OmpMessageSchema,
1489
- assistantMessageEvent: OmpAssistantMessageEventSchema.optional(),
1490
- }),
1491
- z.object({ type: z.literal("message_end"), message: OmpMessageSchema }),
1492
- z.object({
1493
- type: z.literal("tool_execution_start"),
1494
- toolCallId: IDENTIFIER,
1495
- toolName: NAME,
1496
- args: BoundedToolPayloadSchema,
1497
- }),
1498
- z.object({
1499
- type: z.literal("tool_execution_update"),
1500
- toolCallId: IDENTIFIER,
1501
- toolName: NAME,
1502
- args: BoundedToolPayloadSchema.optional(),
1503
- partialResult: OmpToolResultPayloadSchema,
1504
- }),
1505
- z.object({
1506
- type: z.literal("tool_stream_update"),
1507
- toolCallId: IDENTIFIER,
1508
- toolName: NAME,
1509
- update: BoundedToolPayloadSchema,
1510
- }),
1511
- z.object({
1512
- type: z.literal("tool_execution_end"),
1513
- toolCallId: IDENTIFIER,
1514
- toolName: NAME,
1515
- result: OmpToolResultPayloadSchema,
1516
- isError: z.boolean().optional(),
1517
- }),
1518
- OmpCompactionStartSchema,
1519
- OmpCompactionEndSchema,
1520
- ]);
1521
- const OmpGoalSchema = z.object({
1522
- id: IDENTIFIER.optional(),
1523
- objective: TEXT.optional(),
1524
- status: boundedString(256).optional(),
1525
- tokenBudget: z.number().finite().nonnegative().optional(),
1526
- tokensUsed: z.number().finite().nonnegative().optional(),
1527
- timeUsedSeconds: z.number().finite().nonnegative().optional(),
1528
- createdAt: boundedString(128).optional(),
1529
- updatedAt: boundedString(128).optional(),
1530
- });
1531
- const OmpGoalModeStateSchema = z.object({
1532
- enabled: z.boolean().optional(),
1533
- mode: boundedString(256).optional(),
1534
- reason: boundedString(4_096).optional(),
1535
- goal: OmpGoalSchema.optional(),
1536
- });
1537
- const OmpTodoReminderEventSchema = z.object({
1538
- type: z.literal("todo_reminder"),
1539
- todos: z
1540
- .array(
1541
- z.object({
1542
- id: IDENTIFIER.optional(),
1543
- content: boundedString(16_384),
1544
- status: z.enum(["pending", "in_progress", "blocked", "completed", "abandoned"]),
1545
- }),
1546
- )
1547
- .max(MAX_TODOS),
1548
- attempt: z.number().int().nonnegative().safe().optional(),
1549
- maxAttempts: z.number().int().nonnegative().safe().optional(),
1550
- });
1551
- const OmpThinkingLevelChangedEventSchema = z.object({
1552
- type: z.literal("thinking_level_changed"),
1553
- thinkingLevel: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
1554
- configured: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
1555
- resolved: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
1556
- });
1557
- const OmpAutoRetryStartEventSchema = z.object({
1558
- type: z.literal("auto_retry_start"),
1559
- attempt: z.number().int().nonnegative().safe(),
1560
- maxAttempts: z.number().int().positive().safe(),
1561
- delayMs: z.number().int().nonnegative().safe(),
1562
- errorMessage: boundedString(64 * 1024),
1563
- errorId: z.number().int().safe().optional(),
1564
- });
1565
- const OmpAutoRetryEndEventSchema = z.object({
1566
- type: z.literal("auto_retry_end"),
1567
- success: z.boolean(),
1568
- attempt: z.number().int().nonnegative().safe(),
1569
- finalError: boundedString(64 * 1024).optional(),
1570
- retryErrors: BoundedToolPayloadSchema.optional(),
1571
- recoveredErrors: BoundedToolPayloadSchema.optional(),
1572
- });
1573
- const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
1574
- ...OmpCoreAgentEventSchema.options,
1575
- z.object({ type: z.literal("config_warnings_changed") }),
1576
- z.object({ type: z.literal("advisor_cost_changed") }),
1577
- z.object({ type: z.literal("advisor_yielded") }),
1578
- z.object({
1579
- type: z.literal("ttsr_triggered"),
1580
- rules: z.array(BoundedToolPayloadSchema).max(MAX_ARRAY_ITEMS),
1581
- }),
1582
- OmpTodoReminderEventSchema,
1583
- z.object({ type: z.literal("todo_auto_clear") }),
1584
- z.object({
1585
- type: z.literal("irc_message"),
1586
- message: OmpMessageSchema.refine((message) => message.role === "custom"),
1587
- }),
1588
- z.object({ type: z.literal("model_changed") }),
1589
- OmpThinkingLevelChangedEventSchema,
1590
- z.object({
1591
- type: z.literal("goal_updated"),
1592
- goal: OmpGoalSchema.nullable().optional(),
1593
- state: OmpGoalModeStateSchema.optional(),
1594
- }),
1595
- OmpAutoRetryStartEventSchema,
1596
- OmpAutoRetryEndEventSchema,
1597
- z.object({
1598
- type: z.literal("retry_fallback_applied"),
1599
- from: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1600
- to: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1601
- role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1602
- }),
1603
- z.object({
1604
- type: z.literal("retry_fallback_succeeded"),
1605
- model: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1606
- role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1607
- }),
1608
- z.object({
1609
- type: z.literal("auto_compaction_start"),
1610
- reason: boundedString(4_096),
1611
- action: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
1612
- }),
1613
- z.object({
1614
- type: z.literal("auto_compaction_end"),
1615
- action: NAME.optional(),
1616
- result: OmpCompactionResultSchema.nullable().optional(),
1617
- aborted: z.boolean().optional(),
1618
- willRetry: z.boolean().optional(),
1619
- errorMessage: boundedString(64 * 1024).optional(),
1620
- skipped: z.boolean().optional(),
1621
- }),
1622
- z.object({
1623
- type: z.literal("notice"),
1624
- id: IDENTIFIER.optional(),
1625
- level: z.enum(["info", "warning", "error"]),
1626
- message: boundedString(64 * 1024),
1627
- source: boundedString(MAX_NAME_LENGTH).optional(),
1628
- }),
1629
- ]);
1630
- const OmpSubagentStatusSchema = z.enum([
1631
- "pending",
1632
- "running",
1633
- "started",
1634
- "completed",
1635
- "failed",
1636
- "aborted",
1637
- ]);
1638
- const OmpSubagentLifecyclePayloadSchema = z.object({
1639
- id: IDENTIFIER,
1640
- agent: NAME,
1641
- agentSource: NAME.optional(),
1642
- description: boundedString(64 * 1024).optional(),
1643
- status: OmpSubagentStatusSchema,
1644
- sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
1645
- parentToolCallId: IDENTIFIER.optional(),
1646
- index: z.number().int().nonnegative().max(10_000),
1647
- detached: z.boolean().optional(),
1648
- });
1649
- const OmpSubagentProgressSchema = z.object({
1650
- id: IDENTIFIER,
1651
- status: OmpSubagentStatusSchema,
1652
- description: boundedString(64 * 1024).optional(),
1653
- currentTool: BoundedToolPayloadSchema.optional(),
1654
- recentTools: z.array(BoundedToolPayloadSchema).max(64).optional(),
1655
- recentOutput: z.array(BoundedToolPayloadSchema).max(128).optional(),
1656
- resolvedModel: NAME.optional(),
1657
- });
1658
- const OmpSubagentProgressPayloadSchema = z.object({
1659
- index: z.number().int().nonnegative().max(10_000),
1660
- agent: NAME,
1661
- agentSource: NAME.optional(),
1662
- task: TEXT,
1663
- parentToolCallId: IDENTIFIER.optional(),
1664
- assignment: TEXT.optional(),
1665
- progress: OmpSubagentProgressSchema,
1666
- sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
1667
- detached: z.boolean().optional(),
1668
- });
1669
- const ExtensionUiBase = { type: z.literal("extension_ui_request"), id: IDENTIFIER };
1670
- const OmpExtensionUiRequestSchema = z.discriminatedUnion("method", [
1671
- z
1672
- .object({
1673
- ...ExtensionUiBase,
1674
- method: z.literal("select"),
1675
- title: boundedString(4_096),
1676
- options: z.array(boundedString(4_096)).min(1).max(128),
1677
- optionDetails: z
1678
- .array(z.object({ description: boundedString(16_384).optional() }).strict())
1679
- .max(128)
1680
- .optional(),
1681
- timeout: z.number().nonnegative().finite().optional(),
1682
- })
1683
- .strict()
1684
- .superRefine((request, context) => {
1685
- if (request.optionDetails && request.optionDetails.length !== request.options.length) {
1686
- context.addIssue({ code: "custom", message: "invalid extension UI request" });
1687
- }
1688
- }),
1689
- z
1690
- .object({
1691
- ...ExtensionUiBase,
1692
- method: z.literal("confirm"),
1693
- title: boundedString(4_096),
1694
- message: boundedString(64 * 1024),
1695
- timeout: z.number().nonnegative().finite().optional(),
1696
- })
1697
- .strict(),
1698
- z
1699
- .object({
1700
- ...ExtensionUiBase,
1701
- method: z.literal("input"),
1702
- title: boundedString(4_096),
1703
- placeholder: boundedString(4_096).optional(),
1704
- prefill: TEXT.optional(),
1705
- timeout: z.number().nonnegative().finite().optional(),
1706
- })
1707
- .strict(),
1708
- z
1709
- .object({
1710
- ...ExtensionUiBase,
1711
- method: z.literal("editor"),
1712
- title: boundedString(4_096),
1713
- prefill: TEXT.optional(),
1714
- promptStyle: z.boolean().optional(),
1715
- timeout: z.number().nonnegative().finite().optional(),
1716
- })
1717
- .strict(),
1718
- z.object({ ...ExtensionUiBase, method: z.literal("cancel"), targetId: IDENTIFIER }).strict(),
1719
- z
1720
- .object({
1721
- ...ExtensionUiBase,
1722
- method: z.literal("notify"),
1723
- message: boundedString(64 * 1024),
1724
- notifyType: z.enum(["info", "warning", "error"]).optional(),
1725
- })
1726
- .strict(),
1727
- z
1728
- .object({
1729
- ...ExtensionUiBase,
1730
- method: z.literal("setStatus"),
1731
- statusKey: NAME,
1732
- statusText: boundedString(16_384).optional(),
1733
- })
1734
- .strict(),
1735
- z
1736
- .object({
1737
- ...ExtensionUiBase,
1738
- method: z.literal("setWidget"),
1739
- widgetKey: NAME,
1740
- widgetLines: z.array(boundedString(16_384)).max(128).optional(),
1741
- widgetPlacement: z.enum(["aboveEditor", "belowEditor"]).optional(),
1742
- })
1743
- .strict(),
1744
- z
1745
- .object({ ...ExtensionUiBase, method: z.literal("setTitle"), title: boundedString(4_096) })
1746
- .strict(),
1747
- z.object({ ...ExtensionUiBase, method: z.literal("set_editor_text"), text: TEXT }).strict(),
1748
- z
1749
- .object({
1750
- ...ExtensionUiBase,
1751
- method: z.literal("open_url"),
1752
- url: boundedString(16_384),
1753
- launchUrl: boundedString(16_384).optional(),
1754
- instructions: boundedString(64 * 1024).optional(),
1755
- })
1756
- .strict(),
1757
- ]);
1758
- const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
1759
- ...OmpAgentSessionEventSchema.options,
1760
- z.object({
1761
- type: z.literal("subagent_lifecycle"),
1762
- payload: OmpSubagentLifecyclePayloadSchema,
1763
- }),
1764
- z.object({
1765
- type: z.literal("subagent_progress"),
1766
- payload: OmpSubagentProgressPayloadSchema,
1767
- }),
1768
- z.object({
1769
- type: z.literal("subagent_event"),
1770
- payload: z.object({ id: IDENTIFIER, event: OmpAgentSessionEventSchema }),
1771
- }),
1772
- z.object({
1773
- type: z.literal("available_commands_update"),
1774
- commands: z.array(OmpAvailableCommandSchema).max(MAX_ARRAY_ITEMS),
1775
- }),
1776
- z.object({ type: z.literal("command_output"), text: TEXT.optional() }),
1777
- OmpExtensionUiRequestSchema,
1778
- z.object({
1779
- type: z.literal("prompt_result"),
1780
- id: IDENTIFIER.optional(),
1781
- agentInvoked: z.boolean(),
1782
- }),
1783
- OmpHostToolCallSchema,
1784
- OmpHostToolCancelSchema,
1785
- OmpToolApprovalRequestSchema,
1786
- OmpToolApprovalCancelSchema,
1787
- ]);
1788
- type OptionalDetailsMapper = (value: unknown, taskResult?: boolean) => unknown;
1789
-
1790
- function mapRecordField(
1791
- record: Record<string, unknown>,
1792
- key: string,
1793
- map: OptionalDetailsMapper,
1794
- taskResult = false,
1795
- ): Record<string, unknown> {
1796
- if (!Object.hasOwn(record, key)) return record;
1797
- const next = map(record[key], taskResult);
1798
- return next === record[key] ? record : { ...record, [key]: next };
1799
- }
1800
-
1801
- function mapMessageList(value: unknown, map: OptionalDetailsMapper): unknown {
1802
- if (!Array.isArray(value)) return value;
1803
- let changed = false;
1804
- const messages = value.map((message) => {
1805
- const taskResult =
1806
- message !== null &&
1807
- typeof message === "object" &&
1808
- !Array.isArray(message) &&
1809
- (message as Record<string, unknown>).role === "toolResult" &&
1810
- (message as Record<string, unknown>).toolName === "task";
1811
- const next = map(message, taskResult);
1812
- changed ||= next !== message;
1813
- return next;
1814
- });
1815
- return changed ? messages : value;
1816
- }
1817
-
1818
- function mapAgentEventDetails(
1819
- frame: Record<string, unknown>,
1820
- map: OptionalDetailsMapper,
1821
- ): Record<string, unknown> {
1822
- switch (frame.type) {
1823
- case "message_start":
1824
- case "message_update":
1825
- case "message_end": {
1826
- const message = frame.message;
1827
- const taskResult =
1828
- message !== null &&
1829
- typeof message === "object" &&
1830
- !Array.isArray(message) &&
1831
- (message as Record<string, unknown>).role === "toolResult" &&
1832
- (message as Record<string, unknown>).toolName === "task";
1833
- return mapRecordField(frame, "message", map, taskResult);
1834
- }
1835
- case "tool_execution_update":
1836
- return mapRecordField(frame, "partialResult", map, frame.toolName === "task");
1837
- case "tool_execution_end":
1838
- return mapRecordField(frame, "result", map, frame.toolName === "task");
1839
- case "agent_end":
1840
- return mapRecordField(frame, "messages", (messages) => mapMessageList(messages, map));
1841
- case "irc_message":
1842
- return mapRecordField(frame, "message", map);
1843
- default:
1844
- return frame;
1845
- }
1846
- }
1847
-
1848
- function mapRuntimeFrameDetails(
1849
- frame: Record<string, unknown>,
1850
- map: OptionalDetailsMapper,
1851
- ): Record<string, unknown> {
1852
- if (frame.type !== "subagent_event") return mapAgentEventDetails(frame, map);
1853
- if (frame.payload === null || typeof frame.payload !== "object" || Array.isArray(frame.payload)) {
1854
- return frame;
1855
- }
1856
- const payload = frame.payload as Record<string, unknown>;
1857
- if (payload.event === null || typeof payload.event !== "object" || Array.isArray(payload.event)) {
1858
- return frame;
1859
- }
1860
- const event = mapAgentEventDetails(payload.event as Record<string, unknown>, map);
1861
- return event === payload.event ? frame : { ...frame, payload: { ...payload, event } };
1862
- }
1863
-
1864
- function sanitizeMessageListMetadata(value: unknown): unknown {
1865
- return mapMessageList(value, createOptionalMetadataSanitizer());
1866
- }
1867
-
1868
- function sanitizeHistoryResponseData(value: unknown): unknown {
1869
- if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
1870
- return mapRecordField(value as Record<string, unknown>, "messages", sanitizeMessageListMetadata);
1871
- }
1872
-
1873
- function projectSessionStateResponseData(value: unknown): unknown {
1874
- if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
1875
- const source = value as Record<string, unknown>;
1876
- const projected: Record<string, unknown> = {};
1877
- for (const key of [
1878
- "model",
1879
- "thinkingLevel",
1880
- "isStreaming",
1881
- "isCompacting",
1882
- "sessionId",
1883
- "autoCompactionEnabled",
1884
- "contextUsage",
1885
- "sessionFile",
1886
- ] as const) {
1887
- if (Object.hasOwn(source, key)) projected[key] = source[key];
1888
- }
1889
- return projected;
1890
- }
1891
-
1892
- function runtimeFrameCollectionLimit(frame: Record<string, unknown>): number {
1893
- const type = frame.type;
1894
- if (
1895
- type === "message_start" ||
1896
- type === "message_update" ||
1897
- type === "message_end" ||
1898
- type === "agent_end" ||
1899
- type === "subagent_event"
1900
- ) {
1901
- return OMP_MAX_CONTENT_PARTS;
1902
- }
1903
- return 1_024;
1904
- }
1905
- const OmpModelsResultSchema = z.object({
1906
- models: z.array(OmpModelSchema).min(1).max(MAX_MODEL_CATALOG_ITEMS),
1907
- });
1908
- const OmpPromptAckSchema = z.object({ agentInvoked: z.boolean().optional() }).optional();
1909
- const OmpAvailableCommandsResultSchema = z.object({
1910
- commands: z.array(OmpAvailableCommandSchema).max(MAX_ARRAY_ITEMS),
1911
- });
1912
- const OmpBranchMessagesResultSchema = z.object({
1913
- messages: z.array(z.object({ entryId: IDENTIFIER, text: TEXT })).max(1_024),
1914
- });
1915
- const OmpBranchResultSchema = z.object({ text: TEXT, cancelled: z.boolean() });
1916
- const OmpMessagesResultSchema = z.object({
1917
- messages: z.array(OmpMessageSchema).max(MAX_REPLAY_MESSAGES),
1918
- });
1919
- const OmpMessagesPageResultSchema = z.object({
1920
- messages: z.array(OmpMessageSchema).max(OMP_MESSAGE_PAGE_LIMIT),
1921
- nextCursor: boundedString(2_048, 1).refine(isSafeOpaqueCursor).optional(),
1922
- totalMessages: z.number().int().nonnegative().max(MAX_REPLAY_MESSAGES),
1923
- });
1924
- const OmpSubagentsResultSchema = z.object({
1925
- subagents: z
1926
- .array(
1927
- z.object({
1928
- id: IDENTIFIER,
1929
- index: z.number().int().nonnegative().max(10_000),
1930
- agent: NAME,
1931
- agentSource: NAME.optional(),
1932
- description: TEXT.optional(),
1933
- status: OmpSubagentStatusSchema,
1934
- task: TEXT.optional(),
1935
- assignment: TEXT.optional(),
1936
- sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
1937
- lastUpdate: z.number().finite().nonnegative(),
1938
- parentToolCallId: IDENTIFIER.optional(),
1939
- }),
1940
- )
1941
- .max(1_024),
1942
- });
1943
- const OmpSubagentMessagesResultSchema = z.object({
1944
- sessionFile: boundedString(MAX_PATH_LENGTH),
1945
- fromByte: z.number().int().nonnegative(),
1946
- nextByte: z.number().int().nonnegative(),
1947
- reset: z.boolean(),
1948
- messages: z.array(OmpMessageSchema).max(MAX_REPLAY_MESSAGES),
1949
- });
1950
- const ProtocolNegotiationResultSchema = z.object({
1951
- protocolVersion: z.literal(2),
1952
- clientCapabilities: z.object({ typedToolApprovals: z.literal(1).optional() }).optional(),
1953
- });
1954
-
1955
- export type OmpModel = z.infer<typeof OmpModelSchema>;
1956
- export type OmpBranchResult = z.infer<typeof OmpBranchResultSchema>;
1957
- export type OmpSessionState = z.infer<typeof OmpSessionStateSchema>;
1958
- export type OmpSessionStats = z.infer<typeof OmpSessionStatsSchema>;
1959
- export type OmpCompactionResult = z.infer<typeof OmpCompactionResultSchema>;
1960
- export type OmpHostToolDefinition = z.infer<typeof OmpHostToolDefinitionSchema>;
1961
- export type OmpHostToolCall = z.infer<typeof OmpHostToolCallSchema>;
1962
- export type OmpHostToolResult = z.infer<typeof OmpHostToolResultSchema>;
1963
- export type OmpHostToolUpdate = z.infer<typeof OmpHostToolUpdateSchema>;
1964
- export type OmpToolApprovalRequest = z.infer<typeof OmpToolApprovalRequestSchema>;
1965
- export type OmpToolApprovalCancel = z.infer<typeof OmpToolApprovalCancelSchema>;
1966
- export type OmpToolApprovalResponse = z.infer<typeof OmpToolApprovalResponseSchema>;
1967
- export function parseOmpHostToolAgentResult(value: unknown): OmpHostToolResult["result"] {
1968
- return OmpHostToolAgentResultSchema.parse(value);
1969
- }
1970
- export type OmpRpcEvent =
1971
- | z.infer<typeof OmpRuntimeEventSchema>
1972
- | { type: "prompt_error"; id: string; error: string; code?: string }
1973
- | { type: "process_exit"; error: string };
1974
- export type OmpAgentSessionEvent = z.infer<typeof OmpAgentSessionEventSchema>;
1975
- export type OmpSubagentSnapshot = z.infer<typeof OmpSubagentsResultSchema>["subagents"][number];
1976
- export type OmpSubagentEvent = Extract<
1977
- z.infer<typeof OmpRuntimeEventSchema>,
1978
- { type: "subagent_lifecycle" | "subagent_progress" | "subagent_event" }
1979
- >;
1980
- export interface OmpSubagentMessagesResult {
1981
- sessionFile: string;
1982
- fromByte: number;
1983
- nextByte: number;
1984
- reset: boolean;
1985
- messages: OmpMessage[];
1986
- }
1987
- export interface OmpPersistedSubagentMessages {
1988
- sessionFile: string;
1989
- nativeSessionId: string;
1990
- byteLength: number;
1991
- messages: OmpMessage[];
1992
- }
1993
- export interface OmpPersistedSessionMessages {
1994
- sessionFile: string;
1995
- nativeSessionId: string;
1996
- byteLength: number;
1997
- messages: OmpMessage[];
1998
- imageReplayWarning?: true;
1999
- }
2000
-
2001
- export type OmpAvailableCommand = z.infer<typeof OmpAvailableCommandSchema>;
2002
- export type OmpImage = { type: "image"; data: string; mimeType: string };
2003
- export type OmpExtensionUiResponse =
2004
- | { type: "extension_ui_response"; id: string; value: string }
2005
- | { type: "extension_ui_response"; id: string; confirmed: boolean }
2006
- | { type: "extension_ui_response"; id: string; cancelled: true; timedOut?: boolean };
2007
-
2008
88
  export interface OmpRuntimeSession {
2009
89
  readonly maxHostToolFrameBytes?: number;
2010
90
  /** Maximum bytes accepted for one unchunked stdin JSONL frame, including its newline. */
@@ -2078,1275 +158,6 @@ export interface OmpRpcRuntimeOptions {
2078
158
  ) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
2079
159
  }
2080
160
 
2081
- export type OmpRpcRequestRejectionCode = "session_busy" | "stale_cursor" | "unsupported_command";
2082
-
2083
- export class OmpRpcRequestRejectedError extends Error {
2084
- constructor(
2085
- readonly command?: OmpRpcDiagnosticCommand,
2086
- readonly code?: OmpRpcRequestRejectionCode,
2087
- ) {
2088
- super("OMP RPC request failed");
2089
- this.name = "OmpRpcRequestRejectedError";
2090
- }
2091
- }
2092
-
2093
- export class OmpRpcResponseLimitError extends Error {
2094
- constructor(
2095
- readonly command: OmpRpcDiagnosticCommand | undefined,
2096
- readonly bound: OmpRpcBoundDimension,
2097
- readonly actual: number,
2098
- readonly limit: number,
2099
- ) {
2100
- super("OMP RPC response exceeded command limits");
2101
- this.name = "OmpRpcResponseLimitError";
2102
- }
2103
- }
2104
-
2105
- type PendingRequest = {
2106
- resolve(value: unknown): void;
2107
- reject(error: Error): void;
2108
- timer?: TimerHandle;
2109
- command: string;
2110
- beforeResolve?: (value: unknown) => void;
2111
- };
2112
- type StartedRequest = { id: string; promise: Promise<unknown> };
2113
-
2114
- type ReadyFrame = z.infer<typeof OmpReadyFrameSchema>;
2115
- type ChunkFrame = z.infer<typeof OmpChunkFrameSchema>;
2116
-
2117
- type ChunkState = {
2118
- id: string;
2119
- count: number;
2120
- byteLength: number;
2121
- parts: Buffer[];
2122
- receivedBytes: number;
2123
- timer: TimerHandle;
2124
- };
2125
-
2126
- function waitWithTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
2127
- const result = Promise.withResolvers<T>();
2128
- const timer = setTimeout(() => result.reject(new Error(message)), timeoutMs);
2129
- promise.then(
2130
- (value) => {
2131
- clearTimeout(timer);
2132
- result.resolve(value);
2133
- },
2134
- (error) => {
2135
- clearTimeout(timer);
2136
- result.reject(error);
2137
- },
2138
- );
2139
- return result.promise;
2140
- }
2141
-
2142
- class OmpRpcProcess {
2143
- readonly ready: Promise<ReadyFrame>;
2144
- readonly inheritedRedactionValues: readonly string[];
2145
-
2146
- private readonly child: ChildProcessWithoutNullStreams;
2147
- private readonly listeners = new Set<(event: OmpRpcEvent) => void>();
2148
- private readonly pending = new Map<string, PendingRequest>();
2149
- private readonly queuedWrites = new Map<string, number>();
2150
- private readonly pendingOneWayWrites = new Map<
2151
- string,
2152
- { reject(error: Error): void; timer: TimerHandle }
2153
- >();
2154
- private readonly acceptedPromptIds = new Set<string>();
2155
- private readonly exitPromise: Promise<void>;
2156
- private readonly resolveExit: () => void;
2157
- private readonly resolveReady: (frame: ReadyFrame) => void;
2158
- private readonly rejectReady: (error: Error) => void;
2159
- private readonly terminateProcessTree: (pid: number) => Promise<ProcessTreeCleanup>;
2160
- private readonly streamedBlocks = new Map<number, string>();
2161
- private readonly activeToolCallIds = new Set<string>();
2162
- private pendingWriteBytes = 0;
2163
- private commandTextLength = 0;
2164
- private lineParts: Buffer[] = [];
2165
- private lineBytes = 0;
2166
- private discardingLine = false;
2167
- private discardedLineBytes = 0;
2168
- private chunk: ChunkState | null = null;
2169
- private physicalFrameLimit = MAX_PHYSICAL_FRAME_BYTES;
2170
- private reassembledFrameLimit = MAX_REASSEMBLED_FRAME_BYTES;
2171
- private closed = false;
2172
- private exited = false;
2173
- private fatalError: Error | null = null;
2174
- private closePromise: Promise<void> | null = null;
2175
- private treeCleanupPromise: Promise<ProcessTreeCleanup> | null = null;
2176
- private spawnFailedWithoutProcess = false;
2177
- private readyReceived = false;
2178
- private outputSettled = false;
2179
- private turnActive = false;
2180
- private readonly pendingProtocolViolations = new Map<
2181
- ProtocolViolationKey,
2182
- PendingProtocolViolation
2183
- >();
2184
- private protocolViolationTimer: TimerHandle | null = null;
2185
-
2186
- constructor(
2187
- options: OmpStartOptions,
2188
- spawnProcess?: OmpRpcRuntimeOptions["spawnProcess"],
2189
- terminateProcessTree?: OmpRpcRuntimeOptions["terminateProcessTree"],
2190
- private readonly requestTimeoutMs = REQUEST_TIMEOUT_MS,
2191
- private readonly reportProtocolViolation: (
2192
- diagnostic: OmpProtocolViolationDiagnostic,
2193
- ) => void | Promise<void> = (diagnostic) => console.error("OMP protocol violation", diagnostic),
2194
- ) {
2195
- const ready = Promise.withResolvers<ReadyFrame>();
2196
- this.rejectReady = ready.reject;
2197
- this.ready = ready.promise;
2198
- this.resolveReady = ready.resolve;
2199
- const request = buildOmpSpawnRequest(options);
2200
- this.inheritedRedactionValues = request.inheritedRedactionValues;
2201
- this.terminateProcessTree = terminateProcessTree
2202
- ? async (pid) => {
2203
- const outcome = await terminateProcessTree(pid);
2204
- return outcome === true ? "verified" : outcome === "uncertain" ? "uncertain" : "failed";
2205
- }
2206
- : async (pid) =>
2207
- process.platform === "win32"
2208
- ? stopWindowsTree(pid)
2209
- : (await terminatePosixProcessTree(pid, PROCESS_STOP_TIMEOUT_MS))
2210
- ? "verified"
2211
- : "failed";
2212
- try {
2213
- this.child = spawnProcess
2214
- ? spawnProcess(request)
2215
- : spawn(request.command, request.args, {
2216
- cwd: request.cwd,
2217
- env: request.env,
2218
- detached: request.detached,
2219
- windowsHide: true,
2220
- stdio: ["pipe", "pipe", "pipe"],
2221
- });
2222
- } catch (cause) {
2223
- const code = (cause as NodeJS.ErrnoException)?.code;
2224
- throw new Error(
2225
- code === "ENOENT"
2226
- ? "OMP executable was not found"
2227
- : code === "EACCES" || code === "EPERM"
2228
- ? "OMP executable is not runnable"
2229
- : "OMP process could not be launched",
2230
- );
2231
- }
2232
- this.child.stdout.on("data", (chunk: Buffer | string) => this.receiveData(chunk));
2233
- this.child.stdout.once("end", () => this.handleStdoutEnd());
2234
- this.child.stderr.on("data", () => {
2235
- // Stderr is intentionally drained and discarded. It may contain credentials or paths.
2236
- });
2237
- this.child.stdin.on("error", () => {
2238
- this.fail(new Error("OMP RPC input channel failed"));
2239
- });
2240
- const exited = Promise.withResolvers<void>();
2241
- this.exitPromise = exited.promise;
2242
- this.resolveExit = exited.resolve;
2243
- this.child.once("exit", (code, signal) => this.handleProcessExit(code, signal));
2244
- this.child.once("close", () => this.settleOutput());
2245
- this.child.once("error", (cause) => {
2246
- const code = (cause as NodeJS.ErrnoException)?.code;
2247
- if (this.child.pid === undefined && isConfirmedNoProcessSpawnFailure(cause)) {
2248
- this.spawnFailedWithoutProcess = true;
2249
- }
2250
- this.fail(
2251
- new Error(
2252
- code === "ENOENT"
2253
- ? "OMP executable was not found"
2254
- : code === "EACCES" || code === "EPERM"
2255
- ? "OMP executable is not runnable"
2256
- : "OMP process could not be launched",
2257
- ),
2258
- );
2259
- });
2260
- }
2261
-
2262
- private handleProcessExit(code: number | null, signal: NodeJS.Signals | null): void {
2263
- if (this.exited) return;
2264
- this.exited = true;
2265
- void this.startTreeCleanup();
2266
- const detail = code === null ? `signal ${signal ?? "unknown"}` : `code ${code}`;
2267
- const error = new Error(`OMP RPC process exited (${detail})`);
2268
- this.rejectReady(error);
2269
- this.failPending(error);
2270
- this.resolveExit();
2271
- if (!this.closed && !this.fatalError) this.fail(error);
2272
- }
2273
-
2274
- private handleStdoutEnd(): void {
2275
- this.settleOutput();
2276
- if (!this.exited && !this.closed) this.fail(new Error("OMP RPC output channel closed"));
2277
- }
2278
-
2279
- private settleOutput(): void {
2280
- if (this.outputSettled) return;
2281
- this.outputSettled = true;
2282
- if (this.lineBytes > 0 || this.discardingLine) {
2283
- this.recordProtocolViolation("incomplete-frame", {
2284
- reason: "output-ended-mid-frame",
2285
- field: "frame",
2286
- expected: "complete-json-line",
2287
- actualType: "partial-frame",
2288
- maxByteSize: this.discardingLine ? this.discardedLineBytes : this.lineBytes,
2289
- });
2290
- }
2291
- this.lineParts = [];
2292
- this.lineBytes = 0;
2293
- this.discardingLine = false;
2294
- this.discardedLineBytes = 0;
2295
- }
2296
-
2297
- get outboundFrameLimit(): number {
2298
- return this.physicalFrameLimit;
2299
- }
2300
-
2301
- onEvent(listener: (event: OmpRpcEvent) => void): () => void {
2302
- this.listeners.add(listener);
2303
- return () => this.listeners.delete(listener);
2304
- }
2305
-
2306
- applyReadyLimits(frame: ReadyFrame): void {
2307
- if (frame.maxFrameBytes !== undefined) this.physicalFrameLimit = frame.maxFrameBytes;
2308
- if (frame.maxReassembledFrameBytes !== undefined) {
2309
- this.reassembledFrameLimit = frame.maxReassembledFrameBytes;
2310
- }
2311
- }
2312
-
2313
- startRequest(
2314
- command: Record<string, unknown>,
2315
- timeoutMs: number | null = this.requestTimeoutMs,
2316
- beforeResolve?: (value: unknown) => void,
2317
- ): StartedRequest {
2318
- const id = randomUUID();
2319
- if (this.fatalError) return { id, promise: Promise.reject(this.fatalError) };
2320
- if (this.closed || this.exited || !this.child.stdin.writable) {
2321
- return { id, promise: Promise.reject(new Error("OMP RPC process is closed")) };
2322
- }
2323
- let payload: Buffer;
2324
- try {
2325
- payload = Buffer.from(`${JSON.stringify({ ...command, id })}\n`);
2326
- } catch {
2327
- return { id, promise: Promise.reject(new Error("OMP RPC request could not be encoded")) };
2328
- }
2329
- if (payload.byteLength > this.physicalFrameLimit) {
2330
- return {
2331
- id,
2332
- promise: Promise.reject(new Error("OMP RPC request exceeds the negotiated frame limit")),
2333
- };
2334
- }
2335
- if (
2336
- this.pending.size >= MAX_PENDING_REQUESTS ||
2337
- this.pendingWriteBytes + payload.byteLength > MAX_PENDING_WRITE_BYTES
2338
- ) {
2339
- return { id, promise: Promise.reject(new Error("OMP RPC has too many pending requests")) };
2340
- }
2341
- const result = Promise.withResolvers<unknown>();
2342
- const timer =
2343
- timeoutMs === null
2344
- ? undefined
2345
- : setTimeout(() => {
2346
- this.pending.delete(id);
2347
- result.reject(new Error("OMP RPC request timed out"));
2348
- }, timeoutMs);
2349
- this.pending.set(id, {
2350
- resolve: result.resolve,
2351
- reject: result.reject,
2352
- timer,
2353
- command: typeof command.type === "string" ? command.type : "unknown",
2354
- ...(beforeResolve ? { beforeResolve } : {}),
2355
- });
2356
- this.queuedWrites.set(id, payload.byteLength);
2357
- this.pendingWriteBytes += payload.byteLength;
2358
- try {
2359
- this.child.stdin.write(payload, (cause) => {
2360
- this.releaseQueuedWrite(id);
2361
- if (cause) this.fail(new Error("OMP RPC input channel failed"));
2362
- });
2363
- } catch {
2364
- this.releaseQueuedWrite(id);
2365
- this.fail(new Error("OMP RPC input channel failed"));
2366
- }
2367
- return { id, promise: result.promise };
2368
- }
2369
-
2370
- request(
2371
- command: Record<string, unknown>,
2372
- timeoutMs: number | null = this.requestTimeoutMs,
2373
- ): Promise<unknown> {
2374
- return this.startRequest(command, timeoutMs).promise;
2375
- }
2376
-
2377
- send(frame: OmpHostToolResult | OmpHostToolUpdate): void {
2378
- if (this.fatalError) throw this.fatalError;
2379
- if (this.closed || this.exited || !this.child.stdin.writable) {
2380
- throw new Error("OMP RPC process is closed");
2381
- }
2382
- const parsed =
2383
- frame.type === "host_tool_result"
2384
- ? OmpHostToolResultSchema.parse(frame)
2385
- : OmpHostToolUpdateSchema.parse(frame);
2386
- const payload = Buffer.from(`${JSON.stringify(parsed)}\n`);
2387
- if (payload.byteLength > this.physicalFrameLimit) {
2388
- throw new Error("OMP host tool frame exceeds the negotiated frame limit");
2389
- }
2390
- if (this.pendingWriteBytes + payload.byteLength > MAX_PENDING_WRITE_BYTES) {
2391
- throw new Error("OMP RPC has too many pending writes");
2392
- }
2393
- const writeId = randomUUID();
2394
- this.queuedWrites.set(writeId, payload.byteLength);
2395
- this.pendingWriteBytes += payload.byteLength;
2396
- try {
2397
- this.child.stdin.write(payload, (cause) => {
2398
- this.releaseQueuedWrite(writeId);
2399
- if (cause) this.fail(new Error("OMP RPC input channel failed"));
2400
- });
2401
- } catch {
2402
- this.releaseQueuedWrite(writeId);
2403
- this.fail(new Error("OMP RPC input channel failed"));
2404
- throw new Error("OMP RPC input channel failed");
2405
- }
2406
- }
2407
-
2408
- sendFrame(frame: Record<string, unknown>, timeoutMs = this.requestTimeoutMs): Promise<void> {
2409
- if (this.fatalError) return Promise.reject(this.fatalError);
2410
- if (this.closed || this.exited || !this.child.stdin.writable) {
2411
- return Promise.reject(new Error("OMP RPC process is closed"));
2412
- }
2413
- let payload: Buffer;
2414
- try {
2415
- payload = Buffer.from(`${JSON.stringify(frame)}\n`);
2416
- } catch {
2417
- return Promise.reject(new Error("OMP RPC frame could not be encoded"));
2418
- }
2419
- if (payload.byteLength > this.physicalFrameLimit) {
2420
- return Promise.reject(new Error("OMP RPC frame exceeds the negotiated frame limit"));
2421
- }
2422
- if (
2423
- this.pendingOneWayWrites.size >= MAX_PENDING_ONE_WAY_WRITES ||
2424
- this.pendingWriteBytes + payload.byteLength > MAX_PENDING_WRITE_BYTES
2425
- ) {
2426
- return Promise.reject(new Error("OMP RPC has too many pending writes"));
2427
- }
2428
- const token = randomUUID();
2429
- const written = Promise.withResolvers<void>();
2430
- const timer = setTimeout(() => {
2431
- if (!this.pendingOneWayWrites.delete(token)) return;
2432
- this.releaseQueuedWrite(token);
2433
- const error = new Error("OMP RPC write timed out");
2434
- written.reject(error);
2435
- this.fail(error);
2436
- }, timeoutMs);
2437
- this.pendingOneWayWrites.set(token, { reject: written.reject, timer });
2438
- this.queuedWrites.set(token, payload.byteLength);
2439
- this.pendingWriteBytes += payload.byteLength;
2440
- try {
2441
- this.child.stdin.write(payload, (cause) => {
2442
- const pending = this.pendingOneWayWrites.get(token);
2443
- if (!pending) return;
2444
- clearTimeout(pending.timer);
2445
- this.pendingOneWayWrites.delete(token);
2446
- this.releaseQueuedWrite(token);
2447
- if (cause) {
2448
- const error = new Error("OMP RPC input channel failed");
2449
- written.reject(error);
2450
- this.fail(error);
2451
- } else {
2452
- written.resolve();
2453
- }
2454
- });
2455
- } catch {
2456
- clearTimeout(timer);
2457
- this.pendingOneWayWrites.delete(token);
2458
- this.releaseQueuedWrite(token);
2459
- const error = new Error("OMP RPC input channel failed");
2460
- written.reject(error);
2461
- this.fail(error);
2462
- }
2463
- return written.promise;
2464
- }
2465
-
2466
- close(): Promise<void> {
2467
- this.closePromise ??= this.closeProcess();
2468
- return this.closePromise;
2469
- }
2470
-
2471
- private async closeProcess(): Promise<void> {
2472
- this.closed = true;
2473
- this.flushProtocolViolations();
2474
- this.clearChunk();
2475
- this.failPending(new Error("OMP RPC process was closed"));
2476
- const cleanupPromise = this.startTreeCleanup();
2477
- if (!this.exited) {
2478
- try {
2479
- this.child.stdin.end();
2480
- } catch {
2481
- // Continue waiting for process-tree cleanup when the input channel is already closed.
2482
- }
2483
- }
2484
- const cleanup = await cleanupPromise;
2485
- if (cleanup !== "verified") throw new Error("OMP RPC process tree cleanup failed");
2486
- if (
2487
- !this.spawnFailedWithoutProcess &&
2488
- !this.exited &&
2489
- !(await this.waitForExit(PROCESS_STOP_TIMEOUT_MS))
2490
- ) {
2491
- throw new Error("OMP RPC process did not close after tree cleanup");
2492
- }
2493
- }
2494
-
2495
- private startTreeCleanup(): Promise<ProcessTreeCleanup> {
2496
- if (this.treeCleanupPromise) return this.treeCleanupPromise;
2497
- const pid = this.child.pid;
2498
- this.treeCleanupPromise = (
2499
- pid === undefined
2500
- ? Promise.resolve<ProcessTreeCleanup>(
2501
- this.spawnFailedWithoutProcess ? "verified" : "uncertain",
2502
- )
2503
- : this.terminateProcessTree(pid)
2504
- ).catch(() => "failed");
2505
- return this.treeCleanupPromise;
2506
- }
2507
-
2508
- private async waitForExit(timeoutMs: number): Promise<boolean> {
2509
- const timeout = Promise.withResolvers<false>();
2510
- const timer = setTimeout(() => timeout.resolve(false), timeoutMs);
2511
- try {
2512
- return await Promise.race([this.exitPromise.then(() => true), timeout.promise]);
2513
- } finally {
2514
- clearTimeout(timer);
2515
- }
2516
- }
2517
-
2518
- private receiveData(chunk: Buffer | string): void {
2519
- const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2520
- let start = 0;
2521
- for (let index = 0; index < bytes.length; index += 1) {
2522
- if (bytes[index] !== 10) continue;
2523
- const part = bytes.subarray(start, index);
2524
- if (this.discardingLine) {
2525
- this.discardedLineBytes += part.byteLength;
2526
- if (this.discardedLineBytes > MAX_SEMANTIC_FRAME_BYTES) {
2527
- this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
2528
- return;
2529
- }
2530
- this.resetDiscardedLine();
2531
- } else {
2532
- this.appendLinePart(part);
2533
- if (this.fatalError) return;
2534
- if (this.discardingLine) {
2535
- if (this.discardedLineBytes > MAX_SEMANTIC_FRAME_BYTES) {
2536
- this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
2537
- return;
2538
- }
2539
- this.resetDiscardedLine();
2540
- } else this.completeLine();
2541
- }
2542
- start = index + 1;
2543
- }
2544
- if (start >= bytes.length) return;
2545
- const trailing = bytes.subarray(start);
2546
- if (this.discardingLine) {
2547
- this.discardedLineBytes += trailing.byteLength;
2548
- if (this.discardedLineBytes > MAX_SEMANTIC_FRAME_BYTES) {
2549
- this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
2550
- }
2551
- } else {
2552
- this.appendLinePart(trailing);
2553
- }
2554
- }
2555
-
2556
- private resetDiscardedLine(): void {
2557
- this.discardingLine = false;
2558
- this.discardedLineBytes = 0;
2559
- this.lineParts = [];
2560
- this.lineBytes = 0;
2561
- }
2562
-
2563
- private appendLinePart(part: Buffer): void {
2564
- if (part.byteLength === 0) return;
2565
- const nextBytes = this.lineBytes + part.byteLength;
2566
- if (nextBytes > MAX_SEMANTIC_FRAME_BYTES) {
2567
- this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
2568
- return;
2569
- }
2570
- if (this.lineParts.length >= MAX_LINE_PARTS || nextBytes > this.physicalFrameLimit) {
2571
- this.discardedLineBytes = nextBytes;
2572
- this.lineParts = [];
2573
- this.lineBytes = 0;
2574
- this.discardingLine = true;
2575
- this.recordProtocolViolation("frame-limit", {
2576
- reason: "physical-frame-limit",
2577
- field: "frame.byteLength",
2578
- expected: "within-byte-limit",
2579
- actualType: "oversized",
2580
- maxByteSize: nextBytes,
2581
- limitBytes: this.physicalFrameLimit,
2582
- });
2583
- return;
2584
- }
2585
- this.lineParts.push(part);
2586
- this.lineBytes = nextBytes;
2587
- }
2588
-
2589
- private completeLine(): void {
2590
- if (this.lineBytes === 0) return;
2591
- const line = Buffer.concat(this.lineParts, this.lineBytes);
2592
- this.lineParts = [];
2593
- this.lineBytes = 0;
2594
- const payload = line.at(-1) === 13 ? line.subarray(0, -1) : line;
2595
- let decoded: unknown;
2596
- try {
2597
- decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
2598
- } catch {
2599
- this.recordProtocolViolation("invalid-json", {
2600
- reason: "json-decode",
2601
- field: "frame",
2602
- expected: "valid-json",
2603
- actualType: "invalid-json",
2604
- maxByteSize: payload.byteLength,
2605
- });
2606
- return;
2607
- }
2608
- this.receiveDecodedFrame(decoded, payload.byteLength);
2609
- }
2610
-
2611
- private receiveChunk(frame: ChunkFrame): void {
2612
- if (
2613
- frame.byteLength > MAX_SEMANTIC_FRAME_BYTES &&
2614
- ![...this.pending.values()].some(
2615
- (pending) =>
2616
- pending.command === "get_messages" ||
2617
- pending.command === "get_messages_page" ||
2618
- pending.command === "get_subagent_messages" ||
2619
- pending.command === "get_state",
2620
- )
2621
- ) {
2622
- this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
2623
- return;
2624
- }
2625
- if (
2626
- frame.index >= frame.count ||
2627
- frame.byteLength > this.reassembledFrameLimit ||
2628
- frame.data.length % 4 !== 0 ||
2629
- !/^[A-Za-z0-9+/]*={0,2}$/u.test(frame.data)
2630
- ) {
2631
- this.rejectChunk("chunk-metadata", {
2632
- field: "chunk",
2633
- expected: "valid-chunk-frame",
2634
- actualType: "object",
2635
- });
2636
- return;
2637
- }
2638
- const decoded = Buffer.from(frame.data, "base64");
2639
- if (decoded.byteLength > MAX_CHUNK_BYTES) {
2640
- this.rejectChunk("chunk-size", {
2641
- field: "chunk.data",
2642
- expected: "within-byte-limit",
2643
- actualType: "oversized",
2644
- limitBytes: MAX_CHUNK_BYTES,
2645
- });
2646
- return;
2647
- }
2648
- if (!this.chunk) {
2649
- if (frame.index !== 0) {
2650
- this.rejectChunk("chunk-start-index", {
2651
- field: "chunk.index",
2652
- expected: "first-chunk-index-zero",
2653
- actualType: "out-of-order",
2654
- });
2655
- return;
2656
- }
2657
- this.chunk = {
2658
- id: frame.chunkId,
2659
- count: frame.count,
2660
- byteLength: frame.byteLength,
2661
- parts: [],
2662
- receivedBytes: 0,
2663
- timer: setTimeout(
2664
- () =>
2665
- this.rejectChunk("chunk-timeout", {
2666
- field: "chunk.sequence",
2667
- expected: "chunk-before-deadline",
2668
- actualType: "timeout",
2669
- }),
2670
- CHUNK_STALE_MS,
2671
- ),
2672
- };
2673
- }
2674
- const chunk = this.chunk;
2675
- if (
2676
- chunk.id !== frame.chunkId ||
2677
- chunk.count !== frame.count ||
2678
- chunk.byteLength !== frame.byteLength ||
2679
- chunk.parts.length !== frame.index ||
2680
- chunk.receivedBytes + decoded.byteLength > chunk.byteLength ||
2681
- chunk.receivedBytes + decoded.byteLength > this.reassembledFrameLimit
2682
- ) {
2683
- this.rejectChunk("chunk-sequence", {
2684
- field: "chunk.sequence",
2685
- expected: "contiguous-chunk-sequence",
2686
- actualType: "out-of-order",
2687
- });
2688
- return;
2689
- }
2690
- chunk.parts.push(decoded);
2691
- chunk.receivedBytes += decoded.byteLength;
2692
- if (chunk.parts.length !== chunk.count) return;
2693
- if (chunk.receivedBytes !== chunk.byteLength) {
2694
- this.rejectChunk("chunk-byte-count", {
2695
- field: "chunk.byteLength",
2696
- expected: "declared-chunk-byte-count",
2697
- actualType: "mismatched",
2698
- });
2699
- return;
2700
- }
2701
- const reassembled = Buffer.concat(chunk.parts, chunk.receivedBytes);
2702
- this.clearChunk();
2703
- let decodedFrame: unknown;
2704
- try {
2705
- decodedFrame = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(reassembled));
2706
- } catch {
2707
- this.recordProtocolViolation("invalid-json", {
2708
- reason: "chunk-json-decode",
2709
- field: "chunk.data",
2710
- expected: "valid-json",
2711
- actualType: "invalid-json",
2712
- maxByteSize: reassembled.byteLength,
2713
- });
2714
- return;
2715
- }
2716
- this.receiveDecodedFrame(decodedFrame, reassembled.byteLength);
2717
- }
2718
-
2719
- private receiveDecodedFrame(value: unknown, rawByteLength: number): void {
2720
- if (value && typeof value === "object" && !Array.isArray(value)) {
2721
- const frame = value as Record<string, unknown>;
2722
- const pending = typeof frame.id === "string" ? this.pending.get(frame.id) : undefined;
2723
- if (
2724
- frame.type === "response" &&
2725
- (pending?.command === "get_messages" ||
2726
- pending?.command === "get_messages_page" ||
2727
- pending?.command === "get_subagent_messages" ||
2728
- pending?.command === "get_state")
2729
- ) {
2730
- this.receiveResponse(frame);
2731
- return;
2732
- }
2733
- }
2734
- if (this.receiveUnsupportedCommandResponse(value)) return;
2735
- if (rawByteLength > MAX_SEMANTIC_FRAME_BYTES) {
2736
- this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
2737
- return;
2738
- }
2739
- if (this.receiveKnownResponse(value)) return;
2740
- const sanitized = sanitizeLiveDisplayFrame(value);
2741
- const frame = JsonObjectSchema.safeParse(sanitized);
2742
- if (!frame.success) {
2743
- this.recordProtocolViolation("invalid-envelope", {
2744
- reason: "object-envelope",
2745
- field: "frame",
2746
- expected: "object-envelope",
2747
- actualType: protocolDiagnosticActualType(sanitized),
2748
- maxByteSize: rawByteLength,
2749
- });
2750
- return;
2751
- }
2752
- this.receiveFrame(frame.data, rawByteLength);
2753
- }
2754
-
2755
- private receiveKnownResponse(value: unknown): boolean {
2756
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2757
- const frame = value as Record<string, unknown>;
2758
- if (
2759
- frame.type !== "response" ||
2760
- typeof frame.id !== "string" ||
2761
- (!this.pending.has(frame.id) && !this.acceptedPromptIds.has(frame.id))
2762
- ) {
2763
- return false;
2764
- }
2765
- this.receiveResponse(frame);
2766
- return true;
2767
- }
2768
-
2769
- private receiveUnsupportedCommandResponse(value: unknown): boolean {
2770
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2771
- const frame = value as Record<string, unknown>;
2772
- if (
2773
- frame.type !== "response" ||
2774
- frame.id !== undefined ||
2775
- frame.success !== false ||
2776
- typeof frame.command !== "string" ||
2777
- frame.error !== `Unknown command: ${frame.command}`
2778
- ) {
2779
- return false;
2780
- }
2781
- const matches = [...this.pending.entries()].filter(
2782
- ([, pending]) => pending.command === frame.command,
2783
- );
2784
- if (matches.length === 0) return false;
2785
- const [id, pending] = matches[0] ?? [];
2786
- if (!id || !pending) return false;
2787
- this.takePending(id)?.reject(
2788
- new OmpRpcRequestRejectedError(rpcDiagnosticCommand(pending.command), "unsupported_command"),
2789
- );
2790
- return true;
2791
- }
2792
-
2793
- private receiveResponse(frame: Record<string, unknown>): void {
2794
- const rawId = typeof frame.id === "string" ? frame.id : undefined;
2795
- const knownPending = rawId ? this.pending.get(rawId) : undefined;
2796
- const response = OmpResponseFrameSchema.safeParse(frame);
2797
- if (!response.success) {
2798
- const handledAcceptedFailure =
2799
- rawId !== undefined && knownPending === undefined
2800
- ? this.emitAcceptedPromptFailure(rawId, frame)
2801
- : false;
2802
- if (!handledAcceptedFailure) {
2803
- this.recordProtocolViolation("invalid-response", {
2804
- reason: "response-schema",
2805
- frameType: "response",
2806
- field: "response",
2807
- expected: "valid-response-frame",
2808
- actualType: "object",
2809
- });
2810
- }
2811
- if (rawId && knownPending) {
2812
- this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
2813
- }
2814
- return;
2815
- }
2816
- const pending = this.pending.get(response.data.id);
2817
- if (!pending) {
2818
- if (!response.data.success) {
2819
- this.emitAcceptedPromptFailure(response.data.id, response.data);
2820
- }
2821
- return;
2822
- }
2823
- const isBranchHistory = pending.command === "get_branch_messages";
2824
- const isHistory =
2825
- pending.command === "get_messages" ||
2826
- pending.command === "get_messages_page" ||
2827
- pending.command === "get_subagent_messages";
2828
- const rawResponseData =
2829
- pending.command === "get_state"
2830
- ? projectSessionStateResponseData(response.data.data)
2831
- : response.data.data;
2832
- const responseData = isHistory ? sanitizeHistoryResponseData(rawResponseData) : rawResponseData;
2833
- const boundedFrame =
2834
- responseData === response.data.data ? frame : { ...frame, data: responseData };
2835
- const responseItemLimit = isBranchHistory
2836
- ? 1_024
2837
- : isHistory
2838
- ? 100_000
2839
- : pending.command === "get_available_models"
2840
- ? MAX_MODEL_CATALOG_ITEMS
2841
- : MAX_ARRAY_ITEMS;
2842
- const responseByteLimit =
2843
- isBranchHistory || isHistory
2844
- ? Math.min(MAX_REASSEMBLED_FRAME_BYTES, this.reassembledFrameLimit)
2845
- : 2 * 1024 * 1024;
2846
- // Model catalogs are truncated before publication, while the transport still enforces
2847
- // aggregate byte and node budgets over the complete response.
2848
- const responseNodeLimit = isBranchHistory
2849
- ? 4_096
2850
- : isHistory
2851
- ? 400_000
2852
- : pending.command === "get_available_models"
2853
- ? 16_384
2854
- : 2_048;
2855
- const violation = jsonBoundViolation(
2856
- boundedFrame,
2857
- responseByteLimit,
2858
- responseItemLimit,
2859
- MAX_IMAGE_DATA_LENGTH,
2860
- responseNodeLimit,
2861
- );
2862
- if (violation) {
2863
- this.takePending(response.data.id)?.reject(
2864
- new OmpRpcResponseLimitError(
2865
- rpcDiagnosticCommand(pending.command),
2866
- violation.dimension,
2867
- violation.actual,
2868
- violation.limit,
2869
- ),
2870
- );
2871
- return;
2872
- }
2873
- const settled = this.takePending(response.data.id);
2874
- if (!settled) return;
2875
- if (response.data.success) {
2876
- try {
2877
- settled.beforeResolve?.(responseData);
2878
- if (settled.command === "prompt") {
2879
- if (this.acceptedPromptIds.size >= MAX_PENDING_REQUESTS) {
2880
- const oldest = this.acceptedPromptIds.values().next().value;
2881
- if (oldest !== undefined) this.acceptedPromptIds.delete(oldest);
2882
- }
2883
- this.acceptedPromptIds.add(response.data.id);
2884
- }
2885
- settled.resolve(responseData);
2886
- } catch {
2887
- settled.reject(new Error("OMP RPC response is invalid"));
2888
- }
2889
- } else {
2890
- const responseCode = response.data.code;
2891
- const code: OmpRpcRequestRejectionCode | undefined =
2892
- responseCode === "session_busy" ||
2893
- responseCode === "stale_cursor" ||
2894
- responseCode === "unsupported_command"
2895
- ? responseCode
2896
- : response.data.error === `Unknown command: ${pending.command}`
2897
- ? "unsupported_command"
2898
- : undefined;
2899
- settled.reject(new OmpRpcRequestRejectedError(rpcDiagnosticCommand(pending.command), code));
2900
- }
2901
- }
2902
-
2903
- private emitAcceptedPromptFailure(id: string, frame: Record<string, unknown>): boolean {
2904
- if (frame.success !== false || !this.acceptedPromptIds.delete(id)) return false;
2905
- const error = typeof frame.error === "string" ? frame.error : undefined;
2906
- const nativeError =
2907
- error && utf8Bytes(error) <= MAX_RPC_ERROR_BYTES ? error : PROMPT_SCHEDULING_FAILURE;
2908
- const code = typeof frame.code === "string" ? frame.code : undefined;
2909
- const nativeCode = code && utf8Bytes(code) <= MAX_RPC_ERROR_CODE_BYTES ? code : undefined;
2910
- this.emit({
2911
- type: "prompt_error",
2912
- id,
2913
- error: nativeError,
2914
- ...(nativeCode ? { code: nativeCode } : {}),
2915
- });
2916
- return true;
2917
- }
2918
-
2919
- private takePending(id: string): PendingRequest | undefined {
2920
- const pending = this.pending.get(id);
2921
- if (!pending) return undefined;
2922
- clearTimeout(pending.timer);
2923
- this.pending.delete(id);
2924
- return pending;
2925
- }
2926
-
2927
- private releaseQueuedWrite(id: string): void {
2928
- const bytes = this.queuedWrites.get(id);
2929
- if (bytes === undefined) return;
2930
- this.queuedWrites.delete(id);
2931
- this.pendingWriteBytes -= bytes;
2932
- }
2933
-
2934
- private receiveDegradedAgentEnd(value: unknown, onlyUnsafePayload: boolean): boolean {
2935
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2936
- const frame = value as Record<string, unknown>;
2937
- if (frame.type !== "agent_end") return false;
2938
- const envelope = OmpAgentEndEnvelopeSchema.safeParse(frame);
2939
- if (!envelope.success) {
2940
- this.fail(new Error("OMP emitted invalid terminal metadata"));
2941
- return true;
2942
- }
2943
- const structuralFrame = mapRuntimeFrameDetails(frame, omitOptionalDetails);
2944
- const messagesAreSafe =
2945
- frame.messages === undefined ||
2946
- (Array.isArray(frame.messages) &&
2947
- frame.messages.length <= MAX_ARRAY_ITEMS &&
2948
- boundedJsonBytes(
2949
- structuralFrame.messages as unknown[],
2950
- MAX_SEMANTIC_FRAME_BYTES,
2951
- OMP_MAX_CONTENT_PARTS,
2952
- MAX_IMAGE_DATA_LENGTH,
2953
- 4_096,
2954
- ) !== Number.POSITIVE_INFINITY);
2955
- const payloadIsSafe =
2956
- messagesAreSafe &&
2957
- boundedJsonBytes(
2958
- structuralFrame,
2959
- MAX_SEMANTIC_FRAME_BYTES,
2960
- OMP_MAX_CONTENT_PARTS,
2961
- MAX_IMAGE_DATA_LENGTH,
2962
- 4_096,
2963
- ) !== Number.POSITIVE_INFINITY;
2964
- if (onlyUnsafePayload && payloadIsSafe) return false;
2965
- if (envelope.data.isTerminal === false) {
2966
- this.fail(new Error("OMP emitted an invalid nonterminal agent_end payload"));
2967
- return true;
2968
- }
2969
- const observedCount = Array.isArray(frame.messages)
2970
- ? frame.messages.length
2971
- : Object.hasOwn(frame, "messages")
2972
- ? 1
2973
- : undefined;
2974
- const messageCount = Math.max(envelope.data.messageCount ?? 0, observedCount ?? 0, 1);
2975
- this.emit({
2976
- ...envelope.data,
2977
- messageCount,
2978
- });
2979
- this.streamedBlocks.clear();
2980
- this.commandTextLength = 0;
2981
- return true;
2982
- }
2983
-
2984
- private receiveFrame(frame: Record<string, unknown>, rawByteLength: number): void {
2985
- const type = typeof frame.type === "string" && frame.type.length <= 64 ? frame.type : null;
2986
- if (!type) {
2987
- this.recordProtocolViolation("invalid-envelope", {
2988
- reason: "missing-frame-type",
2989
- field: "frame.type",
2990
- expected: "bounded-frame-type",
2991
- actualType: protocolDiagnosticActualType(frame.type),
2992
- maxByteSize: rawByteLength,
2993
- });
2994
- return;
2995
- }
2996
- const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
2997
- if (this.receiveDegradedAgentEnd(safeFrame, true)) return;
2998
- if (
2999
- boundedJsonBytes(
3000
- mapRuntimeFrameDetails(safeFrame, omitOptionalDetails),
3001
- MAX_SEMANTIC_FRAME_BYTES,
3002
- runtimeFrameCollectionLimit(safeFrame),
3003
- MAX_IMAGE_DATA_LENGTH,
3004
- 4_096,
3005
- ) === Number.POSITIVE_INFINITY
3006
- ) {
3007
- this.recordProtocolViolation("frame-limit", {
3008
- reason: "semantic-frame-limit",
3009
- field: "frame.byteLength",
3010
- expected: "within-byte-limit",
3011
- actualType: "oversized",
3012
- maxByteSize: rawByteLength,
3013
- limitBytes: MAX_SEMANTIC_FRAME_BYTES,
3014
- });
3015
- return;
3016
- }
3017
- if (type === "rpc_chunk") {
3018
- const chunk = OmpChunkFrameSchema.safeParse(safeFrame);
3019
- if (!chunk.success) {
3020
- this.rejectChunk("chunk-schema", {
3021
- field: "chunk",
3022
- expected: "valid-chunk-frame",
3023
- actualType: "object",
3024
- });
3025
- } else this.receiveChunk(chunk.data);
3026
- return;
3027
- }
3028
- if (this.chunk) {
3029
- this.clearChunk();
3030
- this.recordProtocolViolation("interleaved-chunk", {
3031
- reason: "frame-interleaved-with-chunk",
3032
- field: "chunk.sequence",
3033
- expected: "no-interleaved-frame",
3034
- actualType: "interleaved",
3035
- });
3036
- }
3037
- if (type === "rpc_frame_error") {
3038
- this.recordProtocolViolation("remote-frame-error", {
3039
- reason: "remote-frame-error",
3040
- frameType: "rpc_frame_error",
3041
- field: "frame",
3042
- expected: "no-remote-frame-error",
3043
- actualType: "remote-error",
3044
- });
3045
- return;
3046
- }
3047
- if (type === "ready") {
3048
- if (this.readyReceived) {
3049
- this.recordProtocolViolation("duplicate-ready", {
3050
- reason: "ready-already-received",
3051
- frameType: "ready",
3052
- field: "ready",
3053
- expected: "single-ready-frame",
3054
- actualType: "duplicate",
3055
- });
3056
- return;
3057
- }
3058
- const ready = OmpReadyFrameSchema.safeParse(safeFrame);
3059
- if (!ready.success) {
3060
- this.recordProtocolViolation("invalid-ready", {
3061
- reason: "ready-schema",
3062
- frameType: "ready",
3063
- field: "ready",
3064
- expected: "valid-ready-frame",
3065
- actualType: "object",
3066
- });
3067
- } else {
3068
- this.readyReceived = true;
3069
- this.resolveReady(ready.data);
3070
- }
3071
- return;
3072
- }
3073
- if (type === "response") {
3074
- this.receiveResponse(safeFrame);
3075
- return;
3076
- }
3077
- const event = OmpRuntimeEventSchema.safeParse(safeFrame);
3078
- if (!event.success) {
3079
- this.rejectMatchingToolApproval(safeFrame);
3080
- if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
3081
- this.recordProtocolViolation("invalid-event", {
3082
- ...invalidEventDiagnosticMetadata(safeFrame, type),
3083
- maxByteSize: rawByteLength,
3084
- });
3085
- return;
3086
- }
3087
- if (!this.acceptEventState(event.data)) {
3088
- this.recordProtocolViolation("invalid-event-state", {
3089
- reason: "event-state-transition",
3090
- eventType: event.data.type,
3091
- field: "event.sequence",
3092
- expected: "valid-event-state-transition",
3093
- actualType: "out-of-order",
3094
- maxByteSize: rawByteLength,
3095
- });
3096
- return;
3097
- }
3098
- this.emit(event.data);
3099
- if (event.data.type === "prompt_result" && event.data.id) {
3100
- this.acceptedPromptIds.delete(event.data.id);
3101
- }
3102
- if (event.data.type === "turn_end" || event.data.type === "agent_end") {
3103
- this.turnActive = false;
3104
- }
3105
- if (
3106
- event.data.type === "message_end" ||
3107
- event.data.type === "turn_end" ||
3108
- event.data.type === "agent_end"
3109
- ) {
3110
- this.streamedBlocks.clear();
3111
- }
3112
- if (event.data.type === "turn_end") {
3113
- this.commandTextLength = 0;
3114
- this.activeToolCallIds.clear();
3115
- }
3116
- }
3117
-
3118
- private rejectMatchingToolApproval(frame: Record<string, unknown>): void {
3119
- if (frame.type !== "tool_approval_request") return;
3120
- const { id, toolCallId } = frame;
3121
- if (!isBoundedToolApprovalId(id) || !isBoundedToolApprovalId(toolCallId)) return;
3122
- void this.sendFrame({
3123
- type: "tool_approval_response",
3124
- id,
3125
- toolCallId,
3126
- cancelled: true,
3127
- }).catch(() => this.fail(new Error("OMP rejected tool approval could not be canceled")));
3128
- }
3129
-
3130
- private acceptEventState(event: z.infer<typeof OmpRuntimeEventSchema>): boolean {
3131
- if (event.type === "turn_start") {
3132
- this.turnActive = true;
3133
- this.streamedBlocks.clear();
3134
- this.commandTextLength = 0;
3135
- this.activeToolCallIds.clear();
3136
- return true;
3137
- }
3138
- if (event.type === "command_output") {
3139
- const nextLength = this.commandTextLength + utf8Bytes(event.text ?? "");
3140
- if (nextLength > MAX_STREAM_TEXT_LENGTH) return false;
3141
- this.commandTextLength = nextLength;
3142
- return true;
3143
- }
3144
- if (event.type === "tool_execution_start") {
3145
- if (
3146
- !this.activeToolCallIds.has(event.toolCallId) &&
3147
- this.activeToolCallIds.size >= MAX_ACTIVE_TOOLS
3148
- ) {
3149
- return false;
3150
- }
3151
- this.activeToolCallIds.add(event.toolCallId);
3152
- return true;
3153
- }
3154
- if (event.type === "tool_execution_update" || event.type === "tool_stream_update") {
3155
- return this.activeToolCallIds.has(event.toolCallId);
3156
- }
3157
- if (event.type === "tool_execution_end") {
3158
- if (!this.activeToolCallIds.has(event.toolCallId)) return false;
3159
- this.activeToolCallIds.delete(event.toolCallId);
3160
- return true;
3161
- }
3162
- if (
3163
- event.type !== "message_start" &&
3164
- event.type !== "message_update" &&
3165
- event.type !== "message_end"
3166
- ) {
3167
- return true;
3168
- }
3169
- if (event.message.role !== "assistant") return true;
3170
- const nextBlocks =
3171
- event.type === "message_start" ? new Map<number, string>() : new Map(this.streamedBlocks);
3172
- const content = event.message.content;
3173
- if (typeof content === "string") {
3174
- nextBlocks.set(0, content);
3175
- } else if (Array.isArray(content)) {
3176
- for (const [index, part] of content.entries()) {
3177
- const text =
3178
- part.type === "text" ? part.text : part.type === "thinking" ? part.thinking : undefined;
3179
- if (text !== undefined) nextBlocks.set(index, text);
3180
- }
3181
- }
3182
- const update = event.type === "message_update" ? event.assistantMessageEvent : undefined;
3183
- if (
3184
- update?.contentIndex !== undefined &&
3185
- update.delta !== undefined &&
3186
- (content === undefined ||
3187
- (Array.isArray(content) && content[update.contentIndex] === undefined))
3188
- ) {
3189
- const current = nextBlocks.get(update.contentIndex) ?? "";
3190
- nextBlocks.set(update.contentIndex, `${current}${update.delta}`);
3191
- }
3192
- let totalLength = 0;
3193
- for (const text of nextBlocks.values()) {
3194
- totalLength += utf8Bytes(text);
3195
- if (totalLength > MAX_STREAM_TEXT_LENGTH) return false;
3196
- }
3197
- this.streamedBlocks.clear();
3198
- for (const [index, text] of nextBlocks) this.streamedBlocks.set(index, text);
3199
- return true;
3200
- }
3201
-
3202
- private protocolDiagnosticPhase(): OmpProtocolDiagnosticPhase {
3203
- if (this.closed || this.exited || this.outputSettled) return "closing";
3204
- if (!this.readyReceived) return "startup";
3205
- for (const pending of this.pending.values()) {
3206
- if (pending.command === "negotiate_protocol") return "negotiation";
3207
- if (pending.command === "prompt") return "active-turn";
3208
- }
3209
- if (this.turnActive || this.acceptedPromptIds.size > 0) return "active-turn";
3210
- return "idle";
3211
- }
3212
-
3213
- private rejectChunk(
3214
- reason: OmpProtocolViolationReason,
3215
- metadata: Omit<
3216
- OmpProtocolViolationDiagnostic,
3217
- "category" | "reason" | "occurrenceCount" | "phase" | "frameType"
3218
- > = {},
3219
- ): void {
3220
- this.clearChunk();
3221
- this.recordProtocolViolation("invalid-chunk", {
3222
- reason,
3223
- frameType: "rpc_chunk",
3224
- ...metadata,
3225
- });
3226
- }
3227
-
3228
- private recordProtocolViolation(
3229
- category: OmpProtocolViolationCategory,
3230
- metadata: Omit<OmpProtocolViolationDiagnostic, "category" | "occurrenceCount" | "phase">,
3231
- ): void {
3232
- const phase = this.protocolDiagnosticPhase();
3233
- if (!this.protocolViolationTimer) {
3234
- this.emitProtocolViolation({ category, occurrenceCount: 1, phase, ...metadata });
3235
- this.protocolViolationTimer = setTimeout(
3236
- () => this.flushProtocolViolations(),
3237
- PROTOCOL_VIOLATION_COALESCE_MS,
3238
- );
3239
- return;
3240
- }
3241
- const key = `${category}:${metadata.reason}` as ProtocolViolationKey;
3242
- const pending = this.pendingProtocolViolations.get(key);
3243
- if (!pending) {
3244
- this.pendingProtocolViolations.set(key, {
3245
- category,
3246
- occurrenceCount: 1,
3247
- phase,
3248
- ...metadata,
3249
- });
3250
- return;
3251
- }
3252
- pending.occurrenceCount = Math.min(Number.MAX_SAFE_INTEGER, pending.occurrenceCount + 1);
3253
- pending.maxByteSize =
3254
- Math.max(pending.maxByteSize ?? 0, metadata.maxByteSize ?? 0) || undefined;
3255
- pending.reason = metadata.reason;
3256
- pending.phase = phase;
3257
- pending.frameType = metadata.frameType;
3258
- pending.field = metadata.field;
3259
- pending.expected = metadata.expected;
3260
- pending.actualType = metadata.actualType;
3261
- pending.limitBytes = metadata.limitBytes;
3262
- }
3263
-
3264
- private flushProtocolViolations(): void {
3265
- if (this.protocolViolationTimer) clearTimeout(this.protocolViolationTimer);
3266
- this.protocolViolationTimer = null;
3267
- for (const diagnostic of this.pendingProtocolViolations.values()) {
3268
- this.emitProtocolViolation(diagnostic);
3269
- }
3270
- this.pendingProtocolViolations.clear();
3271
- }
3272
-
3273
- private emitProtocolViolation(diagnostic: OmpProtocolViolationDiagnostic): void {
3274
- try {
3275
- const reporting = this.reportProtocolViolation(diagnostic);
3276
- if (reporting) void reporting.catch(() => undefined);
3277
- } catch {
3278
- // Diagnostics must never alter transport flow or cleanup.
3279
- }
3280
- }
3281
-
3282
- private fail(error: Error): void {
3283
- if (this.fatalError || this.closed) return;
3284
- this.fatalError = error;
3285
- this.rejectReady(error);
3286
- this.failPending(error);
3287
- this.emit({ type: "process_exit", error: error.message });
3288
- void this.close().catch(() => undefined);
3289
- }
3290
-
3291
- private clearChunk(): void {
3292
- const chunk = this.chunk;
3293
- if (chunk) clearTimeout(chunk.timer);
3294
- this.chunk = null;
3295
- }
3296
-
3297
- private failPending(error: Error): void {
3298
- for (const pending of this.pending.values()) {
3299
- clearTimeout(pending.timer);
3300
- pending.reject(error);
3301
- }
3302
- this.pending.clear();
3303
- for (const pending of this.pendingOneWayWrites.values()) {
3304
- clearTimeout(pending.timer);
3305
- pending.reject(error);
3306
- }
3307
- this.acceptedPromptIds.clear();
3308
- this.pendingOneWayWrites.clear();
3309
- this.queuedWrites.clear();
3310
- this.pendingWriteBytes = 0;
3311
- }
3312
-
3313
- private emit(event: OmpRpcEvent): void {
3314
- for (const listener of this.listeners) listener(event);
3315
- }
3316
- }
3317
-
3318
- function validateReadyMetadata(frame: ReadyFrame): void {
3319
- const metadata = [
3320
- frame.protocolVersion,
3321
- frame.supportedProtocolVersions,
3322
- frame.maxFrameBytes,
3323
- frame.maxReassembledFrameBytes,
3324
- ];
3325
- if (metadata.every((value) => value === undefined)) {
3326
- throw new OmpPublicError("OMP provider requires OMP RPC protocol v2");
3327
- }
3328
- if (metadata.some((value) => value === undefined)) {
3329
- throw new Error("OMP ready frame contains incomplete protocol metadata");
3330
- }
3331
- if (
3332
- frame.protocolVersion !== 1 ||
3333
- !frame.supportedProtocolVersions?.includes(1) ||
3334
- !frame.maxFrameBytes ||
3335
- frame.maxFrameBytes > MAX_PHYSICAL_FRAME_BYTES ||
3336
- !frame.maxReassembledFrameBytes ||
3337
- frame.maxReassembledFrameBytes > MAX_REASSEMBLED_FRAME_BYTES ||
3338
- frame.maxReassembledFrameBytes < frame.maxFrameBytes
3339
- ) {
3340
- throw new Error("OMP ready frame advertises unsupported protocol limits");
3341
- }
3342
- if (frame.maxFrameBytes < MIN_HOST_TOOL_RESULT_FRAME_BYTES) {
3343
- throw new Error("OMP ready frame cannot carry terminal host tool results");
3344
- }
3345
- if (!frame.supportedProtocolVersions.includes(2)) {
3346
- throw new OmpPublicError("OMP provider requires OMP RPC protocol v2");
3347
- }
3348
- }
3349
-
3350
161
  class OmpRpcSession implements OmpRuntimeSession {
3351
162
  get maxHostToolFrameBytes(): number {
3352
163
  return this.process.outboundFrameLimit;