@omercnet/paseo-omp 0.3.0-next.104.1 → 0.3.0-next.105.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "0.3.0-next.104.1",
3
+ "version": "0.3.0-next.105.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -85,6 +85,32 @@ const MAX_COST_USD = 1_000_000_000;
85
85
  const MAX_RPC_ERROR_BYTES = 4_096;
86
86
  const MAX_RPC_ERROR_CODE_BYTES = 256;
87
87
  const PROMPT_SCHEDULING_FAILURE = "OMP prompt scheduling failed";
88
+ const PROTOCOL_VIOLATION_COALESCE_MS = 10_000;
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";
103
+
104
+ export interface OmpProtocolViolationDiagnostic {
105
+ category: OmpProtocolViolationCategory;
106
+ occurrenceCount: number;
107
+ frameType?: "ready" | "response" | "rpc_chunk" | "rpc_frame_error";
108
+ maxByteSize?: number;
109
+ }
110
+
111
+ type PendingProtocolViolation = Omit<OmpProtocolViolationDiagnostic, "occurrenceCount"> & {
112
+ occurrenceCount: number;
113
+ };
88
114
  const MAX_CONTEXT_PERCENT = 1_000_000;
89
115
  function boundedJsonString(maxBytes: number, minBytes = 0) {
90
116
  return z.string().refine((value) => {
@@ -1439,6 +1465,7 @@ export interface OmpRpcRuntimeOptions {
1439
1465
  terminateProcessTree?: (pid: number) => Promise<boolean | "uncertain">;
1440
1466
  environment?: NodeJS.ProcessEnv;
1441
1467
  requestTimeoutMs?: number;
1468
+ reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
1442
1469
  listSessions?: (
1443
1470
  options: OmpSessionListOptions,
1444
1471
  ) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
@@ -1970,12 +1997,20 @@ class OmpRpcProcess {
1970
1997
  private spawnFailedWithoutProcess = false;
1971
1998
  private readyReceived = false;
1972
1999
  private outputSettled = false;
2000
+ private readonly pendingProtocolViolations = new Map<
2001
+ OmpProtocolViolationCategory,
2002
+ PendingProtocolViolation
2003
+ >();
2004
+ private protocolViolationTimer: TimerHandle | null = null;
1973
2005
 
1974
2006
  constructor(
1975
2007
  options: OmpStartOptions,
1976
2008
  spawnProcess?: OmpRpcRuntimeOptions["spawnProcess"],
1977
2009
  terminateProcessTree?: OmpRpcRuntimeOptions["terminateProcessTree"],
1978
2010
  private readonly requestTimeoutMs = REQUEST_TIMEOUT_MS,
2011
+ private readonly reportProtocolViolation: (
2012
+ diagnostic: OmpProtocolViolationDiagnostic,
2013
+ ) => void | Promise<void> = (diagnostic) => console.error("OMP protocol violation", diagnostic),
1979
2014
  ) {
1980
2015
  const ready = Promise.withResolvers<ReadyFrame>();
1981
2016
  this.rejectReady = ready.reject;
@@ -2064,7 +2099,11 @@ class OmpRpcProcess {
2064
2099
  private settleOutput(): void {
2065
2100
  if (this.outputSettled) return;
2066
2101
  this.outputSettled = true;
2067
- if (this.lineBytes > 0 || this.discardingLine) this.recordProtocolViolation();
2102
+ if (this.lineBytes > 0 || this.discardingLine) {
2103
+ this.recordProtocolViolation("incomplete-frame", {
2104
+ maxByteSize: this.discardingLine ? this.discardedLineBytes : this.lineBytes,
2105
+ });
2106
+ }
2068
2107
  this.lineParts = [];
2069
2108
  this.lineBytes = 0;
2070
2109
  this.discardingLine = false;
@@ -2247,6 +2286,7 @@ class OmpRpcProcess {
2247
2286
 
2248
2287
  private async closeProcess(): Promise<void> {
2249
2288
  this.closed = true;
2289
+ this.flushProtocolViolations();
2250
2290
  this.clearChunk();
2251
2291
  this.failPending(new Error("OMP RPC process was closed"));
2252
2292
  if (!this.exited) {
@@ -2343,7 +2383,7 @@ class OmpRpcProcess {
2343
2383
  this.lineParts = [];
2344
2384
  this.lineBytes = 0;
2345
2385
  this.discardingLine = true;
2346
- this.recordProtocolViolation();
2386
+ this.recordProtocolViolation("frame-limit", { maxByteSize: nextBytes });
2347
2387
  return;
2348
2388
  }
2349
2389
  this.lineParts.push(part);
@@ -2364,7 +2404,7 @@ class OmpRpcProcess {
2364
2404
  try {
2365
2405
  decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
2366
2406
  } catch {
2367
- this.recordProtocolViolation();
2407
+ this.recordProtocolViolation("invalid-json", { maxByteSize: payload.byteLength });
2368
2408
  return;
2369
2409
  }
2370
2410
  this.receiveDecodedFrame(decoded, payload.byteLength);
@@ -2434,7 +2474,7 @@ class OmpRpcProcess {
2434
2474
  try {
2435
2475
  decodedFrame = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(reassembled));
2436
2476
  } catch {
2437
- this.recordProtocolViolation();
2477
+ this.recordProtocolViolation("invalid-json", { maxByteSize: reassembled.byteLength });
2438
2478
  return;
2439
2479
  }
2440
2480
  this.receiveDecodedFrame(decodedFrame, reassembled.byteLength);
@@ -2460,10 +2500,10 @@ class OmpRpcProcess {
2460
2500
  const sanitized = sanitizeLiveDisplayFrame(value);
2461
2501
  const frame = JsonObjectSchema.safeParse(sanitized);
2462
2502
  if (!frame.success) {
2463
- this.recordProtocolViolation();
2503
+ this.recordProtocolViolation("invalid-envelope", { maxByteSize: rawByteLength });
2464
2504
  return;
2465
2505
  }
2466
- this.receiveFrame(frame.data);
2506
+ this.receiveFrame(frame.data, rawByteLength);
2467
2507
  }
2468
2508
 
2469
2509
  private receiveKnownResponse(value: unknown): boolean {
@@ -2488,7 +2528,7 @@ class OmpRpcProcess {
2488
2528
  if (rawId && knownPending) {
2489
2529
  this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
2490
2530
  } else if (!rawId || !this.emitAcceptedPromptFailure(rawId, frame)) {
2491
- this.recordProtocolViolation();
2531
+ this.recordProtocolViolation("invalid-response", { frameType: "response" });
2492
2532
  }
2493
2533
  return;
2494
2534
  }
@@ -2643,10 +2683,10 @@ class OmpRpcProcess {
2643
2683
  return true;
2644
2684
  }
2645
2685
 
2646
- private receiveFrame(frame: Record<string, unknown>): void {
2686
+ private receiveFrame(frame: Record<string, unknown>, rawByteLength: number): void {
2647
2687
  const type = typeof frame.type === "string" && frame.type.length <= 64 ? frame.type : null;
2648
2688
  if (!type) {
2649
- this.recordProtocolViolation();
2689
+ this.recordProtocolViolation("invalid-envelope", { maxByteSize: rawByteLength });
2650
2690
  return;
2651
2691
  }
2652
2692
  const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
@@ -2660,7 +2700,7 @@ class OmpRpcProcess {
2660
2700
  4_096,
2661
2701
  ) === Number.POSITIVE_INFINITY
2662
2702
  ) {
2663
- this.recordProtocolViolation();
2703
+ this.recordProtocolViolation("frame-limit", { maxByteSize: rawByteLength });
2664
2704
  return;
2665
2705
  }
2666
2706
  if (type === "rpc_chunk") {
@@ -2671,20 +2711,20 @@ class OmpRpcProcess {
2671
2711
  }
2672
2712
  if (this.chunk) {
2673
2713
  this.clearChunk();
2674
- this.recordProtocolViolation();
2714
+ this.recordProtocolViolation("interleaved-chunk");
2675
2715
  }
2676
2716
  if (type === "rpc_frame_error") {
2677
- this.recordProtocolViolation();
2717
+ this.recordProtocolViolation("remote-frame-error", { frameType: "rpc_frame_error" });
2678
2718
  return;
2679
2719
  }
2680
2720
  if (type === "ready") {
2681
2721
  if (this.readyReceived) {
2682
- this.recordProtocolViolation();
2722
+ this.recordProtocolViolation("duplicate-ready", { frameType: "ready" });
2683
2723
  return;
2684
2724
  }
2685
2725
  const ready = OmpReadyFrameSchema.safeParse(safeFrame);
2686
2726
  if (!ready.success) {
2687
- this.recordProtocolViolation();
2727
+ this.recordProtocolViolation("invalid-ready", { frameType: "ready" });
2688
2728
  } else {
2689
2729
  this.readyReceived = true;
2690
2730
  this.resolveReady(ready.data);
@@ -2699,11 +2739,11 @@ class OmpRpcProcess {
2699
2739
  if (!event.success) {
2700
2740
  this.rejectMatchingToolApproval(safeFrame);
2701
2741
  if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
2702
- this.recordProtocolViolation();
2742
+ this.recordProtocolViolation("invalid-event", { maxByteSize: rawByteLength });
2703
2743
  return;
2704
2744
  }
2705
2745
  if (!this.acceptEventState(event.data)) {
2706
- this.recordProtocolViolation();
2746
+ this.recordProtocolViolation("invalid-event-state", { maxByteSize: rawByteLength });
2707
2747
  return;
2708
2748
  }
2709
2749
  this.emit(event.data);
@@ -2808,11 +2848,48 @@ class OmpRpcProcess {
2808
2848
 
2809
2849
  private rejectChunk(): void {
2810
2850
  this.clearChunk();
2811
- this.recordProtocolViolation();
2851
+ this.recordProtocolViolation("invalid-chunk", { frameType: "rpc_chunk" });
2852
+ }
2853
+
2854
+ private recordProtocolViolation(
2855
+ category: OmpProtocolViolationCategory,
2856
+ metadata: Omit<OmpProtocolViolationDiagnostic, "category" | "occurrenceCount"> = {},
2857
+ ): void {
2858
+ if (!this.protocolViolationTimer) {
2859
+ this.emitProtocolViolation({ category, occurrenceCount: 1, ...metadata });
2860
+ this.protocolViolationTimer = setTimeout(
2861
+ () => this.flushProtocolViolations(),
2862
+ PROTOCOL_VIOLATION_COALESCE_MS,
2863
+ );
2864
+ return;
2865
+ }
2866
+ const pending = this.pendingProtocolViolations.get(category);
2867
+ if (!pending) {
2868
+ this.pendingProtocolViolations.set(category, { category, occurrenceCount: 1, ...metadata });
2869
+ return;
2870
+ }
2871
+ pending.occurrenceCount = Math.min(Number.MAX_SAFE_INTEGER, pending.occurrenceCount + 1);
2872
+ pending.maxByteSize =
2873
+ Math.max(pending.maxByteSize ?? 0, metadata.maxByteSize ?? 0) || undefined;
2874
+ if (pending.frameType !== metadata.frameType) pending.frameType = undefined;
2812
2875
  }
2813
2876
 
2814
- private recordProtocolViolation(): void {
2815
- // Malformed bounded frames are isolated so the following frame starts from clean state.
2877
+ private flushProtocolViolations(): void {
2878
+ if (this.protocolViolationTimer) clearTimeout(this.protocolViolationTimer);
2879
+ this.protocolViolationTimer = null;
2880
+ for (const diagnostic of this.pendingProtocolViolations.values()) {
2881
+ this.emitProtocolViolation(diagnostic);
2882
+ }
2883
+ this.pendingProtocolViolations.clear();
2884
+ }
2885
+
2886
+ private emitProtocolViolation(diagnostic: OmpProtocolViolationDiagnostic): void {
2887
+ try {
2888
+ const reporting = this.reportProtocolViolation(diagnostic);
2889
+ if (reporting) void reporting.catch(() => undefined);
2890
+ } catch {
2891
+ // Diagnostics must never alter transport flow or cleanup.
2892
+ }
2816
2893
  }
2817
2894
 
2818
2895
  private fail(error: Error): void {
@@ -3161,6 +3238,7 @@ export class OmpRpcRuntime implements OmpRuntime {
3161
3238
  this.options.spawnProcess,
3162
3239
  this.options.terminateProcessTree,
3163
3240
  options.requestTimeoutMs ?? this.options.requestTimeoutMs,
3241
+ this.options.reportProtocolViolation,
3164
3242
  );
3165
3243
  const abort = () => void process.close().catch(() => undefined);
3166
3244
  options.signal?.addEventListener("abort", abort, { once: true });