{"record":{"id":"9ed1c19c905aa9ea","repo":"can1357/oh-my-pi","slug":"rpc-chunk-received-before-protocol-negotiation","errorCode":null,"errorMessage":"RPC chunk received before protocol negotiation","messagePattern":"RPC chunk received before protocol negotiation","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/modes/rpc/rpc-client.ts","lineNumber":377,"sourceCode":"\t\t\t} catch {\n\t\t\t\t// The process may already have exited.\n\t\t\t}\n\t\t\tawait this.#waitForExit(child);\n\t\t\tfor (const request of pendingRequests) request.reject(error);\n\t\t};\n\n\t\t// Process lines in background, intercepting the ready signal.\n\t\tconst lines = readJsonl(child.stdout, this.#abortController.signal);\n\t\tvoid (async () => {\n\t\t\tfor await (const line of lines) {\n\t\t\t\tif (!readySettled && isRecord(line) && line.type === \"ready\") {\n\t\t\t\t\tprotocolV2Supported = supportsRpcProtocolV2(line);\n\t\t\t\t\treadySettled = true;\n\t\t\t\t\treadyResolve();\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (isRecord(line) && line.type === \"rpc_chunk\" && !protocolV2Enabled)\n\t\t\t\t\tthrow new Error(\"RPC chunk received before protocol negotiation\");\n\t\t\t\tconst decoded = frameDecoder.push(line);\n\t\t\t\tif (decoded) this.#handleLine(decoded);\n\t\t\t}\n\t\t\t// A closed stdout is terminal even if the child remains alive. Startup\n\t\t\t// failures are reaped by the readyPromise catch below; established\n\t\t\t// workers are reaped here so pending requests cannot hang indefinitely.\n\t\t\tif (!readySettled) {\n\t\t\t\t// Stdout can close before the exit reaper finishes draining stderr.\n\t\t\t\t// child.exited settles only after the stderr tail is complete (for\n\t\t\t\t// nonzero exits), so give it a bounded head start: the exit watcher\n\t\t\t\t// below was registered first and rejects with the real stderr text\n\t\t\t\t// instead of an empty \"Stderr:\" (flaked under full-suite load).\n\t\t\t\tawait Promise.race([child.exited.catch(() => {}), Bun.sleep(250)]);\n\t\t\t\tif (readySettled) return;\n\t\t\t\treadySettled = true;\n\t\t\t\treadyReject(new Error(`Agent output stream ended before ready. Stderr: ${child.peekStderr()}`));\n\t\t\t\treturn;\n\t\t\t}","sourceCodeStart":359,"sourceCodeEnd":395,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/modes/rpc/rpc-client.ts#L359-L395","documentation":"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.","triggerScenarios":"The spawned child (wrong or newer binary) starts emitting rpc_chunk frames immediately instead of first responding to the negotiate_protocol handshake line.","commonSituations":"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.","solutions":["Ensure the spawned binary is the matching/supported version that performs protocol negotiation before sending rpc_chunk frames.","Check any binary-path override (env/config) points at the correct omp binary.","Upgrade both sides so they agree on the protocol before chunked framing.","Report a bug if both sides are at expected versions — this indicates a broken handshake order."],"exampleFix":"// before\nnew Worker(incompatibleChildBinary); // sends rpc_chunk immediately\n// after\nconst client = new RpcClient({ binPath: matchedOmpBinary });\nawait client.start();","handlingStrategy":"try-catch","validationCode":"const binPath = process.env.OMP_BIN ?? defaultOmpPath;\nif (!existsSync(binPath)) throw new Error(\"child binary missing\");","typeGuard":null,"tryCatchPattern":"try {\n  await client.start();\n} catch (err) {\n  if ((err as Error).message.includes(\"before protocol negotiation\")) {\n    logger.error(\"child sent frames before handshake; binary version mismatch\");\n    // restart with a known-good binary\n  } else throw err;\n}","preventionTips":["Pin the child binary version alongside the host.","Avoid overriding the binary path with arbitrary builds.","Keep host and child released from the same build.","Log child stderr to diagnose handshake-order violations."],"tags":["rpc","protocol","handshake","version-mismatch"],"backgroundTag":"protocol-version-mismatch","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}