@omercnet/paseo-omp 0.3.0-next.108.1 → 0.3.0-next.111.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.
@@ -87,30 +87,296 @@ const MAX_RPC_ERROR_CODE_BYTES = 256;
87
87
  const PROMPT_SCHEDULING_FAILURE = "OMP prompt scheduling failed";
88
88
  const PROTOCOL_VIOLATION_COALESCE_MS = 10_000;
89
89
 
90
- export type OmpProtocolViolationCategory =
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";
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];
103
257
 
104
258
  export interface OmpProtocolViolationDiagnostic {
105
259
  category: OmpProtocolViolationCategory;
260
+ reason: OmpProtocolViolationReason;
261
+ phase: OmpProtocolDiagnosticPhase;
106
262
  occurrenceCount: number;
107
- frameType?: "ready" | "response" | "rpc_chunk" | "rpc_frame_error";
263
+ eventType?: OmpProtocolEventType;
264
+ frameType?: "ready" | "response" | "rpc_chunk" | "rpc_frame_error" | "notice";
265
+ field?: OmpProtocolDiagnosticField;
266
+ expected?: OmpProtocolDiagnosticExpectation;
267
+ actualType?: OmpProtocolDiagnosticActualType;
108
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
+ };
109
374
  }
110
375
 
111
376
  type PendingProtocolViolation = Omit<OmpProtocolViolationDiagnostic, "occurrenceCount"> & {
112
377
  occurrenceCount: number;
113
378
  };
379
+ type ProtocolViolationKey = `${OmpProtocolViolationCategory}:${OmpProtocolViolationReason}`;
114
380
  const MAX_CONTEXT_PERCENT = 1_000_000;
115
381
  function boundedJsonString(maxBytes: number, minBytes = 0) {
116
382
  return z.string().refine((value) => {
@@ -1997,8 +2263,9 @@ class OmpRpcProcess {
1997
2263
  private spawnFailedWithoutProcess = false;
1998
2264
  private readyReceived = false;
1999
2265
  private outputSettled = false;
2266
+ private turnActive = false;
2000
2267
  private readonly pendingProtocolViolations = new Map<
2001
- OmpProtocolViolationCategory,
2268
+ ProtocolViolationKey,
2002
2269
  PendingProtocolViolation
2003
2270
  >();
2004
2271
  private protocolViolationTimer: TimerHandle | null = null;
@@ -2101,6 +2368,10 @@ class OmpRpcProcess {
2101
2368
  this.outputSettled = true;
2102
2369
  if (this.lineBytes > 0 || this.discardingLine) {
2103
2370
  this.recordProtocolViolation("incomplete-frame", {
2371
+ reason: "output-ended-mid-frame",
2372
+ field: "frame",
2373
+ expected: "complete-json-line",
2374
+ actualType: "partial-frame",
2104
2375
  maxByteSize: this.discardingLine ? this.discardedLineBytes : this.lineBytes,
2105
2376
  });
2106
2377
  }
@@ -2383,7 +2654,14 @@ class OmpRpcProcess {
2383
2654
  this.lineParts = [];
2384
2655
  this.lineBytes = 0;
2385
2656
  this.discardingLine = true;
2386
- this.recordProtocolViolation("frame-limit", { maxByteSize: nextBytes });
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
+ });
2387
2665
  return;
2388
2666
  }
2389
2667
  this.lineParts.push(part);
@@ -2404,7 +2682,13 @@ class OmpRpcProcess {
2404
2682
  try {
2405
2683
  decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
2406
2684
  } catch {
2407
- this.recordProtocolViolation("invalid-json", { maxByteSize: payload.byteLength });
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
+ });
2408
2692
  return;
2409
2693
  }
2410
2694
  this.receiveDecodedFrame(decoded, payload.byteLength);
@@ -2427,17 +2711,30 @@ class OmpRpcProcess {
2427
2711
  frame.data.length % 4 !== 0 ||
2428
2712
  !/^[A-Za-z0-9+/]*={0,2}$/u.test(frame.data)
2429
2713
  ) {
2430
- this.rejectChunk();
2714
+ this.rejectChunk("chunk-metadata", {
2715
+ field: "chunk",
2716
+ expected: "valid-chunk-frame",
2717
+ actualType: "object",
2718
+ });
2431
2719
  return;
2432
2720
  }
2433
2721
  const decoded = Buffer.from(frame.data, "base64");
2434
2722
  if (decoded.byteLength > MAX_CHUNK_BYTES) {
2435
- 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
+ });
2436
2729
  return;
2437
2730
  }
