Skip to content

TUI can permanently lose transcript scrollback when resize lands during replay/stream (cells become incomplete; later resizes can't restore) #37987

Open

Description

@hidenori-endo

Summary

The codex TUI can permanently lose transcript scrollback when a terminal resize lands while a resumed session is still replaying (or while an agent response is streaming). Resize reflow clears terminal scrollback and re-emits from transcript_cells; when the cells are incomplete at that moment (replay still in progress, stream cells not yet consolidated), the re-emitted scrollback is shorter than the transcript — and unlike #30745, the missing rows are never restored by later resizes, because the rebuild source itself (transcript_cells) is missing them.

Environment

  • codex 0.147.0 TUI (main screen; runs inside a headless terminal host that owns the PTY)
  • Terminal host: herdr 0.8.0 (ghostty-vt based), which resizes the shared PTY whenever the attached client's viewport changes
  • Client: mobile terminal app, 56 columns; toggling the on-screen keyboard changes the viewport between 17 and 45 rows, so resizes arrive repeatedly during a session

Reproduction

  1. codex resume <session-id> for a long session (observed: 4605-line session JSONL).
  2. Confirm the working directory prompt with Enter; the replay starts.
  3. Immediately trigger a resize while the replay/stream is still settling (in my setup: split/close the pane, equivalent to the client viewport changing mid-session).
  4. After the prompt returns, scrollback contains only part of the transcript:
    • observed scroll + viewport total of 76 rows → 51 rows after one resize round-trip, stable at 51 afterwards.
    • The 25 missing rows never come back; repeated resizes re-emit the same 51 rows.
  5. pane read --source recent on the surviving scrollback shows the welcome header, an interrupted-conversation notice and the prompt — no conversation history at all.

Additional observation: resuming the same session ID from two TUI instances at once makes the replay entirely empty — the resumed view shows only the welcome header and prompt, with zero history, until all instances are closed and a single instance resumes the session again.

Expected

  • A resize that lands mid-replay or mid-stream should not permanently shrink scrollback: either the clear+re-emit should wait until transcript_cells is complete, or it should be skipped entirely when cells are known to be incomplete.
  • (Secondary) Resuming a session ID that is already open in another instance should not silently produce an empty replay.

Actual

In tui/src/app/resize_reflow.rs, reflow_transcript_now() returns early when transcript_cells.is_empty() (after clearing pending history lines); when cells are merely partial, the re-emit is exactly the partial set, and every later resize re-emits the same partial set. The initial replay path (begin_initial_history_replay_buffer / finish_initial_history_replay_buffer) buffers display lines separately from cells, so a resize that lands between replay inserts and the flush can commit only a prefix.

Notes

Related issues:

Activity

  1. added
    bugSomething isn't working
    CLIIssues related to the Codex CLI
    TUIIssues related to the terminal user interface: text input, menus and dialogs, and terminal display
    sessionIssues involving session (thread) management, resuming, forking, naming, archiving
    on Aug 11, 2026
  2. jdcodes1 commented on Aug 18, 2026

    @jdcodes1

    Confirmed the guard gap on main (1f41cc5): reflow_transcript_now checks only transcript_cells.is_empty() before clearing the terminal and re-emitting — it never consults initial_history_replay_buffer, the very flag that marks "replay still in progress" (https://github.com/openai/codex/blob/1f41cc5d92/codex-rs/tui/src/app/resize_reflow.rs#L461-L486, buffer machinery at #L124-L160). So a resize that lands inside the replay window purges scrollback (clear_terminal_for_resize_replay) and rebuilds from a partial cell set; since the rebuild source is the partial set, every later resize reproduces the same truncation — permanently, exactly as you observed. Same non-atomic clear+re-emit family as #30745/#37777, but caught at the one stage where the loss is unrecoverable.

    Fix shape: when initial_history_replay_buffer.is_some() (and, for the streaming variant, while stream cells are unconsolidated), don't reflow now — record the pending target width and run the reflow from finish_initial_history_replay_buffer once the cells are complete. That converts the race into a slightly delayed reflow with no data loss.

    The two-instance empty replay is your issue's second bug and matches #38144/#38297 (writer-lock held by the other instance → replay reads an empty projection); worth keeping both linked but tracked separately.

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

    CLIIssues related to the Codex CLITUIIssues related to the terminal user interface: text input, menus and dialogs, and terminal displaybugSomething isn't workingsessionIssues involving session (thread) management, resuming, forking, naming, archiving

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions