CLI/TUI stops reading stdin mid-session while PTY input backlog grows (0.130.0, multi-session)
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
codexCLI 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
codexprocess, 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:
- Run multiple
codexCLI sessions concurrently. - Let them share the same default
$CODEX_HOMEstate/log databases. - After some time, one session can become permanently unresponsive to keyboard input.
- Killing that
codexprocess 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
135bytes, then later141bytes - 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/0fd 1 -> /dev/pts/0fd 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
eventpollfd watchedtfd: 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_pwaiton that stdin-watching epoll - no thread was calling
read(fd 0) - the extra
codexthread that looked like the stdin/input reactor was stuck infutex_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:479166464byteslogs_2.sqlite-wal:102176032byteslogs_2.sqlite-shm:229376bytes
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
codexprocess 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 the0.130.0failure now in case it matches an existing known bug.
3 Comments
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:
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.
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.
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_queuewhile a healthy sibling process sat inepoll_pwait— the input task is queued behind the allocator lock and neverreaches
read(0).The oversized
logs_2.sqliteyou noticed is a symptom, not the cause: Ivacuumed 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-workerthreads sit at 0 % whilecodex-mainand thetokio-rt-workerthreads 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=1drops kernel share from ~48 % to~4 %, but a single-lane runtime blocks the UI during CPU-bound work;
2is areasonable middle ground.