TUI: one failed color requery permanently disables OSC 10/11 re-detection (poisoned cache in requery_default_colors)

Open 💬 0 comments Opened Aug 6, 2026 by lightninglu10

What version of Codex is running?

codex-cli 0.146.0 (macOS, arm64)

What issue are you seeing?

A single failed terminal-color requery permanently disables OSC 10/11 color re-detection for the rest of the process lifetime, leaving the composer/input colors stale forever (e.g. after the OS switches between light and dark appearance). Only restarting Codex recovers it.

The TUI re-detects terminal default colors on Event::FocusGained via terminal_palette::requery_default_colors(). In codex-rs/tui/src/terminal_palette.rs (tag rust-v0.146.0):

pub(super) fn requery_default_colors() {
    if let Ok(mut cache) = default_colors_cache().lock() {
        // Don't try to refresh if the cache is already attempted and failed.
        if cache.attempted && cache.value.is_none() {
            return;
        }

        let fg = query_foreground_color().ok().flatten().and_then(color_to_tuple);
        let bg = query_background_color().ok().flatten().and_then(color_to_tuple);
        cache.value = fg.zip(bg).map(|(fg, bg)| DefaultColors { fg, bg });
        cache.attempted = true;
    }
}

Two problems compound here:

  1. If either crossterm query fails or times out once (for example a focus event delivered while the TUI is busy and the reply misses the read deadline), cache.value = fg.zip(bg) overwrites a previously good cached value with None.
  2. The guard at the top then treats that state as "attempted and failed" and short-circuits every future requery permanently. No later FocusGained can ever repair it.

So one unlucky requery poisons the cache for the life of the process, even in a terminal that answers every OSC 10/11 query correctly.

Observed behavior

Host terminal: xterm.js-based terminal whose PTY layer answers OSC 10/11 queries in well under 1 ms (instrumented). One long-running Codex process:

  • Answered color queries continuously after startup (the host counted 1178 queries, 1178 answers).
  • After one requery at ~5 s of process age, it never emitted another OSC 10/11 query.
  • Subsequent focus reports (CSI I), including synthetic ones injected directly into the PTY hours later, produce no requery at all — consistent with the short-circuit above.
  • Meanwhile sibling Codex processes of the same version in the same terminal (not yet poisoned) requery and repaint correctly on the same CSI I signal.

Net effect for users: after a macOS auto light/dark switch, the composer input background stays in the old theme's color and nothing short of restarting Codex fixes it.

Reproduction sketch

  1. Run Codex in a terminal that supports OSC 10/11 and focus reporting (mode 1004).
  2. Cause exactly one requery to miss its deadline (e.g. deliver a focus event while the TUI is saturated, or briefly delay the OSC reply past the crossterm timeout).
  3. Change the terminal background color and focus the terminal again.
  4. Codex never re-queries; colors remain stale until restart.

Suggested fix

In the failure path of requery_default_colors():

  • Keep the previous cache.value when the fresh query returns None (stale colors are strictly better than none, and the next FocusGained can retry), and/or
  • Don't let a failed requery latch the attempted && value.is_none() short-circuit — reserve that short-circuit for the bounded startup probe, so focus-driven requeries remain retryable.

Either is a small change and makes color re-detection self-healing.

Related: #5107 (unsolicited OSC 11 replies are mis-parsed into the composer), which rules out the host-terminal workaround of proactively pushing color updates.

View original on GitHub ↗