@omercnet/paseo-omp 0.3.0-next.99.1 → 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 +25 -0
- package/server/provider/host-tools.ts +63 -10
- package/server/provider/mcp-transport.ts +2 -1
- package/server/provider/omp-rpc.ts +706 -63
- 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 +290 -72
- 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
|
@@ -70,6 +70,7 @@ const MAX_OPTIONAL_METADATA_NODES = 4_096;
|
|
|
70
70
|
const MAX_TASK_CORRELATION_BYTES = 256 * 1024;
|
|
71
71
|
const MAX_TASK_CORRELATION_ITEMS = 1_024;
|
|
72
72
|
const MAX_TASK_CORRELATION_NODES = 4_096;
|
|
73
|
+
const MAX_MODEL_CATALOG_ITEMS = 4_096;
|
|
73
74
|
// Tool-intensive OMP turns legitimately exceed 64 blocks; transport byte/node budgets remain the
|
|
74
75
|
// primary resource bounds.
|
|
75
76
|
export const OMP_MAX_CONTENT_PARTS = 4_096;
|
|
@@ -84,6 +85,298 @@ const MAX_COST_USD = 1_000_000_000;
|
|
|
84
85
|
const MAX_RPC_ERROR_BYTES = 4_096;
|
|
85
86
|
const MAX_RPC_ERROR_CODE_BYTES = 256;
|
|
86
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}`;
|
|
87
380
|
const MAX_CONTEXT_PERCENT = 1_000_000;
|
|
88
381
|
function boundedJsonString(maxBytes: number, minBytes = 0) {
|
|
89
382
|
return z.string().refine((value) => {
|
|
@@ -115,12 +408,94 @@ function boundedString(maxBytes: number, minBytes = 0) {
|
|
|
115
408
|
return bytes >= minBytes && bytes <= maxBytes;
|
|
116
409
|
});
|
|
117
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
|
+
}
|
|
118
422
|
|
|
119
423
|
const IDENTIFIER = boundedString(MAX_ID_LENGTH, 1);
|
|
120
424
|
const NAME = boundedString(MAX_NAME_LENGTH, 1);
|
|
121
425
|
const OMP_PROVIDER_NAME = NAME.refine((provider) => !provider.includes("/"));
|
|
122
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
|
+
}
|
|
123
497
|
const OmpThinkingLevelSchema = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
498
|
+
const OmpCurrentThinkingLevelSchema = boundedString(32).optional().catch(undefined);
|
|
124
499
|
|
|
125
500
|
function isBoundedJson(
|
|
126
501
|
value: unknown,
|
|
@@ -303,6 +678,14 @@ const OmpContentPartSchema = z
|
|
|
303
678
|
context.addIssue({ code: "custom", message: "invalid image payload" });
|
|
304
679
|
}
|
|
305
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
|
+
);
|
|
306
689
|
const OmpDisplayContentSchema = z.union([
|
|
307
690
|
TEXT,
|
|
308
691
|
z.array(OmpContentPartSchema).max(OMP_MAX_CONTENT_PARTS),
|
|
@@ -371,7 +754,7 @@ export type OmpMessage = OmpMessageIdentity &
|
|
|
371
754
|
const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
|
|
372
755
|
z.object({
|
|
373
756
|
role: z.literal("assistant"),
|
|
374
|
-
content:
|
|
757
|
+
content: OmpAssistantDisplayContentSchema.optional(),
|
|
375
758
|
...OmpMessageIdentityShape,
|
|
376
759
|
errorMessage: boundedString(4_096).nullable().optional(),
|
|
377
760
|
stopReason: boundedString(64).optional(),
|
|
@@ -394,7 +777,13 @@ const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
|
|
|
394
777
|
z.object({
|
|
395
778
|
role: z.literal("bashExecution"),
|
|
396
779
|
command: TEXT,
|
|
397
|
-
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
|
+
),
|
|
398
787
|
exitCode: z.number().int().nullable().optional(),
|
|
399
788
|
cancelled: z.boolean().optional(),
|
|
400
789
|
truncated: z.boolean().optional(),
|
|
@@ -462,19 +851,33 @@ const OmpAvailableCommandSchema = z.object({
|
|
|
462
851
|
.optional(),
|
|
463
852
|
source: boundedString(64).optional(),
|
|
464
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
|
+
});
|
|
465
866
|
const OmpModelSchema = z.object({
|
|
466
867
|
provider: OMP_PROVIDER_NAME,
|
|
467
868
|
id: NAME,
|
|
468
|
-
name: boundedString(MAX_NAME_LENGTH).optional(),
|
|
469
|
-
reasoning: z.boolean().optional(),
|
|
470
|
-
thinking:
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
.
|
|
476
|
-
|
|
477
|
-
|
|
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),
|
|
478
881
|
});
|
|
479
882
|
const TokenCountSchema = z.number().int().nonnegative().max(MAX_TOKEN_COUNT);
|
|
480
883
|
const OptionalTokenCountSchema = TokenCountSchema.nullable().optional();
|
|
@@ -526,13 +929,13 @@ const OmpCompactionResultSchema = z.object({
|
|
|
526
929
|
});
|
|
527
930
|
const OmpSessionStateSchema = z.object({
|
|
528
931
|
model: OmpModelSchema.nullable().optional(),
|
|
529
|
-
thinkingLevel:
|
|
932
|
+
thinkingLevel: OmpCurrentThinkingLevelSchema,
|
|
530
933
|
isStreaming: z.boolean(),
|
|
531
934
|
isCompacting: z.boolean(),
|
|
532
935
|
sessionId: IDENTIFIER,
|
|
533
936
|
autoCompactionEnabled: z.boolean().optional(),
|
|
534
|
-
contextUsage: OmpContextUsageSchema.nullable().optional(),
|
|
535
|
-
sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
|
|
937
|
+
contextUsage: OmpContextUsageSchema.nullable().optional().catch(undefined),
|
|
938
|
+
sessionFile: boundedString(MAX_PATH_LENGTH).optional().catch(undefined),
|
|
536
939
|
});
|
|
537
940
|
const OmpReadyFrameSchema = z.object({
|
|
538
941
|
type: z.literal("ready"),
|
|
@@ -1133,7 +1536,7 @@ function runtimeFrameCollectionLimit(frame: Record<string, unknown>): number {
|
|
|
1133
1536
|
return 1_024;
|
|
1134
1537
|
}
|
|
1135
1538
|
const OmpModelsResultSchema = z.object({
|
|
1136
|
-
models: z.array(OmpModelSchema).min(1).max(
|
|
1539
|
+
models: z.array(OmpModelSchema).min(1).max(MAX_MODEL_CATALOG_ITEMS),
|
|
1137
1540
|
});
|
|
1138
1541
|
const OmpPromptAckSchema = z.object({ agentInvoked: z.boolean().optional() }).optional();
|
|
1139
1542
|
const OmpAvailableCommandsResultSchema = z.object({
|
|
@@ -1178,6 +1581,7 @@ const ProtocolNegotiationResultSchema = z.object({
|
|
|
1178
1581
|
});
|
|
1179
1582
|
|
|
1180
1583
|
export type OmpModel = z.infer<typeof OmpModelSchema>;
|
|
1584
|
+
export type OmpBranchResult = z.infer<typeof OmpBranchResultSchema>;
|
|
1181
1585
|
export type OmpSessionState = z.infer<typeof OmpSessionStateSchema>;
|
|
1182
1586
|
export type OmpSessionStats = z.infer<typeof OmpSessionStatsSchema>;
|
|
1183
1587
|
export type OmpCompactionResult = z.infer<typeof OmpCompactionResultSchema>;
|
|
@@ -1219,6 +1623,7 @@ export interface OmpPersistedSessionMessages {
|
|
|
1219
1623
|
nativeSessionId: string;
|
|
1220
1624
|
byteLength: number;
|
|
1221
1625
|
messages: OmpMessage[];
|
|
1626
|
+
imageReplayWarning?: true;
|
|
1222
1627
|
}
|
|
1223
1628
|
|
|
1224
1629
|
export interface OmpStartOptions {
|
|
@@ -1284,7 +1689,7 @@ export interface OmpRuntimeSession {
|
|
|
1284
1689
|
respondToExtensionUi(response: OmpExtensionUiResponse): Promise<void>;
|
|
1285
1690
|
respondToToolApproval(response: OmpToolApprovalResponse): Promise<void>;
|
|
1286
1691
|
getBranchMessages(): Promise<Array<{ entryId: string; text: string }>>;
|
|
1287
|
-
branch(entryId: string): Promise<
|
|
1692
|
+
branch(entryId: string): Promise<OmpBranchResult>;
|
|
1288
1693
|
readonly canReplayHistory: boolean;
|
|
1289
1694
|
getMessages(): Promise<OmpMessage[]>;
|
|
1290
1695
|
abort(): Promise<void>;
|
|
@@ -1326,11 +1731,19 @@ export interface OmpRpcRuntimeOptions {
|
|
|
1326
1731
|
terminateProcessTree?: (pid: number) => Promise<boolean | "uncertain">;
|
|
1327
1732
|
environment?: NodeJS.ProcessEnv;
|
|
1328
1733
|
requestTimeoutMs?: number;
|
|
1734
|
+
reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
|
|
1329
1735
|
listSessions?: (
|
|
1330
1736
|
options: OmpSessionListOptions,
|
|
1331
1737
|
) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
|
|
1332
1738
|
}
|
|
1333
1739
|
|
|
1740
|
+
export class OmpRpcRequestRejectedError extends Error {
|
|
1741
|
+
constructor() {
|
|
1742
|
+
super("OMP RPC request failed");
|
|
1743
|
+
this.name = "OmpRpcRequestRejectedError";
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1334
1747
|
type PendingRequest = {
|
|
1335
1748
|
resolve(value: unknown): void;
|
|
1336
1749
|
reject(error: Error): void;
|
|
@@ -1850,12 +2263,21 @@ class OmpRpcProcess {
|
|
|
1850
2263
|
private spawnFailedWithoutProcess = false;
|
|
1851
2264
|
private readyReceived = false;
|
|
1852
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;
|
|
1853
2272
|
|
|
1854
2273
|
constructor(
|
|
1855
2274
|
options: OmpStartOptions,
|
|
1856
2275
|
spawnProcess?: OmpRpcRuntimeOptions["spawnProcess"],
|
|
1857
2276
|
terminateProcessTree?: OmpRpcRuntimeOptions["terminateProcessTree"],
|
|
1858
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),
|
|
1859
2281
|
) {
|
|
1860
2282
|
const ready = Promise.withResolvers<ReadyFrame>();
|
|
1861
2283
|
this.rejectReady = ready.reject;
|
|
@@ -1944,7 +2366,15 @@ class OmpRpcProcess {
|
|
|
1944
2366
|
private settleOutput(): void {
|
|
1945
2367
|
if (this.outputSettled) return;
|
|
1946
2368
|
this.outputSettled = true;
|
|
1947
|
-
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
|
+
}
|
|
1948
2378
|
this.lineParts = [];
|
|
1949
2379
|
this.lineBytes = 0;
|
|
1950
2380
|
this.discardingLine = false;
|
|
@@ -2127,16 +2557,17 @@ class OmpRpcProcess {
|
|
|
2127
2557
|
|
|
2128
2558
|
private async closeProcess(): Promise<void> {
|
|
2129
2559
|
this.closed = true;
|
|
2560
|
+
this.flushProtocolViolations();
|
|
2130
2561
|
this.clearChunk();
|
|
2131
2562
|
this.failPending(new Error("OMP RPC process was closed"));
|
|
2563
|
+
const cleanupPromise = this.startTreeCleanup();
|
|
2132
2564
|
if (!this.exited) {
|
|
2133
2565
|
try {
|
|
2134
2566
|
this.child.stdin.end();
|
|
2135
2567
|
} catch {
|
|
2136
|
-
// Continue
|
|
2568
|
+
// Continue waiting for process-tree cleanup when the input channel is already closed.
|
|
2137
2569
|
}
|
|
2138
2570
|
}
|
|
2139
|
-
const cleanupPromise = this.startTreeCleanup();
|
|
2140
2571
|
const cleanup = await cleanupPromise;
|
|
2141
2572
|
if (cleanup !== "verified") throw new Error("OMP RPC process tree cleanup failed");
|
|
2142
2573
|
if (
|
|
@@ -2223,7 +2654,14 @@ class OmpRpcProcess {
|
|
|
2223
2654
|
this.lineParts = [];
|
|
2224
2655
|
this.lineBytes = 0;
|
|
2225
2656
|
this.discardingLine = true;
|
|
2226
|
-
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
|
+
});
|
|
2227
2665
|
return;
|
|
2228
2666
|
}
|
|
2229
2667
|
this.lineParts.push(part);
|
|
@@ -2244,16 +2682,16 @@ class OmpRpcProcess {
|
|
|
2244
2682
|
try {
|
|
2245
2683
|
decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
|
|
2246
2684
|
} catch {
|
|
2247
|
-
this.recordProtocolViolation(
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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
|
+
});
|
|
2254
2692
|
return;
|
|
2255
2693
|
}
|
|
2256
|
-
this.
|
|
2694
|
+
this.receiveDecodedFrame(decoded, payload.byteLength);
|
|
2257
2695
|
}
|
|
2258
2696
|
|
|
2259
2697
|
private receiveChunk(frame: ChunkFrame): void {
|
|
@@ -2273,17 +2711,30 @@ class OmpRpcProcess {
|
|
|
2273
2711
|
frame.data.length % 4 !== 0 ||
|
|
2274
2712
|
!/^[A-Za-z0-9+/]*={0,2}$/u.test(frame.data)
|
|
2275
2713
|
) {
|
|
2276
|
-
this.rejectChunk(
|
|
2714
|
+
this.rejectChunk("chunk-metadata", {
|
|
2715
|
+
field: "chunk",
|
|
2716
|
+
expected: "valid-chunk-frame",
|
|
2717
|
+
actualType: "object",
|
|
2718
|
+
});
|
|
2277
2719
|
return;
|
|
2278
2720
|
}
|
|
2279
2721
|
const decoded = Buffer.from(frame.data, "base64");
|
|
2280
2722
|
if (decoded.byteLength > MAX_CHUNK_BYTES) {
|
|
2281
|
-
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
|
+
});
|
|
2282
2729
|
return;
|
|
2283
2730
|
}
|
|
2284
2731
|
if (!this.chunk) {
|
|
2285
2732
|
if (frame.index !== 0) {
|
|
2286
|
-
this.rejectChunk(
|
|
2733
|
+
this.rejectChunk("chunk-start-index", {
|
|
2734
|
+
field: "chunk.index",
|
|
2735
|
+
expected: "first-chunk-index-zero",
|
|
2736
|
+
actualType: "out-of-order",
|
|
2737
|
+
});
|
|
2287
2738
|
return;
|
|
2288
2739
|
}
|
|
2289
2740
|
this.chunk = {
|
|
@@ -2292,7 +2743,15 @@ class OmpRpcProcess {
|
|
|
2292
2743
|
byteLength: frame.byteLength,
|
|
2293
2744
|
parts: [],
|
|
2294
2745
|
receivedBytes: 0,
|
|
2295
|
-
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
|
+
),
|
|
2296
2755
|
};
|
|
2297
2756
|
}
|
|
2298
2757
|
const chunk = this.chunk;
|
|
@@ -2304,14 +2763,22 @@ class OmpRpcProcess {
|
|
|
2304
2763
|
chunk.receivedBytes + decoded.byteLength > chunk.byteLength ||
|
|
2305
2764
|
chunk.receivedBytes + decoded.byteLength > this.reassembledFrameLimit
|
|
2306
2765
|
) {
|
|
2307
|
-
this.rejectChunk(
|
|
2766
|
+
this.rejectChunk("chunk-sequence", {
|
|
2767
|
+
field: "chunk.sequence",
|
|
2768
|
+
expected: "contiguous-chunk-sequence",
|
|
2769
|
+
actualType: "out-of-order",
|
|
2770
|
+
});
|
|
2308
2771
|
return;
|
|
2309
2772
|
}
|
|
2310
2773
|
chunk.parts.push(decoded);
|
|
2311
2774
|
chunk.receivedBytes += decoded.byteLength;
|
|
2312
2775
|
if (chunk.parts.length !== chunk.count) return;
|
|
2313
2776
|
if (chunk.receivedBytes !== chunk.byteLength) {
|
|
2314
|
-
this.rejectChunk(
|
|
2777
|
+
this.rejectChunk("chunk-byte-count", {
|
|
2778
|
+
field: "chunk.byteLength",
|
|
2779
|
+
expected: "declared-chunk-byte-count",
|
|
2780
|
+
actualType: "mismatched",
|
|
2781
|
+
});
|
|
2315
2782
|
return;
|
|
2316
2783
|
}
|
|
2317
2784
|
const reassembled = Buffer.concat(chunk.parts, chunk.receivedBytes);
|
|
@@ -2320,16 +2787,48 @@ class OmpRpcProcess {
|
|
|
2320
2787
|
try {
|
|
2321
2788
|
decodedFrame = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(reassembled));
|
|
2322
2789
|
} catch {
|
|
2323
|
-
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
|
+
});
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
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"));
|
|
2324
2816
|
return;
|
|
2325
2817
|
}
|
|
2326
|
-
if (this.receiveKnownResponse(
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
|
|
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
|
+
});
|
|
2330
2829
|
return;
|
|
2331
2830
|
}
|
|
2332
|
-
this.receiveFrame(
|
|
2831
|
+
this.receiveFrame(frame.data, rawByteLength);
|
|
2333
2832
|
}
|
|
2334
2833
|
|
|
2335
2834
|
private receiveKnownResponse(value: unknown): boolean {
|
|
@@ -2351,10 +2850,21 @@ class OmpRpcProcess {
|
|
|
2351
2850
|
const knownPending = rawId ? this.pending.get(rawId) : undefined;
|
|
2352
2851
|
const response = OmpResponseFrameSchema.safeParse(frame);
|
|
2353
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
|
+
}
|
|
2354
2866
|
if (rawId && knownPending) {
|
|
2355
2867
|
this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
|
|
2356
|
-
} else if (!rawId || !this.emitAcceptedPromptFailure(rawId, frame)) {
|
|
2357
|
-
this.recordProtocolViolation();
|
|
2358
2868
|
}
|
|
2359
2869
|
return;
|
|
2360
2870
|
}
|
|
@@ -2373,13 +2883,19 @@ class OmpRpcProcess {
|
|
|
2373
2883
|
: response.data.data;
|
|
2374
2884
|
const boundedFrame =
|
|
2375
2885
|
responseData === response.data.data ? frame : { ...frame, data: responseData };
|
|
2376
|
-
const responseItemLimit = isBranchHistory
|
|
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;
|
|
2377
2893
|
const responseByteLimit =
|
|
2378
2894
|
isBranchHistory || isHistory
|
|
2379
2895
|
? Math.min(MAX_REASSEMBLED_FRAME_BYTES, this.reassembledFrameLimit)
|
|
2380
2896
|
: 2 * 1024 * 1024;
|
|
2381
|
-
//
|
|
2382
|
-
// node
|
|
2897
|
+
// Model catalogs are truncated before publication, while the transport still enforces
|
|
2898
|
+
// aggregate byte and node budgets over the complete response.
|
|
2383
2899
|
const responseNodeLimit = isBranchHistory
|
|
2384
2900
|
? 4_096
|
|
2385
2901
|
: isHistory
|
|
@@ -2418,7 +2934,7 @@ class OmpRpcProcess {
|
|
|
2418
2934
|
settled.reject(new Error("OMP RPC response is invalid"));
|
|
2419
2935
|
}
|
|
2420
2936
|
} else {
|
|
2421
|
-
settled.reject(new
|
|
2937
|
+
settled.reject(new OmpRpcRequestRejectedError());
|
|
2422
2938
|
}
|
|
2423
2939
|
}
|
|
2424
2940
|
|
|
@@ -2471,7 +2987,7 @@ class OmpRpcProcess {
|
|
|
2471
2987
|
structuralFrame.messages as unknown[],
|
|
2472
2988
|
MAX_SEMANTIC_FRAME_BYTES,
|
|
2473
2989
|
OMP_MAX_CONTENT_PARTS,
|
|
2474
|
-
|
|
2990
|
+
MAX_IMAGE_DATA_LENGTH,
|
|
2475
2991
|
4_096,
|
|
2476
2992
|
) !== Number.POSITIVE_INFINITY);
|
|
2477
2993
|
const payloadIsSafe =
|
|
@@ -2503,10 +3019,16 @@ class OmpRpcProcess {
|
|
|
2503
3019
|
return true;
|
|
2504
3020
|
}
|
|
2505
3021
|
|
|
2506
|
-
private receiveFrame(frame: Record<string, unknown
|
|
3022
|
+
private receiveFrame(frame: Record<string, unknown>, rawByteLength: number): void {
|
|
2507
3023
|
const type = typeof frame.type === "string" && frame.type.length <= 64 ? frame.type : null;
|
|
2508
3024
|
if (!type) {
|
|
2509
|
-
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
|
+
});
|
|
2510
3032
|
return;
|
|
2511
3033
|
}
|
|
2512
3034
|
const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
|
|
@@ -2520,31 +3042,66 @@ class OmpRpcProcess {
|
|
|
2520
3042
|
4_096,
|
|
2521
3043
|
) === Number.POSITIVE_INFINITY
|
|
2522
3044
|
) {
|
|
2523
|
-
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
|
+
});
|
|
2524
3053
|
return;
|
|
2525
3054
|
}
|
|
2526
3055
|
if (type === "rpc_chunk") {
|
|
2527
3056
|
const chunk = OmpChunkFrameSchema.safeParse(safeFrame);
|
|
2528
|
-
if (!chunk.success)
|
|
2529
|
-
|
|
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);
|
|
2530
3064
|
return;
|
|
2531
3065
|
}
|
|
2532
3066
|
if (this.chunk) {
|
|
2533
3067
|
this.clearChunk();
|
|
2534
|
-
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
|
+
});
|
|
2535
3074
|
}
|
|
2536
3075
|
if (type === "rpc_frame_error") {
|
|
2537
|
-
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
|
+
});
|
|
2538
3083
|
return;
|
|
2539
3084
|
}
|
|
2540
3085
|
if (type === "ready") {
|
|
2541
3086
|
if (this.readyReceived) {
|
|
2542
|
-
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
|
+
});
|
|
2543
3094
|
return;
|
|
2544
3095
|
}
|
|
2545
3096
|
const ready = OmpReadyFrameSchema.safeParse(safeFrame);
|
|
2546
3097
|
if (!ready.success) {
|
|
2547
|
-
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
|
+
});
|
|
2548
3105
|
} else {
|
|
2549
3106
|
this.readyReceived = true;
|
|
2550
3107
|
this.resolveReady(ready.data);
|
|
@@ -2559,17 +3116,30 @@ class OmpRpcProcess {
|
|
|
2559
3116
|
if (!event.success) {
|
|
2560
3117
|
this.rejectMatchingToolApproval(safeFrame);
|
|
2561
3118
|
if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
|
|
2562
|
-
this.recordProtocolViolation(
|
|
3119
|
+
this.recordProtocolViolation("invalid-event", {
|
|
3120
|
+
...invalidEventDiagnosticMetadata(safeFrame, type),
|
|
3121
|
+
maxByteSize: rawByteLength,
|
|
3122
|
+
});
|
|
2563
3123
|
return;
|
|
2564
3124
|
}
|
|
2565
3125
|
if (!this.acceptEventState(event.data)) {
|
|
2566
|
-
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
|
+
});
|
|
2567
3134
|
return;
|
|
2568
3135
|
}
|
|
2569
3136
|
this.emit(event.data);
|
|
2570
3137
|
if (event.data.type === "prompt_result" && event.data.id) {
|
|
2571
3138
|
this.acceptedPromptIds.delete(event.data.id);
|
|
2572
3139
|
}
|
|
3140
|
+
if (event.data.type === "turn_end" || event.data.type === "agent_end") {
|
|
3141
|
+
this.turnActive = false;
|
|
3142
|
+
}
|
|
2573
3143
|
if (
|
|
2574
3144
|
event.data.type === "message_end" ||
|
|
2575
3145
|
event.data.type === "turn_end" ||
|
|
@@ -2597,6 +3167,7 @@ class OmpRpcProcess {
|
|
|
2597
3167
|
|
|
2598
3168
|
private acceptEventState(event: z.infer<typeof OmpRuntimeEventSchema>): boolean {
|
|
2599
3169
|
if (event.type === "turn_start") {
|
|
3170
|
+
this.turnActive = true;
|
|
2600
3171
|
this.streamedBlocks.clear();
|
|
2601
3172
|
this.commandTextLength = 0;
|
|
2602
3173
|
this.activeToolCallIds.clear();
|
|
@@ -2666,13 +3237,84 @@ class OmpRpcProcess {
|
|
|
2666
3237
|
return true;
|
|
2667
3238
|
}
|
|
2668
3239
|
|
|
2669
|
-
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 {
|
|
2670
3258
|
this.clearChunk();
|
|
2671
|
-
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;
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
private flushProtocolViolations(): void {
|
|
3303
|
+
if (this.protocolViolationTimer) clearTimeout(this.protocolViolationTimer);
|
|
3304
|
+
this.protocolViolationTimer = null;
|
|
3305
|
+
for (const diagnostic of this.pendingProtocolViolations.values()) {
|
|
3306
|
+
this.emitProtocolViolation(diagnostic);
|
|
3307
|
+
}
|
|
3308
|
+
this.pendingProtocolViolations.clear();
|
|
2672
3309
|
}
|
|
2673
3310
|
|
|
2674
|
-
private
|
|
2675
|
-
|
|
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
|
+
}
|
|
2676
3318
|
}
|
|
2677
3319
|
|
|
2678
3320
|
private fail(error: Error): void {
|
|
@@ -3021,6 +3663,7 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
3021
3663
|
this.options.spawnProcess,
|
|
3022
3664
|
this.options.terminateProcessTree,
|
|
3023
3665
|
options.requestTimeoutMs ?? this.options.requestTimeoutMs,
|
|
3666
|
+
this.options.reportProtocolViolation,
|
|
3024
3667
|
);
|
|
3025
3668
|
const abort = () => void process.close().catch(() => undefined);
|
|
3026
3669
|
options.signal?.addEventListener("abort", abort, { once: true });
|