CLI/TUI stops reading stdin mid-session while PTY input backlog grows (0.130.0, multi-session)

Open 💬 3 comments Opened May 20, 2026 by felix021

Summary

I hit a Codex CLI/TUI freeze twice where one active session stopped responding to keyboard input entirely, but the underlying PTY continued receiving bytes. Killing the frozen codex process immediately returned control to the shell, which then displayed/consumed the buffered input.

I observed this on codex-cli 0.130.0. I have now upgraded to 0.132.0, but I have not yet confirmed whether the problem still occurs there.

This may be related to #20213, but I am not sure the root cause is the same.

Environment

  • Variant: Codex CLI / TUI
  • Observed version: 0.130.0
  • Current installed version after upgrade: 0.132.0 (not yet validated against this bug)
  • Platform: Linux x86_64
  • Install method: global npm install
  • Multiple concurrent codex CLI instances sharing the default ~/.codex

What I saw

One codex TUI session froze and stopped reacting to input. The shell underneath was not dead:

  • typing keys into the frozen TUI produced no visible response
  • after killing the frozen codex process, the underlying shell immediately received/displayed the buffered keystrokes

This is the second time I have run into this issue.

Reproduction

I do not have a minimal deterministic repro yet, but the common factors were:

  1. Run multiple codex CLI sessions concurrently.
  2. Let them share the same default $CODEX_HOME state/log databases.
  3. After some time, one session can become permanently unresponsive to keyboard input.
  4. Killing that codex process returns to the shell, and the previously typed input is still there.

Evidence collected from the frozen instance

I inspected the frozen process locally using strace, /proc, and PTY queue checks.

1. Input reached the PTY, but Codex stopped reading it

For the frozen session's PTY:

  • unread input backlog was present and stable
  • it stayed at 135 bytes, then later 141 bytes
  • the backlog did not drain while the TUI was frozen

This strongly suggests terminal input reached the PTY but was no longer being consumed by the codex process.

2. Frozen process had stdin/stdout/stderr attached to the right PTY

For the frozen codex process:

  • fd 0 -> /dev/pts/0
  • fd 1 -> /dev/pts/0
  • fd 2 -> /dev/pts/0

3. During the freeze, there was no read(fd 0)

A short strace window on the frozen codex process showed:

  • no read(... </dev/pts/0> ...)
  • no TUI writes to that PTY during the same window

Meanwhile the unread PTY backlog remained nonzero.

4. Codex had an epoll instance watching stdin, but no thread was actually blocked on that epoll anymore

From /proc/<pid>/fdinfo, the frozen process had an epoll instance watching stdin:

  • one eventpoll fd watched tfd: 0 (stdin / the PTY), plus one socket and one eventfd

But at freeze time:

  • no thread in that process was blocked in epoll_wait/epoll_pwait on that stdin-watching epoll
  • no thread was calling read(fd 0)
  • the extra codex thread that looked like the stdin/input reactor was stuck in futex_wait_queue

For comparison, a healthy concurrent codex instance on the same machine still had a dedicated codex thread blocked in epoll_pwait(...) on its stdin-watching epoll.

That difference makes this look like the input reactor for the frozen TUI dropped out of its epoll loop.

Potential correlation with shared SQLite state/logs

I also noticed the shared local databases were fairly large at the time:

  • logs_2.sqlite: 479166464 bytes
  • logs_2.sqlite-wal: 102176032 bytes
  • logs_2.sqlite-shm: 229376 bytes

Because this happened with multiple concurrent Codex sessions sharing the same $CODEX_HOME, this might be related to the SQLite contention/freeze class described in #20213.

However, I do not have proof that SQLite contention is the direct cause of this particular freeze. What I can say confidently is:

  • input reached the PTY
  • the frozen codex process stopped reading stdin
  • its stdin-watching epoll/reactor path appeared to be inactive

Additional notes

  • This was not a browser or terminal transport issue on my side.
  • After killing codex, the shell immediately behaved normally and consumed the buffered input.
  • I will watch whether this still reproduces on 0.132.0, but I wanted to file the evidence from the 0.130.0 failure now in case it matches an existing known bug.

View original on GitHub ↗

3 Comments

Keesan12 · 3 months ago

This sounds like a real split-brain between the UI/session lifecycle and the PTY input drain, not just a generic stdin glitch.

The thing I would want instrumented here is a per-session receipt that records:

  • bytes accepted from the UI
  • bytes actually written into the PTY
  • backlog size over time
  • whether the consumer task was detached, blocked, or cancelled
  • the exact state transition when reads stopped

When the backlog keeps growing while input stops being consumed, it usually means the intake path is still "alive" but the downstream reader or owning session lost authority. That is a better seam to debug than adding more retries around stdin writes.

mehulparmariitr · 1 month ago

Implementation update for the overlapping blocked-input path:

The traces behind #24527 directly paired multi-second synchronous terminal-size calls with already-readable input waiting before Crossterm mapping. The size commit removes backend size queries from ordinary draws and carries resize dimensions through the render pipeline.

That provides one concrete mechanism capable of producing PTY backlog while the TUI task is blocked. It does not claim to explain every complete mid-session input freeze reported here. Direct upstream PR creation is invitation-gated; maintainers can invite the branch or port the commits.

0xmikko · 37 minutes ago

Likely root cause for this. The Linux release is a static musl build with no
custom global allocator, and musl's allocator serialises concurrent allocation
into a futex storm on many-core hosts. That's why your input reactor thread was
stuck in futex_wait_queue while a healthy sibling process sat in
epoll_pwait — the input task is queued behind the allocator lock and never
reaches read(0).

The oversized logs_2.sqlite you noticed is a symptom, not the cause: I
vacuumed everything (312 MB → 288 KB, WAL 86 MB → 1.7 MB) and a freshly started
session was pegged again within four minutes. During a stall the
sqlx-sqlite-worker threads sit at 0 % while codex-main and the
tokio-rt-worker threads burn 90 % each.

Filed separately with a standalone reproduction (~40 lines of Rust, no Codex
involved) and two verified fixes: #41312

Quick numbers from that repro, same source built four ways, 8 threads, 5 s:

| build | useful work | kernel | share in kernel |
| --- | --- | --- | --- |
| musl | 7.20 s | 22.70 s | 75 % |
| musl + mimalloc | 39.98 s | 0.00 s | 0 % |
| glibc | 39.98 s | 0.00 s | 0 % |

Workaround meanwhile: TOKIO_WORKER_THREADS=1 drops kernel share from ~48 % to
~4 %, but a single-lane runtime blocks the UI during CPU-bound work; 2 is a
reasonable middle ground.