Skip to content

P2: scrollback buffer trim cuts on a raw byte offset and splits multi-byte UTF-8 characters #774

Description

@RonenMars

What happened

Observed on real production usage: two sessions on iPhone, both mid-"Reconnecting..." (WS connection lost, scrollback being replayed on reconnect), showing badly garbled terminal output — long chains of â, ·, ââââ... in place of what should be Claude Code's UI borders/box-drawing characters and punctuation, while plain ASCII text (e.g. "Hey! What are you working on?") rendered fine.

Confirmed defect

src/pty-manager.ts:793-796 (same shape in src/codex-pty-runner.ts:762-765) trims the session's output/scrollback buffer with a raw byte-offset cut when it exceeds OUTPUT_BUFFER_MAX:

session.outputBuffer = session.outputBuffer.subarray(
  session.outputBuffer.length - OUTPUT_BUFFER_MAX,
  ...
);

This slices on a byte count with no awareness of UTF-8 character boundaries. Claude Code's terminal UI uses 3-byte UTF-8 box-drawing characters constantly for its borders/decorations — a byte-offset cut has a real chance of landing mid-character, leaving an orphaned partial multi-byte sequence at the start of the buffer.

That buffer is later decoded wholesale via .toString("utf-8") (pty-manager.ts:662, codex-pty-runner.ts:667), and per pty-shared.ts:22-23, this scrollback buffer is exactly what subscribe_session replays to a (re)connecting client — "as much scrollback as the session [holds]."

Why this fits the observation

Both garbled sessions showed "Reconnecting... Connection lost — the content below may be stale" at the exact moment the corruption appeared — reconnect is precisely when this trimmed buffer gets replayed wholesale to the client. The garbage is concentrated in long runs, consistent with a corrupted buffer start propagating through repeated box-drawing-heavy UI redraws (spinner/border lines), while plain ASCII chat text nearby rendered correctly.

Honest caveat — may not be the whole story

The specific visual pattern (â, ·, long ââââ chains) is the classic signature of a UTF-8-bytes-decoded-as-Latin-1 mismatch, which is a slightly different failure shape than what a mid-character truncation typically produces (usually U+FFFD replacement characters, one per orphaned byte). It's possible there's an additional encoding mismatch somewhere in the replay/render path (server-side serialization, WS frame handling, or the mobile client's terminal renderer) compounding this, not yet traced. Filing with the confirmed defect; the exact end-to-end mechanism producing this specific character pattern may need further investigation.

Suggested fix

When trimming outputBuffer to OUTPUT_BUFFER_MAX, cut at a UTF-8-safe boundary rather than an arbitrary byte offset — e.g. scan forward from the byte-offset cut point to the start of the next valid UTF-8 character (skip over any continuation bytes, 0x80-0xBF, at the very start of the trimmed subarray) before assigning it back to outputBuffer. Apply the same fix to both pty-manager.ts and codex-pty-runner.ts (identical bug, duplicated logic).

Verification

  • Add a test that fills outputBuffer past OUTPUT_BUFFER_MAX with content deliberately engineered so the trim point lands mid-multi-byte-character (e.g. many 3-byte box-drawing characters back to back), then asserts the trimmed buffer's .toString("utf-8") contains no U+FFFD/mojibake artifacts at its start.
  • Confirm existing scrollback-replay tests (if any target subscribe_session) still pass unchanged.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Soon, but does not gate a releasebugSomething isn't workinguxUser-facing interaction and polish (CLI, API ergonomics)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions