Skip to content

gondolin attach (pty mode) silently stops delivering stdin to the attached process after some keystrokes, with no error #131

Description

@okrammer

Environment

  • @earendil-works/gondolin v0.12.0 (latest on npm as of testing)
  • Host: macOS, aarch64 (Apple Silicon)
  • Guest: Alpine Linux (built via gondolin build, default getDefaultBuildConfig() + a few extra apk packages, arch aarch64)
  • VM backend: qemu (gondolin's default)
  • Reproduced identically in two terminal emulators (iTerm2, Ghostty)

Summary

Both interactive mechanisms we tried — gondolin attach (custom pty/exec protocol) and vm.enableSsh() (TCP-forwarding to the guest's own sshd) — hang when driven by a heavy-redraw TUI (opencode) under rapid keyboard input. We spent significant effort investigating both and want to be upfront: we do not have a confirmed root cause. We're filing this as a solid reproduction plus a clear account of what we've ruled out (including two specific theories we pursued and disproved), rather than a diagnosis — hopefully still useful as a starting point.


Symptom 1: gondolin attach

gondolin bash --image <image-with-a-tui-installed>:latest -- opencode

Type a normal amount of text (20-40 characters including some backspace, in our testing) — no AI/network requests needed. At some point, further keystrokes (including Ctrl+C) stop having any visible effect. The process stays alive; the terminal isn't disconnected — input just silently stops arriving, and the last-rendered frame stays on screen looking normal.

Symptom 2: vm.enableSsh()

Switching our own tooling to SSH (guest's real sshd, bypassing the custom attach protocol entirely) measurably improved things but did not fix it. Under the same reproduction:

  • Symptom shape is different: not "keys stop, display frozen but normal" — the whole connection appears dead (no further output either).
  • Rate-dependent: rapid typing reliably triggers it; typing slowly does not. Reproduces on a fresh VM/fresh session each time, not just after prolonged use of one session.

What we ruled out, with evidence (applies to both symptoms unless noted)

  1. Not our own code. A completely raw gondolin bash --image ... -- opencode (no wrapper of ours involved) reproduces Symptom 1 identically.
  2. Not terminal-emulator-specific. iTerm2 and Ghostty both affected.
  3. Not triggered by output volume alone. gondolin bash --image ... -- sh -c 'i=0; while true; do i=$((i+1)); echo "line $i"; done', zero keyboard input, ran to ~10 million lines with no stall.
  4. Not fixed by wrapping in tmux (Symptom 1) — same freeze.
  5. Not opencode's own TUI dependency (OpenTUI) getting stuck on an unanswered terminal-capability query — a real, separately-documented category of bug in OpenTUI itself, so worth ruling out explicitly. We captured the exact byte stream sent to the terminal right up to the Symptom-1 freeze (script -c, output written outside the guest's ephemeral filesystem so it survived VM teardown): a cleanly completed, unremarkable render cycle, no dangling/unanswered query — then it just stops, despite continued typing afterward.
  6. Not universal to any TUI. A second, lighter TUI (@earendil-works/pi-coding-agent) under the exact same minimal test showed only minor lag, not the dramatic stop opencode exhibits.

Two specific theories we investigated and now believe are not the explanation

  • sandboxd's shared pty fd (Symptom 1). For pty: true exec sessions, the guest sets stdout_fd = stdin_fd = pty_master (dist/guest/src/sandboxd/main.zig), serviced by one thread per session. We hypothesized a write delivering queued keystrokes could block on the pty's input buffer if the child were briefly slow to read, stalling output too. Plausible on paper, but never confirmed by tracing the actual runtime blocking call, and superseded by the Symptom 2 investigation below happening on a completely different guest binary (sandboxssh, not sandboxd) with a similar rate-dependent hang — suggesting this isn't specific to sandboxd's architecture after all.
  • TcpForwardStream.pushRemote() not checking push()'s return value (Symptom 2). dist/src/sandbox/server-transport.js's pushRemote() calls this.push(data) without checking the boolean return value, and we could find no mechanism anywhere in this path to tell the guest to pause sending more tcp_data for an overwhelmed stream. This looked like a strong, concrete, demonstrable bug (unbounded buffer growth is a well-known risk of ignoring push()'s return value), so we implemented a bounded local queue as a fix, verified the stream-mechanics in isolation via standalone Node scripts, and tested it on a real host. It made no difference — the freeze still happened, and didn't even produce the specific new failure mode (a clean error/disconnect) the fix was designed to produce. That's a meaningful negative result: whatever is actually happening isn't (solely, or perhaps at all) pushRemote() silently absorbing unbounded data. We think this theory should be considered disproven rather than just "didn't help enough."

(Separately, while investigating Symptom 2 we also considered whether VirtioBridge.writeFrame(), same file, has a matching bug — it hardcodes return true regardless of the underlying socket.write() result. After tracing every caller in server-ops.js, we believe this one is not a bug: socket.write() returning false is purely advisory, and every caller treats a false return as "retry the identical payload or hard-fail" — propagating the real value would likely cause duplicate file_write_data chunks or spurious hard failures on ordinary, recoverable backpressure. Flagging only so it isn't independently "fixed" the way we almost did ourselves.)

Where this leaves us: a real, solidly-reproducible hang across two different interactive mechanisms, rate-dependent on rapid typing into a heavy-redraw TUI, with two reasonable-looking theories investigated and disproven. We don't have a next hypothesis to offer. Happy to provide the captured script output, our build config, or run further tests if that would help narrow it down.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions