[PR #12873] fix: prevent TUI freeze from abort signal not propagating in processor #14414

Closed
opened 2026-02-16 18:19:12 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/anomalyco/opencode/pull/12873

State: closed
Merged: No


Summary

  • Fixes a race condition in SessionProcessor.process() where an abort signal during a retryable error (e.g. ECONNRESET from connection teardown) causes a tight busy-wait loop
  • SessionRetry.sleep() rejects immediately when the signal is already aborted, but the rejection is silently swallowed by .catch(() => {}), causing the retry continue to fire without checking abort state
  • The processor re-enters LLM.stream() with an already-aborted signal, which fails again, creating an infinite spin that manifests as futex(FUTEX_WAKE_PRIVATE) busy-wait and freezes the TUI

Changes

  • Check input.abort.aborted at the top of the processor's while(true) loop before starting a new stream
  • Skip retry logic entirely when the abort signal has already fired
  • Check abort state after SessionRetry.sleep() returns, in case abort fired during the delay
  • Add explicit input.abort.aborted check in the return path so the prompt loop receives "stop" instead of "continue"
  • Set AbortedError instead of the underlying network error when the abort signal caused the failure

Test plan

  • Start a session with a long-running LLM request
  • Press Escape twice to abort mid-stream
  • Verify the TUI returns to idle state without freezing
  • Verify no futex(FUTEX_WAKE_PRIVATE) busy-wait in strace output
  • Test abort during retry backoff (trigger rate limit, then abort)
  • Verify the session status event (session.status: idle) reaches the TUI rendering thread

Fixes #12834

**Original Pull Request:** https://github.com/anomalyco/opencode/pull/12873 **State:** closed **Merged:** No --- ## Summary - Fixes a race condition in `SessionProcessor.process()` where an abort signal during a retryable error (e.g. `ECONNRESET` from connection teardown) causes a tight busy-wait loop - `SessionRetry.sleep()` rejects immediately when the signal is already aborted, but the rejection is silently swallowed by `.catch(() => {})`, causing the retry `continue` to fire without checking abort state - The processor re-enters `LLM.stream()` with an already-aborted signal, which fails again, creating an infinite spin that manifests as `futex(FUTEX_WAKE_PRIVATE)` busy-wait and freezes the TUI ### Changes - Check `input.abort.aborted` at the top of the processor's `while(true)` loop before starting a new stream - Skip retry logic entirely when the abort signal has already fired - Check abort state after `SessionRetry.sleep()` returns, in case abort fired during the delay - Add explicit `input.abort.aborted` check in the return path so the prompt loop receives `"stop"` instead of `"continue"` - Set `AbortedError` instead of the underlying network error when the abort signal caused the failure ## Test plan - [ ] Start a session with a long-running LLM request - [ ] Press Escape twice to abort mid-stream - [ ] Verify the TUI returns to idle state without freezing - [ ] Verify no `futex(FUTEX_WAKE_PRIVATE)` busy-wait in `strace` output - [ ] Test abort during retry backoff (trigger rate limit, then abort) - [ ] Verify the session status event (`session.status: idle`) reaches the TUI rendering thread Fixes #12834
yindo added the pull-request label 2026-02-16 18:19:12 -05:00
yindo closed this issue 2026-02-16 18:19:12 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#14414