can1357/oh-my-pi · error

RPC chunk received before protocol negotiation

Error message

RPC chunk received before protocol negotiation

What it means

During start(), the client reads the child's stdout. If a `rpc_chunk` frame arrives before protocol negotiation completed (protocolV2Enabled still false), the client throws because chunked framing cannot be decoded without an agreed protocol version.

Source

Thrown at packages/coding-agent/src/modes/rpc/rpc-client.ts:377

			} catch {
				// The process may already have exited.
			}
			await this.#waitForExit(child);
			for (const request of pendingRequests) request.reject(error);
		};

		// Process lines in background, intercepting the ready signal.
		const lines = readJsonl(child.stdout, this.#abortController.signal);
		void (async () => {
			for await (const line of lines) {
				if (!readySettled && isRecord(line) && line.type === "ready") {
					protocolV2Supported = supportsRpcProtocolV2(line);
					readySettled = true;
					readyResolve();
					continue;
				}
				if (isRecord(line) && line.type === "rpc_chunk" && !protocolV2Enabled)
					throw new Error("RPC chunk received before protocol negotiation");
				const decoded = frameDecoder.push(line);
				if (decoded) this.#handleLine(decoded);
			}
			// A closed stdout is terminal even if the child remains alive. Startup
			// failures are reaped by the readyPromise catch below; established
			// workers are reaped here so pending requests cannot hang indefinitely.
			if (!readySettled) {
				// Stdout can close before the exit reaper finishes draining stderr.
				// child.exited settles only after the stderr tail is complete (for
				// nonzero exits), so give it a bounded head start: the exit watcher
				// below was registered first and rejects with the real stderr text
				// instead of an empty "Stderr:" (flaked under full-suite load).
				await Promise.race([child.exited.catch(() => {}), Bun.sleep(250)]);
				if (readySettled) return;
				readySettled = true;
				readyReject(new Error(`Agent output stream ended before ready. Stderr: ${child.peekStderr()}`));
				return;
			}

View on GitHub (pinned to 9690622007)

Solutions

  1. Ensure the spawned binary is the matching/supported version that performs protocol negotiation before sending rpc_chunk frames.
  2. Check any binary-path override (env/config) points at the correct omp binary.
  3. Upgrade both sides so they agree on the protocol before chunked framing.
  4. Report a bug if both sides are at expected versions — this indicates a broken handshake order.

Example fix

// before
new Worker(incompatibleChildBinary); // sends rpc_chunk immediately
// after
const client = new RpcClient({ binPath: matchedOmpBinary });
await client.start();
Defensive patterns

Strategy: try-catch

Validate before calling

const binPath = process.env.OMP_BIN ?? defaultOmpPath;
if (!existsSync(binPath)) throw new Error("child binary missing");

Try / catch

try {
  await client.start();
} catch (err) {
  if ((err as Error).message.includes("before protocol negotiation")) {
    logger.error("child sent frames before handshake; binary version mismatch");
    // restart with a known-good binary
  } else throw err;
}

Prevention

When it happens

Trigger: The spawned child (wrong or newer binary) starts emitting rpc_chunk frames immediately instead of first responding to the negotiate_protocol handshake line.

Common situations: Version mismatch between the host and the child agent binary — the resolved binary on PATH is a different build than expected, or a custom binary override points at an incompatible client that skips negotiation.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/9ed1c19c905aa9ea. Report an issue: GitHub.