2438
2731
  if (!this.chunk) {
2439
2732
  if (frame.index !== 0) {
2440
- this.rejectChunk();
2733
+ this.rejectChunk("chunk-start-index", {
2734
+ field: "chunk.index",
2735
+ expected: "first-chunk-index-zero",
2736
+ actualType: "out-of-order",
2737
+ });
2441
2738
  return;
2442
2739
  }
2443
2740
  this.chunk = {
@@ -2446,7 +2743,15 @@ class OmpRpcProcess {
2446
2743
  byteLength: frame.byteLength,
2447
2744
  parts: [],
2448
2745
  receivedBytes: 0,
2449
- timer: setTimeout(() => this.rejectChunk(), CHUNK_STALE_MS),
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
+ ),
2450
2755
  };
2451
2756
  }
2452
2757
  const chunk = this.chunk;
@@ -2458,14 +2763,22 @@ class OmpRpcProcess {
2458
2763
  chunk.receivedBytes + decoded.byteLength > chunk.byteLength ||
2459
2764
  chunk.receivedBytes + decoded.byteLength > this.reassembledFrameLimit
2460
2765
  ) {
2461
- this.rejectChunk();
2766
+ this.rejectChunk("chunk-sequence", {
2767
+ field: "chunk.sequence",
2768
+ expected: "contiguous-chunk-sequence",
2769
+ actualType: "out-of-order",
2770
+ });
2462
2771
  return;
2463
2772
  }
2464
2773
  chunk.parts.push(decoded);
2465
2774
  chunk.receivedBytes += decoded.byteLength;
2466
2775
  if (chunk.parts.length !== chunk.count) return;
2467
2776
  if (chunk.receivedBytes !== chunk.byteLength) {
2468
- this.rejectChunk();
2777
+ this.rejectChunk("chunk-byte-count", {
2778
+ field: "chunk.byteLength",
2779
+ expected: "declared-chunk-byte-count",
2780
+ actualType: "mismatched",
2781
+ });
2469
2782
  return;
2470
2783
  }
2471
2784
  const reassembled = Buffer.concat(chunk.parts, chunk.receivedBytes);
@@ -2474,7 +2787,13 @@ class OmpRpcProcess {
2474
2787
  try {
2475
2788
  decodedFrame = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(reassembled));
2476
2789
  } catch {
2477
- this.recordProtocolViolation("invalid-json", { maxByteSize: reassembled.byteLength });
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
+ });
2478
2797
  return;
2479
2798
  }
2480
2799
  this.receiveDecodedFrame(decodedFrame, reassembled.byteLength);
@@ -2500,7 +2819,13 @@ class OmpRpcProcess {
2500
2819
  const sanitized = sanitizeLiveDisplayFrame(value);
2501
2820
  const frame = JsonObjectSchema.safeParse(sanitized);
2502
2821
  if (!frame.success) {
2503
- this.recordProtocolViolation("invalid-envelope", { maxByteSize: rawByteLength });
2822
+ this.recordProtocolViolation("invalid-envelope", {
2823
+ reason: "object-envelope",
2824
+ field: "frame",
2825
+ expected: "object-envelope",
2826
+ actualType: protocolDiagnosticActualType(sanitized),
2827
+ maxByteSize: rawByteLength,
2828
+ });
2504
2829
  return;
2505
2830
  }
2506
2831
  this.receiveFrame(frame.data, rawByteLength);
@@ -2525,10 +2850,21 @@ class OmpRpcProcess {
2525
2850
  const knownPending = rawId ? this.pending.get(rawId) : undefined;
2526
2851
  const response = OmpResponseFrameSchema.safeParse(frame);
2527
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
+ }
2528
2866
  if (rawId && knownPending) {
2529
2867
  this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
2530
- } else if (!rawId || !this.emitAcceptedPromptFailure(rawId, frame)) {
2531
- this.recordProtocolViolation("invalid-response", { frameType: "response" });
2532
2868
  }
2533
2869
  return;
2534
2870
  }
