Skip to content

fix(agent): restore raw mode#367

Open
fry69 wants to merge 2 commits into
antirez:mainfrom
fry69:fry69/fix-tmux
Open

fix(agent): restore raw mode#367
fry69 wants to merge 2 commits into
antirez:mainfrom
fry69:fry69/fix-tmux

Conversation

@fry69

@fry69 fry69 commented Jun 9, 2026

Copy link
Copy Markdown

Fix terminal freeze after bash child processes

Problem

Running any bash command via the agent's bash tool call (e.g., tmux list-sessions) renders the terminal unresponsive to keyboard input. Typed characters don't appear, and the only recovery is to kill the agent process and press Ctrl-L.

After killing the agent, the terminal is compressed to the bottom line - the scroll-region escape sequences the agent left behind are still active - confirming the terminal mode was corrupted, not just the agent's input handling.

Root cause

When the agent spawns sh -c "<cmd>", only stdout/stderr are redirected to a pipe. stdin is left as the original terminal fd. The shell detects stdin is a tty and calls tcsetattr() to set cooked mode (ICANON, ECHO, etc.), overriding linenoise's raw mode. After the child exits the terminal stays cooked. linenoise never re-applies raw mode because enableRawMode() is called once at editor startup.

In cooked mode the terminal driver buffers input line-by-line. The agent's non-blocking read() sees EAGAIN because no complete line is available until Enter is pressed. The user sees nothing happen.

Fix (two layers)

  1. Redirect child stdin to /dev/null (ds4_agent.c:agent_bash_start)

The child now opens /dev/null and dup2's it onto STDIN_FILENO before exec. This prevents the shell from ever calling tcsetattr() on the controlling terminal.

Safety analysis: The bash tool is a non-interactive command runner. The agent never writes to the child's stdin – it only reads stdout/stderr via a pipe. The model supplies all data through command arguments, file writes, or shell pipelines (printf ... | nc host port). Commands that expect interactive terminal input (e.g., ssh, sudo, git commit without -m) never worked because the agent has no mechanism to forward keystrokes to the child. The redirect only breaks things that were already broken, and it fixes the terminal freeze for everything else.

  1. Raw-mode restoration safety net (linenoise.c, ds4_agent.c)

A new public function linenoiseRestoreRawMode() re-applies the same raw-mode flags that enableRawMode() sets. It re-fetches orig_termios via tcgetattr() so it works even if a child process changed the terminal attributes.

The worker thread sets a raw_mode_needs_restore flag (under the worker mutex) in agent_bash_finalize() whenever a bash job finishes. The UI thread checks this flag at the top of its event loop and calls linenoiseRestoreRawMode() if needed. This provides defense-in-depth for any child process that might still touch /dev/tty despite the stdin redirect (e.g., ssh opening /dev/tty directly).

Testing

Before the fix, running tmux list-sessions inside the agent froze the terminal 100% of the time. After the fix, the command runs normally and the terminal stays fully responsive. Repeated testing with various bash commands (including long-running processes and commands that produce ANSI escape sequences) shows no regression.

fix #366

Update: More fixes.

Fix 1 – agent_prompt_yes_no_ex (non‑blocking stdin)

editor_start sets stdin to O_NONBLOCK. The prompt function used fgets(stdin) which immediately returns NULL (EAGAIN) when no data is available, causing the function to treat it as "no" and exit without waiting for the user.

Change: Save the O_NONBLOCK flag, temporarily disable it before fgets, restore it after.

Fix 2 – /quit handler (terminal restoration before exit)

The handler called editor_restore_terminal_layout (scroll‑region reset) but never editor_stop, which is the function that restores stdin flags and disables raw mode. When AGENT_EXIT_NOW was taken (user declines save), exit(0) was called while raw mode was still active and stdin was non‑blocking, leaving the terminal unusable.

Change: Call editor_stop before prompting so that raw mode and non‑blocking are already disabled when we prompt or exit. The AGENT_EXIT_CANCEL case now restarts the editor instead of leaving it half‑stopped.

Fix 3 – linenoiseRestoreRawMode (corrupted orig_termios)

This is the root cause of the garbled /help output. The function unconditionally overwrote the global orig_termios with the current terminal attributes via tcgetattr. While the editor is active, those are the raw‑mode settings (with OPOST cleared). Later, when editor_stop calls disableRawMode, it restores that corrupted orig_termios — the terminal stays in raw mode. Then put outputs \n without carriage return, staggering the lines.

Change: Only update orig_termios when rawmode == 0 (i.e., a child process actually changed the terminal to cooked). If we are already in raw mode, keep the original orig_termios intact so disableRawMode can still restore proper cooked mode.

@fry69 fry69 marked this pull request as draft June 9, 2026 02:38
Three fixes for regressions introduced by the raw-mode restoration
commit (ce5b70b):

1. agent_prompt_yes_no_ex: handle non-blocking stdin
   editor_start sets O_NONBLOCK on stdin; fgets() returned NULL
   immediately (EAGAIN) causing the save prompt to exit without
   waiting for user input.  Temporarily switch to blocking mode
   around the fgets call.

2. /quit handler: stop editor before prompting
   editor_restore_terminal_layout resets the scroll region but does
   not disable raw mode or restore stdin flags.  Call editor_stop
   first so that raw mode is off and stdin is blocking when we
   prompt the user.  Handle AGENT_EXIT_CANCEL by restarting the
   editor instead of leaving it half-stopped.

3. linenoiseRestoreRawMode: do not corrupt orig_termios
   The function unconditionally overwrote orig_termios with the
   current terminal attributes via tcgetattr.  While the editor is
   active those are the raw-mode settings (OPOST cleared).  Later,
   disableRawMode restores this corrupted orig_termios, leaving the
   terminal in raw mode after editor_stop.  puts output then lacks
   carriage returns, causing garbled display (e.g. /help).
   Only update orig_termios when rawmode == 0 (child process
   changed terminal to cooked).
@fry69 fry69 marked this pull request as ready for review June 9, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ds4-agent freezes after running tmux list-sessions

1 participant