Skip to content

fix(codex): strip alt-screen + scrollback-erase from the codex byte stream - #116

Merged
Ark0N merged 2 commits into
Ark0N:masterfrom
aakhter:pr/cod-35-codex-polish
Jun 10, 2026
Merged

fix(codex): strip alt-screen + scrollback-erase from the codex byte stream#116
Ark0N merged 2 commits into
Ark0N:masterfrom
aakhter:pr/cod-35-codex-polish

Conversation

@aakhter

Copy link
Copy Markdown
Contributor

Problem

When running a Codex session, the conversation history disappears (and becomes unscrollable) on every tab switch / pane refresh.

Codex's TUI emits, during startup and on each repaint:

  • alternate-screen toggles — DECSET/DECRST 47 / 1047 / 1049
  • scrollback-erase — CSI 3 J
  • mouse-tracking enables — ?10001007

xterm.js faithfully obeys these: it switches to the alternate buffer (which has no scrollback), wipes the saved lines, and forwards the scroll wheel to the codex process. The net effect is that the user's prior conversation/shell output both vanishes and — even when still present in the buffer — can't be scrolled back to.

Fix

Strip just those sequences from the codex byte stream in two places, leaving the visible-viewport erases (2J / J) intact so codex can still repaint its own rows:

  • Session._handleTerminalOutput — filters the live SSE/WS stream and the persisted terminal buffer at the source, for mode === 'codex'.
  • GET /api/sessions/:id/terminal — applies the same strip (ALT_SCREEN_TOGGLE_PATTERN / ERASE_SCROLLBACK_PATTERN) to the replayed buffer, so a tab-switch replay keeps full scrollback. Mirrors the live-stream strip.

Only mode === 'codex' is affected; Claude/OpenCode/shell paths are untouched.

Tests

Adds test/codex-terminal-output.test.ts:

  • alt-screen toggles and 3J are removed; 2J / J (visible-viewport erases) are preserved
  • a ?1049h … 2J … 3J … ?1049l redraw collapses to just the visible content
  • Ctrl+L redraws and native codex prompt/status/resume-picker redraws pass through without row-repair mangling

npm run test:ci passes locally (2735 passed, 0 failures); tsc --noEmit, eslint, prettier --check, and the frontend-syntax check are all clean.

aakhterand others added 2 commits June 10, 2026 13:18
…tream
Codex's TUI emits alternate-screen toggles (DECSET/DECRST 47/1047/1049),
scrollback-erase (CSI 3 J), and mouse-tracking enables (?1000-1007) during
startup and on every repaint. xterm.js obeys them: it switches to the
scrollback-less alternate buffer, wipes saved lines, and forwards the scroll
wheel to codex — so the user's conversation history both disappears and
becomes unreachable on each tab switch / pane refresh.
Strip these sequences in two places, leaving the visible-viewport erases
(2J / J) intact so codex can still repaint its own rows:
- Session._handleTerminalOutput: filter the live SSE/WS stream and the
persisted terminal buffer at the source, for mode === 'codex'.
- GET /api/sessions/:id/terminal: apply the same strip to the replayed
buffer (ALT_SCREEN_TOGGLE_PATTERN / ERASE_SCROLLBACK_PATTERN) so a
tab-switch replay keeps full scrollback.
Adds test/codex-terminal-output.test.ts covering the strip (alt-screen and
3J removed, 2J/J preserved, Ctrl+L redraws preserved) and confirming codex
output passes through without Ink row-repair mangling.
Co-Authored-By: Saqeb Akhter <saqeb.akhter@gmail.com>
…parity on replay
Review fixes:
- Hold back a trailing partial CSI (digit-only intro, ≤7 chars) in
_handleTerminalOutput and prepend it to the next chunk. PTY chunk
boundaries are arbitrary, so '\x1b[?1049h' can arrive as '\x1b[?104' +
'9h' — the per-chunk strip misses it, xterm obeys the reassembled toggle,
and (with the matching ?1049l stripped) stays stuck in the scrollback-less
alt buffer until the next replay. Complete sequences are never held; the
carry resets with the other buffers in _resetBuffers.
- Replay path now also strips mouse-tracking enables (?1000-?1007), matching
the live strip: buffers persisted BEFORE the live strip existed can still
carry them, and a replayed ?1006h re-hijacks the scroll wheel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@Ark0NArk0N left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. The approach is right — strip at the source so neither the persisted buffer nor the live stream carries the scrollback-killing sequences, with the replay strip as backstop — and the 13 captured-stream tests are genuinely good coverage. Two gaps, both fixed in 7eda39e pushed to this branch:

1. Sequences split across PTY chunk boundaries slipped past the live strip._handleTerminalOutput filters per chunk, but chunk boundaries are arbitrary: \x1b[?1049h can arrive as \x1b[?104 + 9h. xterm's parser is streaming, so it reassembles and obeys the leaked toggle — and since the matching ?1049l (arriving intact) does get stripped, the terminal is left stuck in the scrollback-less alt buffer until the next tab-switch replay: exactly the bug this PR fixes, recurring intermittently. At ~8 bytes per sequence against ~4KB chunk boundaries the per-occurrence odds are small, but codex re-emits these on every repaint, so over a long session a leak is near-certain. Fixed with a small carry: a trailing partial digit-only CSI intro (≤7 chars) is held back and prepended to the next chunk; complete sequences are never held, and the carry resets in _resetBuffers. Two tests added.

2. Replay didn't strip mouse-tracking enables. The live strip removes ?1000?1007, but the GET /terminal replay only stripped alt-screen + 3J — so a codex buffer persisted before this PR still re-hijacks the scroll wheel on every tab switch. Added MOUSE_TRACKING_PATTERN to the replay strip for parity.

Verified in a clean worktree: tsc, eslint, prettier --check clean; full test:ci green (2737 passed, +2 new tests).

@Ark0N
Ark0N merged commit 1cf5c8c into Ark0N:masterJun 10, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@aakhter@Ark0N