@@ -2686,7 +3022,13 @@ class OmpRpcProcess {
2686
3022
  private receiveFrame(frame: Record<string, unknown>, rawByteLength: number): void {
2687
3023
  const type = typeof frame.type === "string" && frame.type.length <= 64 ? frame.type : null;
2688
3024
  if (!type) {
2689
- this.recordProtocolViolation("invalid-envelope", { maxByteSize: rawByteLength });
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
+ });
2690
3032
  return;
2691
3033
  }
2692
3034
  const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
@@ -2700,31 +3042,66 @@ class OmpRpcProcess {
2700
3042
  4_096,
2701
3043
  ) === Number.POSITIVE_INFINITY
2702
3044
  ) {
2703
- this.recordProtocolViolation("frame-limit", { maxByteSize: rawByteLength });
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
+ });
2704
3053
  return;
2705
3054
  }
2706
3055
  if (type === "rpc_chunk") {
2707
3056
  const chunk = OmpChunkFrameSchema.safeParse(safeFrame);
2708
- if (!chunk.success) this.rejectChunk();
2709
- else this.receiveChunk(chunk.data);
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);
2710
3064
  return;
2711
3065
  }
2712
3066
  if (this.chunk) {
2713
3067
  this.clearChunk();
2714
- this.recordProtocolViolation("interleaved-chunk");
3068
+ this.recordProtocolViolation("interleaved-chunk", {
3069
+ reason: "frame-interleaved-with-chunk",
3070
+ field: "chunk.sequence",
3071
+ expected: "no-interleaved-frame",
3072
+ actualType: "interleaved",
3073
+ });
2715
3074
  }
2716
3075
  if (type === "rpc_frame_error") {
2717
- this.recordProtocolViolation("remote-frame-error", { frameType: "rpc_frame_error" });
3076
+ this.recordProtocolViolation("remote-frame-error", {
3077
+ reason: "remote-frame-error",
3078
+ frameType: "rpc_frame_error",
3079
+ field: "frame",
3080
+ expected: "no-remote-frame-error",
3081
+ actualType: "remote-error",
3082
+ });
2718
3083
  return;
2719
3084
  }
2720
3085
  if (type === "ready") {
2721
3086
  if (this.readyReceived) {
2722
- this.recordProtocolViolation("duplicate-ready", { frameType: "ready" });
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
+ });
2723
3094
  return;
2724
3095
  }
2725
3096
  const ready = OmpReadyFrameSchema.safeParse(safeFrame);
2726
3097
  if (!ready.success) {
2727
- this.recordProtocolViolation("invalid-ready", { frameType: "ready" });
3098
+ this.recordProtocolViolation("invalid-ready", {
3099
+ reason: "ready-schema",
3100
+ frameType: "ready",
3101
+ field: "ready",
3102
+ expected: "valid-ready-frame",
3103
+ actualType: "object",
3104
+ });
2728
3105
  } else {
2729
3106
  this.readyReceived = true;
2730
3107
  this.resolveReady(ready.data);
@@ -2739,17 +3116,30 @@ class OmpRpcProcess {
2739
3116
  if (!event.success) {
2740
3117
  this.rejectMatchingToolApproval(safeFrame);
2741
3118
  if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
2742
- this.recordProtocolViolation("invalid-event", { maxByteSize: rawByteLength });
3119
+ this.recordProtocolViolation("invalid-event", {
3120
+ ...invalidEventDiagnosticMetadata(safeFrame, type),
3121
+ maxByteSize: rawByteLength,
3122
+ });
2743
3123
  return;
2744
3124
  }
2745
3125
  if (!this.acceptEventState(event.data)) {
2746
- this.recordProtocolViolation("invalid-event-state", { maxByteSize: rawByteLength });
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
+ });
2747
3134
  return;
2748
3135
  }
