{"record":{"id":"d425b06f7badd5e9","repo":"can1357/oh-my-pi","slug":"rpc-chunk-sequence-length-mismatch","errorCode":null,"errorMessage":"rpc chunk sequence length mismatch","messagePattern":"rpc chunk sequence length mismatch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/modes/rpc/rpc-frame.ts","lineNumber":181,"sourceCode":"\n\t\tif (!this.#pending) {\n\t\t\tif (index !== 0) throw new Error(\"rpc chunk sequence must start at index 0\");\n\t\t\tthis.#pending = { chunkId, count, byteLength, nextIndex: 0, chunks: [], receivedBytes: 0 };\n\t\t}\n\t\tconst pending = this.#pending;\n\t\tif (\n\t\t\tpending.chunkId !== chunkId ||\n\t\t\tpending.count !== count ||\n\t\t\tpending.byteLength !== byteLength ||\n\t\t\tpending.nextIndex !== index\n\t\t)\n\t\t\tthrow new Error(\"rpc chunk sequence mismatch\");\n\t\tpending.chunks.push(bytes);\n\t\tpending.receivedBytes += bytes.byteLength;\n\t\tpending.nextIndex++;\n\t\tif (pending.receivedBytes > pending.byteLength) throw new Error(\"rpc chunk sequence exceeds declared length\");\n\t\tif (pending.nextIndex < pending.count) return undefined;\n\t\tif (pending.receivedBytes !== pending.byteLength) throw new Error(\"rpc chunk sequence length mismatch\");\n\n\t\tthis.#pending = undefined;\n\t\tconst decoded = new TextDecoder(\"utf-8\", { fatal: true }).decode(Buffer.concat(pending.chunks));\n\t\tconst frame: unknown = JSON.parse(decoded);\n\t\tif (!isRecord(frame)) throw new Error(\"rpc frame must be an object\");\n\t\treturn frame;\n\t}\n}\n\nfunction compactTerminalFrame(\n\tframe: object,\n\tstreamedMessageCount: number,\n\tstreamedMessages?: readonly unknown[],\n): object {\n\tif (!isRecord(frame) || frame.type !== \"agent_end\" || !Array.isArray(frame.messages)) return frame;\n\tlet streamed = Number.isSafeInteger(streamedMessageCount)\n\t\t? Math.min(Math.max(0, streamedMessageCount), frame.messages.length)\n\t\t: 0;","sourceCodeStart":163,"sourceCodeEnd":199,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/modes/rpc/rpc-frame.ts#L163-L199","documentation":"RpcFrameDecoder.push() reassembles protocol-v2 rpc_chunk frames. After the final expected chunk arrives, the accumulated byte count must exactly equal the declared frame byteLength; this error means the chunks ended before or after declaring they would, i.e. the chunk stream's metadata (count/byteLength) is inconsistent with its payloads.","triggerScenarios":"Pushing the last chunk (index === count-1) of an rpc_chunk sequence where sum of decoded base64 payload sizes !== byteLength declared in the chunk metadata — e.g. a chunk was dropped and replaced, or an encoder emitted fewer/shorter chunks than advertised.","commonSituations":"A custom or hand-rolled RPC client sends base64 payloads that don't total the declared byteLength; a proxy/truncating pipe drops or corrupts one chunk line; a protocol-version mismatch between encoder (v1 frames into a v2 decoder or vice versa) leaves a stale pending sequence.","solutions":["Fix the sender so each rpc_chunk's data decodes to exactly the advertised sizes: byteLength must equal the full original JSON payload's byte count and count must equal ceil(byteLength / 256KiB).","Verify both sides negotiate the same RPC protocol version (1 or 2); a v1 plain frame arriving mid-chunk-sequence throws 'rpc chunk sequence interrupted' instead, so ensure no frames are interleaved.","Discard the RpcFrameDecoder instance (or its #pending state) after any chunk error and restart reassembly from index 0 — the decoder is stateful and cannot recover mid-sequence.","If messages are truncated by a pipe/proxy, raise the transport's line-size limit or switch to protocol v2 chunking on the sender instead of one giant JSONL line."],"exampleFix":"// before (sender splits payload incorrectly)\nconst chunks = payload.match(/.{1,200000}/gs) ?? [];\n// after\nconst bytes = Buffer.from(json, 'utf8');\nconst RPC_CHUNK_PAYLOAD_BYTES = 256 * 1024;\nconst count = Math.ceil(bytes.byteLength / RPC_CHUNK_PAYLOAD_BYTES);\nfor (let i = 0; i < count; i++) {\n  data: bytes.subarray(i * RPC_CHUNK_PAYLOAD_BYTES, (i + 1) * RPC_CHUNK_PAYLOAD_BYTES).toString('base64')\n}","handlingStrategy":"validation","validationCode":"function isValidChunk(frame: unknown): boolean {\n  if (typeof frame !== \"object\" || frame === null) return false;\n  const f = frame as Record<string, unknown>;\n  return (\n    f.type === \"rpc_chunk\" &&\n    typeof f.chunkId === \"string\" && f.chunkId.length > 0 &&\n    Number.isSafeInteger(f.index) && Number.isSafeInteger(f.count) && Number.isSafeInteger(f.byteLength) &&\n    f.index >= 0 && f.index < f.count &&\n    typeof f.data === \"string\"\n  );\n}","typeGuard":"function isRpcChunkFrame(v: unknown): v is RpcChunkFrame {\n  return typeof v === \"object\" && v !== null && (v as Record<string, unknown>).type === \"rpc_chunk\";\n}","tryCatchPattern":"try {\n  const out = decoder.push(line);\n  if (out) handleFrame(out);\n} catch (err) {\n  decoder = new RpcFrameDecoder(); // discard broken pending state\n  logger.error(\"chunk reassembly failed, restarting stream\", { error: err });\n}","preventionTips":["Use RpcFrameEncoder on the sender so count/byteLength always match the actual chunks","Never interleave plain frames while a chunk sequence is in flight","Negotiate identical protocol versions on both ends","Recreate the decoder after any chunk error — pending state cannot resume"],"tags":["rpc","protocol","chunking","serialization"],"backgroundTag":"rpc-chunk-reassembly-mismatch","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}