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