@omercnet/paseo-omp 0.4.0 → 1.0.0-next.160.1

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