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.

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.

Appendix: 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)