Skip to content

Terminal: write PTY input off the async runtime - #1006

Merged
lovon-spec merged 1 commit into
mainfrom
worktree-fix+pty-write-off-runtime
Oct 22, 2026
Merged

lovon-spec merged 1 commit into
mainfrom
worktree-fix+pty-write-off-runtime

Conversation

@lovon-spec

Copy link
Copy Markdown
Collaborator

What

PtySession::write_input_by did a blocking write_all()+flush() on the PTY master fd inline on the calling thread. For the MCP terminal_write tool that thread is a tokio runtime worker. portable_pty's writer fd is blocking, so a child that stops draining its stdin (a large paste, a canonical-mode line past the tty limit, a raw-mode TUI not reading input) makes the write block indefinitely.

Why it froze the whole daemon

Diagnosed on a live hung daemon (sample(1)): a terminal_write stalled the worker that owned tokio's multi-thread IO/time driver. A raw blocking syscall is invisible to tokio, so the driver was never handed off — every tokio::time timeout stopped firing, no IO was delivered, and the entire daemon (MCP tunnel, terminals, plugin, periodic sweeps) quiesced: all other workers parked on a condvar, zero threads on kqueue. Recurred intermittently under concurrent terminal use (several agents driving terminals through the tunnel).

Full stack of the wedged worker:

web_gateway::serve_http_request → mcp_gate::handle_mcp_post
  → call_tool_by_name_as_caller → tools_terminal::terminal_write_tool
    → PtySession::write_input_by → write_all → write()   [blocked, whole sample]

The fix

The reader side was already correct — reader_loop / wait_for_child run on dedicated std::threads precisely because portable_pty's reads are blocking. The equally-blocking writer was the one left inline. This makes writes symmetric:

  • A dedicated per-session writer thread owns the Box<dyn Write> and performs the blocking write_all.
  • Callers enqueue onto a bounded SyncSender<Vec<u8>> and never block.
  • A child that stalls its stdin applies backpressure against the bounded queue (dropping input with a warning) instead of wedging a runtime worker.
  • The writer mutex is gone from the async path; the close-time EOF and the Windows DSR reply route through the same ordered channel.

Notes

No new test: a faithful repro needs a real OS PTY and a child scheduled to not drain stdin — inherently timing/OS-dependent (the kind of widened-timeout test that's a smell here). The fix is non-blocking by construction and mirrors the existing reader design. terminate() remains an unconditional kill(), so the now-async close-time EOF changes nothing (it was already best-effort — the kill fired immediately after it in the old code too).

PtySession::write_input_by did a blocking write_all()+flush() on the PTY
master fd inline on whatever thread called it. For MCP terminal_write that
thread is a tokio runtime worker. portable_pty's writer fd is blocking, so a
child that stops draining its stdin (a large paste, a canonical-mode line past
the tty limit, a raw-mode TUI not reading input) makes the write block
indefinitely.

Observed on a live daemon: a terminal_write stalled the worker that owned
tokio's multi-thread IO/time driver. A raw blocking syscall is invisible to
tokio, so the driver was never handed off — every tokio::time timeout stopped
firing and no IO was delivered, freezing the whole daemon (MCP tunnel,
terminals, plugin, periodic sweeps) with all other workers parked and zero
threads on kqueue. Recurred intermittently under concurrent terminal use.

The reader was already handled correctly — reader_loop and wait_for_child run
on dedicated std threads precisely because portable_pty's reads are blocking.
The equally-blocking writer was the one left inline. This makes writes
symmetric: a dedicated per-session writer thread owns the Box<dyn Write> and
performs the blocking write_all; callers enqueue onto a bounded SyncSender and
never block. A child that stalls its stdin now applies backpressure against the
bounded queue (dropping input, with a warning) instead of wedging a runtime
worker. The writer mutex is gone from the async path; the close-time EOF and
the Windows DSR reply route through the same ordered channel.
@lovon-spec
lovon-spec marked this pull request as ready for review October 22, 2026 03:26
@lovon-spec
lovon-spec enabled auto-merge October 22, 2026 03:26
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-22T03:26:31.217895Z
We couldn’t complete this review. Comment @codex review to try again.
972ef1f Draft marked ready
🔒 Security Review ⚠️ Failed 2026-10-22T03:26:39.055207Z
We couldn’t complete this review. Comment @codex security review to try again.
972ef1f Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

The account paying for this security review has reached its Codex usage limits. The payer can check the Codex usage dashboard. For personal accounts, using credits requires enabling “Use credits for security reviews” in Code review settings. If you do not manage the paying account, contact this repository's admins.

@lovon-spec
lovon-spec added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 4c21212 Oct 22, 2026
11 of 12 checks passed
@lovon-spec
lovon-spec deleted the worktree-fix+pty-write-off-runtime branch October 22, 2026 03:42
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.

1 participant