Summary
Every session in every window freezes simultaneously and the app accepts no further terminal I/O. A sample shows the single process-wide reader thread (TaskNotifier) blocked for the entire capture in TokenExecutor.addTokens' backpressure semaphore, while the token executor (com.iterm2.mutation queue) is parked in whilePaused and never resumes. The main thread is healthy, so there is no beachball.
Environment
- iTerm2 3.6.11
- macOS 26.5.2 (25F84), Apple Silicon (ARM64)
- ~9h uptime at capture; physical footprint 1.2 GB (peak 2.0 GB)
- Several windows / many sessions open
Symptom
- Every session in every window stops responding to input and stops updating at the same instant. New windows spawn but hang immediately.
- No spinning beachball — the app looks alive, it just accepts no terminal I/O.
- Occurred twice in a few days. Only relaunching iTerm2 recovers it.
What the sample shows
The single process-wide reader thread is blocked for the entire 5s capture (3579/3579 samples):
Thread (TaskNotifier)
-[TaskNotifier run]
-[PTYTask processRead] → -[PTYTask readTask:length:]
-[VT100ScreenMutableState threadedReadTask:length:]
-[VT100ScreenMutableState addTokens:lengthTotal:...]
TokenExecutor.addTokens(_:lengthTotal:...)
OS_dispatch_semaphore.wait(wallTimeout:)
_dispatch_semaphore_wait_slow → semaphore_wait_trap [blocked]
The token executor (com.iterm2.mutation queue) is parked in a pause that never resumes:
DispatchQueue com.iterm2.mutation
closure #1 in TokenExecutorImpl.whilePaused(_:onExecutorQueue:)
_dispatch_semaphore_wait_slow → _dispatch_sema4_wait → semaphore_wait_trap [blocked]
Read of it: addTokens blocks on the executor's backpressure semaphore waiting for the executor to drain, but the executor is paused in whilePaused and never resumes → the semaphore is never signaled → the global reader thread is wedged, so every session freezes. Because TaskNotifier services reads for all sessions process-wide, one stuck instance freezes everything and new sessions can't get read service either.
The main thread is healthy — idle in the normal run loop (nextEventMatchingMask / mach_msg), which is why there's no beachball. Its own synchro-dance path (performSynchroDanceWithBlock → TokenExecutor.whilePaused) uses a timed wait (semaphore_timedwait_trap), whereas the stuck mutation-queue side uses an untimed semaphore_wait_trap. That asymmetry looks like a lost-wakeup race: if the main thread's timed wait expires before the executor reaches the pause point, the pause/resume handshake desyncs and the executor waits forever.
Ruled out on my end
- 0 triggers configured, single profile.
- Scrollback 1000 lines, not unlimited.
Notes
- No reliable repro yet; it appears to correlate with sustained high-volume output (e.g. streaming CLI tools) and a large number of open sessions.
Full sample output attached. Happy to run a debug/nightly build or grab additional captures if useful.