can1357/oh-my-pi · error

rpc chunk sequence length mismatch

Error message

rpc chunk sequence length mismatch

What it means

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.

Source

Thrown at packages/coding-agent/src/modes/rpc/rpc-frame.ts:181

		if (!this.#pending) {
			if (index !== 0) throw new Error("rpc chunk sequence must start at index 0");
			this.#pending = { chunkId, count, byteLength, nextIndex: 0, chunks: [], receivedBytes: 0 };
		}
		const pending = this.#pending;
		if (
			pending.chunkId !== chunkId ||
			pending.count !== count ||
			pending.byteLength !== byteLength ||
			pending.nextIndex !== index
		)
			throw new Error("rpc chunk sequence mismatch");
		pending.chunks.push(bytes);
		pending.receivedBytes += bytes.byteLength;
		pending.nextIndex++;
		if (pending.receivedBytes > pending.byteLength) throw new Error("rpc chunk sequence exceeds declared length");
		if (pending.nextIndex < pending.count) return undefined;
		if (pending.receivedBytes !== pending.byteLength) throw new Error("rpc chunk sequence length mismatch");

		this.#pending = undefined;
		const decoded = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(pending.chunks));
		const frame: unknown = JSON.parse(decoded);
		if (!isRecord(frame)) throw new Error("rpc frame must be an object");
		return frame;
	}
}

function compactTerminalFrame(
	frame: object,
	streamedMessageCount: number,
	streamedMessages?: readonly unknown[],
): object {
	if (!isRecord(frame) || frame.type !== "agent_end" || !Array.isArray(frame.messages)) return frame;
	let streamed = Number.isSafeInteger(streamedMessageCount)
		? Math.min(Math.max(0, streamedMessageCount), frame.messages.length)
		: 0;

View on GitHub (pinned to 9690622007)

Solutions

  1. 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).
  2. 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.
  3. 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.
  4. 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.

Example fix

// before (sender splits payload incorrectly)
const chunks = payload.match(/.{1,200000}/gs) ?? [];
// after
const bytes = Buffer.from(json, 'utf8');
const RPC_CHUNK_PAYLOAD_BYTES = 256 * 1024;
const count = Math.ceil(bytes.byteLength / RPC_CHUNK_PAYLOAD_BYTES);
for (let i = 0; i < count; i++) {
  data: bytes.subarray(i * RPC_CHUNK_PAYLOAD_BYTES, (i + 1) * RPC_CHUNK_PAYLOAD_BYTES).toString('base64')
}
Defensive patterns

Strategy: validation

Validate before calling

function isValidChunk(frame: unknown): boolean {
  if (typeof frame !== "object" || frame === null) return false;
  const f = frame as Record<string, unknown>;
  return (
    f.type === "rpc_chunk" &&
    typeof f.chunkId === "string" && f.chunkId.length > 0 &&
    Number.isSafeInteger(f.index) && Number.isSafeInteger(f.count) && Number.isSafeInteger(f.byteLength) &&
    f.index >= 0 && f.index < f.count &&
    typeof f.data === "string"
  );
}

Type guard

function isRpcChunkFrame(v: unknown): v is RpcChunkFrame {
  return typeof v === "object" && v !== null && (v as Record<string, unknown>).type === "rpc_chunk";
}

Try / catch

try {
  const out = decoder.push(line);
  if (out) handleFrame(out);
} catch (err) {
  decoder = new RpcFrameDecoder(); // discard broken pending state
  logger.error("chunk reassembly failed, restarting stream", { error: err });
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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