@omercnet/paseo-omp 0.3.0 → 0.4.0-next.119.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/client/omp-config-surface.tsx +243 -24
- package/client/omp-config-views.ts +24 -0
- package/client/omp-model-picker-state.ts +145 -0
- package/client/omp-model-picker.tsx +282 -0
- package/client/omp-routing-editor.tsx +307 -0
- package/client/support-diagnostics-state.ts +45 -0
- package/index.server.ts +27 -2
- package/package.json +4 -8
- package/paseo-plugin.json +1 -1
- package/server/omp-models.ts +59 -0
- package/server/omp-settings.ts +30 -20
- package/server/operational-failure-diagnostics.ts +76 -0
- package/server/package-version.ts +2 -0
- package/server/protocol-violation-diagnostics.ts +169 -0
- package/server/provider/catalog.ts +39 -10
- package/server/provider/connection.ts +88 -9
- package/server/provider/host-tools.ts +284 -34
- package/server/provider/mcp-transport.ts +2 -1
- package/server/provider/omp-rpc.ts +1785 -267
- package/server/provider/profile-providers.ts +7 -2
- package/server/provider/registration.ts +12 -2
- package/server/provider/security.ts +8 -10
- package/server/provider/session-descriptors.ts +97 -29
- package/server/provider/session.ts +212 -59
- package/server/provider/subsessions.ts +334 -74
- package/server/provider/timeline-projector.ts +125 -32
- package/server/support-diagnostics.ts +284 -0
- package/shared/omp-models.ts +49 -0
- package/shared/omp-settings.ts +227 -3
- package/shared/support-diagnostics.ts +32 -0
- package/CHANGELOG.md +0 -113
- package/SUPPORT.md +0 -44
- package/TESTING.md +0 -150
- package/docs/alpha-release-checklist.md +0 -68
- package/docs/configuration.md +0 -126
- package/docs/core-provider-issue-audit.md +0 -109
- package/docs/images/mcp-authorization-compact.png +0 -0
- package/docs/images/mcp-controls-wide.png +0 -0
- package/docs/images/plugin-manager.png +0 -0
- package/docs/images/workspace-settings.png +0 -0
- package/docs/installation.md +0 -89
- package/tsconfig.json +0 -16
|
@@ -4,7 +4,13 @@ import { isAbsolute, join } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { ompDataDir } from "../paths";
|
|
6
6
|
import { isValidImagePayload } from "./image";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
boundedJsonBytes,
|
|
9
|
+
boundedJsonMetrics,
|
|
10
|
+
OmpCleanupFailure,
|
|
11
|
+
OmpPublicError,
|
|
12
|
+
utf8Bytes,
|
|
13
|
+
} from "./security";
|
|
8
14
|
import {
|
|
9
15
|
listOmpSessionDescriptors,
|
|
10
16
|
type OmpSessionDescriptor,
|
|
@@ -56,6 +62,15 @@ const MAX_PENDING_ONE_WAY_WRITES = 256;
|
|
|
56
62
|
const MAX_PENDING_WRITE_BYTES = 8 * 1024 * 1024;
|
|
57
63
|
const MAX_LINE_PARTS = 4_096;
|
|
58
64
|
const MAX_ARRAY_ITEMS = 512;
|
|
65
|
+
// OMP read metadata can contain one source entry per displayed line. Bound this optional,
|
|
66
|
+
// opaque field separately so over-budget metadata can be omitted without losing completion events.
|
|
67
|
+
const MAX_OPTIONAL_METADATA_BYTES = MAX_TOOL_PAYLOAD_LENGTH;
|
|
68
|
+
const MAX_OPTIONAL_METADATA_ITEMS = 2_048;
|
|
69
|
+
const MAX_OPTIONAL_METADATA_NODES = 4_096;
|
|
70
|
+
const MAX_TASK_CORRELATION_BYTES = 256 * 1024;
|
|
71
|
+
const MAX_TASK_CORRELATION_ITEMS = 1_024;
|
|
72
|
+
const MAX_TASK_CORRELATION_NODES = 4_096;
|
|
73
|
+
const MAX_MODEL_CATALOG_ITEMS = 4_096;
|
|
59
74
|
// Tool-intensive OMP turns legitimately exceed 64 blocks; transport byte/node budgets remain the
|
|
60
75
|
// primary resource bounds.
|
|
61
76
|
export const OMP_MAX_CONTENT_PARTS = 4_096;
|
|
@@ -70,6 +85,407 @@ const MAX_COST_USD = 1_000_000_000;
|
|
|
70
85
|
const MAX_RPC_ERROR_BYTES = 4_096;
|
|
71
86
|
const MAX_RPC_ERROR_CODE_BYTES = 256;
|
|
72
87
|
const PROMPT_SCHEDULING_FAILURE = "OMP prompt scheduling failed";
|
|
88
|
+
const PROTOCOL_VIOLATION_COALESCE_MS = 10_000;
|
|
89
|
+
const MAX_REPLAY_MESSAGES = 100_000;
|
|
90
|
+
const MAX_REPLAY_BYTES = 64 * 1024 * 1024;
|
|
91
|
+
const MAX_REPLAY_NODES = 400_000;
|
|
92
|
+
const OMP_MESSAGE_PAGE_LIMIT = 256;
|
|
93
|
+
const MAX_MESSAGE_PAGES = 512;
|
|
94
|
+
const MAX_MESSAGE_PAGE_BUSY_RETRIES = 4;
|
|
95
|
+
const MAX_MESSAGE_PAGE_STALE_RESTARTS = 2;
|
|
96
|
+
const MESSAGE_PAGE_RETRY_BASE_MS = 50;
|
|
97
|
+
|
|
98
|
+
export const OMP_MESSAGE_REPLAY_POLICIES = {
|
|
99
|
+
assistant: "render",
|
|
100
|
+
user: "render",
|
|
101
|
+
developer: "ignore",
|
|
102
|
+
toolResult: "render",
|
|
103
|
+
bashExecution: "normalize",
|
|
104
|
+
pythonExecution: "normalize",
|
|
105
|
+
custom: "normalize",
|
|
106
|
+
hookMessage: "normalize",
|
|
107
|
+
branchSummary: "ignore",
|
|
108
|
+
compactionSummary: "ignore",
|
|
109
|
+
fileMention: "ignore",
|
|
110
|
+
} as const;
|
|
111
|
+
|
|
112
|
+
export const OMP_SESSION_EVENT_POLICIES = {
|
|
113
|
+
agent_start: "lifecycle",
|
|
114
|
+
agent_end: "lifecycle",
|
|
115
|
+
turn_start: "lifecycle",
|
|
116
|
+
turn_end: "lifecycle",
|
|
117
|
+
message_start: "normalize",
|
|
118
|
+
message_update: "normalize",
|
|
119
|
+
message_end: "normalize",
|
|
120
|
+
tool_execution_start: "normalize",
|
|
121
|
+
tool_execution_update: "normalize",
|
|
122
|
+
tool_stream_update: "normalize",
|
|
123
|
+
tool_execution_end: "normalize",
|
|
124
|
+
auto_compaction_start: "normalize",
|
|
125
|
+
auto_compaction_end: "normalize",
|
|
126
|
+
auto_retry_start: "normalize",
|
|
127
|
+
auto_retry_end: "normalize",
|
|
128
|
+
retry_fallback_applied: "normalize",
|
|
129
|
+
retry_fallback_succeeded: "normalize",
|
|
130
|
+
model_changed: "normalize",
|
|
131
|
+
config_warnings_changed: "ignore",
|
|
132
|
+
advisor_cost_changed: "ignore",
|
|
133
|
+
advisor_yielded: "normalize",
|
|
134
|
+
ttsr_triggered: "ignore",
|
|
135
|
+
todo_reminder: "normalize",
|
|
136
|
+
todo_auto_clear: "normalize",
|
|
137
|
+
irc_message: "normalize",
|
|
138
|
+
notice: "normalize",
|
|
139
|
+
thinking_level_changed: "normalize",
|
|
140
|
+
goal_updated: "normalize",
|
|
141
|
+
} as const;
|
|
142
|
+
|
|
143
|
+
export const OMP_RPC_COMMAND_POLICIES = {
|
|
144
|
+
negotiate_protocol: "implemented",
|
|
145
|
+
prompt: "implemented",
|
|
146
|
+
steer: "implemented",
|
|
147
|
+
follow_up: "implemented",
|
|
148
|
+
abort: "implemented",
|
|
149
|
+
abort_and_prompt: "unsupported",
|
|
150
|
+
new_session: "unsupported",
|
|
151
|
+
get_state: "implemented",
|
|
152
|
+
set_fast_mode: "unsupported",
|
|
153
|
+
get_available_commands: "implemented",
|
|
154
|
+
set_todos: "unsupported",
|
|
155
|
+
set_host_tools: "implemented",
|
|
156
|
+
set_host_uri_schemes: "unsupported",
|
|
157
|
+
set_subagent_subscription: "implemented",
|
|
158
|
+
get_subagents: "implemented",
|
|
159
|
+
get_subagent_messages: "implemented",
|
|
160
|
+
set_model: "implemented",
|
|
161
|
+
cycle_model: "unsupported",
|
|
162
|
+
get_available_models: "implemented",
|
|
163
|
+
set_thinking_level: "implemented",
|
|
164
|
+
cycle_thinking_level: "unsupported",
|
|
165
|
+
set_steering_mode: "unsupported",
|
|
166
|
+
set_follow_up_mode: "unsupported",
|
|
167
|
+
set_interrupt_mode: "unsupported",
|
|
168
|
+
compact: "implemented",
|
|
169
|
+
set_auto_compaction: "implemented",
|
|
170
|
+
set_auto_retry: "unsupported",
|
|
171
|
+
abort_retry: "unsupported",
|
|
172
|
+
bash: "unsupported",
|
|
173
|
+
abort_bash: "unsupported",
|
|
174
|
+
get_session_stats: "implemented",
|
|
175
|
+
export_html: "unsupported",
|
|
176
|
+
switch_session: "unsupported",
|
|
177
|
+
branch: "implemented",
|
|
178
|
+
get_branch_messages: "implemented",
|
|
179
|
+
get_last_assistant_text: "unsupported",
|
|
180
|
+
set_session_name: "unsupported",
|
|
181
|
+
handoff: "implemented",
|
|
182
|
+
get_messages: "implemented",
|
|
183
|
+
get_messages_page: "implemented",
|
|
184
|
+
get_login_providers: "unsupported",
|
|
185
|
+
login: "unsupported",
|
|
186
|
+
} as const;
|
|
187
|
+
export const OMP_RPC_RESPONSE_POLICIES = OMP_RPC_COMMAND_POLICIES;
|
|
188
|
+
export const OMP_PROTOCOL_VIOLATION_CATEGORIES = [
|
|
189
|
+
"duplicate-ready",
|
|
190
|
+
"frame-limit",
|
|
191
|
+
"incomplete-frame",
|
|
192
|
+
"interleaved-chunk",
|
|
193
|
+
"invalid-chunk",
|
|
194
|
+
"invalid-envelope",
|
|
195
|
+
"invalid-event",
|
|
196
|
+
"invalid-event-state",
|
|
197
|
+
"invalid-json",
|
|
198
|
+
"invalid-ready",
|
|
199
|
+
"invalid-response",
|
|
200
|
+
"remote-frame-error",
|
|
201
|
+
] as const;
|
|
202
|
+
export type OmpProtocolViolationCategory = (typeof OMP_PROTOCOL_VIOLATION_CATEGORIES)[number];
|
|
203
|
+
export const OMP_PROTOCOL_VIOLATION_REASONS = [
|
|
204
|
+
"output-ended-mid-frame",
|
|
205
|
+
"physical-frame-limit",
|
|
206
|
+
"semantic-frame-limit",
|
|
207
|
+
"json-decode",
|
|
208
|
+
"chunk-json-decode",
|
|
209
|
+
"object-envelope",
|
|
210
|
+
"missing-frame-type",
|
|
211
|
+
"response-schema",
|
|
212
|
+
"chunk-schema",
|
|
213
|
+
"chunk-metadata",
|
|
214
|
+
"chunk-size",
|
|
215
|
+
"chunk-start-index",
|
|
216
|
+
"chunk-sequence",
|
|
217
|
+
"chunk-byte-count",
|
|
218
|
+
"chunk-timeout",
|
|
219
|
+
"frame-interleaved-with-chunk",
|
|
220
|
+
"remote-frame-error",
|
|
221
|
+
"ready-already-received",
|
|
222
|
+
"ready-schema",
|
|
223
|
+
"notice-level-type",
|
|
224
|
+
"notice-message-type",
|
|
225
|
+
"notice-schema",
|
|
226
|
+
"event-schema",
|
|
227
|
+
"message-event-schema",
|
|
228
|
+
"tool-event-schema",
|
|
229
|
+
"lifecycle-event-schema",
|
|
230
|
+
"subagent-event-schema",
|
|
231
|
+
"configuration-event-schema",
|
|
232
|
+
"extension-ui-schema",
|
|
233
|
+
"unknown-event-type",
|
|
234
|
+
"event-state-transition",
|
|
235
|
+
] as const;
|
|
236
|
+
export type OmpProtocolViolationReason = (typeof OMP_PROTOCOL_VIOLATION_REASONS)[number];
|
|
237
|
+
export const OMP_PROTOCOL_DIAGNOSTIC_PHASES = [
|
|
238
|
+
"startup",
|
|
239
|
+
"negotiation",
|
|
240
|
+
"idle",
|
|
241
|
+
"active-turn",
|
|
242
|
+
"closing",
|
|
243
|
+
] as const;
|
|
244
|
+
export type OmpProtocolDiagnosticPhase = (typeof OMP_PROTOCOL_DIAGNOSTIC_PHASES)[number];
|
|
245
|
+
|
|
246
|
+
export const OMP_PROTOCOL_EVENT_TYPES = [
|
|
247
|
+
"agent_start",
|
|
248
|
+
"agent_end",
|
|
249
|
+
"turn_start",
|
|
250
|
+
"turn_end",
|
|
251
|
+
"message_start",
|
|
252
|
+
"message_update",
|
|
253
|
+
"message_end",
|
|
254
|
+
"tool_execution_start",
|
|
255
|
+
"tool_execution_update",
|
|
256
|
+
"tool_execution_end",
|
|
257
|
+
"tool_stream_update",
|
|
258
|
+
"compaction_start",
|
|
259
|
+
"compaction_end",
|
|
260
|
+
"subagent_lifecycle",
|
|
261
|
+
"subagent_progress",
|
|
262
|
+
"subagent_event",
|
|
263
|
+
"todo_reminder",
|
|
264
|
+
"model_changed",
|
|
265
|
+
"config_warnings_changed",
|
|
266
|
+
"advisor_cost_changed",
|
|
267
|
+
"ttsr_triggered",
|
|
268
|
+
"irc_message",
|
|
269
|
+
"thinking_level_changed",
|
|
270
|
+
"goal_updated",
|
|
271
|
+
"auto_retry_start",
|
|
272
|
+
"auto_retry_end",
|
|
273
|
+
"retry_fallback_applied",
|
|
274
|
+
"retry_fallback_succeeded",
|
|
275
|
+
"todo_auto_clear",
|
|
276
|
+
"auto_compaction_start",
|
|
277
|
+
"auto_compaction_end",
|
|
278
|
+
"available_commands_update",
|
|
279
|
+
"notice",
|
|
280
|
+
"command_output",
|
|
281
|
+
"extension_ui_request",
|
|
282
|
+
"prompt_result",
|
|
283
|
+
"host_tool_call",
|
|
284
|
+
"host_tool_cancel",
|
|
285
|
+
"tool_approval_request",
|
|
286
|
+
"tool_approval_cancel",
|
|
287
|
+
"advisor_yielded",
|
|
288
|
+
] as const;
|
|
289
|
+
export type OmpProtocolEventType = (typeof OMP_PROTOCOL_EVENT_TYPES)[number];
|
|
290
|
+
|
|
291
|
+
export const OMP_PROTOCOL_DIAGNOSTIC_FIELDS = [
|
|
292
|
+
"frame",
|
|
293
|
+
"frame.byteLength",
|
|
294
|
+
"frame.type",
|
|
295
|
+
"response",
|
|
296
|
+
"ready",
|
|
297
|
+
"event",
|
|
298
|
+
"event.sequence",
|
|
299
|
+
"notice.level",
|
|
300
|
+
"notice.message",
|
|
301
|
+
"chunk",
|
|
302
|
+
"chunk.data",
|
|
303
|
+
"chunk.index",
|
|
304
|
+
"chunk.sequence",
|
|
305
|
+
"chunk.byteLength",
|
|
306
|
+
] as const;
|
|
307
|
+
export type OmpProtocolDiagnosticField = (typeof OMP_PROTOCOL_DIAGNOSTIC_FIELDS)[number];
|
|
308
|
+
|
|
309
|
+
export const OMP_PROTOCOL_DIAGNOSTIC_EXPECTATIONS = [
|
|
310
|
+
"complete-json-line",
|
|
311
|
+
"within-byte-limit",
|
|
312
|
+
"valid-json",
|
|
313
|
+
"object-envelope",
|
|
314
|
+
"bounded-frame-type",
|
|
315
|
+
"valid-response-frame",
|
|
316
|
+
"valid-ready-frame",
|
|
317
|
+
"single-ready-frame",
|
|
318
|
+
"valid-event-frame",
|
|
319
|
+
"valid-event-state-transition",
|
|
320
|
+
"valid-message-event",
|
|
321
|
+
"valid-tool-event",
|
|
322
|
+
"valid-lifecycle-event",
|
|
323
|
+
"valid-subagent-event",
|
|
324
|
+
"valid-configuration-event",
|
|
325
|
+
"valid-extension-ui-event",
|
|
326
|
+
"known-event-type",
|
|
327
|
+
"notice-level-enum",
|
|
328
|
+
"notice-message-string",
|
|
329
|
+
"valid-chunk-frame",
|
|
330
|
+
"valid-base64-chunk",
|
|
331
|
+
"first-chunk-index-zero",
|
|
332
|
+
"contiguous-chunk-sequence",
|
|
333
|
+
"declared-chunk-byte-count",
|
|
334
|
+
"chunk-before-deadline",
|
|
335
|
+
"no-interleaved-frame",
|
|
336
|
+
"no-remote-frame-error",
|
|
337
|
+
] as const;
|
|
338
|
+
export type OmpProtocolDiagnosticExpectation =
|
|
339
|
+
(typeof OMP_PROTOCOL_DIAGNOSTIC_EXPECTATIONS)[number];
|
|
340
|
+
|
|
341
|
+
export const OMP_PROTOCOL_DIAGNOSTIC_ACTUAL_TYPES = [
|
|
342
|
+
"missing",
|
|
343
|
+
"null",
|
|
344
|
+
"array",
|
|
345
|
+
"object",
|
|
346
|
+
"string",
|
|
347
|
+
"number",
|
|
348
|
+
"boolean",
|
|
349
|
+
"invalid-json",
|
|
350
|
+
"partial-frame",
|
|
351
|
+
"oversized",
|
|
352
|
+
"duplicate",
|
|
353
|
+
"interleaved",
|
|
354
|
+
"out-of-order",
|
|
355
|
+
"timeout",
|
|
356
|
+
"remote-error",
|
|
357
|
+
"mismatched",
|
|
358
|
+
] as const;
|
|
359
|
+
export type OmpProtocolDiagnosticActualType = (typeof OMP_PROTOCOL_DIAGNOSTIC_ACTUAL_TYPES)[number];
|
|
360
|
+
|
|
361
|
+
export interface OmpProtocolViolationDiagnostic {
|
|
362
|
+
category: OmpProtocolViolationCategory;
|
|
363
|
+
reason: OmpProtocolViolationReason;
|
|
364
|
+
phase: OmpProtocolDiagnosticPhase;
|
|
365
|
+
occurrenceCount: number;
|
|
366
|
+
eventType?: OmpProtocolEventType;
|
|
367
|
+
frameType?: "ready" | "response" | "rpc_chunk" | "rpc_frame_error" | "notice";
|
|
368
|
+
field?: OmpProtocolDiagnosticField;
|
|
369
|
+
expected?: OmpProtocolDiagnosticExpectation;
|
|
370
|
+
actualType?: OmpProtocolDiagnosticActualType;
|
|
371
|
+
maxByteSize?: number;
|
|
372
|
+
limitBytes?: number;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function protocolDiagnosticActualType(value: unknown): OmpProtocolDiagnosticActualType {
|
|
376
|
+
if (value === undefined) return "missing";
|
|
377
|
+
if (value === null) return "null";
|
|
378
|
+
if (Array.isArray(value)) return "array";
|
|
379
|
+
if (typeof value === "object") return "object";
|
|
380
|
+
if (typeof value === "string") return "string";
|
|
381
|
+
if (typeof value === "number") return "number";
|
|
382
|
+
if (typeof value === "boolean") return "boolean";
|
|
383
|
+
return "object";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function isProtocolEventType(value: string): value is OmpProtocolEventType {
|
|
387
|
+
return (OMP_PROTOCOL_EVENT_TYPES as readonly string[]).includes(value);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function eventSchemaClassification(
|
|
391
|
+
eventType: OmpProtocolEventType,
|
|
392
|
+
): Pick<OmpProtocolViolationDiagnostic, "reason" | "expected"> {
|
|
393
|
+
switch (eventType) {
|
|
394
|
+
case "message_start":
|
|
395
|
+
case "message_update":
|
|
396
|
+
case "message_end":
|
|
397
|
+
return { reason: "message-event-schema", expected: "valid-message-event" };
|
|
398
|
+
case "irc_message":
|
|
399
|
+
return { reason: "message-event-schema", expected: "valid-message-event" };
|
|
400
|
+
case "tool_execution_start":
|
|
401
|
+
case "tool_execution_update":
|
|
402
|
+
case "tool_stream_update":
|
|
403
|
+
case "tool_execution_end":
|
|
404
|
+
case "host_tool_call":
|
|
405
|
+
case "host_tool_cancel":
|
|
406
|
+
case "tool_approval_request":
|
|
407
|
+
case "tool_approval_cancel":
|
|
408
|
+
return { reason: "tool-event-schema", expected: "valid-tool-event" };
|
|
409
|
+
case "subagent_lifecycle":
|
|
410
|
+
case "subagent_progress":
|
|
411
|
+
case "subagent_event":
|
|
412
|
+
return { reason: "subagent-event-schema", expected: "valid-subagent-event" };
|
|
413
|
+
case "model_changed":
|
|
414
|
+
case "thinking_level_changed":
|
|
415
|
+
case "goal_updated":
|
|
416
|
+
case "todo_reminder":
|
|
417
|
+
case "todo_auto_clear":
|
|
418
|
+
case "available_commands_update":
|
|
419
|
+
case "retry_fallback_applied":
|
|
420
|
+
case "retry_fallback_succeeded":
|
|
421
|
+
case "config_warnings_changed":
|
|
422
|
+
case "advisor_cost_changed":
|
|
423
|
+
case "ttsr_triggered":
|
|
424
|
+
return { reason: "configuration-event-schema", expected: "valid-configuration-event" };
|
|
425
|
+
case "extension_ui_request":
|
|
426
|
+
return { reason: "extension-ui-schema", expected: "valid-extension-ui-event" };
|
|
427
|
+
default:
|
|
428
|
+
return { reason: "lifecycle-event-schema", expected: "valid-lifecycle-event" };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function invalidEventDiagnosticMetadata(
|
|
433
|
+
frame: Record<string, unknown>,
|
|
434
|
+
type: string,
|
|
435
|
+
): Pick<
|
|
436
|
+
OmpProtocolViolationDiagnostic,
|
|
437
|
+
"reason" | "eventType" | "frameType" | "field" | "expected" | "actualType"
|
|
438
|
+
> {
|
|
439
|
+
if (!isProtocolEventType(type)) {
|
|
440
|
+
return {
|
|
441
|
+
reason: "unknown-event-type",
|
|
442
|
+
field: "frame.type",
|
|
443
|
+
expected: "known-event-type",
|
|
444
|
+
actualType: "string",
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (type !== "notice") {
|
|
448
|
+
return {
|
|
449
|
+
...eventSchemaClassification(type),
|
|
450
|
+
eventType: type,
|
|
451
|
+
field: "event",
|
|
452
|
+
actualType: "object",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (typeof frame.level !== "string") {
|
|
456
|
+
return {
|
|
457
|
+
reason: "notice-level-type",
|
|
458
|
+
eventType: "notice",
|
|
459
|
+
frameType: "notice",
|
|
460
|
+
field: "notice.level",
|
|
461
|
+
expected: "notice-level-enum",
|
|
462
|
+
actualType: protocolDiagnosticActualType(frame.level),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (typeof frame.message !== "string") {
|
|
466
|
+
return {
|
|
467
|
+
reason: "notice-message-type",
|
|
468
|
+
eventType: "notice",
|
|
469
|
+
frameType: "notice",
|
|
470
|
+
field: "notice.message",
|
|
471
|
+
expected: "notice-message-string",
|
|
472
|
+
actualType: protocolDiagnosticActualType(frame.message),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
reason: "notice-schema",
|
|
477
|
+
eventType: "notice",
|
|
478
|
+
frameType: "notice",
|
|
479
|
+
field: "event",
|
|
480
|
+
expected: "valid-event-frame",
|
|
481
|
+
actualType: "object",
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
type PendingProtocolViolation = Omit<OmpProtocolViolationDiagnostic, "occurrenceCount"> & {
|
|
486
|
+
occurrenceCount: number;
|
|
487
|
+
};
|
|
488
|
+
type ProtocolViolationKey = `${OmpProtocolViolationCategory}:${OmpProtocolViolationReason}`;
|
|
73
489
|
const MAX_CONTEXT_PERCENT = 1_000_000;
|
|
74
490
|
function boundedJsonString(maxBytes: number, minBytes = 0) {
|
|
75
491
|
return z.string().refine((value) => {
|
|
@@ -101,12 +517,101 @@ function boundedString(maxBytes: number, minBytes = 0) {
|
|
|
101
517
|
return bytes >= minBytes && bytes <= maxBytes;
|
|
102
518
|
});
|
|
103
519
|
}
|
|
520
|
+
function isSafeOpaqueCursor(value: string): boolean {
|
|
521
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
522
|
+
const code = value.charCodeAt(index);
|
|
523
|
+
if (code <= 0x1f || code === 0x7f) return false;
|
|
524
|
+
}
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
function boundedOptionalStringArray(maxItems: number, maxBytes: number) {
|
|
528
|
+
return z.unknown().transform((value): string[] | undefined => {
|
|
529
|
+
if (!Array.isArray(value)) return undefined;
|
|
530
|
+
return value
|
|
531
|
+
.filter(
|
|
532
|
+
(item): item is string =>
|
|
533
|
+
typeof item === "string" && item.length > 0 && utf8Bytes(item) <= maxBytes,
|
|
534
|
+
)
|
|
535
|
+
.slice(0, maxItems);
|
|
536
|
+
});
|
|
537
|
+
}
|
|
104
538
|
|
|
105
539
|
const IDENTIFIER = boundedString(MAX_ID_LENGTH, 1);
|
|
106
540
|
const NAME = boundedString(MAX_NAME_LENGTH, 1);
|
|
107
541
|
const OMP_PROVIDER_NAME = NAME.refine((provider) => !provider.includes("/"));
|
|
108
542
|
const TEXT = boundedString(MAX_TEXT_LENGTH);
|
|
543
|
+
const RAW_DISPLAY_TEXT = boundedString(MAX_IMAGE_DATA_LENGTH);
|
|
544
|
+
const DISPLAY_TRUNCATION_MARKER = "<truncated>";
|
|
545
|
+
|
|
546
|
+
function boundRawDisplayContent(value: unknown): unknown {
|
|
547
|
+
if (typeof value === "string") {
|
|
548
|
+
return utf8Bytes(value) <= MAX_IMAGE_DATA_LENGTH ? value : DISPLAY_TRUNCATION_MARKER;
|
|
549
|
+
}
|
|
550
|
+
if (!Array.isArray(value)) return value;
|
|
551
|
+
let totalBytes = 0;
|
|
552
|
+
for (const part of value) {
|
|
553
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) continue;
|
|
554
|
+
const record = part as Record<string, unknown>;
|
|
555
|
+
if (typeof record.text === "string") totalBytes += utf8Bytes(record.text);
|
|
556
|
+
if (typeof record.thinking === "string") totalBytes += utf8Bytes(record.thinking);
|
|
557
|
+
}
|
|
558
|
+
if (totalBytes <= MAX_IMAGE_DATA_LENGTH) return value;
|
|
559
|
+
|
|
560
|
+
let retainedMarker = false;
|
|
561
|
+
return value.map((part) => {
|
|
562
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) return part;
|
|
563
|
+
const copy = { ...(part as Record<string, unknown>) };
|
|
564
|
+
for (const key of ["text", "thinking"] as const) {
|
|
565
|
+
if (typeof copy[key] !== "string") continue;
|
|
566
|
+
if (retainedMarker) delete copy[key];
|
|
567
|
+
else {
|
|
568
|
+
copy[key] = DISPLAY_TRUNCATION_MARKER;
|
|
569
|
+
retainedMarker = true;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return copy;
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function sanitizeLiveMessageDisplay(value: unknown): unknown {
|
|
577
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
578
|
+
const message = value as Record<string, unknown>;
|
|
579
|
+
if (message.role === "assistant") {
|
|
580
|
+
const content = boundRawDisplayContent(message.content);
|
|
581
|
+
return content === message.content ? value : { ...message, content };
|
|
582
|
+
}
|
|
583
|
+
if (
|
|
584
|
+
(message.role === "bashExecution" || message.role === "pythonExecution") &&
|
|
585
|
+
typeof message.output === "string" &&
|
|
586
|
+
utf8Bytes(message.output) > MAX_IMAGE_DATA_LENGTH
|
|
587
|
+
) {
|
|
588
|
+
return { ...message, output: DISPLAY_TRUNCATION_MARKER };
|
|
589
|
+
}
|
|
590
|
+
return value;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function sanitizeLiveDisplayFrame(value: unknown): unknown {
|
|
594
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
595
|
+
const frame = value as Record<string, unknown>;
|
|
596
|
+
if (
|
|
597
|
+
frame.type === "message_start" ||
|
|
598
|
+
frame.type === "message_update" ||
|
|
599
|
+
frame.type === "message_end"
|
|
600
|
+
) {
|
|
601
|
+
const message = sanitizeLiveMessageDisplay(frame.message);
|
|
602
|
+
return message === frame.message ? value : { ...frame, message };
|
|
603
|
+
}
|
|
604
|
+
if (frame.type !== "agent_end" || !Array.isArray(frame.messages)) return value;
|
|
605
|
+
let changed = false;
|
|
606
|
+
const messages = frame.messages.map((message) => {
|
|
607
|
+
const sanitized = sanitizeLiveMessageDisplay(message);
|
|
608
|
+
if (sanitized !== message) changed = true;
|
|
609
|
+
return sanitized;
|
|
610
|
+
});
|
|
611
|
+
return changed ? { ...frame, messages } : value;
|
|
612
|
+
}
|
|
109
613
|
const OmpThinkingLevelSchema = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
614
|
+
const OmpCurrentThinkingLevelSchema = boundedString(32).optional().catch(undefined);
|
|
110
615
|
|
|
111
616
|
function isBoundedJson(
|
|
112
617
|
value: unknown,
|
|
@@ -118,6 +623,320 @@ function isBoundedJson(
|
|
|
118
623
|
boundedJsonBytes(value, maxBytes, maxItems, maxBytes, maxNodes) !== Number.POSITIVE_INFINITY
|
|
119
624
|
);
|
|
120
625
|
}
|
|
626
|
+
const MAX_RESPONSE_DEPTH = 16;
|
|
627
|
+
export const OMP_RPC_BOUND_DIMENSIONS = [
|
|
628
|
+
"bytes",
|
|
629
|
+
"items",
|
|
630
|
+
"nodes",
|
|
631
|
+
"depth",
|
|
632
|
+
"string-bytes",
|
|
633
|
+
] as const;
|
|
634
|
+
export type OmpRpcBoundDimension = (typeof OMP_RPC_BOUND_DIMENSIONS)[number];
|
|
635
|
+
export const OMP_RPC_DIAGNOSTIC_COMMANDS = [
|
|
636
|
+
"negotiate_protocol",
|
|
637
|
+
"get_state",
|
|
638
|
+
"get_session_stats",
|
|
639
|
+
"get_available_models",
|
|
640
|
+
"get_available_commands",
|
|
641
|
+
"set_subagent_subscription",
|
|
642
|
+
"get_subagents",
|
|
643
|
+
"get_subagent_messages",
|
|
644
|
+
"prompt",
|
|
645
|
+
"compact",
|
|
646
|
+
"set_auto_compaction",
|
|
647
|
+
"set_model",
|
|
648
|
+
"set_thinking_level",
|
|
649
|
+
"handoff",
|
|
650
|
+
"get_branch_messages",
|
|
651
|
+
"branch",
|
|
652
|
+
"get_messages_page",
|
|
653
|
+
"get_messages",
|
|
654
|
+
"abort",
|
|
655
|
+
"set_host_tools",
|
|
656
|
+
] as const;
|
|
657
|
+
export type OmpRpcDiagnosticCommand = (typeof OMP_RPC_DIAGNOSTIC_COMMANDS)[number];
|
|
658
|
+
type JsonBoundViolation = {
|
|
659
|
+
dimension: OmpRpcBoundDimension;
|
|
660
|
+
actual: number;
|
|
661
|
+
limit: number;
|
|
662
|
+
};
|
|
663
|
+
type JsonBoundInspection = { bytes: number; nodes: number; violation?: JsonBoundViolation };
|
|
664
|
+
|
|
665
|
+
function rpcDiagnosticCommand(value: string): OmpRpcDiagnosticCommand | undefined {
|
|
666
|
+
return (OMP_RPC_DIAGNOSTIC_COMMANDS as readonly string[]).includes(value)
|
|
667
|
+
? (value as OmpRpcDiagnosticCommand)
|
|
668
|
+
: undefined;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function encodedJsonStringBytes(value: string): number {
|
|
672
|
+
let bytes = 2;
|
|
673
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
674
|
+
const code = value.charCodeAt(index);
|
|
675
|
+
if (code === 0x22 || code === 0x5c) {
|
|
676
|
+
bytes += 2;
|
|
677
|
+
} else if (code <= 0x1f) {
|
|
678
|
+
bytes +=
|
|
679
|
+
code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d ? 2 : 6;
|
|
680
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
681
|
+
const low = value.charCodeAt(index + 1);
|
|
682
|
+
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
683
|
+
bytes += 4;
|
|
684
|
+
index += 1;
|
|
685
|
+
} else {
|
|
686
|
+
bytes += 6;
|
|
687
|
+
}
|
|
688
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
689
|
+
bytes += 6;
|
|
690
|
+
} else {
|
|
691
|
+
bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : 3;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return bytes;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function inspectJsonBounds(
|
|
698
|
+
value: unknown,
|
|
699
|
+
maxBytes: number,
|
|
700
|
+
maxItems: number,
|
|
701
|
+
maxStringBytes: number,
|
|
702
|
+
maxNodes: number,
|
|
703
|
+
): JsonBoundInspection {
|
|
704
|
+
const stack: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }];
|
|
705
|
+
let nodes = 0;
|
|
706
|
+
let bytes = 0;
|
|
707
|
+
const addBytes = (count: number): JsonBoundViolation | undefined => {
|
|
708
|
+
bytes += count;
|
|
709
|
+
return bytes > maxBytes ? { dimension: "bytes", actual: bytes, limit: maxBytes } : undefined;
|
|
710
|
+
};
|
|
711
|
+
while (stack.length > 0) {
|
|
712
|
+
const current = stack.pop();
|
|
713
|
+
if (!current) break;
|
|
714
|
+
nodes += 1;
|
|
715
|
+
if (nodes > maxNodes)
|
|
716
|
+
return { bytes, nodes, violation: { dimension: "nodes", actual: nodes, limit: maxNodes } };
|
|
717
|
+
if (current.depth > MAX_RESPONSE_DEPTH) {
|
|
718
|
+
return {
|
|
719
|
+
bytes,
|
|
720
|
+
nodes,
|
|
721
|
+
violation: { dimension: "depth", actual: current.depth, limit: MAX_RESPONSE_DEPTH },
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
const item = current.value;
|
|
725
|
+
if (item === null) {
|
|
726
|
+
const violation = addBytes(4);
|
|
727
|
+
if (violation) return { bytes, nodes, violation };
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (typeof item === "boolean") {
|
|
731
|
+
const violation = addBytes(item ? 4 : 5);
|
|
732
|
+
if (violation) return { bytes, nodes, violation };
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (typeof item === "number") {
|
|
736
|
+
const violation = addBytes((JSON.stringify(item) ?? "null").length);
|
|
737
|
+
if (violation) return { bytes, nodes, violation };
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (typeof item === "string") {
|
|
741
|
+
const itemBytes = utf8Bytes(item);
|
|
742
|
+
if (itemBytes > maxStringBytes) {
|
|
743
|
+
return {
|
|
744
|
+
bytes,
|
|
745
|
+
nodes,
|
|
746
|
+
violation: { dimension: "string-bytes", actual: itemBytes, limit: maxStringBytes },
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
const violation = addBytes(encodedJsonStringBytes(item));
|
|
750
|
+
if (violation) return { bytes, nodes, violation };
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (typeof item !== "object") continue;
|
|
754
|
+
if (Array.isArray(item)) {
|
|
755
|
+
if (item.length > maxItems) {
|
|
756
|
+
return {
|
|
757
|
+
bytes,
|
|
758
|
+
nodes,
|
|
759
|
+
violation: { dimension: "items", actual: item.length, limit: maxItems },
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
const violation = addBytes(2 + Math.max(0, item.length - 1));
|
|
763
|
+
if (violation) return { bytes, nodes, violation };
|
|
764
|
+
for (let index = item.length - 1; index >= 0; index -= 1) {
|
|
765
|
+
stack.push({ value: item[index], depth: current.depth + 1 });
|
|
766
|
+
}
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const entries = Object.entries(item).filter(([, child]) => child !== undefined);
|
|
770
|
+
if (entries.length > maxItems) {
|
|
771
|
+
return {
|
|
772
|
+
bytes,
|
|
773
|
+
nodes,
|
|
774
|
+
violation: { dimension: "items", actual: entries.length, limit: maxItems },
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
let violation = addBytes(2 + Math.max(0, entries.length - 1) + entries.length);
|
|
778
|
+
if (violation) return { bytes, nodes, violation };
|
|
779
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
780
|
+
const [key, child] = entries[index] as [string, unknown];
|
|
781
|
+
violation = addBytes(encodedJsonStringBytes(key));
|
|
782
|
+
if (violation) return { bytes, nodes, violation };
|
|
783
|
+
stack.push({ value: child, depth: current.depth + 1 });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return { bytes, nodes };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function jsonBoundViolation(
|
|
790
|
+
value: unknown,
|
|
791
|
+
maxBytes: number,
|
|
792
|
+
maxItems: number,
|
|
793
|
+
maxStringBytes: number,
|
|
794
|
+
maxNodes: number,
|
|
795
|
+
): JsonBoundViolation | undefined {
|
|
796
|
+
return inspectJsonBounds(value, maxBytes, maxItems, maxStringBytes, maxNodes).violation;
|
|
797
|
+
}
|
|
798
|
+
function optionalMetadataMetrics(value: unknown) {
|
|
799
|
+
return boundedJsonMetrics(
|
|
800
|
+
value,
|
|
801
|
+
MAX_OPTIONAL_METADATA_BYTES,
|
|
802
|
+
MAX_OPTIONAL_METADATA_ITEMS,
|
|
803
|
+
MAX_OPTIONAL_METADATA_BYTES,
|
|
804
|
+
MAX_OPTIONAL_METADATA_NODES,
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function optionalMetadataIsBounded(value: unknown): boolean {
|
|
809
|
+
return optionalMetadataMetrics(value) !== undefined;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function omitUnsafeOptionalDetails(value: unknown): unknown {
|
|
813
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
814
|
+
const record = value as Record<string, unknown>;
|
|
815
|
+
if (!Object.hasOwn(record, "details") || optionalMetadataIsBounded(record.details)) return value;
|
|
816
|
+
const { details: _details, ...safe } = record;
|
|
817
|
+
return safe;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const TASK_RESULT_STATUSES: Readonly<Record<string, true>> = {
|
|
821
|
+
pending: true,
|
|
822
|
+
running: true,
|
|
823
|
+
completed: true,
|
|
824
|
+
failed: true,
|
|
825
|
+
error: true,
|
|
826
|
+
aborted: true,
|
|
827
|
+
canceled: true,
|
|
828
|
+
cancelled: true,
|
|
829
|
+
};
|
|
830
|
+
const TASK_PROGRESS_STATUSES: Readonly<Record<string, true>> = {
|
|
831
|
+
pending: true,
|
|
832
|
+
running: true,
|
|
833
|
+
completed: true,
|
|
834
|
+
failed: true,
|
|
835
|
+
aborted: true,
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
function boundedTaskId(value: unknown): value is string {
|
|
839
|
+
return typeof value === "string" && value.length > 0 && utf8Bytes(value) <= MAX_ID_LENGTH;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function taskCorrelationDetails(value: unknown): unknown {
|
|
843
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
844
|
+
const details = value as Record<string, unknown>;
|
|
845
|
+
if (!Array.isArray(details.results) || details.results.length > MAX_TASK_CORRELATION_ITEMS)
|
|
846
|
+
return;
|
|
847
|
+
const results: Record<string, unknown>[] = [];
|
|
848
|
+
for (const value of details.results) {
|
|
849
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
850
|
+
const result = value as Record<string, unknown>;
|
|
851
|
+
if (!boundedTaskId(result.id)) return;
|
|
852
|
+
const safe: Record<string, unknown> = { id: result.id };
|
|
853
|
+
if (typeof result.status === "string" && Object.hasOwn(TASK_RESULT_STATUSES, result.status)) {
|
|
854
|
+
safe.status = result.status;
|
|
855
|
+
}
|
|
856
|
+
if (typeof result.aborted === "boolean") safe.aborted = result.aborted;
|
|
857
|
+
if (typeof result.exitCode === "number" && Number.isFinite(result.exitCode)) {
|
|
858
|
+
safe.exitCode = result.exitCode;
|
|
859
|
+
}
|
|
860
|
+
if (
|
|
861
|
+
result.error !== undefined &&
|
|
862
|
+
boundedJsonMetrics(result.error, 4_096, 32, 4_096, 64) !== undefined
|
|
863
|
+
) {
|
|
864
|
+
safe.error = result.error;
|
|
865
|
+
}
|
|
866
|
+
results.push(safe);
|
|
867
|
+
}
|
|
868
|
+
let progress: Record<string, unknown>[] | undefined;
|
|
869
|
+
if (details.progress !== undefined) {
|
|
870
|
+
if (!Array.isArray(details.progress) || details.progress.length > MAX_TASK_CORRELATION_ITEMS) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
progress = [];
|
|
874
|
+
for (const value of details.progress) {
|
|
875
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
876
|
+
const item = value as Record<string, unknown>;
|
|
877
|
+
if (
|
|
878
|
+
!boundedTaskId(item.id) ||
|
|
879
|
+
typeof item.index !== "number" ||
|
|
880
|
+
!Number.isInteger(item.index) ||
|
|
881
|
+
item.index < 0 ||
|
|
882
|
+
item.index >= MAX_TASK_CORRELATION_ITEMS ||
|
|
883
|
+
typeof item.status !== "string" ||
|
|
884
|
+
!Object.hasOwn(TASK_PROGRESS_STATUSES, item.status)
|
|
885
|
+
) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
progress.push({ id: item.id, index: item.index, status: item.status });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const correlation = { results, ...(progress ? { progress } : {}) };
|
|
892
|
+
return boundedJsonMetrics(
|
|
893
|
+
correlation,
|
|
894
|
+
MAX_TASK_CORRELATION_BYTES,
|
|
895
|
+
MAX_TASK_CORRELATION_ITEMS,
|
|
896
|
+
MAX_TASK_CORRELATION_BYTES,
|
|
897
|
+
MAX_TASK_CORRELATION_NODES,
|
|
898
|
+
)
|
|
899
|
+
? correlation
|
|
900
|
+
: undefined;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function omitOptionalDetails(value: unknown, preserveTaskCorrelation = false): unknown {
|
|
904
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
905
|
+
const record = value as Record<string, unknown>;
|
|
906
|
+
if (!Object.hasOwn(record, "details")) return value;
|
|
907
|
+
const { details: _details, ...structural } = record;
|
|
908
|
+
if (!preserveTaskCorrelation) return structural;
|
|
909
|
+
const correlation = taskCorrelationDetails(record.details);
|
|
910
|
+
return correlation === undefined ? structural : { ...structural, details: correlation };
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function createOptionalMetadataSanitizer(): (value: unknown, taskResult?: boolean) => unknown {
|
|
914
|
+
let retainedBytes = 0;
|
|
915
|
+
let retainedNodes = 0;
|
|
916
|
+
return (value, taskResult = false) => {
|
|
917
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
918
|
+
const record = value as Record<string, unknown>;
|
|
919
|
+
if (!Object.hasOwn(record, "details")) return value;
|
|
920
|
+
const metrics = optionalMetadataMetrics(record.details);
|
|
921
|
+
if (
|
|
922
|
+
metrics &&
|
|
923
|
+
retainedBytes + metrics.bytes <= MAX_OPTIONAL_METADATA_BYTES &&
|
|
924
|
+
retainedNodes + metrics.nodes <= MAX_OPTIONAL_METADATA_NODES
|
|
925
|
+
) {
|
|
926
|
+
retainedBytes += metrics.bytes;
|
|
927
|
+
retainedNodes += metrics.nodes;
|
|
928
|
+
return value;
|
|
929
|
+
}
|
|
930
|
+
const correlation = taskResult ? taskCorrelationDetails(record.details) : undefined;
|
|
931
|
+
const { details: _details, ...safe } = record;
|
|
932
|
+
return correlation === undefined ? safe : { ...safe, details: correlation };
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const OmpOptionalMetadataSchema = z.preprocess(
|
|
937
|
+
(value) => (optionalMetadataIsBounded(value) ? value : undefined),
|
|
938
|
+
z.unknown().optional(),
|
|
939
|
+
);
|
|
121
940
|
|
|
122
941
|
const OmpContentPartSchema = z
|
|
123
942
|
.object({
|
|
@@ -147,6 +966,14 @@ const OmpContentPartSchema = z
|
|
|
147
966
|
context.addIssue({ code: "custom", message: "invalid image payload" });
|
|
148
967
|
}
|
|
149
968
|
});
|
|
969
|
+
const OmpAssistantContentPartSchema = OmpContentPartSchema.safeExtend({
|
|
970
|
+
text: RAW_DISPLAY_TEXT.optional(),
|
|
971
|
+
thinking: RAW_DISPLAY_TEXT.optional(),
|
|
972
|
+
});
|
|
973
|
+
const OmpAssistantDisplayContentSchema = z.preprocess(
|
|
974
|
+
boundRawDisplayContent,
|
|
975
|
+
z.union([RAW_DISPLAY_TEXT, z.array(OmpAssistantContentPartSchema).max(OMP_MAX_CONTENT_PARTS)]),
|
|
976
|
+
);
|
|
150
977
|
const OmpDisplayContentSchema = z.union([
|
|
151
978
|
TEXT,
|
|
152
979
|
z.array(OmpContentPartSchema).max(OMP_MAX_CONTENT_PARTS),
|
|
@@ -165,60 +992,51 @@ const OmpMessageIdentityShape = {
|
|
|
165
992
|
responseId: IDENTIFIER.optional(),
|
|
166
993
|
images: OmpImageArraySchema.optional(),
|
|
167
994
|
timestamp: z.number().finite().optional(),
|
|
168
|
-
details:
|
|
169
|
-
.unknown()
|
|
170
|
-
.refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096))
|
|
171
|
-
.optional(),
|
|
995
|
+
details: OmpOptionalMetadataSchema,
|
|
172
996
|
};
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
997
|
+
const OmpOpaqueMessagePayloadSchema = z
|
|
998
|
+
.unknown()
|
|
999
|
+
.refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 8_192));
|
|
1000
|
+
const OmpExecutionOutputSchema = z.preprocess(
|
|
1001
|
+
(value) =>
|
|
1002
|
+
typeof value === "string" && utf8Bytes(value) > MAX_IMAGE_DATA_LENGTH
|
|
1003
|
+
? DISPLAY_TRUNCATION_MARKER
|
|
1004
|
+
: value,
|
|
1005
|
+
RAW_DISPLAY_TEXT.optional(),
|
|
1006
|
+
);
|
|
1007
|
+
const OmpExecutionMessageShape = {
|
|
1008
|
+
output: OmpExecutionOutputSchema,
|
|
1009
|
+
exitCode: z.number().int().nullable().optional(),
|
|
1010
|
+
cancelled: z.boolean().optional(),
|
|
1011
|
+
truncated: z.boolean().optional(),
|
|
1012
|
+
excludeFromContext: z.boolean().optional(),
|
|
1013
|
+
meta: OmpOptionalMetadataSchema,
|
|
189
1014
|
};
|
|
190
|
-
|
|
191
|
-
(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
output?: string;
|
|
211
|
-
exitCode?: number | null;
|
|
212
|
-
cancelled?: boolean;
|
|
213
|
-
truncated?: boolean;
|
|
214
|
-
}
|
|
215
|
-
| { role: "custom"; customType?: string; content?: unknown; display?: boolean }
|
|
216
|
-
);
|
|
1015
|
+
const OmpCustomMessageShape = {
|
|
1016
|
+
customType: NAME.optional(),
|
|
1017
|
+
content: OmpDisplayContentSchema.optional(),
|
|
1018
|
+
display: z.boolean().optional(),
|
|
1019
|
+
attribution: OmpOpaqueMessagePayloadSchema.optional(),
|
|
1020
|
+
};
|
|
1021
|
+
const OmpFileMentionSchema = z
|
|
1022
|
+
.object({
|
|
1023
|
+
path: boundedString(MAX_PATH_LENGTH),
|
|
1024
|
+
content: TEXT,
|
|
1025
|
+
lineCount: z.number().int().nonnegative().safe().optional(),
|
|
1026
|
+
byteSize: z.number().int().nonnegative().safe().optional(),
|
|
1027
|
+
skippedReason: z.enum(["tooLarge", "binary"]).optional(),
|
|
1028
|
+
image: OmpContentPartSchema.optional(),
|
|
1029
|
+
})
|
|
1030
|
+
.superRefine((file, context) => {
|
|
1031
|
+
if (file.image && file.image.type !== "image") {
|
|
1032
|
+
context.addIssue({ code: "custom", message: "invalid file mention image" });
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
217
1035
|
|
|
218
|
-
const OmpMessageSchema
|
|
1036
|
+
const OmpMessageSchema = z.discriminatedUnion("role", [
|
|
219
1037
|
z.object({
|
|
220
1038
|
role: z.literal("assistant"),
|
|
221
|
-
content:
|
|
1039
|
+
content: OmpAssistantDisplayContentSchema.optional(),
|
|
222
1040
|
...OmpMessageIdentityShape,
|
|
223
1041
|
errorMessage: boundedString(4_096).nullable().optional(),
|
|
224
1042
|
stopReason: boundedString(64).optional(),
|
|
@@ -228,36 +1046,70 @@ const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
|
|
|
228
1046
|
content: OmpDisplayContentSchema,
|
|
229
1047
|
...OmpMessageIdentityShape,
|
|
230
1048
|
}),
|
|
1049
|
+
z.object({
|
|
1050
|
+
role: z.literal("developer"),
|
|
1051
|
+
content: OmpDisplayContentSchema,
|
|
1052
|
+
attribution: OmpOpaqueMessagePayloadSchema.optional(),
|
|
1053
|
+
synthetic: z.boolean().optional(),
|
|
1054
|
+
userInitiated: z.boolean().optional(),
|
|
1055
|
+
providerPayload: OmpOpaqueMessagePayloadSchema.optional(),
|
|
1056
|
+
...OmpMessageIdentityShape,
|
|
1057
|
+
}),
|
|
231
1058
|
z.object({
|
|
232
1059
|
role: z.literal("toolResult"),
|
|
233
1060
|
toolCallId: IDENTIFIER,
|
|
234
1061
|
toolName: NAME,
|
|
235
|
-
content:
|
|
236
|
-
.unknown()
|
|
237
|
-
.refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 8_192)),
|
|
1062
|
+
content: OmpOpaqueMessagePayloadSchema,
|
|
238
1063
|
isError: z.boolean().optional(),
|
|
1064
|
+
attribution: OmpOpaqueMessagePayloadSchema.optional(),
|
|
1065
|
+
prunedAt: z.number().finite().nonnegative().optional(),
|
|
1066
|
+
providerMetadata: OmpOpaqueMessagePayloadSchema.optional(),
|
|
1067
|
+
useless: z.boolean().optional(),
|
|
239
1068
|
...OmpMessageIdentityShape,
|
|
240
1069
|
}),
|
|
241
1070
|
z.object({
|
|
242
1071
|
role: z.literal("bashExecution"),
|
|
243
1072
|
command: TEXT,
|
|
244
|
-
|
|
245
|
-
exitCode: z.number().int().nullable().optional(),
|
|
246
|
-
cancelled: z.boolean().optional(),
|
|
247
|
-
truncated: z.boolean().optional(),
|
|
1073
|
+
...OmpExecutionMessageShape,
|
|
248
1074
|
...OmpMessageIdentityShape,
|
|
249
1075
|
}),
|
|
250
1076
|
z.object({
|
|
251
|
-
role: z.literal("
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
1077
|
+
role: z.literal("pythonExecution"),
|
|
1078
|
+
code: TEXT,
|
|
1079
|
+
...OmpExecutionMessageShape,
|
|
1080
|
+
...OmpMessageIdentityShape,
|
|
1081
|
+
}),
|
|
1082
|
+
z.object({ role: z.literal("custom"), ...OmpCustomMessageShape, ...OmpMessageIdentityShape }),
|
|
1083
|
+
z.object({
|
|
1084
|
+
role: z.literal("hookMessage"),
|
|
1085
|
+
...OmpCustomMessageShape,
|
|
1086
|
+
...OmpMessageIdentityShape,
|
|
1087
|
+
}),
|
|
1088
|
+
z.object({
|
|
1089
|
+
role: z.literal("branchSummary"),
|
|
1090
|
+
summary: TEXT,
|
|
1091
|
+
fromId: IDENTIFIER,
|
|
1092
|
+
...OmpMessageIdentityShape,
|
|
1093
|
+
}),
|
|
1094
|
+
z.object({
|
|
1095
|
+
role: z.literal("compactionSummary"),
|
|
1096
|
+
summary: TEXT,
|
|
1097
|
+
shortSummary: TEXT.optional(),
|
|
1098
|
+
tokensBefore: z.number().finite().nonnegative(),
|
|
1099
|
+
tokensAfter: z.number().finite().nonnegative().optional(),
|
|
1100
|
+
method: NAME.optional(),
|
|
1101
|
+
providerPayload: OmpOpaqueMessagePayloadSchema.optional(),
|
|
1102
|
+
blocks: z.array(OmpContentPartSchema).max(OMP_MAX_CONTENT_PARTS).optional(),
|
|
1103
|
+
warning: boundedString(64 * 1024).optional(),
|
|
1104
|
+
...OmpMessageIdentityShape,
|
|
1105
|
+
}),
|
|
1106
|
+
z.object({
|
|
1107
|
+
role: z.literal("fileMention"),
|
|
1108
|
+
files: z.array(OmpFileMentionSchema).max(MAX_ARRAY_ITEMS),
|
|
258
1109
|
...OmpMessageIdentityShape,
|
|
259
1110
|
}),
|
|
260
1111
|
]);
|
|
1112
|
+
export type OmpMessage = z.infer<typeof OmpMessageSchema>;
|
|
261
1113
|
|
|
262
1114
|
const OmpAssistantMessageEventSchema = z
|
|
263
1115
|
.object({
|
|
@@ -309,19 +1161,33 @@ const OmpAvailableCommandSchema = z.object({
|
|
|
309
1161
|
.optional(),
|
|
310
1162
|
source: boundedString(64).optional(),
|
|
311
1163
|
});
|
|
1164
|
+
const OmpThinkingMetadataSchema = z
|
|
1165
|
+
.unknown()
|
|
1166
|
+
.transform((value): { efforts?: string[]; defaultLevel?: string } | undefined => {
|
|
1167
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1168
|
+
const record = value as Record<string, unknown>;
|
|
1169
|
+
const efforts = boundedOptionalStringArray(16, 32).parse(record.efforts);
|
|
1170
|
+
const defaultLevel = boundedString(32).optional().catch(undefined).parse(record.defaultLevel);
|
|
1171
|
+
return {
|
|
1172
|
+
...(efforts !== undefined ? { efforts } : {}),
|
|
1173
|
+
...(defaultLevel !== undefined ? { defaultLevel } : {}),
|
|
1174
|
+
};
|
|
1175
|
+
});
|
|
312
1176
|
const OmpModelSchema = z.object({
|
|
313
1177
|
provider: OMP_PROVIDER_NAME,
|
|
314
1178
|
id: NAME,
|
|
315
|
-
name: boundedString(MAX_NAME_LENGTH).optional(),
|
|
316
|
-
reasoning: z.boolean().optional(),
|
|
317
|
-
thinking:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
.
|
|
323
|
-
|
|
324
|
-
|
|
1179
|
+
name: boundedString(MAX_NAME_LENGTH).optional().catch(undefined),
|
|
1180
|
+
reasoning: z.boolean().optional().catch(undefined),
|
|
1181
|
+
thinking: OmpThinkingMetadataSchema.optional(),
|
|
1182
|
+
input: boundedOptionalStringArray(16, MAX_NAME_LENGTH).optional(),
|
|
1183
|
+
contextWindow: z
|
|
1184
|
+
.number()
|
|
1185
|
+
.int()
|
|
1186
|
+
.nonnegative()
|
|
1187
|
+
.max(100_000_000)
|
|
1188
|
+
.nullable()
|
|
1189
|
+
.optional()
|
|
1190
|
+
.catch(undefined),
|
|
325
1191
|
});
|
|
326
1192
|
const TokenCountSchema = z.number().int().nonnegative().max(MAX_TOKEN_COUNT);
|
|
327
1193
|
const OptionalTokenCountSchema = TokenCountSchema.nullable().optional();
|
|
@@ -373,13 +1239,13 @@ const OmpCompactionResultSchema = z.object({
|
|
|
373
1239
|
});
|
|
374
1240
|
const OmpSessionStateSchema = z.object({
|
|
375
1241
|
model: OmpModelSchema.nullable().optional(),
|
|
376
|
-
thinkingLevel:
|
|
1242
|
+
thinkingLevel: OmpCurrentThinkingLevelSchema,
|
|
377
1243
|
isStreaming: z.boolean(),
|
|
378
1244
|
isCompacting: z.boolean(),
|
|
379
1245
|
sessionId: IDENTIFIER,
|
|
380
1246
|
autoCompactionEnabled: z.boolean().optional(),
|
|
381
|
-
contextUsage: OmpContextUsageSchema.nullable().optional(),
|
|
382
|
-
sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
|
|
1247
|
+
contextUsage: OmpContextUsageSchema.nullable().optional().catch(undefined),
|
|
1248
|
+
sessionFile: boundedString(MAX_PATH_LENGTH).optional().catch(undefined),
|
|
383
1249
|
});
|
|
384
1250
|
const OmpReadyFrameSchema = z.object({
|
|
385
1251
|
type: z.literal("ready"),
|
|
@@ -392,6 +1258,7 @@ const OmpReadyFrameSchema = z.object({
|
|
|
392
1258
|
const OmpResponseFrameSchema = z.object({
|
|
393
1259
|
type: z.literal("response"),
|
|
394
1260
|
id: IDENTIFIER,
|
|
1261
|
+
command: boundedString(64, 1).optional(),
|
|
395
1262
|
success: z.boolean(),
|
|
396
1263
|
data: z.unknown().optional(),
|
|
397
1264
|
error: boundedString(MAX_RPC_ERROR_BYTES).optional(),
|
|
@@ -409,6 +1276,16 @@ const JsonObjectSchema = z.record(z.string(), z.unknown());
|
|
|
409
1276
|
const BoundedToolPayloadSchema = z
|
|
410
1277
|
.unknown()
|
|
411
1278
|
.refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096));
|
|
1279
|
+
const OmpToolResultPayloadSchema = z.preprocess(
|
|
1280
|
+
omitUnsafeOptionalDetails,
|
|
1281
|
+
z
|
|
1282
|
+
.unknown()
|
|
1283
|
+
.refine(
|
|
1284
|
+
(value) =>
|
|
1285
|
+
isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, MAX_OPTIONAL_METADATA_ITEMS, 8_192) &&
|
|
1286
|
+
isBoundedJson(omitOptionalDetails(value), MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096),
|
|
1287
|
+
),
|
|
1288
|
+
);
|
|
412
1289
|
const OmpHostToolDefinitionSchema = z.object({
|
|
413
1290
|
name: NAME,
|
|
414
1291
|
label: NAME.optional(),
|
|
@@ -587,7 +1464,7 @@ const OmpCompactionEndSchema = z.object({
|
|
|
587
1464
|
errorMessage: boundedString(4_096).optional(),
|
|
588
1465
|
skipped: z.boolean().optional(),
|
|
589
1466
|
});
|
|
590
|
-
const
|
|
1467
|
+
const OmpCoreAgentEventSchema = z.discriminatedUnion("type", [
|
|
591
1468
|
z.object({ type: z.literal("agent_start") }),
|
|
592
1469
|
z.object({
|
|
593
1470
|
type: z.literal("agent_end"),
|
|
@@ -616,13 +1493,19 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
|
|
|
616
1493
|
toolCallId: IDENTIFIER,
|
|
617
1494
|
toolName: NAME,
|
|
618
1495
|
args: BoundedToolPayloadSchema.optional(),
|
|
619
|
-
partialResult:
|
|
1496
|
+
partialResult: OmpToolResultPayloadSchema,
|
|
1497
|
+
}),
|
|
1498
|
+
z.object({
|
|
1499
|
+
type: z.literal("tool_stream_update"),
|
|
1500
|
+
toolCallId: IDENTIFIER,
|
|
1501
|
+
toolName: NAME,
|
|
1502
|
+
update: BoundedToolPayloadSchema,
|
|
620
1503
|
}),
|
|
621
1504
|
z.object({
|
|
622
1505
|
type: z.literal("tool_execution_end"),
|
|
623
1506
|
toolCallId: IDENTIFIER,
|
|
624
1507
|
toolName: NAME,
|
|
625
|
-
result:
|
|
1508
|
+
result: OmpToolResultPayloadSchema,
|
|
626
1509
|
isError: z.boolean().optional(),
|
|
627
1510
|
}),
|
|
628
1511
|
OmpCompactionStartSchema,
|
|
@@ -644,6 +1527,99 @@ const OmpGoalModeStateSchema = z.object({
|
|
|
644
1527
|
reason: boundedString(4_096).optional(),
|
|
645
1528
|
goal: OmpGoalSchema.optional(),
|
|
646
1529
|
});
|
|
1530
|
+
const OmpTodoReminderEventSchema = z.object({
|
|
1531
|
+
type: z.literal("todo_reminder"),
|
|
1532
|
+
todos: z
|
|
1533
|
+
.array(
|
|
1534
|
+
z.object({
|
|
1535
|
+
id: IDENTIFIER.optional(),
|
|
1536
|
+
content: boundedString(16_384),
|
|
1537
|
+
status: z.enum(["pending", "in_progress", "blocked", "completed", "abandoned"]),
|
|
1538
|
+
}),
|
|
1539
|
+
)
|
|
1540
|
+
.max(MAX_TODOS),
|
|
1541
|
+
attempt: z.number().int().nonnegative().safe().optional(),
|
|
1542
|
+
maxAttempts: z.number().int().nonnegative().safe().optional(),
|
|
1543
|
+
});
|
|
1544
|
+
const OmpThinkingLevelChangedEventSchema = z.object({
|
|
1545
|
+
type: z.literal("thinking_level_changed"),
|
|
1546
|
+
thinkingLevel: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
|
|
1547
|
+
configured: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
|
|
1548
|
+
resolved: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
|
|
1549
|
+
});
|
|
1550
|
+
const OmpAutoRetryStartEventSchema = z.object({
|
|
1551
|
+
type: z.literal("auto_retry_start"),
|
|
1552
|
+
attempt: z.number().int().nonnegative().safe(),
|
|
1553
|
+
maxAttempts: z.number().int().positive().safe(),
|
|
1554
|
+
delayMs: z.number().int().nonnegative().safe(),
|
|
1555
|
+
errorMessage: boundedString(64 * 1024),
|
|
1556
|
+
errorId: z.number().int().safe().optional(),
|
|
1557
|
+
});
|
|
1558
|
+
const OmpAutoRetryEndEventSchema = z.object({
|
|
1559
|
+
type: z.literal("auto_retry_end"),
|
|
1560
|
+
success: z.boolean(),
|
|
1561
|
+
attempt: z.number().int().nonnegative().safe(),
|
|
1562
|
+
finalError: boundedString(64 * 1024).optional(),
|
|
1563
|
+
retryErrors: BoundedToolPayloadSchema.optional(),
|
|
1564
|
+
recoveredErrors: BoundedToolPayloadSchema.optional(),
|
|
1565
|
+
});
|
|
1566
|
+
const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
|
|
1567
|
+
...OmpCoreAgentEventSchema.options,
|
|
1568
|
+
z.object({ type: z.literal("config_warnings_changed") }),
|
|
1569
|
+
z.object({ type: z.literal("advisor_cost_changed") }),
|
|
1570
|
+
z.object({ type: z.literal("advisor_yielded") }),
|
|
1571
|
+
z.object({
|
|
1572
|
+
type: z.literal("ttsr_triggered"),
|
|
1573
|
+
rules: z.array(BoundedToolPayloadSchema).max(MAX_ARRAY_ITEMS),
|
|
1574
|
+
}),
|
|
1575
|
+
OmpTodoReminderEventSchema,
|
|
1576
|
+
z.object({ type: z.literal("todo_auto_clear") }),
|
|
1577
|
+
z.object({
|
|
1578
|
+
type: z.literal("irc_message"),
|
|
1579
|
+
message: OmpMessageSchema.refine((message) => message.role === "custom"),
|
|
1580
|
+
}),
|
|
1581
|
+
z.object({ type: z.literal("model_changed") }),
|
|
1582
|
+
OmpThinkingLevelChangedEventSchema,
|
|
1583
|
+
z.object({
|
|
1584
|
+
type: z.literal("goal_updated"),
|
|
1585
|
+
goal: OmpGoalSchema.nullable().optional(),
|
|
1586
|
+
state: OmpGoalModeStateSchema.optional(),
|
|
1587
|
+
}),
|
|
1588
|
+
OmpAutoRetryStartEventSchema,
|
|
1589
|
+
OmpAutoRetryEndEventSchema,
|
|
1590
|
+
z.object({
|
|
1591
|
+
type: z.literal("retry_fallback_applied"),
|
|
1592
|
+
from: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
1593
|
+
to: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
1594
|
+
role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
1595
|
+
}),
|
|
1596
|
+
z.object({
|
|
1597
|
+
type: z.literal("retry_fallback_succeeded"),
|
|
1598
|
+
model: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
1599
|
+
role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
1600
|
+
}),
|
|
1601
|
+
z.object({
|
|
1602
|
+
type: z.literal("auto_compaction_start"),
|
|
1603
|
+
reason: boundedString(4_096),
|
|
1604
|
+
action: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
1605
|
+
}),
|
|
1606
|
+
z.object({
|
|
1607
|
+
type: z.literal("auto_compaction_end"),
|
|
1608
|
+
action: NAME.optional(),
|
|
1609
|
+
result: OmpCompactionResultSchema.nullable().optional(),
|
|
1610
|
+
aborted: z.boolean().optional(),
|
|
1611
|
+
willRetry: z.boolean().optional(),
|
|
1612
|
+
errorMessage: boundedString(64 * 1024).optional(),
|
|
1613
|
+
skipped: z.boolean().optional(),
|
|
1614
|
+
}),
|
|
1615
|
+
z.object({
|
|
1616
|
+
type: z.literal("notice"),
|
|
1617
|
+
id: IDENTIFIER.optional(),
|
|
1618
|
+
level: z.enum(["info", "warning", "error"]),
|
|
1619
|
+
message: boundedString(64 * 1024),
|
|
1620
|
+
source: boundedString(MAX_NAME_LENGTH).optional(),
|
|
1621
|
+
}),
|
|
1622
|
+
]);
|
|
647
1623
|
const OmpSubagentStatusSchema = z.enum([
|
|
648
1624
|
"pending",
|
|
649
1625
|
"running",
|
|
@@ -786,80 +1762,10 @@ const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
|
|
|
786
1762
|
type: z.literal("subagent_event"),
|
|
787
1763
|
payload: z.object({ id: IDENTIFIER, event: OmpAgentSessionEventSchema }),
|
|
788
1764
|
}),
|
|
789
|
-
z.object({
|
|
790
|
-
type: z.literal("todo_reminder"),
|
|
791
|
-
todos: z
|
|
792
|
-
.array(
|
|
793
|
-
z.object({
|
|
794
|
-
id: IDENTIFIER.optional(),
|
|
795
|
-
content: boundedString(16_384),
|
|
796
|
-
status: z.enum(["pending", "in_progress", "blocked", "completed", "abandoned"]),
|
|
797
|
-
}),
|
|
798
|
-
)
|
|
799
|
-
.max(MAX_TODOS),
|
|
800
|
-
}),
|
|
801
|
-
z.object({ type: z.literal("model_changed") }),
|
|
802
|
-
z.object({
|
|
803
|
-
type: z.literal("thinking_level_changed"),
|
|
804
|
-
thinkingLevel: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
|
|
805
|
-
}),
|
|
806
|
-
z.object({
|
|
807
|
-
type: z.literal("goal_updated"),
|
|
808
|
-
goal: OmpGoalSchema.nullable().optional(),
|
|
809
|
-
state: OmpGoalModeStateSchema.optional(),
|
|
810
|
-
}),
|
|
811
|
-
z.object({
|
|
812
|
-
type: z.literal("auto_retry_start"),
|
|
813
|
-
attempt: z.number().int().nonnegative().safe(),
|
|
814
|
-
maxAttempts: z.number().int().positive().safe(),
|
|
815
|
-
delayMs: z.number().int().nonnegative().safe(),
|
|
816
|
-
errorMessage: boundedString(64 * 1024),
|
|
817
|
-
errorId: z.number().int().safe().optional(),
|
|
818
|
-
}),
|
|
819
|
-
z.object({
|
|
820
|
-
type: z.literal("auto_retry_end"),
|
|
821
|
-
success: z.boolean(),
|
|
822
|
-
attempt: z.number().int().nonnegative().safe(),
|
|
823
|
-
finalError: boundedString(64 * 1024).optional(),
|
|
824
|
-
recoveredErrors: BoundedToolPayloadSchema.optional(),
|
|
825
|
-
}),
|
|
826
|
-
z.object({
|
|
827
|
-
type: z.literal("retry_fallback_applied"),
|
|
828
|
-
from: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
829
|
-
to: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
830
|
-
role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
831
|
-
}),
|
|
832
|
-
z.object({
|
|
833
|
-
type: z.literal("retry_fallback_succeeded"),
|
|
834
|
-
model: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
835
|
-
role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
836
|
-
}),
|
|
837
|
-
z.object({ type: z.literal("todo_auto_clear") }),
|
|
838
|
-
z.object({
|
|
839
|
-
type: z.literal("auto_compaction_start"),
|
|
840
|
-
reason: boundedString(4_096),
|
|
841
|
-
action: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
|
|
842
|
-
}),
|
|
843
|
-
z.object({
|
|
844
|
-
type: z.literal("auto_compaction_end"),
|
|
845
|
-
action: NAME.optional(),
|
|
846
|
-
result: OmpCompactionResultSchema.nullable().optional(),
|
|
847
|
-
aborted: z.boolean().optional(),
|
|
848
|
-
willRetry: z.boolean().optional(),
|
|
849
|
-
errorMessage: boundedString(64 * 1024).optional(),
|
|
850
|
-
skipped: z.boolean().optional(),
|
|
851
|
-
}),
|
|
852
1765
|
z.object({
|
|
853
1766
|
type: z.literal("available_commands_update"),
|
|
854
1767
|
commands: z.array(OmpAvailableCommandSchema).max(MAX_ARRAY_ITEMS),
|
|
855
1768
|
}),
|
|
856
|
-
z.object({
|
|
857
|
-
type: z.literal("notice"),
|
|
858
|
-
id: IDENTIFIER.optional(),
|
|
859
|
-
level: z.enum(["info", "warning", "error"]),
|
|
860
|
-
message: boundedString(64 * 1024),
|
|
861
|
-
source: boundedString(MAX_NAME_LENGTH).optional(),
|
|
862
|
-
}),
|
|
863
1769
|
z.object({ type: z.literal("command_output"), text: TEXT.optional() }),
|
|
864
1770
|
OmpExtensionUiRequestSchema,
|
|
865
1771
|
z.object({
|
|
@@ -871,10 +1777,126 @@ const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
|
|
|
871
1777
|
OmpHostToolCancelSchema,
|
|
872
1778
|
OmpToolApprovalRequestSchema,
|
|
873
1779
|
OmpToolApprovalCancelSchema,
|
|
874
|
-
z.object({ type: z.literal("advisor_yielded") }),
|
|
875
1780
|
]);
|
|
1781
|
+
type OptionalDetailsMapper = (value: unknown, taskResult?: boolean) => unknown;
|
|
1782
|
+
|
|
1783
|
+
function mapRecordField(
|
|
1784
|
+
record: Record<string, unknown>,
|
|
1785
|
+
key: string,
|
|
1786
|
+
map: OptionalDetailsMapper,
|
|
1787
|
+
taskResult = false,
|
|
1788
|
+
): Record<string, unknown> {
|
|
1789
|
+
if (!Object.hasOwn(record, key)) return record;
|
|
1790
|
+
const next = map(record[key], taskResult);
|
|
1791
|
+
return next === record[key] ? record : { ...record, [key]: next };
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function mapMessageList(value: unknown, map: OptionalDetailsMapper): unknown {
|
|
1795
|
+
if (!Array.isArray(value)) return value;
|
|
1796
|
+
let changed = false;
|
|
1797
|
+
const messages = value.map((message) => {
|
|
1798
|
+
const taskResult =
|
|
1799
|
+
message !== null &&
|
|
1800
|
+
typeof message === "object" &&
|
|
1801
|
+
!Array.isArray(message) &&
|
|
1802
|
+
(message as Record<string, unknown>).role === "toolResult" &&
|
|
1803
|
+
(message as Record<string, unknown>).toolName === "task";
|
|
1804
|
+
const next = map(message, taskResult);
|
|
1805
|
+
changed ||= next !== message;
|
|
1806
|
+
return next;
|
|
1807
|
+
});
|
|
1808
|
+
return changed ? messages : value;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function mapAgentEventDetails(
|
|
1812
|
+
frame: Record<string, unknown>,
|
|
1813
|
+
map: OptionalDetailsMapper,
|
|
1814
|
+
): Record<string, unknown> {
|
|
1815
|
+
switch (frame.type) {
|
|
1816
|
+
case "message_start":
|
|
1817
|
+
case "message_update":
|
|
1818
|
+
case "message_end": {
|
|
1819
|
+
const message = frame.message;
|
|
1820
|
+
const taskResult =
|
|
1821
|
+
message !== null &&
|
|
1822
|
+
typeof message === "object" &&
|
|
1823
|
+
!Array.isArray(message) &&
|
|
1824
|
+
(message as Record<string, unknown>).role === "toolResult" &&
|
|
1825
|
+
(message as Record<string, unknown>).toolName === "task";
|
|
1826
|
+
return mapRecordField(frame, "message", map, taskResult);
|
|
1827
|
+
}
|
|
1828
|
+
case "tool_execution_update":
|
|
1829
|
+
return mapRecordField(frame, "partialResult", map, frame.toolName === "task");
|
|
1830
|
+
case "tool_execution_end":
|
|
1831
|
+
return mapRecordField(frame, "result", map, frame.toolName === "task");
|
|
1832
|
+
case "agent_end":
|
|
1833
|
+
return mapRecordField(frame, "messages", (messages) => mapMessageList(messages, map));
|
|
1834
|
+
case "irc_message":
|
|
1835
|
+
return mapRecordField(frame, "message", map);
|
|
1836
|
+
default:
|
|
1837
|
+
return frame;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
function mapRuntimeFrameDetails(
|
|
1842
|
+
frame: Record<string, unknown>,
|
|
1843
|
+
map: OptionalDetailsMapper,
|
|
1844
|
+
): Record<string, unknown> {
|
|
1845
|
+
if (frame.type !== "subagent_event") return mapAgentEventDetails(frame, map);
|
|
1846
|
+
if (frame.payload === null || typeof frame.payload !== "object" || Array.isArray(frame.payload)) {
|
|
1847
|
+
return frame;
|
|
1848
|
+
}
|
|
1849
|
+
const payload = frame.payload as Record<string, unknown>;
|
|
1850
|
+
if (payload.event === null || typeof payload.event !== "object" || Array.isArray(payload.event)) {
|
|
1851
|
+
return frame;
|
|
1852
|
+
}
|
|
1853
|
+
const event = mapAgentEventDetails(payload.event as Record<string, unknown>, map);
|
|
1854
|
+
return event === payload.event ? frame : { ...frame, payload: { ...payload, event } };
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
function sanitizeMessageListMetadata(value: unknown): unknown {
|
|
1858
|
+
return mapMessageList(value, createOptionalMetadataSanitizer());
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function sanitizeHistoryResponseData(value: unknown): unknown {
|
|
1862
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1863
|
+
return mapRecordField(value as Record<string, unknown>, "messages", sanitizeMessageListMetadata);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function projectSessionStateResponseData(value: unknown): unknown {
|
|
1867
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1868
|
+
const source = value as Record<string, unknown>;
|
|
1869
|
+
const projected: Record<string, unknown> = {};
|
|
1870
|
+
for (const key of [
|
|
1871
|
+
"model",
|
|
1872
|
+
"thinkingLevel",
|
|
1873
|
+
"isStreaming",
|
|
1874
|
+
"isCompacting",
|
|
1875
|
+
"sessionId",
|
|
1876
|
+
"autoCompactionEnabled",
|
|
1877
|
+
"contextUsage",
|
|
1878
|
+
"sessionFile",
|
|
1879
|
+
] as const) {
|
|
1880
|
+
if (Object.hasOwn(source, key)) projected[key] = source[key];
|
|
1881
|
+
}
|
|
1882
|
+
return projected;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function runtimeFrameCollectionLimit(frame: Record<string, unknown>): number {
|
|
1886
|
+
const type = frame.type;
|
|
1887
|
+
if (
|
|
1888
|
+
type === "message_start" ||
|
|
1889
|
+
type === "message_update" ||
|
|
1890
|
+
type === "message_end" ||
|
|
1891
|
+
type === "agent_end" ||
|
|
1892
|
+
type === "subagent_event"
|
|
1893
|
+
) {
|
|
1894
|
+
return OMP_MAX_CONTENT_PARTS;
|
|
1895
|
+
}
|
|
1896
|
+
return 1_024;
|
|
1897
|
+
}
|
|
876
1898
|
const OmpModelsResultSchema = z.object({
|
|
877
|
-
models: z.array(OmpModelSchema).min(1).max(
|
|
1899
|
+
models: z.array(OmpModelSchema).min(1).max(MAX_MODEL_CATALOG_ITEMS),
|
|
878
1900
|
});
|
|
879
1901
|
const OmpPromptAckSchema = z.object({ agentInvoked: z.boolean().optional() }).optional();
|
|
880
1902
|
const OmpAvailableCommandsResultSchema = z.object({
|
|
@@ -885,7 +1907,12 @@ const OmpBranchMessagesResultSchema = z.object({
|
|
|
885
1907
|
});
|
|
886
1908
|
const OmpBranchResultSchema = z.object({ text: TEXT, cancelled: z.boolean() });
|
|
887
1909
|
const OmpMessagesResultSchema = z.object({
|
|
888
|
-
messages: z.array(OmpMessageSchema).max(
|
|
1910
|
+
messages: z.array(OmpMessageSchema).max(MAX_REPLAY_MESSAGES),
|
|
1911
|
+
});
|
|
1912
|
+
const OmpMessagesPageResultSchema = z.object({
|
|
1913
|
+
messages: z.array(OmpMessageSchema).max(OMP_MESSAGE_PAGE_LIMIT),
|
|
1914
|
+
nextCursor: boundedString(2_048, 1).refine(isSafeOpaqueCursor).optional(),
|
|
1915
|
+
totalMessages: z.number().int().nonnegative().max(MAX_REPLAY_MESSAGES),
|
|
889
1916
|
});
|
|
890
1917
|
const OmpSubagentsResultSchema = z.object({
|
|
891
1918
|
subagents: z
|
|
@@ -911,7 +1938,7 @@ const OmpSubagentMessagesResultSchema = z.object({
|
|
|
911
1938
|
fromByte: z.number().int().nonnegative(),
|
|
912
1939
|
nextByte: z.number().int().nonnegative(),
|
|
913
1940
|
reset: z.boolean(),
|
|
914
|
-
messages: z.array(OmpMessageSchema).max(
|
|
1941
|
+
messages: z.array(OmpMessageSchema).max(MAX_REPLAY_MESSAGES),
|
|
915
1942
|
});
|
|
916
1943
|
const ProtocolNegotiationResultSchema = z.object({
|
|
917
1944
|
protocolVersion: z.literal(2),
|
|
@@ -919,6 +1946,7 @@ const ProtocolNegotiationResultSchema = z.object({
|
|
|
919
1946
|
});
|
|
920
1947
|
|
|
921
1948
|
export type OmpModel = z.infer<typeof OmpModelSchema>;
|
|
1949
|
+
export type OmpBranchResult = z.infer<typeof OmpBranchResultSchema>;
|
|
922
1950
|
export type OmpSessionState = z.infer<typeof OmpSessionStateSchema>;
|
|
923
1951
|
export type OmpSessionStats = z.infer<typeof OmpSessionStatsSchema>;
|
|
924
1952
|
export type OmpCompactionResult = z.infer<typeof OmpCompactionResultSchema>;
|
|
@@ -960,6 +1988,7 @@ export interface OmpPersistedSessionMessages {
|
|
|
960
1988
|
nativeSessionId: string;
|
|
961
1989
|
byteLength: number;
|
|
962
1990
|
messages: OmpMessage[];
|
|
1991
|
+
imageReplayWarning?: true;
|
|
963
1992
|
}
|
|
964
1993
|
|
|
965
1994
|
export interface OmpStartOptions {
|
|
@@ -1025,9 +2054,9 @@ export interface OmpRuntimeSession {
|
|
|
1025
2054
|
respondToExtensionUi(response: OmpExtensionUiResponse): Promise<void>;
|
|
1026
2055
|
respondToToolApproval(response: OmpToolApprovalResponse): Promise<void>;
|
|
1027
2056
|
getBranchMessages(): Promise<Array<{ entryId: string; text: string }>>;
|
|
1028
|
-
branch(entryId: string): Promise<
|
|
2057
|
+
branch(entryId: string): Promise<OmpBranchResult>;
|
|
1029
2058
|
readonly canReplayHistory: boolean;
|
|
1030
|
-
getMessages(): Promise<OmpMessage[]>;
|
|
2059
|
+
getMessages(signal?: AbortSignal): Promise<OmpMessage[]>;
|
|
1031
2060
|
abort(): Promise<void>;
|
|
1032
2061
|
setHostTools(tools: readonly OmpHostToolDefinition[]): Promise<string[]>;
|
|
1033
2062
|
sendHostToolResult(result: OmpHostToolResult): void;
|
|
@@ -1048,6 +2077,7 @@ export interface OmpRuntime {
|
|
|
1048
2077
|
readPersistedSubagentTranscript(options: {
|
|
1049
2078
|
parentSessionFile: string;
|
|
1050
2079
|
childTranscriptId: string;
|
|
2080
|
+
sessionFile?: string;
|
|
1051
2081
|
cwd: string;
|
|
1052
2082
|
signal?: AbortSignal;
|
|
1053
2083
|
}): Promise<OmpPersistedSubagentMessages>;
|
|
@@ -1067,11 +2097,36 @@ export interface OmpRpcRuntimeOptions {
|
|
|
1067
2097
|
terminateProcessTree?: (pid: number) => Promise<boolean | "uncertain">;
|
|
1068
2098
|
environment?: NodeJS.ProcessEnv;
|
|
1069
2099
|
requestTimeoutMs?: number;
|
|
2100
|
+
reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
|
|
1070
2101
|
listSessions?: (
|
|
1071
2102
|
options: OmpSessionListOptions,
|
|
1072
2103
|
) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
|
|
1073
2104
|
}
|
|
1074
2105
|
|
|
2106
|
+
export type OmpRpcRequestRejectionCode = "session_busy" | "stale_cursor" | "unsupported_command";
|
|
2107
|
+
|
|
2108
|
+
export class OmpRpcRequestRejectedError extends Error {
|
|
2109
|
+
constructor(
|
|
2110
|
+
readonly command?: OmpRpcDiagnosticCommand,
|
|
2111
|
+
readonly code?: OmpRpcRequestRejectionCode,
|
|
2112
|
+
) {
|
|
2113
|
+
super("OMP RPC request failed");
|
|
2114
|
+
this.name = "OmpRpcRequestRejectedError";
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
export class OmpRpcResponseLimitError extends Error {
|
|
2119
|
+
constructor(
|
|
2120
|
+
readonly command: OmpRpcDiagnosticCommand | undefined,
|
|
2121
|
+
readonly bound: OmpRpcBoundDimension,
|
|
2122
|
+
readonly actual: number,
|
|
2123
|
+
readonly limit: number,
|
|
2124
|
+
) {
|
|
2125
|
+
super("OMP RPC response exceeded command limits");
|
|
2126
|
+
this.name = "OmpRpcResponseLimitError";
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
1075
2130
|
type PendingRequest = {
|
|
1076
2131
|
resolve(value: unknown): void;
|
|
1077
2132
|
reject(error: Error): void;
|
|
@@ -1591,12 +2646,21 @@ class OmpRpcProcess {
|
|
|
1591
2646
|
private spawnFailedWithoutProcess = false;
|
|
1592
2647
|
private readyReceived = false;
|
|
1593
2648
|
private outputSettled = false;
|
|
2649
|
+
private turnActive = false;
|
|
2650
|
+
private readonly pendingProtocolViolations = new Map<
|
|
2651
|
+
ProtocolViolationKey,
|
|
2652
|
+
PendingProtocolViolation
|
|
2653
|
+
>();
|
|
2654
|
+
private protocolViolationTimer: TimerHandle | null = null;
|
|
1594
2655
|
|
|
1595
2656
|
constructor(
|
|
1596
2657
|
options: OmpStartOptions,
|
|
1597
2658
|
spawnProcess?: OmpRpcRuntimeOptions["spawnProcess"],
|
|
1598
2659
|
terminateProcessTree?: OmpRpcRuntimeOptions["terminateProcessTree"],
|
|
1599
2660
|
private readonly requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
2661
|
+
private readonly reportProtocolViolation: (
|
|
2662
|
+
diagnostic: OmpProtocolViolationDiagnostic,
|
|
2663
|
+
) => void | Promise<void> = (diagnostic) => console.error("OMP protocol violation", diagnostic),
|
|
1600
2664
|
) {
|
|
1601
2665
|
const ready = Promise.withResolvers<ReadyFrame>();
|
|
1602
2666
|
this.rejectReady = ready.reject;
|
|
@@ -1685,7 +2749,15 @@ class OmpRpcProcess {
|
|
|
1685
2749
|
private settleOutput(): void {
|
|
1686
2750
|
if (this.outputSettled) return;
|
|
1687
2751
|
this.outputSettled = true;
|
|
1688
|
-
if (this.lineBytes > 0 || this.discardingLine)
|
|
2752
|
+
if (this.lineBytes > 0 || this.discardingLine) {
|
|
2753
|
+
this.recordProtocolViolation("incomplete-frame", {
|
|
2754
|
+
reason: "output-ended-mid-frame",
|
|
2755
|
+
field: "frame",
|
|
2756
|
+
expected: "complete-json-line",
|
|
2757
|
+
actualType: "partial-frame",
|
|
2758
|
+
maxByteSize: this.discardingLine ? this.discardedLineBytes : this.lineBytes,
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
1689
2761
|
this.lineParts = [];
|
|
1690
2762
|
this.lineBytes = 0;
|
|
1691
2763
|
this.discardingLine = false;
|
|
@@ -1868,16 +2940,17 @@ class OmpRpcProcess {
|
|
|
1868
2940
|
|
|
1869
2941
|
private async closeProcess(): Promise<void> {
|
|
1870
2942
|
this.closed = true;
|
|
2943
|
+
this.flushProtocolViolations();
|
|
1871
2944
|
this.clearChunk();
|
|
1872
2945
|
this.failPending(new Error("OMP RPC process was closed"));
|
|
2946
|
+
const cleanupPromise = this.startTreeCleanup();
|
|
1873
2947
|
if (!this.exited) {
|
|
1874
2948
|
try {
|
|
1875
2949
|
this.child.stdin.end();
|
|
1876
2950
|
} catch {
|
|
1877
|
-
// Continue
|
|
2951
|
+
// Continue waiting for process-tree cleanup when the input channel is already closed.
|
|
1878
2952
|
}
|
|
1879
2953
|
}
|
|
1880
|
-
const cleanupPromise = this.startTreeCleanup();
|
|
1881
2954
|
const cleanup = await cleanupPromise;
|
|
1882
2955
|
if (cleanup !== "verified") throw new Error("OMP RPC process tree cleanup failed");
|
|
1883
2956
|
if (
|
|
@@ -1928,8 +3001,13 @@ class OmpRpcProcess {
|
|
|
1928
3001
|
} else {
|
|
1929
3002
|
this.appendLinePart(part);
|
|
1930
3003
|
if (this.fatalError) return;
|
|
1931
|
-
if (this.discardingLine)
|
|
1932
|
-
|
|
3004
|
+
if (this.discardingLine) {
|
|
3005
|
+
if (this.discardedLineBytes > MAX_SEMANTIC_FRAME_BYTES) {
|
|
3006
|
+
this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
this.resetDiscardedLine();
|
|
3010
|
+
} else this.completeLine();
|
|
1933
3011
|
}
|
|
1934
3012
|
start = index + 1;
|
|
1935
3013
|
}
|
|
@@ -1964,7 +3042,14 @@ class OmpRpcProcess {
|
|
|
1964
3042
|
this.lineParts = [];
|
|
1965
3043
|
this.lineBytes = 0;
|
|
1966
3044
|
this.discardingLine = true;
|
|
1967
|
-
this.recordProtocolViolation(
|
|
3045
|
+
this.recordProtocolViolation("frame-limit", {
|
|
3046
|
+
reason: "physical-frame-limit",
|
|
3047
|
+
field: "frame.byteLength",
|
|
3048
|
+
expected: "within-byte-limit",
|
|
3049
|
+
actualType: "oversized",
|
|
3050
|
+
maxByteSize: nextBytes,
|
|
3051
|
+
limitBytes: this.physicalFrameLimit,
|
|
3052
|
+
});
|
|
1968
3053
|
return;
|
|
1969
3054
|
}
|
|
1970
3055
|
this.lineParts.push(part);
|
|
@@ -1977,32 +3062,20 @@ class OmpRpcProcess {
|
|
|
1977
3062
|
this.lineParts = [];
|
|
1978
3063
|
this.lineBytes = 0;
|
|
1979
3064
|
const payload = line.at(-1) === 13 ? line.subarray(0, -1) : line;
|
|
1980
|
-
if (payload.byteLength > MAX_SEMANTIC_FRAME_BYTES) {
|
|
1981
|
-
this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
|
|
1982
|
-
return;
|
|
1983
|
-
}
|
|
1984
3065
|
let decoded: unknown;
|
|
1985
3066
|
try {
|
|
1986
3067
|
decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
|
|
1987
3068
|
} catch {
|
|
1988
|
-
this.recordProtocolViolation(
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
Number.POSITIVE_INFINITY
|
|
1996
|
-
) {
|
|
1997
|
-
this.recordProtocolViolation();
|
|
1998
|
-
return;
|
|
1999
|
-
}
|
|
2000
|
-
const frame = JsonObjectSchema.safeParse(decoded);
|
|
2001
|
-
if (!frame.success) {
|
|
2002
|
-
this.recordProtocolViolation();
|
|
3069
|
+
this.recordProtocolViolation("invalid-json", {
|
|
3070
|
+
reason: "json-decode",
|
|
3071
|
+
field: "frame",
|
|
3072
|
+
expected: "valid-json",
|
|
3073
|
+
actualType: "invalid-json",
|
|
3074
|
+
maxByteSize: payload.byteLength,
|
|
3075
|
+
});
|
|
2003
3076
|
return;
|
|
2004
3077
|
}
|
|
2005
|
-
this.
|
|
3078
|
+
this.receiveDecodedFrame(decoded, payload.byteLength);
|
|
2006
3079
|
}
|
|
2007
3080
|
|
|
2008
3081
|
private receiveChunk(frame: ChunkFrame): void {
|
|
@@ -2010,7 +3083,10 @@ class OmpRpcProcess {
|
|
|
2010
3083
|
frame.byteLength > MAX_SEMANTIC_FRAME_BYTES &&
|
|
2011
3084
|
![...this.pending.values()].some(
|
|
2012
3085
|
(pending) =>
|
|
2013
|
-
pending.command === "get_messages" ||
|
|
3086
|
+
pending.command === "get_messages" ||
|
|
3087
|
+
pending.command === "get_messages_page" ||
|
|
3088
|
+
pending.command === "get_subagent_messages" ||
|
|
3089
|
+
pending.command === "get_state",
|
|
2014
3090
|
)
|
|
2015
3091
|
) {
|
|
2016
3092
|
this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
|
|
@@ -2022,17 +3098,30 @@ class OmpRpcProcess {
|
|
|
2022
3098
|
frame.data.length % 4 !== 0 ||
|
|
2023
3099
|
!/^[A-Za-z0-9+/]*={0,2}$/u.test(frame.data)
|
|
2024
3100
|
) {
|
|
2025
|
-
this.rejectChunk(
|
|
3101
|
+
this.rejectChunk("chunk-metadata", {
|
|
3102
|
+
field: "chunk",
|
|
3103
|
+
expected: "valid-chunk-frame",
|
|
3104
|
+
actualType: "object",
|
|
3105
|
+
});
|
|
2026
3106
|
return;
|
|
2027
3107
|
}
|
|
2028
3108
|
const decoded = Buffer.from(frame.data, "base64");
|
|
2029
3109
|
if (decoded.byteLength > MAX_CHUNK_BYTES) {
|
|
2030
|
-
this.rejectChunk(
|
|
3110
|
+
this.rejectChunk("chunk-size", {
|
|
3111
|
+
field: "chunk.data",
|
|
3112
|
+
expected: "within-byte-limit",
|
|
3113
|
+
actualType: "oversized",
|
|
3114
|
+
limitBytes: MAX_CHUNK_BYTES,
|
|
3115
|
+
});
|
|
2031
3116
|
return;
|
|
2032
3117
|
}
|
|
2033
3118
|
if (!this.chunk) {
|
|
2034
3119
|
if (frame.index !== 0) {
|
|
2035
|
-
this.rejectChunk(
|
|
3120
|
+
this.rejectChunk("chunk-start-index", {
|
|
3121
|
+
field: "chunk.index",
|
|
3122
|
+
expected: "first-chunk-index-zero",
|
|
3123
|
+
actualType: "out-of-order",
|
|
3124
|
+
});
|
|
2036
3125
|
return;
|
|
2037
3126
|
}
|
|
2038
3127
|
this.chunk = {
|
|
@@ -2041,7 +3130,15 @@ class OmpRpcProcess {
|
|
|
2041
3130
|
byteLength: frame.byteLength,
|
|
2042
3131
|
parts: [],
|
|
2043
3132
|
receivedBytes: 0,
|
|
2044
|
-
timer: setTimeout(
|
|
3133
|
+
timer: setTimeout(
|
|
3134
|
+
() =>
|
|
3135
|
+
this.rejectChunk("chunk-timeout", {
|
|
3136
|
+
field: "chunk.sequence",
|
|
3137
|
+
expected: "chunk-before-deadline",
|
|
3138
|
+
actualType: "timeout",
|
|
3139
|
+
}),
|
|
3140
|
+
CHUNK_STALE_MS,
|
|
3141
|
+
),
|
|
2045
3142
|
};
|
|
2046
3143
|
}
|
|
2047
3144
|
const chunk = this.chunk;
|
|
@@ -2053,14 +3150,22 @@ class OmpRpcProcess {
|
|
|
2053
3150
|
chunk.receivedBytes + decoded.byteLength > chunk.byteLength ||
|
|
2054
3151
|
chunk.receivedBytes + decoded.byteLength > this.reassembledFrameLimit
|
|
2055
3152
|
) {
|
|
2056
|
-
this.rejectChunk(
|
|
3153
|
+
this.rejectChunk("chunk-sequence", {
|
|
3154
|
+
field: "chunk.sequence",
|
|
3155
|
+
expected: "contiguous-chunk-sequence",
|
|
3156
|
+
actualType: "out-of-order",
|
|
3157
|
+
});
|
|
2057
3158
|
return;
|
|
2058
3159
|
}
|
|
2059
3160
|
chunk.parts.push(decoded);
|
|
2060
3161
|
chunk.receivedBytes += decoded.byteLength;
|
|
2061
3162
|
if (chunk.parts.length !== chunk.count) return;
|
|
2062
3163
|
if (chunk.receivedBytes !== chunk.byteLength) {
|
|
2063
|
-
this.rejectChunk(
|
|
3164
|
+
this.rejectChunk("chunk-byte-count", {
|
|
3165
|
+
field: "chunk.byteLength",
|
|
3166
|
+
expected: "declared-chunk-byte-count",
|
|
3167
|
+
actualType: "mismatched",
|
|
3168
|
+
});
|
|
2064
3169
|
return;
|
|
2065
3170
|
}
|
|
2066
3171
|
const reassembled = Buffer.concat(chunk.parts, chunk.receivedBytes);
|
|
@@ -2069,29 +3174,52 @@ class OmpRpcProcess {
|
|
|
2069
3174
|
try {
|
|
2070
3175
|
decodedFrame = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(reassembled));
|
|
2071
3176
|
} catch {
|
|
2072
|
-
this.recordProtocolViolation(
|
|
3177
|
+
this.recordProtocolViolation("invalid-json", {
|
|
3178
|
+
reason: "chunk-json-decode",
|
|
3179
|
+
field: "chunk.data",
|
|
3180
|
+
expected: "valid-json",
|
|
3181
|
+
actualType: "invalid-json",
|
|
3182
|
+
maxByteSize: reassembled.byteLength,
|
|
3183
|
+
});
|
|
2073
3184
|
return;
|
|
2074
3185
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
3186
|
+
this.receiveDecodedFrame(decodedFrame, reassembled.byteLength);
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
private receiveDecodedFrame(value: unknown, rawByteLength: number): void {
|
|
3190
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
3191
|
+
const frame = value as Record<string, unknown>;
|
|
3192
|
+
const pending = typeof frame.id === "string" ? this.pending.get(frame.id) : undefined;
|
|
3193
|
+
if (
|
|
3194
|
+
frame.type === "response" &&
|
|
3195
|
+
(pending?.command === "get_messages" ||
|
|
3196
|
+
pending?.command === "get_messages_page" ||
|
|
3197
|
+
pending?.command === "get_subagent_messages" ||
|
|
3198
|
+
pending?.command === "get_state")
|
|
3199
|
+
) {
|
|
3200
|
+
this.receiveResponse(frame);
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
if (this.receiveUnsupportedCommandResponse(value)) return;
|
|
3205
|
+
if (rawByteLength > MAX_SEMANTIC_FRAME_BYTES) {
|
|
3206
|
+
this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
|
|
2087
3207
|
return;
|
|
2088
3208
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
3209
|
+
if (this.receiveKnownResponse(value)) return;
|
|
3210
|
+
const sanitized = sanitizeLiveDisplayFrame(value);
|
|
3211
|
+
const frame = JsonObjectSchema.safeParse(sanitized);
|
|
3212
|
+
if (!frame.success) {
|
|
3213
|
+
this.recordProtocolViolation("invalid-envelope", {
|
|
3214
|
+
reason: "object-envelope",
|
|
3215
|
+
field: "frame",
|
|
3216
|
+
expected: "object-envelope",
|
|
3217
|
+
actualType: protocolDiagnosticActualType(sanitized),
|
|
3218
|
+
maxByteSize: rawByteLength,
|
|
3219
|
+
});
|
|
2092
3220
|
return;
|
|
2093
3221
|
}
|
|
2094
|
-
this.receiveFrame(
|
|
3222
|
+
this.receiveFrame(frame.data, rawByteLength);
|
|
2095
3223
|
}
|
|
2096
3224
|
|
|
2097
3225
|
private receiveKnownResponse(value: unknown): boolean {
|
|
@@ -2108,15 +3236,50 @@ class OmpRpcProcess {
|
|
|
2108
3236
|
return true;
|
|
2109
3237
|
}
|
|
2110
3238
|
|
|
3239
|
+
private receiveUnsupportedCommandResponse(value: unknown): boolean {
|
|
3240
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
3241
|
+
const frame = value as Record<string, unknown>;
|
|
3242
|
+
if (
|
|
3243
|
+
frame.type !== "response" ||
|
|
3244
|
+
frame.id !== undefined ||
|
|
3245
|
+
frame.success !== false ||
|
|
3246
|
+
typeof frame.command !== "string" ||
|
|
3247
|
+
frame.error !== `Unknown command: ${frame.command}`
|
|
3248
|
+
) {
|
|
3249
|
+
return false;
|
|
3250
|
+
}
|
|
3251
|
+
const matches = [...this.pending.entries()].filter(
|
|
3252
|
+
([, pending]) => pending.command === frame.command,
|
|
3253
|
+
);
|
|
3254
|
+
if (matches.length === 0) return false;
|
|
3255
|
+
const [id, pending] = matches[0] ?? [];
|
|
3256
|
+
if (!id || !pending) return false;
|
|
3257
|
+
this.takePending(id)?.reject(
|
|
3258
|
+
new OmpRpcRequestRejectedError(rpcDiagnosticCommand(pending.command), "unsupported_command"),
|
|
3259
|
+
);
|
|
3260
|
+
return true;
|
|
3261
|
+
}
|
|
3262
|
+
|
|
2111
3263
|
private receiveResponse(frame: Record<string, unknown>): void {
|
|
2112
3264
|
const rawId = typeof frame.id === "string" ? frame.id : undefined;
|
|
2113
3265
|
const knownPending = rawId ? this.pending.get(rawId) : undefined;
|
|
2114
3266
|
const response = OmpResponseFrameSchema.safeParse(frame);
|
|
2115
3267
|
if (!response.success) {
|
|
3268
|
+
const handledAcceptedFailure =
|
|
3269
|
+
rawId !== undefined && knownPending === undefined
|
|
3270
|
+
? this.emitAcceptedPromptFailure(rawId, frame)
|
|
3271
|
+
: false;
|
|
3272
|
+
if (!handledAcceptedFailure) {
|
|
3273
|
+
this.recordProtocolViolation("invalid-response", {
|
|
3274
|
+
reason: "response-schema",
|
|
3275
|
+
frameType: "response",
|
|
3276
|
+
field: "response",
|
|
3277
|
+
expected: "valid-response-frame",
|
|
3278
|
+
actualType: "object",
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
2116
3281
|
if (rawId && knownPending) {
|
|
2117
3282
|
this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
|
|
2118
|
-
} else if (!rawId || !this.emitAcceptedPromptFailure(rawId, frame)) {
|
|
2119
|
-
this.recordProtocolViolation();
|
|
2120
3283
|
}
|
|
2121
3284
|
return;
|
|
2122
3285
|
}
|
|
@@ -2129,14 +3292,29 @@ class OmpRpcProcess {
|
|
|
2129
3292
|
}
|
|
2130
3293
|
const isBranchHistory = pending.command === "get_branch_messages";
|
|
2131
3294
|
const isHistory =
|
|
2132
|
-
pending.command === "get_messages" ||
|
|
2133
|
-
|
|
3295
|
+
pending.command === "get_messages" ||
|
|
3296
|
+
pending.command === "get_messages_page" ||
|
|
3297
|
+
pending.command === "get_subagent_messages";
|
|
3298
|
+
const rawResponseData =
|
|
3299
|
+
pending.command === "get_state"
|
|
3300
|
+
? projectSessionStateResponseData(response.data.data)
|
|
3301
|
+
: response.data.data;
|
|
3302
|
+
const responseData = isHistory ? sanitizeHistoryResponseData(rawResponseData) : rawResponseData;
|
|
3303
|
+
const boundedFrame =
|
|
3304
|
+
responseData === response.data.data ? frame : { ...frame, data: responseData };
|
|
3305
|
+
const responseItemLimit = isBranchHistory
|
|
3306
|
+
? 1_024
|
|
3307
|
+
: isHistory
|
|
3308
|
+
? 100_000
|
|
3309
|
+
: pending.command === "get_available_models"
|
|
3310
|
+
? MAX_MODEL_CATALOG_ITEMS
|
|
3311
|
+
: MAX_ARRAY_ITEMS;
|
|
2134
3312
|
const responseByteLimit =
|
|
2135
3313
|
isBranchHistory || isHistory
|
|
2136
3314
|
? Math.min(MAX_REASSEMBLED_FRAME_BYTES, this.reassembledFrameLimit)
|
|
2137
3315
|
: 2 * 1024 * 1024;
|
|
2138
|
-
//
|
|
2139
|
-
// node
|
|
3316
|
+
// Model catalogs are truncated before publication, while the transport still enforces
|
|
3317
|
+
// aggregate byte and node budgets over the complete response.
|
|
2140
3318
|
const responseNodeLimit = isBranchHistory
|
|
2141
3319
|
? 4_096
|
|
2142
3320
|
: isHistory
|
|
@@ -2144,17 +3322,21 @@ class OmpRpcProcess {
|
|
|
2144
3322
|
: pending.command === "get_available_models"
|
|
2145
3323
|
? 16_384
|
|
2146
3324
|
: 2_048;
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
) {
|
|
3325
|
+
const violation = jsonBoundViolation(
|
|
3326
|
+
boundedFrame,
|
|
3327
|
+
responseByteLimit,
|
|
3328
|
+
responseItemLimit,
|
|
3329
|
+
MAX_IMAGE_DATA_LENGTH,
|
|
3330
|
+
responseNodeLimit,
|
|
3331
|
+
);
|
|
3332
|
+
if (violation) {
|
|
2156
3333
|
this.takePending(response.data.id)?.reject(
|
|
2157
|
-
new
|
|
3334
|
+
new OmpRpcResponseLimitError(
|
|
3335
|
+
rpcDiagnosticCommand(pending.command),
|
|
3336
|
+
violation.dimension,
|
|
3337
|
+
violation.actual,
|
|
3338
|
+
violation.limit,
|
|
3339
|
+
),
|
|
2158
3340
|
);
|
|
2159
3341
|
return;
|
|
2160
3342
|
}
|
|
@@ -2162,7 +3344,7 @@ class OmpRpcProcess {
|
|
|
2162
3344
|
if (!settled) return;
|
|
2163
3345
|
if (response.data.success) {
|
|
2164
3346
|
try {
|
|
2165
|
-
settled.beforeResolve?.(
|
|
3347
|
+
settled.beforeResolve?.(responseData);
|
|
2166
3348
|
if (settled.command === "prompt") {
|
|
2167
3349
|
if (this.acceptedPromptIds.size >= MAX_PENDING_REQUESTS) {
|
|
2168
3350
|
const oldest = this.acceptedPromptIds.values().next().value;
|
|
@@ -2170,12 +3352,21 @@ class OmpRpcProcess {
|
|
|
2170
3352
|
}
|
|
2171
3353
|
this.acceptedPromptIds.add(response.data.id);
|
|
2172
3354
|
}
|
|
2173
|
-
settled.resolve(
|
|
3355
|
+
settled.resolve(responseData);
|
|
2174
3356
|
} catch {
|
|
2175
3357
|
settled.reject(new Error("OMP RPC response is invalid"));
|
|
2176
3358
|
}
|
|
2177
3359
|
} else {
|
|
2178
|
-
|
|
3360
|
+
const responseCode = response.data.code;
|
|
3361
|
+
const code: OmpRpcRequestRejectionCode | undefined =
|
|
3362
|
+
responseCode === "session_busy" ||
|
|
3363
|
+
responseCode === "stale_cursor" ||
|
|
3364
|
+
responseCode === "unsupported_command"
|
|
3365
|
+
? responseCode
|
|
3366
|
+
: response.data.error === `Unknown command: ${pending.command}`
|
|
3367
|
+
? "unsupported_command"
|
|
3368
|
+
: undefined;
|
|
3369
|
+
settled.reject(new OmpRpcRequestRejectedError(rpcDiagnosticCommand(pending.command), code));
|
|
2179
3370
|
}
|
|
2180
3371
|
}
|
|
2181
3372
|
|
|
@@ -2219,21 +3410,27 @@ class OmpRpcProcess {
|
|
|
2219
3410
|
this.fail(new Error("OMP emitted invalid terminal metadata"));
|
|
2220
3411
|
return true;
|
|
2221
3412
|
}
|
|
3413
|
+
const structuralFrame = mapRuntimeFrameDetails(frame, omitOptionalDetails);
|
|
2222
3414
|
const messagesAreSafe =
|
|
2223
3415
|
frame.messages === undefined ||
|
|
2224
3416
|
(Array.isArray(frame.messages) &&
|
|
2225
3417
|
frame.messages.length <= MAX_ARRAY_ITEMS &&
|
|
2226
3418
|
boundedJsonBytes(
|
|
2227
|
-
|
|
3419
|
+
structuralFrame.messages as unknown[],
|
|
2228
3420
|
MAX_SEMANTIC_FRAME_BYTES,
|
|
2229
|
-
|
|
2230
|
-
|
|
3421
|
+
OMP_MAX_CONTENT_PARTS,
|
|
3422
|
+
MAX_IMAGE_DATA_LENGTH,
|
|
2231
3423
|
4_096,
|
|
2232
3424
|
) !== Number.POSITIVE_INFINITY);
|
|
2233
3425
|
const payloadIsSafe =
|
|
2234
3426
|
messagesAreSafe &&
|
|
2235
|
-
boundedJsonBytes(
|
|
2236
|
-
|
|
3427
|
+
boundedJsonBytes(
|
|
3428
|
+
structuralFrame,
|
|
3429
|
+
MAX_SEMANTIC_FRAME_BYTES,
|
|
3430
|
+
OMP_MAX_CONTENT_PARTS,
|
|
3431
|
+
MAX_IMAGE_DATA_LENGTH,
|
|
3432
|
+
4_096,
|
|
3433
|
+
) !== Number.POSITIVE_INFINITY;
|
|
2237
3434
|
if (onlyUnsafePayload && payloadIsSafe) return false;
|
|
2238
3435
|
if (envelope.data.isTerminal === false) {
|
|
2239
3436
|
this.fail(new Error("OMP emitted an invalid nonterminal agent_end payload"));
|
|
@@ -2254,42 +3451,89 @@ class OmpRpcProcess {
|
|
|
2254
3451
|
return true;
|
|
2255
3452
|
}
|
|
2256
3453
|
|
|
2257
|
-
private receiveFrame(frame: Record<string, unknown
|
|
3454
|
+
private receiveFrame(frame: Record<string, unknown>, rawByteLength: number): void {
|
|
2258
3455
|
const type = typeof frame.type === "string" && frame.type.length <= 64 ? frame.type : null;
|
|
2259
3456
|
if (!type) {
|
|
2260
|
-
this.recordProtocolViolation(
|
|
3457
|
+
this.recordProtocolViolation("invalid-envelope", {
|
|
3458
|
+
reason: "missing-frame-type",
|
|
3459
|
+
field: "frame.type",
|
|
3460
|
+
expected: "bounded-frame-type",
|
|
3461
|
+
actualType: protocolDiagnosticActualType(frame.type),
|
|
3462
|
+
maxByteSize: rawByteLength,
|
|
3463
|
+
});
|
|
2261
3464
|
return;
|
|
2262
3465
|
}
|
|
2263
|
-
|
|
3466
|
+
const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
|
|
3467
|
+
if (this.receiveDegradedAgentEnd(safeFrame, true)) return;
|
|
2264
3468
|
if (
|
|
2265
|
-
boundedJsonBytes(
|
|
2266
|
-
|
|
3469
|
+
boundedJsonBytes(
|
|
3470
|
+
mapRuntimeFrameDetails(safeFrame, omitOptionalDetails),
|
|
3471
|
+
MAX_SEMANTIC_FRAME_BYTES,
|
|
3472
|
+
runtimeFrameCollectionLimit(safeFrame),
|
|
3473
|
+
MAX_IMAGE_DATA_LENGTH,
|
|
3474
|
+
4_096,
|
|
3475
|
+
) === Number.POSITIVE_INFINITY
|
|
2267
3476
|
) {
|
|
2268
|
-
this.recordProtocolViolation(
|
|
3477
|
+
this.recordProtocolViolation("frame-limit", {
|
|
3478
|
+
reason: "semantic-frame-limit",
|
|
3479
|
+
field: "frame.byteLength",
|
|
3480
|
+
expected: "within-byte-limit",
|
|
3481
|
+
actualType: "oversized",
|
|
3482
|
+
maxByteSize: rawByteLength,
|
|
3483
|
+
limitBytes: MAX_SEMANTIC_FRAME_BYTES,
|
|
3484
|
+
});
|
|
2269
3485
|
return;
|
|
2270
3486
|
}
|
|
2271
3487
|
if (type === "rpc_chunk") {
|
|
2272
|
-
const chunk = OmpChunkFrameSchema.safeParse(
|
|
2273
|
-
if (!chunk.success)
|
|
2274
|
-
|
|
3488
|
+
const chunk = OmpChunkFrameSchema.safeParse(safeFrame);
|
|
3489
|
+
if (!chunk.success) {
|
|
3490
|
+
this.rejectChunk("chunk-schema", {
|
|
3491
|
+
field: "chunk",
|
|
3492
|
+
expected: "valid-chunk-frame",
|
|
3493
|
+
actualType: "object",
|
|
3494
|
+
});
|
|
3495
|
+
} else this.receiveChunk(chunk.data);
|
|
2275
3496
|
return;
|
|
2276
3497
|
}
|
|
2277
3498
|
if (this.chunk) {
|
|
2278
3499
|
this.clearChunk();
|
|
2279
|
-
this.recordProtocolViolation(
|
|
3500
|
+
this.recordProtocolViolation("interleaved-chunk", {
|
|
3501
|
+
reason: "frame-interleaved-with-chunk",
|
|
3502
|
+
field: "chunk.sequence",
|
|
3503
|
+
expected: "no-interleaved-frame",
|
|
3504
|
+
actualType: "interleaved",
|
|
3505
|
+
});
|
|
2280
3506
|
}
|
|
2281
3507
|
if (type === "rpc_frame_error") {
|
|
2282
|
-
this.recordProtocolViolation(
|
|
3508
|
+
this.recordProtocolViolation("remote-frame-error", {
|
|
3509
|
+
reason: "remote-frame-error",
|
|
3510
|
+
frameType: "rpc_frame_error",
|
|
3511
|
+
field: "frame",
|
|
3512
|
+
expected: "no-remote-frame-error",
|
|
3513
|
+
actualType: "remote-error",
|
|
3514
|
+
});
|
|
2283
3515
|
return;
|
|
2284
3516
|
}
|
|
2285
3517
|
if (type === "ready") {
|
|
2286
3518
|
if (this.readyReceived) {
|
|
2287
|
-
this.recordProtocolViolation(
|
|
3519
|
+
this.recordProtocolViolation("duplicate-ready", {
|
|
3520
|
+
reason: "ready-already-received",
|
|
3521
|
+
frameType: "ready",
|
|
3522
|
+
field: "ready",
|
|
3523
|
+
expected: "single-ready-frame",
|
|
3524
|
+
actualType: "duplicate",
|
|
3525
|
+
});
|
|
2288
3526
|
return;
|
|
2289
3527
|
}
|
|
2290
|
-
const ready = OmpReadyFrameSchema.safeParse(
|
|
3528
|
+
const ready = OmpReadyFrameSchema.safeParse(safeFrame);
|
|
2291
3529
|
if (!ready.success) {
|
|
2292
|
-
this.recordProtocolViolation(
|
|
3530
|
+
this.recordProtocolViolation("invalid-ready", {
|
|
3531
|
+
reason: "ready-schema",
|
|
3532
|
+
frameType: "ready",
|
|
3533
|
+
field: "ready",
|
|
3534
|
+
expected: "valid-ready-frame",
|
|
3535
|
+
actualType: "object",
|
|
3536
|
+
});
|
|
2293
3537
|
} else {
|
|
2294
3538
|
this.readyReceived = true;
|
|
2295
3539
|
this.resolveReady(ready.data);
|
|
@@ -2297,24 +3541,37 @@ class OmpRpcProcess {
|
|
|
2297
3541
|
return;
|
|
2298
3542
|
}
|
|
2299
3543
|
if (type === "response") {
|
|
2300
|
-
this.receiveResponse(
|
|
3544
|
+
this.receiveResponse(safeFrame);
|
|
2301
3545
|
return;
|
|
2302
3546
|
}
|
|
2303
|
-
const event = OmpRuntimeEventSchema.safeParse(
|
|
3547
|
+
const event = OmpRuntimeEventSchema.safeParse(safeFrame);
|
|
2304
3548
|
if (!event.success) {
|
|
2305
|
-
this.rejectMatchingToolApproval(
|
|
2306
|
-
if (type === "agent_end" && this.receiveDegradedAgentEnd(
|
|
2307
|
-
this.recordProtocolViolation(
|
|
3549
|
+
this.rejectMatchingToolApproval(safeFrame);
|
|
3550
|
+
if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
|
|
3551
|
+
this.recordProtocolViolation("invalid-event", {
|
|
3552
|
+
...invalidEventDiagnosticMetadata(safeFrame, type),
|
|
3553
|
+
maxByteSize: rawByteLength,
|
|
3554
|
+
});
|
|
2308
3555
|
return;
|
|
2309
3556
|
}
|
|
2310
3557
|
if (!this.acceptEventState(event.data)) {
|
|
2311
|
-
this.recordProtocolViolation(
|
|
3558
|
+
this.recordProtocolViolation("invalid-event-state", {
|
|
3559
|
+
reason: "event-state-transition",
|
|
3560
|
+
eventType: event.data.type,
|
|
3561
|
+
field: "event.sequence",
|
|
3562
|
+
expected: "valid-event-state-transition",
|
|
3563
|
+
actualType: "out-of-order",
|
|
3564
|
+
maxByteSize: rawByteLength,
|
|
3565
|
+
});
|
|
2312
3566
|
return;
|
|
2313
3567
|
}
|
|
2314
3568
|
this.emit(event.data);
|
|
2315
3569
|
if (event.data.type === "prompt_result" && event.data.id) {
|
|
2316
3570
|
this.acceptedPromptIds.delete(event.data.id);
|
|
2317
3571
|
}
|
|
3572
|
+
if (event.data.type === "turn_end" || event.data.type === "agent_end") {
|
|
3573
|
+
this.turnActive = false;
|
|
3574
|
+
}
|
|
2318
3575
|
if (
|
|
2319
3576
|
event.data.type === "message_end" ||
|
|
2320
3577
|
event.data.type === "turn_end" ||
|
|
@@ -2342,6 +3599,7 @@ class OmpRpcProcess {
|
|
|
2342
3599
|
|
|
2343
3600
|
private acceptEventState(event: z.infer<typeof OmpRuntimeEventSchema>): boolean {
|
|
2344
3601
|
if (event.type === "turn_start") {
|
|
3602
|
+
this.turnActive = true;
|
|
2345
3603
|
this.streamedBlocks.clear();
|
|
2346
3604
|
this.commandTextLength = 0;
|
|
2347
3605
|
this.activeToolCallIds.clear();
|
|
@@ -2363,7 +3621,7 @@ class OmpRpcProcess {
|
|
|
2363
3621
|
this.activeToolCallIds.add(event.toolCallId);
|
|
2364
3622
|
return true;
|
|
2365
3623
|
}
|
|
2366
|
-
if (event.type === "tool_execution_update") {
|
|
3624
|
+
if (event.type === "tool_execution_update" || event.type === "tool_stream_update") {
|
|
2367
3625
|
return this.activeToolCallIds.has(event.toolCallId);
|
|
2368
3626
|
}
|
|
2369
3627
|
if (event.type === "tool_execution_end") {
|
|
@@ -2411,13 +3669,84 @@ class OmpRpcProcess {
|
|
|
2411
3669
|
return true;
|
|
2412
3670
|
}
|
|
2413
3671
|
|
|
2414
|
-
private
|
|
3672
|
+
private protocolDiagnosticPhase(): OmpProtocolDiagnosticPhase {
|
|
3673
|
+
if (this.closed || this.exited || this.outputSettled) return "closing";
|
|
3674
|
+
if (!this.readyReceived) return "startup";
|
|
3675
|
+
for (const pending of this.pending.values()) {
|
|
3676
|
+
if (pending.command === "negotiate_protocol") return "negotiation";
|
|
3677
|
+
if (pending.command === "prompt") return "active-turn";
|
|
3678
|
+
}
|
|
3679
|
+
if (this.turnActive || this.acceptedPromptIds.size > 0) return "active-turn";
|
|
3680
|
+
return "idle";
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
private rejectChunk(
|
|
3684
|
+
reason: OmpProtocolViolationReason,
|
|
3685
|
+
metadata: Omit<
|
|
3686
|
+
OmpProtocolViolationDiagnostic,
|
|
3687
|
+
"category" | "reason" | "occurrenceCount" | "phase" | "frameType"
|
|
3688
|
+
> = {},
|
|
3689
|
+
): void {
|
|
2415
3690
|
this.clearChunk();
|
|
2416
|
-
this.recordProtocolViolation(
|
|
3691
|
+
this.recordProtocolViolation("invalid-chunk", {
|
|
3692
|
+
reason,
|
|
3693
|
+
frameType: "rpc_chunk",
|
|
3694
|
+
...metadata,
|
|
3695
|
+
});
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
private recordProtocolViolation(
|
|
3699
|
+
category: OmpProtocolViolationCategory,
|
|
3700
|
+
metadata: Omit<OmpProtocolViolationDiagnostic, "category" | "occurrenceCount" | "phase">,
|
|
3701
|
+
): void {
|
|
3702
|
+
const phase = this.protocolDiagnosticPhase();
|
|
3703
|
+
if (!this.protocolViolationTimer) {
|
|
3704
|
+
this.emitProtocolViolation({ category, occurrenceCount: 1, phase, ...metadata });
|
|
3705
|
+
this.protocolViolationTimer = setTimeout(
|
|
3706
|
+
() => this.flushProtocolViolations(),
|
|
3707
|
+
PROTOCOL_VIOLATION_COALESCE_MS,
|
|
3708
|
+
);
|
|
3709
|
+
return;
|
|
3710
|
+
}
|
|
3711
|
+
const key = `${category}:${metadata.reason}` as ProtocolViolationKey;
|
|
3712
|
+
const pending = this.pendingProtocolViolations.get(key);
|
|
3713
|
+
if (!pending) {
|
|
3714
|
+
this.pendingProtocolViolations.set(key, {
|
|
3715
|
+
category,
|
|
3716
|
+
occurrenceCount: 1,
|
|
3717
|
+
phase,
|
|
3718
|
+
...metadata,
|
|
3719
|
+
});
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
pending.occurrenceCount = Math.min(Number.MAX_SAFE_INTEGER, pending.occurrenceCount + 1);
|
|
3723
|
+
pending.maxByteSize =
|
|
3724
|
+
Math.max(pending.maxByteSize ?? 0, metadata.maxByteSize ?? 0) || undefined;
|
|
3725
|
+
pending.reason = metadata.reason;
|
|
3726
|
+
pending.phase = phase;
|
|
3727
|
+
pending.frameType = metadata.frameType;
|
|
3728
|
+
pending.field = metadata.field;
|
|
3729
|
+
pending.expected = metadata.expected;
|
|
3730
|
+
pending.actualType = metadata.actualType;
|
|
3731
|
+
pending.limitBytes = metadata.limitBytes;
|
|
2417
3732
|
}
|
|
2418
3733
|
|
|
2419
|
-
private
|
|
2420
|
-
|
|
3734
|
+
private flushProtocolViolations(): void {
|
|
3735
|
+
if (this.protocolViolationTimer) clearTimeout(this.protocolViolationTimer);
|
|
3736
|
+
this.protocolViolationTimer = null;
|
|
3737
|
+
for (const diagnostic of this.pendingProtocolViolations.values()) {
|
|
3738
|
+
this.emitProtocolViolation(diagnostic);
|
|
3739
|
+
}
|
|
3740
|
+
this.pendingProtocolViolations.clear();
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
private emitProtocolViolation(diagnostic: OmpProtocolViolationDiagnostic): void {
|
|
3744
|
+
try {
|
|
3745
|
+
const reporting = this.reportProtocolViolation(diagnostic);
|
|
3746
|
+
if (reporting) void reporting.catch(() => undefined);
|
|
3747
|
+
} catch {
|
|
3748
|
+
// Diagnostics must never alter transport flow or cleanup.
|
|
3749
|
+
}
|
|
2421
3750
|
}
|
|
2422
3751
|
|
|
2423
3752
|
private fail(error: Error): void {
|
|
@@ -2503,6 +3832,7 @@ class OmpRpcSession implements OmpRuntimeSession {
|
|
|
2503
3832
|
readonly inheritedRedactionValues: readonly string[],
|
|
2504
3833
|
readonly supportsTypedToolApprovals: boolean,
|
|
2505
3834
|
) {}
|
|
3835
|
+
private messagePagingSupport: "unknown" | "supported" | "unsupported" = "unknown";
|
|
2506
3836
|
|
|
2507
3837
|
onEvent(listener: (event: OmpRpcEvent) => void): () => void {
|
|
2508
3838
|
return this.process.onEvent(listener);
|
|
@@ -2516,6 +3846,195 @@ class OmpRpcSession implements OmpRuntimeSession {
|
|
|
2516
3846
|
return OmpSessionStatsSchema.parse(await this.process.request({ type: "get_session_stats" }));
|
|
2517
3847
|
}
|
|
2518
3848
|
|
|
3849
|
+
async getMessages(signal?: AbortSignal): Promise<OmpMessage[]> {
|
|
3850
|
+
if (!this.canReplayHistory) {
|
|
3851
|
+
throw new Error("OMP history replay requires negotiated RPC protocol v2");
|
|
3852
|
+
}
|
|
3853
|
+
if (this.messagePagingSupport === "unsupported") {
|
|
3854
|
+
const legacy = OmpMessagesResultSchema.parse(
|
|
3855
|
+
await this.requestWithSignal({ type: "get_messages" }, signal),
|
|
3856
|
+
);
|
|
3857
|
+
return legacy.messages;
|
|
3858
|
+
}
|
|
3859
|
+
let staleRestarts = 0;
|
|
3860
|
+
let busyRetries = 0;
|
|
3861
|
+
while (true) {
|
|
3862
|
+
const messages: OmpMessage[] = [];
|
|
3863
|
+
const seenCursors = new Set<string>();
|
|
3864
|
+
const seenMessageIdentities = new Set<string>();
|
|
3865
|
+
let pageCount = 0;
|
|
3866
|
+
let cursor: string | undefined;
|
|
3867
|
+
let totalMessages: number | undefined;
|
|
3868
|
+
let aggregateBytes = 0;
|
|
3869
|
+
let aggregateNodes = 0;
|
|
3870
|
+
try {
|
|
3871
|
+
while (true) {
|
|
3872
|
+
signal?.throwIfAborted();
|
|
3873
|
+
let rawPage: unknown;
|
|
3874
|
+
try {
|
|
3875
|
+
rawPage = await this.requestWithSignal(
|
|
3876
|
+
{
|
|
3877
|
+
type: "get_messages_page",
|
|
3878
|
+
...(cursor ? { cursor } : {}),
|
|
3879
|
+
limit: OMP_MESSAGE_PAGE_LIMIT,
|
|
3880
|
+
},
|
|
3881
|
+
signal,
|
|
3882
|
+
);
|
|
3883
|
+
} catch (error) {
|
|
3884
|
+
if (
|
|
3885
|
+
error instanceof OmpRpcRequestRejectedError &&
|
|
3886
|
+
error.code === "unsupported_command"
|
|
3887
|
+
) {
|
|
3888
|
+
this.messagePagingSupport = "unsupported";
|
|
3889
|
+
const legacy = OmpMessagesResultSchema.parse(
|
|
3890
|
+
await this.requestWithSignal({ type: "get_messages" }, signal),
|
|
3891
|
+
);
|
|
3892
|
+
return legacy.messages;
|
|
3893
|
+
}
|
|
3894
|
+
if (
|
|
3895
|
+
error instanceof OmpRpcRequestRejectedError &&
|
|
3896
|
+
error.code === "session_busy" &&
|
|
3897
|
+
busyRetries < MAX_MESSAGE_PAGE_BUSY_RETRIES
|
|
3898
|
+
) {
|
|
3899
|
+
const delayMs = MESSAGE_PAGE_RETRY_BASE_MS * 2 ** busyRetries;
|
|
3900
|
+
busyRetries += 1;
|
|
3901
|
+
await this.waitForReplayRetry(delayMs, signal);
|
|
3902
|
+
continue;
|
|
3903
|
+
}
|
|
3904
|
+
throw error;
|
|
3905
|
+
}
|
|
3906
|
+
pageCount += 1;
|
|
3907
|
+
if (pageCount > MAX_MESSAGE_PAGES) {
|
|
3908
|
+
throw new OmpRpcResponseLimitError(
|
|
3909
|
+
"get_messages_page",
|
|
3910
|
+
"items",
|
|
3911
|
+
pageCount,
|
|
3912
|
+
MAX_MESSAGE_PAGES,
|
|
3913
|
+
);
|
|
3914
|
+
}
|
|
3915
|
+
const page = OmpMessagesPageResultSchema.parse(rawPage);
|
|
3916
|
+
this.messagePagingSupport = "supported";
|
|
3917
|
+
if (totalMessages !== undefined && page.totalMessages !== totalMessages) {
|
|
3918
|
+
throw new Error("OMP message pagination returned an inconsistent total");
|
|
3919
|
+
}
|
|
3920
|
+
totalMessages ??= page.totalMessages;
|
|
3921
|
+
const nextMessageCount = messages.length + page.messages.length;
|
|
3922
|
+
if (nextMessageCount > totalMessages || nextMessageCount > MAX_REPLAY_MESSAGES) {
|
|
3923
|
+
throw new OmpRpcResponseLimitError(
|
|
3924
|
+
"get_messages_page",
|
|
3925
|
+
"items",
|
|
3926
|
+
nextMessageCount,
|
|
3927
|
+
Math.min(totalMessages, MAX_REPLAY_MESSAGES),
|
|
3928
|
+
);
|
|
3929
|
+
}
|
|
3930
|
+
const pageIdentities = new Set<string>();
|
|
3931
|
+
for (const message of page.messages) {
|
|
3932
|
+
const identity = message.entryId ?? message.id ?? message.responseId;
|
|
3933
|
+
if (!identity) continue;
|
|
3934
|
+
if (seenMessageIdentities.has(identity)) {
|
|
3935
|
+
throw new Error("OMP message pagination repeated a message identity");
|
|
3936
|
+
}
|
|
3937
|
+
pageIdentities.add(identity);
|
|
3938
|
+
}
|
|
3939
|
+
const inspection = inspectJsonBounds(
|
|
3940
|
+
page.messages,
|
|
3941
|
+
MAX_REPLAY_BYTES,
|
|
3942
|
+
MAX_REPLAY_MESSAGES,
|
|
3943
|
+
MAX_IMAGE_DATA_LENGTH,
|
|
3944
|
+
MAX_REPLAY_NODES,
|
|
3945
|
+
);
|
|
3946
|
+
if (inspection.violation) {
|
|
3947
|
+
throw new OmpRpcResponseLimitError(
|
|
3948
|
+
"get_messages_page",
|
|
3949
|
+
inspection.violation.dimension,
|
|
3950
|
+
inspection.violation.actual,
|
|
3951
|
+
inspection.violation.limit,
|
|
3952
|
+
);
|
|
3953
|
+
}
|
|
3954
|
+
aggregateBytes += inspection.bytes;
|
|
3955
|
+
aggregateNodes += inspection.nodes;
|
|
3956
|
+
if (aggregateBytes > MAX_REPLAY_BYTES) {
|
|
3957
|
+
throw new OmpRpcResponseLimitError(
|
|
3958
|
+
"get_messages_page",
|
|
3959
|
+
"bytes",
|
|
3960
|
+
aggregateBytes,
|
|
3961
|
+
MAX_REPLAY_BYTES,
|
|
3962
|
+
);
|
|
3963
|
+
}
|
|
3964
|
+
if (aggregateNodes > MAX_REPLAY_NODES) {
|
|
3965
|
+
throw new OmpRpcResponseLimitError(
|
|
3966
|
+
"get_messages_page",
|
|
3967
|
+
"nodes",
|
|
3968
|
+
aggregateNodes,
|
|
3969
|
+
MAX_REPLAY_NODES,
|
|
3970
|
+
);
|
|
3971
|
+
}
|
|
3972
|
+
for (const identity of pageIdentities) seenMessageIdentities.add(identity);
|
|
3973
|
+
messages.push(...page.messages);
|
|
3974
|
+
const nextCursor = page.nextCursor;
|
|
3975
|
+
if (!nextCursor) {
|
|
3976
|
+
if (messages.length !== totalMessages) {
|
|
3977
|
+
throw new Error("OMP message pagination ended before the advertised total");
|
|
3978
|
+
}
|
|
3979
|
+
return messages;
|
|
3980
|
+
}
|
|
3981
|
+
if (page.messages.length === 0 || messages.length >= totalMessages) {
|
|
3982
|
+
throw new Error("OMP message pagination did not make progress");
|
|
3983
|
+
}
|
|
3984
|
+
if (seenCursors.has(nextCursor)) {
|
|
3985
|
+
throw new Error("OMP message pagination repeated a cursor");
|
|
3986
|
+
}
|
|
3987
|
+
seenCursors.add(nextCursor);
|
|
3988
|
+
cursor = nextCursor;
|
|
3989
|
+
}
|
|
3990
|
+
} catch (error) {
|
|
3991
|
+
if (
|
|
3992
|
+
error instanceof OmpRpcRequestRejectedError &&
|
|
3993
|
+
error.code === "stale_cursor" &&
|
|
3994
|
+
staleRestarts < MAX_MESSAGE_PAGE_STALE_RESTARTS
|
|
3995
|
+
) {
|
|
3996
|
+
staleRestarts += 1;
|
|
3997
|
+
continue;
|
|
3998
|
+
}
|
|
3999
|
+
throw error;
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
private async requestWithSignal(
|
|
4005
|
+
command: Record<string, unknown>,
|
|
4006
|
+
signal?: AbortSignal,
|
|
4007
|
+
): Promise<unknown> {
|
|
4008
|
+
signal?.throwIfAborted();
|
|
4009
|
+
const request = this.process.request(command);
|
|
4010
|
+
if (!signal) return request;
|
|
4011
|
+
const aborted = Promise.withResolvers<never>();
|
|
4012
|
+
const onAbort = () => aborted.reject(signal.reason);
|
|
4013
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4014
|
+
try {
|
|
4015
|
+
return await Promise.race([request, aborted.promise]);
|
|
4016
|
+
} finally {
|
|
4017
|
+
signal.removeEventListener("abort", onAbort);
|
|
4018
|
+
void request.catch(() => undefined);
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
private async waitForReplayRetry(delayMs: number, signal?: AbortSignal): Promise<void> {
|
|
4023
|
+
signal?.throwIfAborted();
|
|
4024
|
+
if (!signal) {
|
|
4025
|
+
await waitMs(delayMs);
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4028
|
+
const aborted = Promise.withResolvers<never>();
|
|
4029
|
+
const onAbort = () => aborted.reject(signal.reason);
|
|
4030
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4031
|
+
try {
|
|
4032
|
+
await Promise.race([waitMs(delayMs), aborted.promise]);
|
|
4033
|
+
} finally {
|
|
4034
|
+
signal.removeEventListener("abort", onAbort);
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
|
|
2519
4038
|
async compact(customInstructions?: string): Promise<OmpCompactionResult> {
|
|
2520
4039
|
const instructions =
|
|
2521
4040
|
customInstructions === undefined
|
|
@@ -2611,16 +4130,6 @@ class OmpRpcSession implements OmpRuntimeSession {
|
|
|
2611
4130
|
return result.data;
|
|
2612
4131
|
}
|
|
2613
4132
|
|
|
2614
|
-
async getMessages(): Promise<OmpMessage[]> {
|
|
2615
|
-
if (!this.canReplayHistory) {
|
|
2616
|
-
throw new Error("OMP history replay requires negotiated RPC protocol v2");
|
|
2617
|
-
}
|
|
2618
|
-
const result = OmpMessagesResultSchema.parse(
|
|
2619
|
-
await this.process.request({ type: "get_messages" }),
|
|
2620
|
-
);
|
|
2621
|
-
return result.messages;
|
|
2622
|
-
}
|
|
2623
|
-
|
|
2624
4133
|
async setHostTools(tools: readonly OmpHostToolDefinition[]): Promise<string[]> {
|
|
2625
4134
|
const safeTools = z.array(OmpHostToolDefinitionSchema).max(MAX_HOST_TOOLS).parse(tools);
|
|
2626
4135
|
if (safeTools.length === 0) return [];
|
|
@@ -2728,12 +4237,16 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
2728
4237
|
);
|
|
2729
4238
|
return {
|
|
2730
4239
|
...transcript,
|
|
2731
|
-
messages: z
|
|
4240
|
+
messages: z
|
|
4241
|
+
.array(OmpMessageSchema)
|
|
4242
|
+
.max(MAX_REPLAY_MESSAGES)
|
|
4243
|
+
.parse(sanitizeMessageListMetadata(transcript.messages)),
|
|
2732
4244
|
};
|
|
2733
4245
|
}
|
|
2734
4246
|
async readPersistedSubagentTranscript(options: {
|
|
2735
4247
|
parentSessionFile: string;
|
|
2736
4248
|
childTranscriptId: string;
|
|
4249
|
+
sessionFile?: string;
|
|
2737
4250
|
cwd: string;
|
|
2738
4251
|
signal?: AbortSignal;
|
|
2739
4252
|
}): Promise<OmpPersistedSubagentMessages> {
|
|
@@ -2742,10 +4255,14 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
2742
4255
|
options.childTranscriptId,
|
|
2743
4256
|
options.cwd,
|
|
2744
4257
|
options.signal,
|
|
4258
|
+
options.sessionFile,
|
|
2745
4259
|
);
|
|
2746
4260
|
return {
|
|
2747
4261
|
...transcript,
|
|
2748
|
-
messages: z
|
|
4262
|
+
messages: z
|
|
4263
|
+
.array(OmpMessageSchema)
|
|
4264
|
+
.max(MAX_REPLAY_MESSAGES)
|
|
4265
|
+
.parse(sanitizeMessageListMetadata(transcript.messages)),
|
|
2749
4266
|
};
|
|
2750
4267
|
}
|
|
2751
4268
|
|
|
@@ -2760,6 +4277,7 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
2760
4277
|
this.options.spawnProcess,
|
|
2761
4278
|
this.options.terminateProcessTree,
|
|
2762
4279
|
options.requestTimeoutMs ?? this.options.requestTimeoutMs,
|
|
4280
|
+
this.options.reportProtocolViolation,
|
|
2763
4281
|
);
|
|
2764
4282
|
const abort = () => void process.close().catch(() => undefined);
|
|
2765
4283
|
options.signal?.addEventListener("abort", abort, { once: true });
|