@omercnet/paseo-omp 0.3.0 → 0.4.0-next.114.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 +2 -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 +60 -8
- package/server/provider/host-tools.ts +284 -34
- package/server/provider/mcp-transport.ts +2 -1
- package/server/provider/omp-rpc.ts +1011 -107
- 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 +45 -11
- package/server/provider/session.ts +200 -58
- package/server/provider/subsessions.ts +311 -73
- package/server/provider/timeline-projector.ts +34 -11
- 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,298 @@ 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
|
+
|
|
90
|
+
export const OMP_PROTOCOL_VIOLATION_CATEGORIES = [
|
|
91
|
+
"duplicate-ready",
|
|
92
|
+
"frame-limit",
|
|
93
|
+
"incomplete-frame",
|
|
94
|
+
"interleaved-chunk",
|
|
95
|
+
"invalid-chunk",
|
|
96
|
+
"invalid-envelope",
|
|
97
|
+
"invalid-event",
|
|
98
|
+
"invalid-event-state",
|
|
99
|
+
"invalid-json",
|
|
100
|
+
"invalid-ready",
|
|
101
|
+
"invalid-response",
|
|
102
|
+
"remote-frame-error",
|
|
103
|
+
] as const;
|
|
104
|
+
export type OmpProtocolViolationCategory = (typeof OMP_PROTOCOL_VIOLATION_CATEGORIES)[number];
|
|
105
|
+
export const OMP_PROTOCOL_VIOLATION_REASONS = [
|
|
106
|
+
"output-ended-mid-frame",
|
|
107
|
+
"physical-frame-limit",
|
|
108
|
+
"semantic-frame-limit",
|
|
109
|
+
"json-decode",
|
|
110
|
+
"chunk-json-decode",
|
|
111
|
+
"object-envelope",
|
|
112
|
+
"missing-frame-type",
|
|
113
|
+
"response-schema",
|
|
114
|
+
"chunk-schema",
|
|
115
|
+
"chunk-metadata",
|
|
116
|
+
"chunk-size",
|
|
117
|
+
"chunk-start-index",
|
|
118
|
+
"chunk-sequence",
|
|
119
|
+
"chunk-byte-count",
|
|
120
|
+
"chunk-timeout",
|
|
121
|
+
"frame-interleaved-with-chunk",
|
|
122
|
+
"remote-frame-error",
|
|
123
|
+
"ready-already-received",
|
|
124
|
+
"ready-schema",
|
|
125
|
+
"notice-level-type",
|
|
126
|
+
"notice-message-type",
|
|
127
|
+
"notice-schema",
|
|
128
|
+
"event-schema",
|
|
129
|
+
"message-event-schema",
|
|
130
|
+
"tool-event-schema",
|
|
131
|
+
"lifecycle-event-schema",
|
|
132
|
+
"subagent-event-schema",
|
|
133
|
+
"configuration-event-schema",
|
|
134
|
+
"extension-ui-schema",
|
|
135
|
+
"unknown-event-type",
|
|
136
|
+
"event-state-transition",
|
|
137
|
+
] as const;
|
|
138
|
+
export type OmpProtocolViolationReason = (typeof OMP_PROTOCOL_VIOLATION_REASONS)[number];
|
|
139
|
+
export const OMP_PROTOCOL_DIAGNOSTIC_PHASES = [
|
|
140
|
+
"startup",
|
|
141
|
+
"negotiation",
|
|
142
|
+
"idle",
|
|
143
|
+
"active-turn",
|
|
144
|
+
"closing",
|
|
145
|
+
] as const;
|
|
146
|
+
export type OmpProtocolDiagnosticPhase = (typeof OMP_PROTOCOL_DIAGNOSTIC_PHASES)[number];
|
|
147
|
+
|
|
148
|
+
export const OMP_PROTOCOL_EVENT_TYPES = [
|
|
149
|
+
"agent_start",
|
|
150
|
+
"agent_end",
|
|
151
|
+
"turn_start",
|
|
152
|
+
"turn_end",
|
|
153
|
+
"message_start",
|
|
154
|
+
"message_update",
|
|
155
|
+
"message_end",
|
|
156
|
+
"tool_execution_start",
|
|
157
|
+
"tool_execution_update",
|
|
158
|
+
"tool_execution_end",
|
|
159
|
+
"compaction_start",
|
|
160
|
+
"compaction_end",
|
|
161
|
+
"subagent_lifecycle",
|
|
162
|
+
"subagent_progress",
|
|
163
|
+
"subagent_event",
|
|
164
|
+
"todo_reminder",
|
|
165
|
+
"model_changed",
|
|
166
|
+
"thinking_level_changed",
|
|
167
|
+
"goal_updated",
|
|
168
|
+
"auto_retry_start",
|
|
169
|
+
"auto_retry_end",
|
|
170
|
+
"retry_fallback_applied",
|
|
171
|
+
"retry_fallback_succeeded",
|
|
172
|
+
"todo_auto_clear",
|
|
173
|
+
"auto_compaction_start",
|
|
174
|
+
"auto_compaction_end",
|
|
175
|
+
"available_commands_update",
|
|
176
|
+
"notice",
|
|
177
|
+
"command_output",
|
|
178
|
+
"extension_ui_request",
|
|
179
|
+
"prompt_result",
|
|
180
|
+
"host_tool_call",
|
|
181
|
+
"host_tool_cancel",
|
|
182
|
+
"tool_approval_request",
|
|
183
|
+
"tool_approval_cancel",
|
|
184
|
+
"advisor_yielded",
|
|
185
|
+
] as const;
|
|
186
|
+
export type OmpProtocolEventType = (typeof OMP_PROTOCOL_EVENT_TYPES)[number];
|
|
187
|
+
|
|
188
|
+
export const OMP_PROTOCOL_DIAGNOSTIC_FIELDS = [
|
|
189
|
+
"frame",
|
|
190
|
+
"frame.byteLength",
|
|
191
|
+
"frame.type",
|
|
192
|
+
"response",
|
|
193
|
+
"ready",
|
|
194
|
+
"event",
|
|
195
|
+
"event.sequence",
|
|
196
|
+
"notice.level",
|
|
197
|
+
"notice.message",
|
|
198
|
+
"chunk",
|
|
199
|
+
"chunk.data",
|
|
200
|
+
"chunk.index",
|
|
201
|
+
"chunk.sequence",
|
|
202
|
+
"chunk.byteLength",
|
|
203
|
+
] as const;
|
|
204
|
+
export type OmpProtocolDiagnosticField = (typeof OMP_PROTOCOL_DIAGNOSTIC_FIELDS)[number];
|
|
205
|
+
|
|
206
|
+
export const OMP_PROTOCOL_DIAGNOSTIC_EXPECTATIONS = [
|
|
207
|
+
"complete-json-line",
|
|
208
|
+
"within-byte-limit",
|
|
209
|
+
"valid-json",
|
|
210
|
+
"object-envelope",
|
|
211
|
+
"bounded-frame-type",
|
|
212
|
+
"valid-response-frame",
|
|
213
|
+
"valid-ready-frame",
|
|
214
|
+
"single-ready-frame",
|
|
215
|
+
"valid-event-frame",
|
|
216
|
+
"valid-event-state-transition",
|
|
217
|
+
"valid-message-event",
|
|
218
|
+
"valid-tool-event",
|
|
219
|
+
"valid-lifecycle-event",
|
|
220
|
+
"valid-subagent-event",
|
|
221
|
+
"valid-configuration-event",
|
|
222
|
+
"valid-extension-ui-event",
|
|
223
|
+
"known-event-type",
|
|
224
|
+
"notice-level-enum",
|
|
225
|
+
"notice-message-string",
|
|
226
|
+
"valid-chunk-frame",
|
|
227
|
+
"valid-base64-chunk",
|
|
228
|
+
"first-chunk-index-zero",
|
|
229
|
+
"contiguous-chunk-sequence",
|
|
230
|
+
"declared-chunk-byte-count",
|
|
231
|
+
"chunk-before-deadline",
|
|
232
|
+
"no-interleaved-frame",
|
|
233
|
+
"no-remote-frame-error",
|
|
234
|
+
] as const;
|
|
235
|
+
export type OmpProtocolDiagnosticExpectation =
|
|
236
|
+
(typeof OMP_PROTOCOL_DIAGNOSTIC_EXPECTATIONS)[number];
|
|
237
|
+
|
|
238
|
+
export const OMP_PROTOCOL_DIAGNOSTIC_ACTUAL_TYPES = [
|
|
239
|
+
"missing",
|
|
240
|
+
"null",
|
|
241
|
+
"array",
|
|
242
|
+
"object",
|
|
243
|
+
"string",
|
|
244
|
+
"number",
|
|
245
|
+
"boolean",
|
|
246
|
+
"invalid-json",
|
|
247
|
+
"partial-frame",
|
|
248
|
+
"oversized",
|
|
249
|
+
"duplicate",
|
|
250
|
+
"interleaved",
|
|
251
|
+
"out-of-order",
|
|
252
|
+
"timeout",
|
|
253
|
+
"remote-error",
|
|
254
|
+
"mismatched",
|
|
255
|
+
] as const;
|
|
256
|
+
export type OmpProtocolDiagnosticActualType = (typeof OMP_PROTOCOL_DIAGNOSTIC_ACTUAL_TYPES)[number];
|
|
257
|
+
|
|
258
|
+
export interface OmpProtocolViolationDiagnostic {
|
|
259
|
+
category: OmpProtocolViolationCategory;
|
|
260
|
+
reason: OmpProtocolViolationReason;
|
|
261
|
+
phase: OmpProtocolDiagnosticPhase;
|
|
262
|
+
occurrenceCount: number;
|
|
263
|
+
eventType?: OmpProtocolEventType;
|
|
264
|
+
frameType?: "ready" | "response" | "rpc_chunk" | "rpc_frame_error" | "notice";
|
|
265
|
+
field?: OmpProtocolDiagnosticField;
|
|
266
|
+
expected?: OmpProtocolDiagnosticExpectation;
|
|
267
|
+
actualType?: OmpProtocolDiagnosticActualType;
|
|
268
|
+
maxByteSize?: number;
|
|
269
|
+
limitBytes?: number;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function protocolDiagnosticActualType(value: unknown): OmpProtocolDiagnosticActualType {
|
|
273
|
+
if (value === undefined) return "missing";
|
|
274
|
+
if (value === null) return "null";
|
|
275
|
+
if (Array.isArray(value)) return "array";
|
|
276
|
+
if (typeof value === "object") return "object";
|
|
277
|
+
if (typeof value === "string") return "string";
|
|
278
|
+
if (typeof value === "number") return "number";
|
|
279
|
+
if (typeof value === "boolean") return "boolean";
|
|
280
|
+
return "object";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isProtocolEventType(value: string): value is OmpProtocolEventType {
|
|
284
|
+
return (OMP_PROTOCOL_EVENT_TYPES as readonly string[]).includes(value);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function eventSchemaClassification(
|
|
288
|
+
eventType: OmpProtocolEventType,
|
|
289
|
+
): Pick<OmpProtocolViolationDiagnostic, "reason" | "expected"> {
|
|
290
|
+
switch (eventType) {
|
|
291
|
+
case "message_start":
|
|
292
|
+
case "message_update":
|
|
293
|
+
case "message_end":
|
|
294
|
+
return { reason: "message-event-schema", expected: "valid-message-event" };
|
|
295
|
+
case "tool_execution_start":
|
|
296
|
+
case "tool_execution_update":
|
|
297
|
+
case "tool_execution_end":
|
|
298
|
+
case "host_tool_call":
|
|
299
|
+
case "host_tool_cancel":
|
|
300
|
+
case "tool_approval_request":
|
|
301
|
+
case "tool_approval_cancel":
|
|
302
|
+
return { reason: "tool-event-schema", expected: "valid-tool-event" };
|
|
303
|
+
case "subagent_lifecycle":
|
|
304
|
+
case "subagent_progress":
|
|
305
|
+
case "subagent_event":
|
|
306
|
+
return { reason: "subagent-event-schema", expected: "valid-subagent-event" };
|
|
307
|
+
case "model_changed":
|
|
308
|
+
case "thinking_level_changed":
|
|
309
|
+
case "goal_updated":
|
|
310
|
+
case "todo_reminder":
|
|
311
|
+
case "todo_auto_clear":
|
|
312
|
+
case "available_commands_update":
|
|
313
|
+
case "retry_fallback_applied":
|
|
314
|
+
case "retry_fallback_succeeded":
|
|
315
|
+
return { reason: "configuration-event-schema", expected: "valid-configuration-event" };
|
|
316
|
+
case "extension_ui_request":
|
|
317
|
+
return { reason: "extension-ui-schema", expected: "valid-extension-ui-event" };
|
|
318
|
+
default:
|
|
319
|
+
return { reason: "lifecycle-event-schema", expected: "valid-lifecycle-event" };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function invalidEventDiagnosticMetadata(
|
|
324
|
+
frame: Record<string, unknown>,
|
|
325
|
+
type: string,
|
|
326
|
+
): Pick<
|
|
327
|
+
OmpProtocolViolationDiagnostic,
|
|
328
|
+
"reason" | "eventType" | "frameType" | "field" | "expected" | "actualType"
|
|
329
|
+
> {
|
|
330
|
+
if (!isProtocolEventType(type)) {
|
|
331
|
+
return {
|
|
332
|
+
reason: "unknown-event-type",
|
|
333
|
+
field: "frame.type",
|
|
334
|
+
expected: "known-event-type",
|
|
335
|
+
actualType: "string",
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (type !== "notice") {
|
|
339
|
+
return {
|
|
340
|
+
...eventSchemaClassification(type),
|
|
341
|
+
eventType: type,
|
|
342
|
+
field: "event",
|
|
343
|
+
actualType: "object",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (typeof frame.level !== "string") {
|
|
347
|
+
return {
|
|
348
|
+
reason: "notice-level-type",
|
|
349
|
+
eventType: "notice",
|
|
350
|
+
frameType: "notice",
|
|
351
|
+
field: "notice.level",
|
|
352
|
+
expected: "notice-level-enum",
|
|
353
|
+
actualType: protocolDiagnosticActualType(frame.level),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (typeof frame.message !== "string") {
|
|
357
|
+
return {
|
|
358
|
+
reason: "notice-message-type",
|
|
359
|
+
eventType: "notice",
|
|
360
|
+
frameType: "notice",
|
|
361
|
+
field: "notice.message",
|
|
362
|
+
expected: "notice-message-string",
|
|
363
|
+
actualType: protocolDiagnosticActualType(frame.message),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
reason: "notice-schema",
|
|
368
|
+
eventType: "notice",
|
|
369
|
+
frameType: "notice",
|
|
370
|
+
field: "event",
|
|
371
|
+
expected: "valid-event-frame",
|
|
372
|
+
actualType: "object",
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
type PendingProtocolViolation = Omit<OmpProtocolViolationDiagnostic, "occurrenceCount"> & {
|
|
377
|
+
occurrenceCount: number;
|
|
378
|
+
};
|
|
379
|
+
type ProtocolViolationKey = `${OmpProtocolViolationCategory}:${OmpProtocolViolationReason}`;
|
|
73
380
|
const MAX_CONTEXT_PERCENT = 1_000_000;
|
|
74
381
|
function boundedJsonString(maxBytes: number, minBytes = 0) {
|
|
75
382
|
return z.string().refine((value) => {
|
|
@@ -101,12 +408,94 @@ function boundedString(maxBytes: number, minBytes = 0) {
|
|
|
101
408
|
return bytes >= minBytes && bytes <= maxBytes;
|
|
102
409
|
});
|
|
103
410
|
}
|
|
411
|
+
function boundedOptionalStringArray(maxItems: number, maxBytes: number) {
|
|
412
|
+
return z.unknown().transform((value): string[] | undefined => {
|
|
413
|
+
if (!Array.isArray(value)) return undefined;
|
|
414
|
+
return value
|
|
415
|
+
.filter(
|
|
416
|
+
(item): item is string =>
|
|
417
|
+
typeof item === "string" && item.length > 0 && utf8Bytes(item) <= maxBytes,
|
|
418
|
+
)
|
|
419
|
+
.slice(0, maxItems);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
104
422
|
|
|
105
423
|
const IDENTIFIER = boundedString(MAX_ID_LENGTH, 1);
|
|
106
424
|
const NAME = boundedString(MAX_NAME_LENGTH, 1);
|
|
107
425
|
const OMP_PROVIDER_NAME = NAME.refine((provider) => !provider.includes("/"));
|
|
108
426
|
const TEXT = boundedString(MAX_TEXT_LENGTH);
|
|
427
|
+
const RAW_DISPLAY_TEXT = boundedString(MAX_IMAGE_DATA_LENGTH);
|
|
428
|
+
const DISPLAY_TRUNCATION_MARKER = "<truncated>";
|
|
429
|
+
|
|
430
|
+
function boundRawDisplayContent(value: unknown): unknown {
|
|
431
|
+
if (typeof value === "string") {
|
|
432
|
+
return utf8Bytes(value) <= MAX_IMAGE_DATA_LENGTH ? value : DISPLAY_TRUNCATION_MARKER;
|
|
433
|
+
}
|
|
434
|
+
if (!Array.isArray(value)) return value;
|
|
435
|
+
let totalBytes = 0;
|
|
436
|
+
for (const part of value) {
|
|
437
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) continue;
|
|
438
|
+
const record = part as Record<string, unknown>;
|
|
439
|
+
if (typeof record.text === "string") totalBytes += utf8Bytes(record.text);
|
|
440
|
+
if (typeof record.thinking === "string") totalBytes += utf8Bytes(record.thinking);
|
|
441
|
+
}
|
|
442
|
+
if (totalBytes <= MAX_IMAGE_DATA_LENGTH) return value;
|
|
443
|
+
|
|
444
|
+
let retainedMarker = false;
|
|
445
|
+
return value.map((part) => {
|
|
446
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) return part;
|
|
447
|
+
const copy = { ...(part as Record<string, unknown>) };
|
|
448
|
+
for (const key of ["text", "thinking"] as const) {
|
|
449
|
+
if (typeof copy[key] !== "string") continue;
|
|
450
|
+
if (retainedMarker) delete copy[key];
|
|
451
|
+
else {
|
|
452
|
+
copy[key] = DISPLAY_TRUNCATION_MARKER;
|
|
453
|
+
retainedMarker = true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return copy;
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function sanitizeLiveMessageDisplay(value: unknown): unknown {
|
|
461
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
462
|
+
const message = value as Record<string, unknown>;
|
|
463
|
+
if (message.role === "assistant") {
|
|
464
|
+
const content = boundRawDisplayContent(message.content);
|
|
465
|
+
return content === message.content ? value : { ...message, content };
|
|
466
|
+
}
|
|
467
|
+
if (
|
|
468
|
+
message.role === "bashExecution" &&
|
|
469
|
+
typeof message.output === "string" &&
|
|
470
|
+
utf8Bytes(message.output) > MAX_IMAGE_DATA_LENGTH
|
|
471
|
+
) {
|
|
472
|
+
return { ...message, output: DISPLAY_TRUNCATION_MARKER };
|
|
473
|
+
}
|
|
474
|
+
return value;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function sanitizeLiveDisplayFrame(value: unknown): unknown {
|
|
478
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
479
|
+
const frame = value as Record<string, unknown>;
|
|
480
|
+
if (
|
|
481
|
+
frame.type === "message_start" ||
|
|
482
|
+
frame.type === "message_update" ||
|
|
483
|
+
frame.type === "message_end"
|
|
484
|
+
) {
|
|
485
|
+
const message = sanitizeLiveMessageDisplay(frame.message);
|
|
486
|
+
return message === frame.message ? value : { ...frame, message };
|
|
487
|
+
}
|
|
488
|
+
if (frame.type !== "agent_end" || !Array.isArray(frame.messages)) return value;
|
|
489
|
+
let changed = false;
|
|
490
|
+
const messages = frame.messages.map((message) => {
|
|
491
|
+
const sanitized = sanitizeLiveMessageDisplay(message);
|
|
492
|
+
if (sanitized !== message) changed = true;
|
|
493
|
+
return sanitized;
|
|
494
|
+
});
|
|
495
|
+
return changed ? { ...frame, messages } : value;
|
|
496
|
+
}
|
|
109
497
|
const OmpThinkingLevelSchema = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
498
|
+
const OmpCurrentThinkingLevelSchema = boundedString(32).optional().catch(undefined);
|
|
110
499
|
|
|
111
500
|
function isBoundedJson(
|
|
112
501
|
value: unknown,
|
|
@@ -118,6 +507,148 @@ function isBoundedJson(
|
|
|
118
507
|
boundedJsonBytes(value, maxBytes, maxItems, maxBytes, maxNodes) !== Number.POSITIVE_INFINITY
|
|
119
508
|
);
|
|
120
509
|
}
|
|
510
|
+
function optionalMetadataMetrics(value: unknown) {
|
|
511
|
+
return boundedJsonMetrics(
|
|
512
|
+
value,
|
|
513
|
+
MAX_OPTIONAL_METADATA_BYTES,
|
|
514
|
+
MAX_OPTIONAL_METADATA_ITEMS,
|
|
515
|
+
MAX_OPTIONAL_METADATA_BYTES,
|
|
516
|
+
MAX_OPTIONAL_METADATA_NODES,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function optionalMetadataIsBounded(value: unknown): boolean {
|
|
521
|
+
return optionalMetadataMetrics(value) !== undefined;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function omitUnsafeOptionalDetails(value: unknown): unknown {
|
|
525
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
526
|
+
const record = value as Record<string, unknown>;
|
|
527
|
+
if (!Object.hasOwn(record, "details") || optionalMetadataIsBounded(record.details)) return value;
|
|
528
|
+
const { details: _details, ...safe } = record;
|
|
529
|
+
return safe;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const TASK_RESULT_STATUSES: Readonly<Record<string, true>> = {
|
|
533
|
+
pending: true,
|
|
534
|
+
running: true,
|
|
535
|
+
completed: true,
|
|
536
|
+
failed: true,
|
|
537
|
+
error: true,
|
|
538
|
+
aborted: true,
|
|
539
|
+
canceled: true,
|
|
540
|
+
cancelled: true,
|
|
541
|
+
};
|
|
542
|
+
const TASK_PROGRESS_STATUSES: Readonly<Record<string, true>> = {
|
|
543
|
+
pending: true,
|
|
544
|
+
running: true,
|
|
545
|
+
completed: true,
|
|
546
|
+
failed: true,
|
|
547
|
+
aborted: true,
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
function boundedTaskId(value: unknown): value is string {
|
|
551
|
+
return typeof value === "string" && value.length > 0 && utf8Bytes(value) <= MAX_ID_LENGTH;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function taskCorrelationDetails(value: unknown): unknown {
|
|
555
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
556
|
+
const details = value as Record<string, unknown>;
|
|
557
|
+
if (!Array.isArray(details.results) || details.results.length > MAX_TASK_CORRELATION_ITEMS)
|
|
558
|
+
return;
|
|
559
|
+
const results: Record<string, unknown>[] = [];
|
|
560
|
+
for (const value of details.results) {
|
|
561
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
562
|
+
const result = value as Record<string, unknown>;
|
|
563
|
+
if (!boundedTaskId(result.id)) return;
|
|
564
|
+
const safe: Record<string, unknown> = { id: result.id };
|
|
565
|
+
if (typeof result.status === "string" && Object.hasOwn(TASK_RESULT_STATUSES, result.status)) {
|
|
566
|
+
safe.status = result.status;
|
|
567
|
+
}
|
|
568
|
+
if (typeof result.aborted === "boolean") safe.aborted = result.aborted;
|
|
569
|
+
if (typeof result.exitCode === "number" && Number.isFinite(result.exitCode)) {
|
|
570
|
+
safe.exitCode = result.exitCode;
|
|
571
|
+
}
|
|
572
|
+
if (
|
|
573
|
+
result.error !== undefined &&
|
|
574
|
+
boundedJsonMetrics(result.error, 4_096, 32, 4_096, 64) !== undefined
|
|
575
|
+
) {
|
|
576
|
+
safe.error = result.error;
|
|
577
|
+
}
|
|
578
|
+
results.push(safe);
|
|
579
|
+
}
|
|
580
|
+
let progress: Record<string, unknown>[] | undefined;
|
|
581
|
+
if (details.progress !== undefined) {
|
|
582
|
+
if (!Array.isArray(details.progress) || details.progress.length > MAX_TASK_CORRELATION_ITEMS) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
progress = [];
|
|
586
|
+
for (const value of details.progress) {
|
|
587
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
588
|
+
const item = value as Record<string, unknown>;
|
|
589
|
+
if (
|
|
590
|
+
!boundedTaskId(item.id) ||
|
|
591
|
+
typeof item.index !== "number" ||
|
|
592
|
+
!Number.isInteger(item.index) ||
|
|
593
|
+
item.index < 0 ||
|
|
594
|
+
item.index >= MAX_TASK_CORRELATION_ITEMS ||
|
|
595
|
+
typeof item.status !== "string" ||
|
|
596
|
+
!Object.hasOwn(TASK_PROGRESS_STATUSES, item.status)
|
|
597
|
+
) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
progress.push({ id: item.id, index: item.index, status: item.status });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const correlation = { results, ...(progress ? { progress } : {}) };
|
|
604
|
+
return boundedJsonMetrics(
|
|
605
|
+
correlation,
|
|
606
|
+
MAX_TASK_CORRELATION_BYTES,
|
|
607
|
+
MAX_TASK_CORRELATION_ITEMS,
|
|
608
|
+
MAX_TASK_CORRELATION_BYTES,
|
|
609
|
+
MAX_TASK_CORRELATION_NODES,
|
|
610
|
+
)
|
|
611
|
+
? correlation
|
|
612
|
+
: undefined;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function omitOptionalDetails(value: unknown, preserveTaskCorrelation = false): unknown {
|
|
616
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
617
|
+
const record = value as Record<string, unknown>;
|
|
618
|
+
if (!Object.hasOwn(record, "details")) return value;
|
|
619
|
+
const { details: _details, ...structural } = record;
|
|
620
|
+
if (!preserveTaskCorrelation) return structural;
|
|
621
|
+
const correlation = taskCorrelationDetails(record.details);
|
|
622
|
+
return correlation === undefined ? structural : { ...structural, details: correlation };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function createOptionalMetadataSanitizer(): (value: unknown, taskResult?: boolean) => unknown {
|
|
626
|
+
let retainedBytes = 0;
|
|
627
|
+
let retainedNodes = 0;
|
|
628
|
+
return (value, taskResult = false) => {
|
|
629
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
630
|
+
const record = value as Record<string, unknown>;
|
|
631
|
+
if (!Object.hasOwn(record, "details")) return value;
|
|
632
|
+
const metrics = optionalMetadataMetrics(record.details);
|
|
633
|
+
if (
|
|
634
|
+
metrics &&
|
|
635
|
+
retainedBytes + metrics.bytes <= MAX_OPTIONAL_METADATA_BYTES &&
|
|
636
|
+
retainedNodes + metrics.nodes <= MAX_OPTIONAL_METADATA_NODES
|
|
637
|
+
) {
|
|
638
|
+
retainedBytes += metrics.bytes;
|
|
639
|
+
retainedNodes += metrics.nodes;
|
|
640
|
+
return value;
|
|
641
|
+
}
|
|
642
|
+
const correlation = taskResult ? taskCorrelationDetails(record.details) : undefined;
|
|
643
|
+
const { details: _details, ...safe } = record;
|
|
644
|
+
return correlation === undefined ? safe : { ...safe, details: correlation };
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const OmpOptionalMetadataSchema = z.preprocess(
|
|
649
|
+
(value) => (optionalMetadataIsBounded(value) ? value : undefined),
|
|
650
|
+
z.unknown().optional(),
|
|
651
|
+
);
|
|
121
652
|
|
|
122
653
|
const OmpContentPartSchema = z
|
|
123
654
|
.object({
|
|
@@ -147,6 +678,14 @@ const OmpContentPartSchema = z
|
|
|
147
678
|
context.addIssue({ code: "custom", message: "invalid image payload" });
|
|
148
679
|
}
|
|
149
680
|
});
|
|
681
|
+
const OmpAssistantContentPartSchema = OmpContentPartSchema.safeExtend({
|
|
682
|
+
text: RAW_DISPLAY_TEXT.optional(),
|
|
683
|
+
thinking: RAW_DISPLAY_TEXT.optional(),
|
|
684
|
+
});
|
|
685
|
+
const OmpAssistantDisplayContentSchema = z.preprocess(
|
|
686
|
+
boundRawDisplayContent,
|
|
687
|
+
z.union([RAW_DISPLAY_TEXT, z.array(OmpAssistantContentPartSchema).max(OMP_MAX_CONTENT_PARTS)]),
|
|
688
|
+
);
|
|
150
689
|
const OmpDisplayContentSchema = z.union([
|
|
151
690
|
TEXT,
|
|
152
691
|
z.array(OmpContentPartSchema).max(OMP_MAX_CONTENT_PARTS),
|
|
@@ -165,10 +704,7 @@ const OmpMessageIdentityShape = {
|
|
|
165
704
|
responseId: IDENTIFIER.optional(),
|
|
166
705
|
images: OmpImageArraySchema.optional(),
|
|
167
706
|
timestamp: z.number().finite().optional(),
|
|
168
|
-
details:
|
|
169
|
-
.unknown()
|
|
170
|
-
.refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096))
|
|
171
|
-
.optional(),
|
|
707
|
+
details: OmpOptionalMetadataSchema,
|
|
172
708
|
};
|
|
173
709
|
type OmpContentPart = z.infer<typeof OmpContentPartSchema>;
|
|
174
710
|
type OmpMessageIdentity = {
|
|
@@ -218,7 +754,7 @@ export type OmpMessage = OmpMessageIdentity &
|
|
|
218
754
|
const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
|
|
219
755
|
z.object({
|
|
220
756
|
role: z.literal("assistant"),
|
|
221
|
-
content:
|
|
757
|
+
content: OmpAssistantDisplayContentSchema.optional(),
|
|
222
758
|
...OmpMessageIdentityShape,
|
|
223
759
|
errorMessage: boundedString(4_096).nullable().optional(),
|
|
224
760
|
stopReason: boundedString(64).optional(),
|
|
@@ -241,7 +777,13 @@ const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
|
|
|
241
777
|
z.object({
|
|
242
778
|
role: z.literal("bashExecution"),
|
|
243
779
|
command: TEXT,
|
|
244
|
-
output:
|
|
780
|
+
output: z.preprocess(
|
|
781
|
+
(value) =>
|
|
782
|
+
typeof value === "string" && utf8Bytes(value) > MAX_IMAGE_DATA_LENGTH
|
|
783
|
+
? DISPLAY_TRUNCATION_MARKER
|
|
784
|
+
: value,
|
|
785
|
+
RAW_DISPLAY_TEXT.optional(),
|
|
786
|
+
),
|
|
245
787
|
exitCode: z.number().int().nullable().optional(),
|
|
246
788
|
cancelled: z.boolean().optional(),
|
|
247
789
|
truncated: z.boolean().optional(),
|
|
@@ -309,19 +851,33 @@ const OmpAvailableCommandSchema = z.object({
|
|
|
309
851
|
.optional(),
|
|
310
852
|
source: boundedString(64).optional(),
|
|
311
853
|
});
|
|
854
|
+
const OmpThinkingMetadataSchema = z
|
|
855
|
+
.unknown()
|
|
856
|
+
.transform((value): { efforts?: string[]; defaultLevel?: string } | undefined => {
|
|
857
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
858
|
+
const record = value as Record<string, unknown>;
|
|
859
|
+
const efforts = boundedOptionalStringArray(16, 32).parse(record.efforts);
|
|
860
|
+
const defaultLevel = boundedString(32).optional().catch(undefined).parse(record.defaultLevel);
|
|
861
|
+
return {
|
|
862
|
+
...(efforts !== undefined ? { efforts } : {}),
|
|
863
|
+
...(defaultLevel !== undefined ? { defaultLevel } : {}),
|
|
864
|
+
};
|
|
865
|
+
});
|
|
312
866
|
const OmpModelSchema = z.object({
|
|
313
867
|
provider: OMP_PROVIDER_NAME,
|
|
314
868
|
id: NAME,
|
|
315
|
-
name: boundedString(MAX_NAME_LENGTH).optional(),
|
|
316
|
-
reasoning: z.boolean().optional(),
|
|
317
|
-
thinking:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
.
|
|
323
|
-
|
|
324
|
-
|
|
869
|
+
name: boundedString(MAX_NAME_LENGTH).optional().catch(undefined),
|
|
870
|
+
reasoning: z.boolean().optional().catch(undefined),
|
|
871
|
+
thinking: OmpThinkingMetadataSchema.optional(),
|
|
872
|
+
input: boundedOptionalStringArray(16, MAX_NAME_LENGTH).optional(),
|
|
873
|
+
contextWindow: z
|
|
874
|
+
.number()
|
|
875
|
+
.int()
|
|
876
|
+
.nonnegative()
|
|
877
|
+
.max(100_000_000)
|
|
878
|
+
.nullable()
|
|
879
|
+
.optional()
|
|
880
|
+
.catch(undefined),
|
|
325
881
|
});
|
|
326
882
|
const TokenCountSchema = z.number().int().nonnegative().max(MAX_TOKEN_COUNT);
|
|
327
883
|
const OptionalTokenCountSchema = TokenCountSchema.nullable().optional();
|
|
@@ -373,13 +929,13 @@ const OmpCompactionResultSchema = z.object({
|
|
|
373
929
|
});
|
|
374
930
|
const OmpSessionStateSchema = z.object({
|
|
375
931
|
model: OmpModelSchema.nullable().optional(),
|
|
376
|
-
thinkingLevel:
|
|
932
|
+
thinkingLevel: OmpCurrentThinkingLevelSchema,
|
|
377
933
|
isStreaming: z.boolean(),
|
|
378
934
|
isCompacting: z.boolean(),
|
|
379
935
|
sessionId: IDENTIFIER,
|
|
380
936
|
autoCompactionEnabled: z.boolean().optional(),
|
|
381
|
-
contextUsage: OmpContextUsageSchema.nullable().optional(),
|
|
382
|
-
sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
|
|
937
|
+
contextUsage: OmpContextUsageSchema.nullable().optional().catch(undefined),
|
|
938
|
+
sessionFile: boundedString(MAX_PATH_LENGTH).optional().catch(undefined),
|
|
383
939
|
});
|
|
384
940
|
const OmpReadyFrameSchema = z.object({
|
|
385
941
|
type: z.literal("ready"),
|
|
@@ -409,6 +965,16 @@ const JsonObjectSchema = z.record(z.string(), z.unknown());
|
|
|
409
965
|
const BoundedToolPayloadSchema = z
|
|
410
966
|
.unknown()
|
|
411
967
|
.refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096));
|
|
968
|
+
const OmpToolResultPayloadSchema = z.preprocess(
|
|
969
|
+
omitUnsafeOptionalDetails,
|
|
970
|
+
z
|
|
971
|
+
.unknown()
|
|
972
|
+
.refine(
|
|
973
|
+
(value) =>
|
|
974
|
+
isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, MAX_OPTIONAL_METADATA_ITEMS, 8_192) &&
|
|
975
|
+
isBoundedJson(omitOptionalDetails(value), MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096),
|
|
976
|
+
),
|
|
977
|
+
);
|
|
412
978
|
const OmpHostToolDefinitionSchema = z.object({
|
|
413
979
|
name: NAME,
|
|
414
980
|
label: NAME.optional(),
|
|
@@ -616,13 +1182,13 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
|
|
|
616
1182
|
toolCallId: IDENTIFIER,
|
|
617
1183
|
toolName: NAME,
|
|
618
1184
|
args: BoundedToolPayloadSchema.optional(),
|
|
619
|
-
partialResult:
|
|
1185
|
+
partialResult: OmpToolResultPayloadSchema,
|
|
620
1186
|
}),
|
|
621
1187
|
z.object({
|
|
622
1188
|
type: z.literal("tool_execution_end"),
|
|
623
1189
|
toolCallId: IDENTIFIER,
|
|
624
1190
|
toolName: NAME,
|
|
625
|
-
result:
|
|
1191
|
+
result: OmpToolResultPayloadSchema,
|
|
626
1192
|
isError: z.boolean().optional(),
|
|
627
1193
|
}),
|
|
628
1194
|
OmpCompactionStartSchema,
|
|
@@ -873,8 +1439,104 @@ const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
|
|
|
873
1439
|
OmpToolApprovalCancelSchema,
|
|
874
1440
|
z.object({ type: z.literal("advisor_yielded") }),
|
|
875
1441
|
]);
|
|
1442
|
+
type OptionalDetailsMapper = (value: unknown, taskResult?: boolean) => unknown;
|
|
1443
|
+
|
|
1444
|
+
function mapRecordField(
|
|
1445
|
+
record: Record<string, unknown>,
|
|
1446
|
+
key: string,
|
|
1447
|
+
map: OptionalDetailsMapper,
|
|
1448
|
+
taskResult = false,
|
|
1449
|
+
): Record<string, unknown> {
|
|
1450
|
+
if (!Object.hasOwn(record, key)) return record;
|
|
1451
|
+
const next = map(record[key], taskResult);
|
|
1452
|
+
return next === record[key] ? record : { ...record, [key]: next };
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function mapMessageList(value: unknown, map: OptionalDetailsMapper): unknown {
|
|
1456
|
+
if (!Array.isArray(value)) return value;
|
|
1457
|
+
let changed = false;
|
|
1458
|
+
const messages = value.map((message) => {
|
|
1459
|
+
const taskResult =
|
|
1460
|
+
message !== null &&
|
|
1461
|
+
typeof message === "object" &&
|
|
1462
|
+
!Array.isArray(message) &&
|
|
1463
|
+
(message as Record<string, unknown>).role === "toolResult" &&
|
|
1464
|
+
(message as Record<string, unknown>).toolName === "task";
|
|
1465
|
+
const next = map(message, taskResult);
|
|
1466
|
+
changed ||= next !== message;
|
|
1467
|
+
return next;
|
|
1468
|
+
});
|
|
1469
|
+
return changed ? messages : value;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function mapAgentEventDetails(
|
|
1473
|
+
frame: Record<string, unknown>,
|
|
1474
|
+
map: OptionalDetailsMapper,
|
|
1475
|
+
): Record<string, unknown> {
|
|
1476
|
+
switch (frame.type) {
|
|
1477
|
+
case "message_start":
|
|
1478
|
+
case "message_update":
|
|
1479
|
+
case "message_end": {
|
|
1480
|
+
const message = frame.message;
|
|
1481
|
+
const taskResult =
|
|
1482
|
+
message !== null &&
|
|
1483
|
+
typeof message === "object" &&
|
|
1484
|
+
!Array.isArray(message) &&
|
|
1485
|
+
(message as Record<string, unknown>).role === "toolResult" &&
|
|
1486
|
+
(message as Record<string, unknown>).toolName === "task";
|
|
1487
|
+
return mapRecordField(frame, "message", map, taskResult);
|
|
1488
|
+
}
|
|
1489
|
+
case "tool_execution_update":
|
|
1490
|
+
return mapRecordField(frame, "partialResult", map, frame.toolName === "task");
|
|
1491
|
+
case "tool_execution_end":
|
|
1492
|
+
return mapRecordField(frame, "result", map, frame.toolName === "task");
|
|
1493
|
+
case "agent_end":
|
|
1494
|
+
return mapRecordField(frame, "messages", (messages) => mapMessageList(messages, map));
|
|
1495
|
+
default:
|
|
1496
|
+
return frame;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function mapRuntimeFrameDetails(
|
|
1501
|
+
frame: Record<string, unknown>,
|
|
1502
|
+
map: OptionalDetailsMapper,
|
|
1503
|
+
): Record<string, unknown> {
|
|
1504
|
+
if (frame.type !== "subagent_event") return mapAgentEventDetails(frame, map);
|
|
1505
|
+
if (frame.payload === null || typeof frame.payload !== "object" || Array.isArray(frame.payload)) {
|
|
1506
|
+
return frame;
|
|
1507
|
+
}
|
|
1508
|
+
const payload = frame.payload as Record<string, unknown>;
|
|
1509
|
+
if (payload.event === null || typeof payload.event !== "object" || Array.isArray(payload.event)) {
|
|
1510
|
+
return frame;
|
|
1511
|
+
}
|
|
1512
|
+
const event = mapAgentEventDetails(payload.event as Record<string, unknown>, map);
|
|
1513
|
+
return event === payload.event ? frame : { ...frame, payload: { ...payload, event } };
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function sanitizeMessageListMetadata(value: unknown): unknown {
|
|
1517
|
+
return mapMessageList(value, createOptionalMetadataSanitizer());
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function sanitizeHistoryResponseData(value: unknown): unknown {
|
|
1521
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1522
|
+
return mapRecordField(value as Record<string, unknown>, "messages", sanitizeMessageListMetadata);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function runtimeFrameCollectionLimit(frame: Record<string, unknown>): number {
|
|
1526
|
+
const type = frame.type;
|
|
1527
|
+
if (
|
|
1528
|
+
type === "message_start" ||
|
|
1529
|
+
type === "message_update" ||
|
|
1530
|
+
type === "message_end" ||
|
|
1531
|
+
type === "agent_end" ||
|
|
1532
|
+
type === "subagent_event"
|
|
1533
|
+
) {
|
|
1534
|
+
return OMP_MAX_CONTENT_PARTS;
|
|
1535
|
+
}
|
|
1536
|
+
return 1_024;
|
|
1537
|
+
}
|
|
876
1538
|
const OmpModelsResultSchema = z.object({
|
|
877
|
-
models: z.array(OmpModelSchema).min(1).max(
|
|
1539
|
+
models: z.array(OmpModelSchema).min(1).max(MAX_MODEL_CATALOG_ITEMS),
|
|
878
1540
|
});
|
|
879
1541
|
const OmpPromptAckSchema = z.object({ agentInvoked: z.boolean().optional() }).optional();
|
|
880
1542
|
const OmpAvailableCommandsResultSchema = z.object({
|
|
@@ -919,6 +1581,7 @@ const ProtocolNegotiationResultSchema = z.object({
|
|
|
919
1581
|
});
|
|
920
1582
|
|
|
921
1583
|
export type OmpModel = z.infer<typeof OmpModelSchema>;
|
|
1584
|
+
export type OmpBranchResult = z.infer<typeof OmpBranchResultSchema>;
|
|
922
1585
|
export type OmpSessionState = z.infer<typeof OmpSessionStateSchema>;
|
|
923
1586
|
export type OmpSessionStats = z.infer<typeof OmpSessionStatsSchema>;
|
|
924
1587
|
export type OmpCompactionResult = z.infer<typeof OmpCompactionResultSchema>;
|
|
@@ -960,6 +1623,7 @@ export interface OmpPersistedSessionMessages {
|
|
|
960
1623
|
nativeSessionId: string;
|
|
961
1624
|
byteLength: number;
|
|
962
1625
|
messages: OmpMessage[];
|
|
1626
|
+
imageReplayWarning?: true;
|
|
963
1627
|
}
|
|
964
1628
|
|
|
965
1629
|
export interface OmpStartOptions {
|
|
@@ -1025,7 +1689,7 @@ export interface OmpRuntimeSession {
|
|
|
1025
1689
|
respondToExtensionUi(response: OmpExtensionUiResponse): Promise<void>;
|
|
1026
1690
|
respondToToolApproval(response: OmpToolApprovalResponse): Promise<void>;
|
|
1027
1691
|
getBranchMessages(): Promise<Array<{ entryId: string; text: string }>>;
|
|
1028
|
-
branch(entryId: string): Promise<
|
|
1692
|
+
branch(entryId: string): Promise<OmpBranchResult>;
|
|
1029
1693
|
readonly canReplayHistory: boolean;
|
|
1030
1694
|
getMessages(): Promise<OmpMessage[]>;
|
|
1031
1695
|
abort(): Promise<void>;
|
|
@@ -1067,11 +1731,19 @@ export interface OmpRpcRuntimeOptions {
|
|
|
1067
1731
|
terminateProcessTree?: (pid: number) => Promise<boolean | "uncertain">;
|
|
1068
1732
|
environment?: NodeJS.ProcessEnv;
|
|
1069
1733
|
requestTimeoutMs?: number;
|
|
1734
|
+
reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
|
|
1070
1735
|
listSessions?: (
|
|
1071
1736
|
options: OmpSessionListOptions,
|
|
1072
1737
|
) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
|
|
1073
1738
|
}
|
|
1074
1739
|
|
|
1740
|
+
export class OmpRpcRequestRejectedError extends Error {
|
|
1741
|
+
constructor() {
|
|
1742
|
+
super("OMP RPC request failed");
|
|
1743
|
+
this.name = "OmpRpcRequestRejectedError";
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1075
1747
|
type PendingRequest = {
|
|
1076
1748
|
resolve(value: unknown): void;
|
|
1077
1749
|
reject(error: Error): void;
|
|
@@ -1591,12 +2263,21 @@ class OmpRpcProcess {
|
|
|
1591
2263
|
private spawnFailedWithoutProcess = false;
|
|
1592
2264
|
private readyReceived = false;
|
|
1593
2265
|
private outputSettled = false;
|
|
2266
|
+
private turnActive = false;
|
|
2267
|
+
private readonly pendingProtocolViolations = new Map<
|
|
2268
|
+
ProtocolViolationKey,
|
|
2269
|
+
PendingProtocolViolation
|
|
2270
|
+
>();
|
|
2271
|
+
private protocolViolationTimer: TimerHandle | null = null;
|
|
1594
2272
|
|
|
1595
2273
|
constructor(
|
|
1596
2274
|
options: OmpStartOptions,
|
|
1597
2275
|
spawnProcess?: OmpRpcRuntimeOptions["spawnProcess"],
|
|
1598
2276
|
terminateProcessTree?: OmpRpcRuntimeOptions["terminateProcessTree"],
|
|
1599
2277
|
private readonly requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
2278
|
+
private readonly reportProtocolViolation: (
|
|
2279
|
+
diagnostic: OmpProtocolViolationDiagnostic,
|
|
2280
|
+
) => void | Promise<void> = (diagnostic) => console.error("OMP protocol violation", diagnostic),
|
|
1600
2281
|
) {
|
|
1601
2282
|
const ready = Promise.withResolvers<ReadyFrame>();
|
|
1602
2283
|
this.rejectReady = ready.reject;
|
|
@@ -1685,7 +2366,15 @@ class OmpRpcProcess {
|
|
|
1685
2366
|
private settleOutput(): void {
|
|
1686
2367
|
if (this.outputSettled) return;
|
|
1687
2368
|
this.outputSettled = true;
|
|
1688
|
-
if (this.lineBytes > 0 || this.discardingLine)
|
|
2369
|
+
if (this.lineBytes > 0 || this.discardingLine) {
|
|
2370
|
+
this.recordProtocolViolation("incomplete-frame", {
|
|
2371
|
+
reason: "output-ended-mid-frame",
|
|
2372
|
+
field: "frame",
|
|
2373
|
+
expected: "complete-json-line",
|
|
2374
|
+
actualType: "partial-frame",
|
|
2375
|
+
maxByteSize: this.discardingLine ? this.discardedLineBytes : this.lineBytes,
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
1689
2378
|
this.lineParts = [];
|
|
1690
2379
|
this.lineBytes = 0;
|
|
1691
2380
|
this.discardingLine = false;
|
|
@@ -1868,16 +2557,17 @@ class OmpRpcProcess {
|
|
|
1868
2557
|
|
|
1869
2558
|
private async closeProcess(): Promise<void> {
|
|
1870
2559
|
this.closed = true;
|
|
2560
|
+
this.flushProtocolViolations();
|
|
1871
2561
|
this.clearChunk();
|
|
1872
2562
|
this.failPending(new Error("OMP RPC process was closed"));
|
|
2563
|
+
const cleanupPromise = this.startTreeCleanup();
|
|
1873
2564
|
if (!this.exited) {
|
|
1874
2565
|
try {
|
|
1875
2566
|
this.child.stdin.end();
|
|
1876
2567
|
} catch {
|
|
1877
|
-
// Continue
|
|
2568
|
+
// Continue waiting for process-tree cleanup when the input channel is already closed.
|
|
1878
2569
|
}
|
|
1879
2570
|
}
|
|
1880
|
-
const cleanupPromise = this.startTreeCleanup();
|
|
1881
2571
|
const cleanup = await cleanupPromise;
|
|
1882
2572
|
if (cleanup !== "verified") throw new Error("OMP RPC process tree cleanup failed");
|
|
1883
2573
|
if (
|
|
@@ -1964,7 +2654,14 @@ class OmpRpcProcess {
|
|
|
1964
2654
|
this.lineParts = [];
|
|
1965
2655
|
this.lineBytes = 0;
|
|
1966
2656
|
this.discardingLine = true;
|
|
1967
|
-
this.recordProtocolViolation(
|
|
2657
|
+
this.recordProtocolViolation("frame-limit", {
|
|
2658
|
+
reason: "physical-frame-limit",
|
|
2659
|
+
field: "frame.byteLength",
|
|
2660
|
+
expected: "within-byte-limit",
|
|
2661
|
+
actualType: "oversized",
|
|
2662
|
+
maxByteSize: nextBytes,
|
|
2663
|
+
limitBytes: this.physicalFrameLimit,
|
|
2664
|
+
});
|
|
1968
2665
|
return;
|
|
1969
2666
|
}
|
|
1970
2667
|
this.lineParts.push(part);
|
|
@@ -1985,24 +2682,16 @@ class OmpRpcProcess {
|
|
|
1985
2682
|
try {
|
|
1986
2683
|
decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
|
|
1987
2684
|
} 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();
|
|
2685
|
+
this.recordProtocolViolation("invalid-json", {
|
|
2686
|
+
reason: "json-decode",
|
|
2687
|
+
field: "frame",
|
|
2688
|
+
expected: "valid-json",
|
|
2689
|
+
actualType: "invalid-json",
|
|
2690
|
+
maxByteSize: payload.byteLength,
|
|
2691
|
+
});
|
|
2003
2692
|
return;
|
|
2004
2693
|
}
|
|
2005
|
-
this.
|
|
2694
|
+
this.receiveDecodedFrame(decoded, payload.byteLength);
|
|
2006
2695
|
}
|
|
2007
2696
|
|
|
2008
2697
|
private receiveChunk(frame: ChunkFrame): void {
|
|
@@ -2022,17 +2711,30 @@ class OmpRpcProcess {
|
|
|
2022
2711
|
frame.data.length % 4 !== 0 ||
|
|
2023
2712
|
!/^[A-Za-z0-9+/]*={0,2}$/u.test(frame.data)
|
|
2024
2713
|
) {
|
|
2025
|
-
this.rejectChunk(
|
|
2714
|
+
this.rejectChunk("chunk-metadata", {
|
|
2715
|
+
field: "chunk",
|
|
2716
|
+
expected: "valid-chunk-frame",
|
|
2717
|
+
actualType: "object",
|
|
2718
|
+
});
|
|
2026
2719
|
return;
|
|
2027
2720
|
}
|
|
2028
2721
|
const decoded = Buffer.from(frame.data, "base64");
|
|
2029
2722
|
if (decoded.byteLength > MAX_CHUNK_BYTES) {
|
|
2030
|
-
this.rejectChunk(
|
|
2723
|
+
this.rejectChunk("chunk-size", {
|
|
2724
|
+
field: "chunk.data",
|
|
2725
|
+
expected: "within-byte-limit",
|
|
2726
|
+
actualType: "oversized",
|
|
2727
|
+
limitBytes: MAX_CHUNK_BYTES,
|
|
2728
|
+
});
|
|
2031
2729
|
return;
|
|
2032
2730
|
}
|
|
2033
2731
|
if (!this.chunk) {
|
|
2034
2732
|
if (frame.index !== 0) {
|
|
2035
|
-
this.rejectChunk(
|
|
2733
|
+
this.rejectChunk("chunk-start-index", {
|
|
2734
|
+
field: "chunk.index",
|
|
2735
|
+
expected: "first-chunk-index-zero",
|
|
2736
|
+
actualType: "out-of-order",
|
|
2737
|
+
});
|
|
2036
2738
|
return;
|
|
2037
2739
|
}
|
|
2038
2740
|
this.chunk = {
|
|
@@ -2041,7 +2743,15 @@ class OmpRpcProcess {
|
|
|
2041
2743
|
byteLength: frame.byteLength,
|
|
2042
2744
|
parts: [],
|
|
2043
2745
|
receivedBytes: 0,
|
|
2044
|
-
timer: setTimeout(
|
|
2746
|
+
timer: setTimeout(
|
|
2747
|
+
() =>
|
|
2748
|
+
this.rejectChunk("chunk-timeout", {
|
|
2749
|
+
field: "chunk.sequence",
|
|
2750
|
+
expected: "chunk-before-deadline",
|
|
2751
|
+
actualType: "timeout",
|
|
2752
|
+
}),
|
|
2753
|
+
CHUNK_STALE_MS,
|
|
2754
|
+
),
|
|
2045
2755
|
};
|
|
2046
2756
|
}
|
|
2047
2757
|
const chunk = this.chunk;
|
|
@@ -2053,14 +2763,22 @@ class OmpRpcProcess {
|
|
|
2053
2763
|
chunk.receivedBytes + decoded.byteLength > chunk.byteLength ||
|
|
2054
2764
|
chunk.receivedBytes + decoded.byteLength > this.reassembledFrameLimit
|
|
2055
2765
|
) {
|
|
2056
|
-
this.rejectChunk(
|
|
2766
|
+
this.rejectChunk("chunk-sequence", {
|
|
2767
|
+
field: "chunk.sequence",
|
|
2768
|
+
expected: "contiguous-chunk-sequence",
|
|
2769
|
+
actualType: "out-of-order",
|
|
2770
|
+
});
|
|
2057
2771
|
return;
|
|
2058
2772
|
}
|
|
2059
2773
|
chunk.parts.push(decoded);
|
|
2060
2774
|
chunk.receivedBytes += decoded.byteLength;
|
|
2061
2775
|
if (chunk.parts.length !== chunk.count) return;
|
|
2062
2776
|
if (chunk.receivedBytes !== chunk.byteLength) {
|
|
2063
|
-
this.rejectChunk(
|
|
2777
|
+
this.rejectChunk("chunk-byte-count", {
|
|
2778
|
+
field: "chunk.byteLength",
|
|
2779
|
+
expected: "declared-chunk-byte-count",
|
|
2780
|
+
actualType: "mismatched",
|
|
2781
|
+
});
|
|
2064
2782
|
return;
|
|
2065
2783
|
}
|
|
2066
2784
|
const reassembled = Buffer.concat(chunk.parts, chunk.receivedBytes);
|
|
@@ -2069,29 +2787,48 @@ class OmpRpcProcess {
|
|
|
2069
2787
|
try {
|
|
2070
2788
|
decodedFrame = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(reassembled));
|
|
2071
2789
|
} catch {
|
|
2072
|
-
this.recordProtocolViolation(
|
|
2790
|
+
this.recordProtocolViolation("invalid-json", {
|
|
2791
|
+
reason: "chunk-json-decode",
|
|
2792
|
+
field: "chunk.data",
|
|
2793
|
+
expected: "valid-json",
|
|
2794
|
+
actualType: "invalid-json",
|
|
2795
|
+
maxByteSize: reassembled.byteLength,
|
|
2796
|
+
});
|
|
2073
2797
|
return;
|
|
2074
2798
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2799
|
+
this.receiveDecodedFrame(decodedFrame, reassembled.byteLength);
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
private receiveDecodedFrame(value: unknown, rawByteLength: number): void {
|
|
2803
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2804
|
+
const frame = value as Record<string, unknown>;
|
|
2805
|
+
const pending = typeof frame.id === "string" ? this.pending.get(frame.id) : undefined;
|
|
2806
|
+
if (
|
|
2807
|
+
frame.type === "response" &&
|
|
2808
|
+
(pending?.command === "get_messages" || pending?.command === "get_subagent_messages")
|
|
2809
|
+
) {
|
|
2810
|
+
this.receiveResponse(frame);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
if (rawByteLength > MAX_SEMANTIC_FRAME_BYTES) {
|
|
2815
|
+
this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
|
|
2087
2816
|
return;
|
|
2088
2817
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2818
|
+
if (this.receiveKnownResponse(value)) return;
|
|
2819
|
+
const sanitized = sanitizeLiveDisplayFrame(value);
|
|
2820
|
+
const frame = JsonObjectSchema.safeParse(sanitized);
|
|
2821
|
+
if (!frame.success) {
|
|
2822
|
+
this.recordProtocolViolation("invalid-envelope", {
|
|
2823
|
+
reason: "object-envelope",
|
|
2824
|
+
field: "frame",
|
|
2825
|
+
expected: "object-envelope",
|
|
2826
|
+
actualType: protocolDiagnosticActualType(sanitized),
|
|
2827
|
+
maxByteSize: rawByteLength,
|
|
2828
|
+
});
|
|
2092
2829
|
return;
|
|
2093
2830
|
}
|
|
2094
|
-
this.receiveFrame(
|
|
2831
|
+
this.receiveFrame(frame.data, rawByteLength);
|
|
2095
2832
|
}
|
|
2096
2833
|
|
|
2097
2834
|
private receiveKnownResponse(value: unknown): boolean {
|
|
@@ -2113,10 +2850,21 @@ class OmpRpcProcess {
|
|
|
2113
2850
|
const knownPending = rawId ? this.pending.get(rawId) : undefined;
|
|
2114
2851
|
const response = OmpResponseFrameSchema.safeParse(frame);
|
|
2115
2852
|
if (!response.success) {
|
|
2853
|
+
const handledAcceptedFailure =
|
|
2854
|
+
rawId !== undefined && knownPending === undefined
|
|
2855
|
+
? this.emitAcceptedPromptFailure(rawId, frame)
|
|
2856
|
+
: false;
|
|
2857
|
+
if (!handledAcceptedFailure) {
|
|
2858
|
+
this.recordProtocolViolation("invalid-response", {
|
|
2859
|
+
reason: "response-schema",
|
|
2860
|
+
frameType: "response",
|
|
2861
|
+
field: "response",
|
|
2862
|
+
expected: "valid-response-frame",
|
|
2863
|
+
actualType: "object",
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2116
2866
|
if (rawId && knownPending) {
|
|
2117
2867
|
this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
|
|
2118
|
-
} else if (!rawId || !this.emitAcceptedPromptFailure(rawId, frame)) {
|
|
2119
|
-
this.recordProtocolViolation();
|
|
2120
2868
|
}
|
|
2121
2869
|
return;
|
|
2122
2870
|
}
|
|
@@ -2130,13 +2878,24 @@ class OmpRpcProcess {
|
|
|
2130
2878
|
const isBranchHistory = pending.command === "get_branch_messages";
|
|
2131
2879
|
const isHistory =
|
|
2132
2880
|
pending.command === "get_messages" || pending.command === "get_subagent_messages";
|
|
2133
|
-
const
|
|
2881
|
+
const responseData = isHistory
|
|
2882
|
+
? sanitizeHistoryResponseData(response.data.data)
|
|
2883
|
+
: response.data.data;
|
|
2884
|
+
const boundedFrame =
|
|
2885
|
+
responseData === response.data.data ? frame : { ...frame, data: responseData };
|
|
2886
|
+
const responseItemLimit = isBranchHistory
|
|
2887
|
+
? 1_024
|
|
2888
|
+
: isHistory
|
|
2889
|
+
? 100_000
|
|
2890
|
+
: pending.command === "get_available_models"
|
|
2891
|
+
? MAX_MODEL_CATALOG_ITEMS
|
|
2892
|
+
: MAX_ARRAY_ITEMS;
|
|
2134
2893
|
const responseByteLimit =
|
|
2135
2894
|
isBranchHistory || isHistory
|
|
2136
2895
|
? Math.min(MAX_REASSEMBLED_FRAME_BYTES, this.reassembledFrameLimit)
|
|
2137
2896
|
: 2 * 1024 * 1024;
|
|
2138
|
-
//
|
|
2139
|
-
// node
|
|
2897
|
+
// Model catalogs are truncated before publication, while the transport still enforces
|
|
2898
|
+
// aggregate byte and node budgets over the complete response.
|
|
2140
2899
|
const responseNodeLimit = isBranchHistory
|
|
2141
2900
|
? 4_096
|
|
2142
2901
|
: isHistory
|
|
@@ -2146,7 +2905,7 @@ class OmpRpcProcess {
|
|
|
2146
2905
|
: 2_048;
|
|
2147
2906
|
if (
|
|
2148
2907
|
boundedJsonBytes(
|
|
2149
|
-
|
|
2908
|
+
boundedFrame,
|
|
2150
2909
|
responseByteLimit,
|
|
2151
2910
|
responseItemLimit,
|
|
2152
2911
|
MAX_IMAGE_DATA_LENGTH,
|
|
@@ -2162,7 +2921,7 @@ class OmpRpcProcess {
|
|
|
2162
2921
|
if (!settled) return;
|
|
2163
2922
|
if (response.data.success) {
|
|
2164
2923
|
try {
|
|
2165
|
-
settled.beforeResolve?.(
|
|
2924
|
+
settled.beforeResolve?.(responseData);
|
|
2166
2925
|
if (settled.command === "prompt") {
|
|
2167
2926
|
if (this.acceptedPromptIds.size >= MAX_PENDING_REQUESTS) {
|
|
2168
2927
|
const oldest = this.acceptedPromptIds.values().next().value;
|
|
@@ -2170,12 +2929,12 @@ class OmpRpcProcess {
|
|
|
2170
2929
|
}
|
|
2171
2930
|
this.acceptedPromptIds.add(response.data.id);
|
|
2172
2931
|
}
|
|
2173
|
-
settled.resolve(
|
|
2932
|
+
settled.resolve(responseData);
|
|
2174
2933
|
} catch {
|
|
2175
2934
|
settled.reject(new Error("OMP RPC response is invalid"));
|
|
2176
2935
|
}
|
|
2177
2936
|
} else {
|
|
2178
|
-
settled.reject(new
|
|
2937
|
+
settled.reject(new OmpRpcRequestRejectedError());
|
|
2179
2938
|
}
|
|
2180
2939
|
}
|
|
2181
2940
|
|
|
@@ -2219,21 +2978,27 @@ class OmpRpcProcess {
|
|
|
2219
2978
|
this.fail(new Error("OMP emitted invalid terminal metadata"));
|
|
2220
2979
|
return true;
|
|
2221
2980
|
}
|
|
2981
|
+
const structuralFrame = mapRuntimeFrameDetails(frame, omitOptionalDetails);
|
|
2222
2982
|
const messagesAreSafe =
|
|
2223
2983
|
frame.messages === undefined ||
|
|
2224
2984
|
(Array.isArray(frame.messages) &&
|
|
2225
2985
|
frame.messages.length <= MAX_ARRAY_ITEMS &&
|
|
2226
2986
|
boundedJsonBytes(
|
|
2227
|
-
|
|
2987
|
+
structuralFrame.messages as unknown[],
|
|
2228
2988
|
MAX_SEMANTIC_FRAME_BYTES,
|
|
2229
|
-
|
|
2230
|
-
|
|
2989
|
+
OMP_MAX_CONTENT_PARTS,
|
|
2990
|
+
MAX_IMAGE_DATA_LENGTH,
|
|
2231
2991
|
4_096,
|
|
2232
2992
|
) !== Number.POSITIVE_INFINITY);
|
|
2233
2993
|
const payloadIsSafe =
|
|
2234
2994
|
messagesAreSafe &&
|
|
2235
|
-
boundedJsonBytes(
|
|
2236
|
-
|
|
2995
|
+
boundedJsonBytes(
|
|
2996
|
+
structuralFrame,
|
|
2997
|
+
MAX_SEMANTIC_FRAME_BYTES,
|
|
2998
|
+
OMP_MAX_CONTENT_PARTS,
|
|
2999
|
+
MAX_IMAGE_DATA_LENGTH,
|
|
3000
|
+
4_096,
|
|
3001
|
+
) !== Number.POSITIVE_INFINITY;
|
|
2237
3002
|
if (onlyUnsafePayload && payloadIsSafe) return false;
|
|
2238
3003
|
if (envelope.data.isTerminal === false) {
|
|
2239
3004
|
this.fail(new Error("OMP emitted an invalid nonterminal agent_end payload"));
|
|
@@ -2254,42 +3019,89 @@ class OmpRpcProcess {
|
|
|
2254
3019
|
return true;
|
|
2255
3020
|
}
|
|
2256
3021
|
|
|
2257
|
-
private receiveFrame(frame: Record<string, unknown
|
|
3022
|
+
private receiveFrame(frame: Record<string, unknown>, rawByteLength: number): void {
|
|
2258
3023
|
const type = typeof frame.type === "string" && frame.type.length <= 64 ? frame.type : null;
|
|
2259
3024
|
if (!type) {
|
|
2260
|
-
this.recordProtocolViolation(
|
|
3025
|
+
this.recordProtocolViolation("invalid-envelope", {
|
|
3026
|
+
reason: "missing-frame-type",
|
|
3027
|
+
field: "frame.type",
|
|
3028
|
+
expected: "bounded-frame-type",
|
|
3029
|
+
actualType: protocolDiagnosticActualType(frame.type),
|
|
3030
|
+
maxByteSize: rawByteLength,
|
|
3031
|
+
});
|
|
2261
3032
|
return;
|
|
2262
3033
|
}
|
|
2263
|
-
|
|
3034
|
+
const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
|
|
3035
|
+
if (this.receiveDegradedAgentEnd(safeFrame, true)) return;
|
|
2264
3036
|
if (
|
|
2265
|
-
boundedJsonBytes(
|
|
2266
|
-
|
|
3037
|
+
boundedJsonBytes(
|
|
3038
|
+
mapRuntimeFrameDetails(safeFrame, omitOptionalDetails),
|
|
3039
|
+
MAX_SEMANTIC_FRAME_BYTES,
|
|
3040
|
+
runtimeFrameCollectionLimit(safeFrame),
|
|
3041
|
+
MAX_IMAGE_DATA_LENGTH,
|
|
3042
|
+
4_096,
|
|
3043
|
+
) === Number.POSITIVE_INFINITY
|
|
2267
3044
|
) {
|
|
2268
|
-
this.recordProtocolViolation(
|
|
3045
|
+
this.recordProtocolViolation("frame-limit", {
|
|
3046
|
+
reason: "semantic-frame-limit",
|
|
3047
|
+
field: "frame.byteLength",
|
|
3048
|
+
expected: "within-byte-limit",
|
|
3049
|
+
actualType: "oversized",
|
|
3050
|
+
maxByteSize: rawByteLength,
|
|
3051
|
+
limitBytes: MAX_SEMANTIC_FRAME_BYTES,
|
|
3052
|
+
});
|
|
2269
3053
|
return;
|
|
2270
3054
|
}
|
|
2271
3055
|
if (type === "rpc_chunk") {
|
|
2272
|
-
const chunk = OmpChunkFrameSchema.safeParse(
|
|
2273
|
-
if (!chunk.success)
|
|
2274
|
-
|
|
3056
|
+
const chunk = OmpChunkFrameSchema.safeParse(safeFrame);
|
|
3057
|
+
if (!chunk.success) {
|
|
3058
|
+
this.rejectChunk("chunk-schema", {
|
|
3059
|
+
field: "chunk",
|
|
3060
|
+
expected: "valid-chunk-frame",
|
|
3061
|
+
actualType: "object",
|
|
3062
|
+
});
|
|
3063
|
+
} else this.receiveChunk(chunk.data);
|
|
2275
3064
|
return;
|
|
2276
3065
|
}
|
|
2277
3066
|
if (this.chunk) {
|
|
2278
3067
|
this.clearChunk();
|
|
2279
|
-
this.recordProtocolViolation(
|
|
3068
|
+
this.recordProtocolViolation("interleaved-chunk", {
|
|
3069
|
+
reason: "frame-interleaved-with-chunk",
|
|
3070
|
+
field: "chunk.sequence",
|
|
3071
|
+
expected: "no-interleaved-frame",
|
|
3072
|
+
actualType: "interleaved",
|
|
3073
|
+
});
|
|
2280
3074
|
}
|
|
2281
3075
|
if (type === "rpc_frame_error") {
|
|
2282
|
-
this.recordProtocolViolation(
|
|
3076
|
+
this.recordProtocolViolation("remote-frame-error", {
|
|
3077
|
+
reason: "remote-frame-error",
|
|
3078
|
+
frameType: "rpc_frame_error",
|
|
3079
|
+
field: "frame",
|
|
3080
|
+
expected: "no-remote-frame-error",
|
|
3081
|
+
actualType: "remote-error",
|
|
3082
|
+
});
|
|
2283
3083
|
return;
|
|
2284
3084
|
}
|
|
2285
3085
|
if (type === "ready") {
|
|
2286
3086
|
if (this.readyReceived) {
|
|
2287
|
-
this.recordProtocolViolation(
|
|
3087
|
+
this.recordProtocolViolation("duplicate-ready", {
|
|
3088
|
+
reason: "ready-already-received",
|
|
3089
|
+
frameType: "ready",
|
|
3090
|
+
field: "ready",
|
|
3091
|
+
expected: "single-ready-frame",
|
|
3092
|
+
actualType: "duplicate",
|
|
3093
|
+
});
|
|
2288
3094
|
return;
|
|
2289
3095
|
}
|
|
2290
|
-
const ready = OmpReadyFrameSchema.safeParse(
|
|
3096
|
+
const ready = OmpReadyFrameSchema.safeParse(safeFrame);
|
|
2291
3097
|
if (!ready.success) {
|
|
2292
|
-
this.recordProtocolViolation(
|
|
3098
|
+
this.recordProtocolViolation("invalid-ready", {
|
|
3099
|
+
reason: "ready-schema",
|
|
3100
|
+
frameType: "ready",
|
|
3101
|
+
field: "ready",
|
|
3102
|
+
expected: "valid-ready-frame",
|
|
3103
|
+
actualType: "object",
|
|
3104
|
+
});
|
|
2293
3105
|
} else {
|
|
2294
3106
|
this.readyReceived = true;
|
|
2295
3107
|
this.resolveReady(ready.data);
|
|
@@ -2297,24 +3109,37 @@ class OmpRpcProcess {
|
|
|
2297
3109
|
return;
|
|
2298
3110
|
}
|
|
2299
3111
|
if (type === "response") {
|
|
2300
|
-
this.receiveResponse(
|
|
3112
|
+
this.receiveResponse(safeFrame);
|
|
2301
3113
|
return;
|
|
2302
3114
|
}
|
|
2303
|
-
const event = OmpRuntimeEventSchema.safeParse(
|
|
3115
|
+
const event = OmpRuntimeEventSchema.safeParse(safeFrame);
|
|
2304
3116
|
if (!event.success) {
|
|
2305
|
-
this.rejectMatchingToolApproval(
|
|
2306
|
-
if (type === "agent_end" && this.receiveDegradedAgentEnd(
|
|
2307
|
-
this.recordProtocolViolation(
|
|
3117
|
+
this.rejectMatchingToolApproval(safeFrame);
|
|
3118
|
+
if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
|
|
3119
|
+
this.recordProtocolViolation("invalid-event", {
|
|
3120
|
+
...invalidEventDiagnosticMetadata(safeFrame, type),
|
|
3121
|
+
maxByteSize: rawByteLength,
|
|
3122
|
+
});
|
|
2308
3123
|
return;
|
|
2309
3124
|
}
|
|
2310
3125
|
if (!this.acceptEventState(event.data)) {
|
|
2311
|
-
this.recordProtocolViolation(
|
|
3126
|
+
this.recordProtocolViolation("invalid-event-state", {
|
|
3127
|
+
reason: "event-state-transition",
|
|
3128
|
+
eventType: event.data.type,
|
|
3129
|
+
field: "event.sequence",
|
|
3130
|
+
expected: "valid-event-state-transition",
|
|
3131
|
+
actualType: "out-of-order",
|
|
3132
|
+
maxByteSize: rawByteLength,
|
|
3133
|
+
});
|
|
2312
3134
|
return;
|
|
2313
3135
|
}
|
|
2314
3136
|
this.emit(event.data);
|
|
2315
3137
|
if (event.data.type === "prompt_result" && event.data.id) {
|
|
2316
3138
|
this.acceptedPromptIds.delete(event.data.id);
|
|
2317
3139
|
}
|
|
3140
|
+
if (event.data.type === "turn_end" || event.data.type === "agent_end") {
|
|
3141
|
+
this.turnActive = false;
|
|
3142
|
+
}
|
|
2318
3143
|
if (
|
|
2319
3144
|
event.data.type === "message_end" ||
|
|
2320
3145
|
event.data.type === "turn_end" ||
|
|
@@ -2342,6 +3167,7 @@ class OmpRpcProcess {
|
|
|
2342
3167
|
|
|
2343
3168
|
private acceptEventState(event: z.infer<typeof OmpRuntimeEventSchema>): boolean {
|
|
2344
3169
|
if (event.type === "turn_start") {
|
|
3170
|
+
this.turnActive = true;
|
|
2345
3171
|
this.streamedBlocks.clear();
|
|
2346
3172
|
this.commandTextLength = 0;
|
|
2347
3173
|
this.activeToolCallIds.clear();
|
|
@@ -2411,13 +3237,84 @@ class OmpRpcProcess {
|
|
|
2411
3237
|
return true;
|
|
2412
3238
|
}
|
|
2413
3239
|
|
|
2414
|
-
private
|
|
3240
|
+
private protocolDiagnosticPhase(): OmpProtocolDiagnosticPhase {
|
|
3241
|
+
if (this.closed || this.exited || this.outputSettled) return "closing";
|
|
3242
|
+
if (!this.readyReceived) return "startup";
|
|
3243
|
+
for (const pending of this.pending.values()) {
|
|
3244
|
+
if (pending.command === "negotiate_protocol") return "negotiation";
|
|
3245
|
+
if (pending.command === "prompt") return "active-turn";
|
|
3246
|
+
}
|
|
3247
|
+
if (this.turnActive || this.acceptedPromptIds.size > 0) return "active-turn";
|
|
3248
|
+
return "idle";
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
private rejectChunk(
|
|
3252
|
+
reason: OmpProtocolViolationReason,
|
|
3253
|
+
metadata: Omit<
|
|
3254
|
+
OmpProtocolViolationDiagnostic,
|
|
3255
|
+
"category" | "reason" | "occurrenceCount" | "phase" | "frameType"
|
|
3256
|
+
> = {},
|
|
3257
|
+
): void {
|
|
2415
3258
|
this.clearChunk();
|
|
2416
|
-
this.recordProtocolViolation(
|
|
3259
|
+
this.recordProtocolViolation("invalid-chunk", {
|
|
3260
|
+
reason,
|
|
3261
|
+
frameType: "rpc_chunk",
|
|
3262
|
+
...metadata,
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
private recordProtocolViolation(
|
|
3267
|
+
category: OmpProtocolViolationCategory,
|
|
3268
|
+
metadata: Omit<OmpProtocolViolationDiagnostic, "category" | "occurrenceCount" | "phase">,
|
|
3269
|
+
): void {
|
|
3270
|
+
const phase = this.protocolDiagnosticPhase();
|
|
3271
|
+
if (!this.protocolViolationTimer) {
|
|
3272
|
+
this.emitProtocolViolation({ category, occurrenceCount: 1, phase, ...metadata });
|
|
3273
|
+
this.protocolViolationTimer = setTimeout(
|
|
3274
|
+
() => this.flushProtocolViolations(),
|
|
3275
|
+
PROTOCOL_VIOLATION_COALESCE_MS,
|
|
3276
|
+
);
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
const key = `${category}:${metadata.reason}` as ProtocolViolationKey;
|
|
3280
|
+
const pending = this.pendingProtocolViolations.get(key);
|
|
3281
|
+
if (!pending) {
|
|
3282
|
+
this.pendingProtocolViolations.set(key, {
|
|
3283
|
+
category,
|
|
3284
|
+
occurrenceCount: 1,
|
|
3285
|
+
phase,
|
|
3286
|
+
...metadata,
|
|
3287
|
+
});
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
pending.occurrenceCount = Math.min(Number.MAX_SAFE_INTEGER, pending.occurrenceCount + 1);
|
|
3291
|
+
pending.maxByteSize =
|
|
3292
|
+
Math.max(pending.maxByteSize ?? 0, metadata.maxByteSize ?? 0) || undefined;
|
|
3293
|
+
pending.reason = metadata.reason;
|
|
3294
|
+
pending.phase = phase;
|
|
3295
|
+
pending.frameType = metadata.frameType;
|
|
3296
|
+
pending.field = metadata.field;
|
|
3297
|
+
pending.expected = metadata.expected;
|
|
3298
|
+
pending.actualType = metadata.actualType;
|
|
3299
|
+
pending.limitBytes = metadata.limitBytes;
|
|
2417
3300
|
}
|
|
2418
3301
|
|
|
2419
|
-
private
|
|
2420
|
-
|
|
3302
|
+
private flushProtocolViolations(): void {
|
|
3303
|
+
if (this.protocolViolationTimer) clearTimeout(this.protocolViolationTimer);
|
|
3304
|
+
this.protocolViolationTimer = null;
|
|
3305
|
+
for (const diagnostic of this.pendingProtocolViolations.values()) {
|
|
3306
|
+
this.emitProtocolViolation(diagnostic);
|
|
3307
|
+
}
|
|
3308
|
+
this.pendingProtocolViolations.clear();
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
private emitProtocolViolation(diagnostic: OmpProtocolViolationDiagnostic): void {
|
|
3312
|
+
try {
|
|
3313
|
+
const reporting = this.reportProtocolViolation(diagnostic);
|
|
3314
|
+
if (reporting) void reporting.catch(() => undefined);
|
|
3315
|
+
} catch {
|
|
3316
|
+
// Diagnostics must never alter transport flow or cleanup.
|
|
3317
|
+
}
|
|
2421
3318
|
}
|
|
2422
3319
|
|
|
2423
3320
|
private fail(error: Error): void {
|
|
@@ -2728,7 +3625,10 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
2728
3625
|
);
|
|
2729
3626
|
return {
|
|
2730
3627
|
...transcript,
|
|
2731
|
-
messages: z
|
|
3628
|
+
messages: z
|
|
3629
|
+
.array(OmpMessageSchema)
|
|
3630
|
+
.max(100_000)
|
|
3631
|
+
.parse(sanitizeMessageListMetadata(transcript.messages)),
|
|
2732
3632
|
};
|
|
2733
3633
|
}
|
|
2734
3634
|
async readPersistedSubagentTranscript(options: {
|
|
@@ -2745,7 +3645,10 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
2745
3645
|
);
|
|
2746
3646
|
return {
|
|
2747
3647
|
...transcript,
|
|
2748
|
-
messages: z
|
|
3648
|
+
messages: z
|
|
3649
|
+
.array(OmpMessageSchema)
|
|
3650
|
+
.max(100_000)
|
|
3651
|
+
.parse(sanitizeMessageListMetadata(transcript.messages)),
|
|
2749
3652
|
};
|
|
2750
3653
|
}
|
|
2751
3654
|
|
|
@@ -2760,6 +3663,7 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
2760
3663
|
this.options.spawnProcess,
|
|
2761
3664
|
this.options.terminateProcessTree,
|
|
2762
3665
|
options.requestTimeoutMs ?? this.options.requestTimeoutMs,
|
|
3666
|
+
this.options.reportProtocolViolation,
|
|
2763
3667
|
);
|
|
2764
3668
|
const abort = () => void process.close().catch(() => undefined);
|
|
2765
3669
|
options.signal?.addEventListener("abort", abort, { once: true });
|