2749
3136
  this.emit(event.data);
2750
3137
  if (event.data.type === "prompt_result" && event.data.id) {
2751
3138
  this.acceptedPromptIds.delete(event.data.id);
2752
3139
  }
3140
+ if (event.data.type === "turn_end" || event.data.type === "agent_end") {
3141
+ this.turnActive = false;
3142
+ }
2753
3143
  if (
2754
3144
  event.data.type === "message_end" ||
2755
3145
  event.data.type === "turn_end" ||
@@ -2777,6 +3167,7 @@ class OmpRpcProcess {
2777
3167
 
2778
3168
  private acceptEventState(event: z.infer<typeof OmpRuntimeEventSchema>): boolean {
2779
3169
  if (event.type === "turn_start") {
3170
+ this.turnActive = true;
2780
3171
  this.streamedBlocks.clear();
2781
3172
  this.commandTextLength = 0;
2782
3173
  this.activeToolCallIds.clear();
@@ -2846,32 +3237,66 @@ class OmpRpcProcess {
2846
3237
  return true;
2847
3238
  }
2848
3239
 
2849
- private rejectChunk(): void {
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 {
2850
3258
  this.clearChunk();
2851
- this.recordProtocolViolation("invalid-chunk", { frameType: "rpc_chunk" });
3259
+ this.recordProtocolViolation("invalid-chunk", {
3260
+ reason,
3261
+ frameType: "rpc_chunk",
3262
+ ...metadata,
3263
+ });
2852
3264
  }
2853
3265
 
2854
3266
  private recordProtocolViolation(
2855
3267
  category: OmpProtocolViolationCategory,
2856
- metadata: Omit<OmpProtocolViolationDiagnostic, "category" | "occurrenceCount"> = {},
3268
+ metadata: Omit<OmpProtocolViolationDiagnostic, "category" | "occurrenceCount" | "phase">,
2857
3269
  ): void {
3270
+ const phase = this.protocolDiagnosticPhase();
2858
3271
  if (!this.protocolViolationTimer) {
2859
- this.emitProtocolViolation({ category, occurrenceCount: 1, ...metadata });
3272
+ this.emitProtocolViolation({ category, occurrenceCount: 1, phase, ...metadata });
2860
3273
  this.protocolViolationTimer = setTimeout(
2861
3274
  () => this.flushProtocolViolations(),
2862
3275
  PROTOCOL_VIOLATION_COALESCE_MS,
2863
3276
  );
2864
3277
  return;
2865
3278
  }
2866
- const pending = this.pendingProtocolViolations.get(category);
3279
+ const key = `${category}:${metadata.reason}` as ProtocolViolationKey;
3280
+ const pending = this.pendingProtocolViolations.get(key);
2867
3281
  if (!pending) {
2868
- this.pendingProtocolViolations.set(category, { category, occurrenceCount: 1, ...metadata });
3282
+ this.pendingProtocolViolations.set(key, {
3283
+ category,
3284
+ occurrenceCount: 1,
3285
+ phase,
3286
+ ...metadata,
3287
+ });
2869
3288
  return;
2870
3289
  }
2871
3290
  pending.occurrenceCount = Math.min(Number.MAX_SAFE_INTEGER, pending.occurrenceCount + 1);
2872
3291
  pending.maxByteSize =
2873
3292
  Math.max(pending.maxByteSize ?? 0, metadata.maxByteSize ?? 0) || undefined;
2874
- if (pending.frameType !== metadata.frameType) pending.frameType = 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;
2875
3300
  }
2876
3301
 
2877
3302
  private flushProtocolViolations(): void {