Hmbown/CodeWhale · error · Error

Runtime event stream ended inside a frame

Error message

Runtime event stream ended inside a frame

What it means

With requireBoundary enabled (threadEvents uses it), the stream must end exactly on a frame boundary. If buffered text remains after the connection closes and final decoding, parseEventStream() throws, indicating a truncated or corrupt SSE stream rather than silently dropping a partial event.

Solutions

  1. Reconnect and resume the replay with { sinceSeq: lastProcessedSeq } to get the missing tail.
  2. Add retry/backoff around stream consumption for transient network drops.
  3. Check intermediary (proxy/LB) timeouts and raise them for long-lived SSE connections.
  4. Investigate runtime logs if truncation recurs — the server likely crashed mid-frame.

Example fix

// before
for await (const ev of client.threadEvents(id)) handle(ev);
// after
let since;
for (;;) {
  try {
    for await (const ev of client.threadEvents(id, { sinceSeq: since })) {
      handle(ev); since = ev.seq;
    }
    break;
  } catch (err) {
    if (!err.message.includes("ended inside a frame")) throw err;
    await sleep(500);
  }
}
Defensive patterns

Strategy: retry

Try / catch

let since;
while (true) {
  try {
    for await (const ev of client.threadEvents(id, { sinceSeq: since })) { handle(ev); since = ev.seq; }
    break;
  } catch (err) {
    if (!err.message.includes("ended inside a frame")) throw err;
    await backoff();
  }
}

Prevention

When it happens

Trigger: The runtime or an intermediary closes the TCP connection mid-frame — network drop, proxy timeout, server crash, or a runtime that writes a partial event and exits. threadEvents passes requireBoundary: true.

Common situations: Load balancer idle/short timeouts killing long SSE connections; container OOM-killed mid-write; client machine sleeping during a long replay.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/b5544304ecb2649c. Report an issue: GitHub.

Appendix: source

Thrown at npm/runtime-sdk/index.js:255

async function* parseEventStream(body, { maxFrameChars = Infinity, requireBoundary = false } = {}) {
  const decoder = new TextDecoder("utf-8", { fatal: requireBoundary });
  let buffer = "";
  for await (const chunk of body) {
    buffer += decoder.decode(chunk, { stream: true });
    let boundary;
    while ((boundary = eventStreamBoundary(buffer)) !== null) {
      if (boundary.index > maxFrameChars) throw new Error("Runtime event frame exceeds the size limit");
      const frame = buffer.slice(0, boundary.index);
      buffer = buffer.slice(boundary.index + boundary.length);
      const event = parseSseFrame(frame);
      if (event !== undefined) {
        yield event;
      }
    }
    if (buffer.length > maxFrameChars) throw new Error("Runtime event frame exceeds the size limit");
  }
  buffer += decoder.decode();
  if (requireBoundary && buffer.trim()) throw new Error("Runtime event stream ended inside a frame");
  const event = parseSseFrame(buffer);
  if (event !== undefined) {
    yield event;
  }
}

function eventStreamBoundary(buffer) {
  const lf = buffer.indexOf("\n\n");
  const crlf = buffer.indexOf("\r\n\r\n");
  if (lf < 0 && crlf < 0) {
    return null;
  }
  if (crlf >= 0 && (lf < 0 || crlf < lf)) {
    return { index: crlf, length: 4 };
  }
  return { index: lf, length: 2 };
}

View on GitHub (pinned to 433685b202)