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.
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 insrc/codex-pty-runner.ts:762-765) trims the session's output/scrollback buffer with a raw byte-offset cut when it exceedsOUTPUT_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 perpty-shared.ts:22-23, this scrollback buffer is exactly whatsubscribe_sessionreplays 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
outputBuffertoOUTPUT_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 tooutputBuffer. Apply the same fix to bothpty-manager.tsandcodex-pty-runner.ts(identical bug, duplicated logic).Verification
outputBufferpastOUTPUT_BUFFER_MAXwith 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.subscribe_session) still pass unchanged.