-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Terminal stack teardown — drop tmux, keep xterm #3991
Terminal stack teardown — drop tmux, keep xtermA teardown of AO's terminal stack: today's implementation (tmux on macOS/Linux, conpty on Windows, xterm.js renderer) vs. direct PTY ownership, with concrete simplifications and fixes. Every claim is verified against code in this repo (not docs). What's there today (verified)
The two layers (this is the key mental model)
They're orthogonal. Replacing the multiplexer does not replace the renderer, and vice-versa. Recommendation 1 — replace tmux with direct PTY
Feature parity, with the benefit of switching:
Cross-cutting benefits: drops the The one cost, and how to recover ittmux survives daemon restarts; an in-daemon PTY dies with it. AO already has the recovery shape on Windows (separate pty-host + registry + Renderer: keep xterm, fix the addonRenderer bugs are separate from the multiplexer. If "garbled output / cursor jumps" is WebGL-related, the lever is the WebGL addon version (0.19.0 stable is the usual suspect — What I'd do first
PTY lifecycle cleanup — accumulation is "everything around the kill path"A comparable codebase hit the macOS PTY-master limit (509/511) after a handful of parallel workspaces and root-caused it to lifecycle gaps around an otherwise-correct per-terminal kill path. The findings map directly onto AO's runtime/reaper:
The kill path itself is usually correct; accumulation comes from the lifecycle around it. Cheap wins: idle TTL + cap, kill-by-cwd backstop, and park = disconnect. Terminal wheel/scroll handling: fractional lines + an escape hatchA comparable terminal layer replaces xterm's stock wheel handling with a custom one that accumulates fractional lines across events (signed), handles Terminal mode replay: reattach must re-assert mode stateA comparable codebase hit a subtle terminal bug worth guarding against in AO's replay path: a kitty-aware TUI (Codex) emits The fix is VSCode's scaffolding with a corrected policy: run a headless xterm on the host, feed every PTY chunk to it alongside the FIFO, and on replay build a mode-state preamble from the headless state (kitty → |
All reactions
Replies: 3 comments
Part 1 addendum — the WebGL renderer has an unpatched texture-memory bug
Concrete, actionable finding for the "garbled output / cursor jumps" terminal bugs.
The bug. AO ships @xterm/addon-webgl 0.19.0 unpatched. In that build, the glyph-atlas max texture size is taken unclamped from the GPU:
this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE)
On GPUs/drivers that report MAX_TEXTURE_SIZE = 16384, each glyph-atlas page can be allocated at 163842 RGBA ≈ 1 GiB. Theme changes, font changes, terminal splits, and reconnects each add pages, multiplying that.
The fix (proven in a comparable codebase that patches the newer addon line) is a three-line change:
- clamp
maxTextureSizetoMath.min(4096, ...)in the atlas / glyph renderer, - clamp
_deviceMaxTextureSizetoMath.min(4096, ...)in the WebGL renderer, - zero
canvas.width/canvas.heightwhen an atlas page is removed, releasing the backing store immediately instead of waiting for GC.
This is the same class of bug AO's own XtermTerminal.tsx is already dancing around (the zero-sized-renderer Viewport crash, the WebGL atlas warm-up one-frame transient, the 600ms font-metric backstop), and it plausibly explains #3309 (smudged output + erratic cursor jumps) and #3803 (viewport disposal).
Actions, cheapest first:
- Stopgap: force the canvas renderer (the
loadRendererfallback already exists) and see if the bugs disappear — that confirms the WebGL path as the cause. - Proper fix: backport the 3-line clamp + eager-free to 0.19.0 via a
patchedDependenciesentry, or upgrade to the addon line that already carries it.
This is the renderer half of the terminal stack — fully independent of the tmux → direct-PTY change above. Both halves can proceed separately.
All reactions
After a second independent pass over current main and the upstream renderer sources, I would amend the recommendation rather than make "tmux → direct PTY + ring" the first implementation step.
The runtime/renderer split is right: tmux is replaceable, and xterm can remain the browser painter. A detached PTY host is also a reasonable destination. The part I no longer think is correct is the feature-parity claim for a raw replay ring.
The missing abstraction is a terminal authority
The mode-replay section at the end of the original post actually exposes the gap. AO's current Windows Ring keeps 1,000 newline-delimited chunks of raw output and omits the current partial line from Snapshot. A suffix of output bytes is not a terminal snapshot:
- it may start in the middle of an escape sequence;
- it does not describe the current normal/alternate screen, cursor, margins, modes, palette, title, or hyperlinks;
- replaying it at a different grid can produce a different screen;
- a short mode preamble repairs a few flags, but not the screen and scrollback state that those bytes produced.
There is another ownership problem: terminal queries. With direct PTY fan-out, every attached renderer can see DA/DSR queries and potentially answer them, while a deliberately disconnected/headless pane has no renderer to answer. Current AO deliberately avoids forwarding xterm's generic onData because it includes terminal-generated control replies that can corrupt the real PTY. Direct PTY alone therefore does not eliminate the suspected response/reconnect mechanism in #2094; it can move the ambiguity from tmux to N browser terminals.
I would introduce a TerminalAuthority boundary with these responsibilities:
- A detached, versioned host owns the PTY/ConPTY and child process.
- One headless VT model consumes every child byte, owns normal/alternate buffers, scrollback, cursor and mode state, and answers terminal queries exactly once even with zero viewers.
@xterm/headlessis one possible prototype, with explicit extensions for state a stock serializer does not cover (notably kitty); an embeddable Go parser would avoid adding a Node runtime to the daemon. - Attach is an atomic snapshot/subscription operation:
{epoch, sequence, grid, snapshot}followed by updates after that sequence. Slow viewers get a bounded queue and resynchronize from a newer snapshot rather than blocking the PTY or accumulating unbounded bytes. - The authority owns input arbitration and one explicit resize policy. A PTY still has one grid; removing tmux does not remove the multi-viewer sizing decision. AO's current host chooses the largest single client by area, which is a policy worth preserving or deliberately replacing.
- The host is registered by session + launch generation, and exposes separate facts for host alive, PTY child alive, and managed workload alive.
The browser can still use xterm. Initially the authority can encode a full ANSI repaint plus ordered ANSI updates; later the protocol could carry semantic cells/diffs to a custom painter without changing PTY ownership. This gives us a migration path rather than coupling the runtime rewrite to a renderer rewrite.
A detached host plus registry/socket is sufficient for daemon restart. FD handoff is only needed to replace the host itself, and it does not make a host crash recoverable unless another process already holds the master FD and the VT state is checkpointed/transferred. The wire contract can be shared across OSes, but Unix PTY, ConPTY, and process containment will remain platform-specific implementations.
Two smaller corrections to the parity table:
- AO does not currently use tmux
paste-buffer; it uses chunkedsend-keys -lplus an enter delay. - "Byte-perfect output" is not the right acceptance criterion. Different ANSI streams can produce the same screen, while replaying identical bytes from the wrong initial state can produce a different screen. The useful invariant is terminal-state convergence after attach.
Lifecycle: repair ownership, not "idle"
Persisting disposeRequestedAt and retrying idempotent cleanup is a good idea. I would not add a blanket idle TTL or make kill-by-CWD an ownership backstop:
idleis a valid durable agent/shell state, not evidence that the user has abandoned it. A resource cap should reject a new session, evict only safely exited/terminated entries, or apply an explicit user policy—not silently terminate an idle worker.- CWD is neither necessary nor sufficient proof of ownership: an unrelated user process can be inside the worktree, and an owned daemon can
chdiraway. Prefer launch-generation identity plus platform containment (for example cgroups on Linux and Job Objects on Windows), with process groups/registries and visible residual-process reporting where the OS cannot provide a hard container. - The app-quit statement is stale on current
main: an app-owned daemon now self-stops through the supervisor link, with a fallback group kill, while terminal sessions intentionally survive for adoption. - "Park = disconnect" is a useful resource mode, not a universal rule. Retained terminals currently share one mux WebSocket, although each hidden xterm/parser/WebGL context still costs resources. The connection avoids replay/repaint on every route switch. I would keep a small measured hot set and disconnect an LRU cold set under a viewer/CPU/memory budget. Open PR fix(terminal): stabilize xterm viewport, session-switch render, colors, and activity state #4010 's six-terminal LRU is a concrete version of that experiment; a canonical snapshot is what ultimately makes cold reconnect safe.
The wheel proposal is also largely already present: the current handler accumulates pixel deltas and handles line/page modes. The remaining useful refinement is to preserve fractional line-mode deltas too, reset the accumulator when the buffer/mouse-routing context changes, and optionally add a typed diagnostic flag; it is not a missing core behavior.
WebGL addendum: good experiment, different evidence
Forcing canvas as an A/B test is sound. The more specific texture claim needs qualification:
- Upstream still reads raw
MAX_TEXTURE_SIZEin bothWebglRendererandGlyphRenderer, so a newer official line does not currently carry the proposed 4096 clamp/eager canvas release. - Ordinary atlas pages start at 512 and grow through repeated four-page merges (there is a separate oversized-glyph path). A 16K RGBA canvas is indeed about 1 GiB, but that is a worst-case allocation, not the normal size of every atlas page.
- Xterm handle replacement can dispose queued viewport work #3803 already has a distinct evidenced cause: queued viewport work runs after an old xterm is disposed. It should not be counted as evidence for the atlas-size hypothesis.
- For bug(desktop): terminal panel shows smudged/garbled output and cursor jumps erratically during agent activity #3309 , current upstream contains directly relevant fixes for shared-atlas garbling across terminals, atlas page overflow, and stale rendering after a shared atlas clear. Those are stronger candidates for a retained multi-terminal UI than the unmeasured 16K-page theory.
I would test 0.19.0 WebGL vs canvas vs a build/backport containing those upstream fixes, while recording atlas page count/dimensions, live xterm/WebGL-context count, context loss, renderer memory, and the exact output trace. A 4096 clamp can still be reasonable defense-in-depth, but it should not be labeled the confirmed fix before that matrix reproduces the failure.
Safer migration order
- Define the authority protocol and terminal-state convergence tests; wrap the existing tmux and ConPTY paths behind it.
- Add a Unix detached-host implementation with a real VT model. Do not use the current raw ring as the correctness layer.
- Replay recorded traces and test startup with no viewer, attach mid-escape, alternate-screen state, mode bytes aged out, DA/DSR exactly once, resize, multiple client sizes, backpressure, renderer reload, daemon restart, and host failure.
- Make the new host opt-in, compare resource usage and state snapshots, then make it default.
- Remove tmux only after the new authority satisfies those invariants.
So I still agree that tmux can probably be removed. I disagree that deleting it first is the simplification boundary: build the terminal authority first, then tmux becomes just one adapter we can retire safely.
All reactions
Part 3 — renderer landscape and canonical AO terminal directionDate: 2026年08月17日 This is the canonical continuation checkpoint for discussion #3991. It builds on the original proposal, WebGL addendum, and terminal-authority correction. It independently compares open-source terminal and agent-orchestration projects with AO and focuses on four boundaries that are often conflated: PTY/process ownership, authoritative terminal state, transport/replay, and final painting. Executive answerVerified: xterm.js integration is a real source of bugs and complexity in AO and in mature peer applications. AO, VS Code, Wave, and Tabby all carry renderer fallback, WebGL context-loss recovery, resize/fit coordination, and host-specific input workarounds. AO and Tabby also reach into xterm private internals. These are not hypothetical costs. Correction: xterm.js is not the source of most terminal-system failure modes, and replacing it with a native embedded renderer would not eliminate PTY ownership, process lifetime, reconnect ordering, backpressure, resize authority, or terminal-query routing problems. A native renderer replaces part of the VT/input/rendering stack and introduces native-view, GPU, FFI, packaging, signing, accessibility, and per-platform integration work. Recommendation for AO: do not make a big-bang xterm-to-native rewrite. First separate a durable terminal authority from interchangeable terminal painters: The first renderer bake-off should stay inside AO's existing Electron/web boundary: compare a current xterm baseline with What "embedded terminal" can meanThe phrase covers three materially different products:
This distinction is the key to interpreting the projects below. Revisions and methodAll claims below are based on source or first-party documentation at these exact revisions. "Verified" means directly supported by those sources. "Inference for AO" is architectural analysis, not a claim made by the upstream project. Architecture matrix
AO baseline: renderer and authority are already separateVerifiedOn Unix, AO starts a fresh On Windows, a detached host owns ConPTY, keeps a bounded output ring, broadcasts live bytes, and atomically attaches a client to a ring snapshot before live fan-out (host ownership and sizing, snapshot boundary). This is durable process/transport ownership, but a suffix of raw output is not the same as a canonical screen and mode snapshot. The desktop sends JSON/base64 output frames over a shared WebSocket mux (wire and socket pool, pooled connection). xterm then parses and paints those bytes. AO's xterm component documents and implements concrete integration work:
Inference for AOThe last point is not merely a rendering bug. It is an ownership ambiguity: which emulator is authorized to answer DA/DSR and related terminal queries, and whether a reply came from current output or historical repaint. A native painter would still need that contract. Swapping only xterm leaves tmux/ConPTY lifetime, raw-stream replay, backpressure, and resize arbitration unchanged. AO is also testing an old xterm baselineVerified: AO pins Inference for AO: the fair baseline is not today's 5.5 stack. Before concluding that xterm must go, AO should run the same corpus against stable 6.0 and a pinned 6.1 beta, with a non-WebGL fallback. That upgrade can plausibly fix flicker and corruption cases; it cannot fix AO's transport, PTY, replay, or resize-authority defects. A beta should be shipped only after AO's own trace and lifecycle gates, not because VS Code happens to track beta releases. Full native embedding: cmux and SwiftTermcmuxVerified: cmux is a macOS-only Swift/AppKit application. Its README says it is not Electron and uses libghostty for GPU rendering (implementation summary, scope, platform). It creates a Ghostty surface by passing an AppKit The most relevant capability for AO is Ghostty manual I/O. In that mode Ghostty spawns no child process; input is delivered to a host callback, and externally owned output is injected with cmux does not prove transparent persistence of arbitrary child processes. Its restore contract saves layouts, directories, and best-effort scrollback but says arbitrary live process state is not checkpointed (restore contract). Inference for AO: cmux is the strongest evidence that a libghostty painter could improve a native macOS terminal surface. It is not evidence of a drop-in cross-platform Electron replacement. Hosting an AppKit/Metal surface beside Chromium requires native view and lifecycle integration; Linux and Windows would need another complete surface or a separate AO renderer. cmux's source itself contains substantial native lifecycle, focus, input, resize, and renderer-recovery code, so the complexity moves rather than disappears. electron-libghosttyVerified: Inference for AO: this validates feasibility but also exposes the cost hidden by the phrase "embed Ghostty": DOM/native z-order, clipping, scroll synchronization, focus, DPI, IPC, N-API ABI, signing, and crash ownership all become product code. It is a useful later macOS prototype, not the cleanest first test of whether xterm's painter is AO's problem. Ghostty API scopeVerified: Ghostty describes Inference for AO: "use libghostty" currently presents two different risk profiles:
The latter can replace terminal-state parsing but does not replace xterm as a complete UI widget. SwiftTermVerified: SwiftTerm offers a headless engine plus AppKit/UIKit terminal views and a convenience local-process view; its macOS PTY helper uses Inference for AO: SwiftTerm is another credible Apple-native prototype candidate. AO should feed it externally owned terminal data instead of using a convenience view that owns the process; otherwise a view's lifetime would become the agent's lifetime. It does not solve AO's Windows/Linux painter requirement. Ghostty-backed web painters: wterm, ghostty-web, and ResttyThese projects matter more to AO's first renderer experiment than a native surface because they keep Electron, browser/mobile delivery, and the deployment boundary constant. wtermVerified: wterm separates a pluggable terminal core from a DOM painter. It offers a lightweight Zig/WASM core or an opt-in It is not renderer-bug-free. Its DOM painter manually handles wide cells and block glyphs; one source-level workaround pixel-snaps CSS gradients specifically because Claude Code's horizontal rule exposed inconsistent browser rounding (block-glyph workaround). Inference for AO: wterm plus ghostty-webVerified: ghostty-web exposes an xterm-compatible API around a Ghostty WASM state engine and a custom Canvas2D renderer, explicitly advertising migration by swapping the import (scope and API, usage). It implements its own selection, scrolling, link detection, input, dirty-row painting, and disposal above the VT core. The repository contains a regression test for stale cells becoming visible after scroll growth under reset-heavy output (viewport-corruption regression). That is useful counter-evidence to "Ghostty core means rendering bugs disappear": the core can be correct while the viewport/cache/painter around it is not. Inference for AO: this is the lowest-friction parser-and-painter swap because its API resembles xterm. It avoids WebGL context and glyph-atlas failure classes, but Canvas2D still owns DPI, font metrics, selection hit testing, and redraw scheduling. It is a good bake-off candidate, not yet evidence for a production migration. ResttyVerified: Restty combines Inference for AO: Restty is the most ambitious web-native candidate, but it rebuilds the same difficult GPU resource surface—glyph atlases, context/device lifecycle, shaders, fallbacks—that makes xterm WebGL costly. It may ultimately outperform both xterm and DOM painting, but its present maturity and GPU complexity make it a later benchmark rather than AO's first replacement candidate. Browser-widget lineage: VS Code, Wave, and TabbyVS CodeVerified: VS Code uses both node-pty and xterm packages (dependencies). PTYs live in a separate pty-host service with heartbeat/restart and attach/input/resize APIs (host lifecycle, process operations). For persistent terminals, the host feeds output into The client creates xterm and dynamically falls back from WebGL to DOM on context loss; it notes WebGL and DOM renderers can calculate different cell dimensions and therefore requests another resize (client construction, renderer fallback). Inference for AO: VS Code is strong precedent for a durable authority plus disposable xterm painter. It also demonstrates that introducing a headless emulator does not remove mode/query/replay complexity. Using the same emulator family for headless snapshots and client rendering reduces semantic drift, but it couples authority behavior to xterm's parser and serializer coverage. Wave TerminalVerified: Wave's Go shell controller owns the PTY, continuously appends raw bytes to a circular terminal file, and separately handles input/resize (circular output file, PTY pump). The browser owns xterm, its serializer, and WebGL/DOM switching with context-loss fallback (xterm setup, renderer fallback). For restore, the frontend loads a previously serialized xterm cache at its recorded byte offset, then consumes the remaining raw output tail. During idle periods it serializes current xterm state back to storage (cache plus tail, idle serialization). Inference for AO: this is a practical cache design, not a single server-side terminal authority. If no renderer has consumed recent output, server-side semantic state and terminal replies are not inherently current. It illustrates how split ownership can work for an interactive desktop, but it is weaker for multi-viewer, remote, or crash-consistent AO attachments. TabbyVerified: Tabby's Electron main process wraps node-pty and uses an explicit acknowledgement queue to pause and resume PTY flow under renderer backpressure (PTY and acknowledgement queue, PTY ownership and IPC operations). A renderer proxy can reconnect to a surviving PTY by ID (renderer proxy). Its xterm frontend implements flow control, WebGL recovery, input/resize, and private Inference for AO: Tabby directly validates the user's concern: browser-terminal integration can require brittle internals. It also validates a separate point: backpressure and PTY lifetime live outside the renderer and remain necessary after a renderer swap. Server-owned semantic state: Zellij and HerdrZellijVerified: a Zellij terminal pane owns a server-side grid/VTE parser and feeds every PTY byte into it. It pauses or intercepts some host queries and tracks pending input (pane state, PTY-byte handling). Rendering produces semantic character chunks from the grid; the output layer builds per-client changes and encodes them to ANSI (pane rendering, per-client output, ANSI serialization). Its server-to-client render payload is an ANSI string, which the client writes to stdout (IPC message, client output). HerdrVerified: Herdr owns its PTYs, feeds output into vendored Inference for AOThese designs move reconnect, inspection, and slow-client policy onto authoritative visual state. A later frame can safely supersede an unsent earlier frame because it represents a complete current result; arbitrary raw VT chunks cannot be dropped this way. They do not show that AO can eliminate all painters. Zellij and Herdr can rely on an outer terminal because their primary clients are terminal applications. For AO, one of these must still be true:
A custom cell painter is a major product in its own right: Unicode width and shaping, bidi policy, cursor and selection, IME, keyboard protocols, hyperlinks, clipboard, accessibility, images, ligatures, font fallback, and GPU lifetime all become AO's responsibility. Native semantic models: WezTerm and ArborWezTermVerified: WezTerm's native mux does not treat a renderer as the sole copy of terminal state. Its Inference for AO: WezTerm is the strongest architectural reference for a state-authoritative mux with incremental painting. It is not a drop-in embeddable AO widget, but its sequence/change seam is closer to the correct long-term contract than forwarding an unbounded historical byte suffix to every painter. ArborVerified: Arbor, another agent/worktree desktop, keeps daemon-backed terminal sessions, uses an Alacritty-derived emulator by default, offers experimental Inference for AO: Arbor demonstrates that a custom native cell view is viable when the product defines the supported terminal semantics. Its narrow snapshot is also a warning: a screen suitable for common agent output is not automatically full fidelity for arbitrary TUIs, graphics protocols, query replies, and accessibility. AO should state that product choice explicitly if it ever narrows the contract. Agent orchestrators and terminal managersSuperset: durable PTY plus headless xterm authorityVerified: Superset's session object says it owns a PTY subprocess, a headless emulator, attached clients, and disk output. It feeds PTY output through the headless emulator before broadcasting and queues emulator work to prevent blocking the I/O path (session ownership and construction, headless setup and query responses, output pipeline, emulator queue). The authority waits for a consistent emulator boundary, serializes a snapshot, and attaches live output after that boundary (snapshot boundary, attach). Its headless wrapper uses The PTY daemon can hand inherited PTY master descriptors and snapshots to a replacement process (daemon handoff). Inference for AO: this is the closest evidence for the proposed terminal-authority shape, not necessarily a code target. It shows both its value and its cost: consistent snapshots and a sole query responder become possible, but headless write scheduling, mode completeness, handoff, and renderer rehydration are substantial systems work. The extra hand-maintained mode parser also shows a semantic-drift risk when the underlying emulator does not expose every state needed by the product. Vibe Kanban: simple PTY-per-WebSocket baselineVerified: Vibe Kanban creates a Inference for AO: this is an intentionally simpler endpoint, useful as a complexity floor. It does not provide AO's persistent agent terminal, canonical replay, multi-viewer, or daemon-restart requirements. A renderer that reconnects cannot restore a session when the backend creates a new shell per connection. Agent Deck: tmux is the terminal authorityVerified: Agent Deck describes itself as a tmux-backed terminal session manager (README). It creates detached tmux sessions, configures them, and uses Inference for AO: this avoids embedded renderer bugs only because the user's existing terminal paints. It retains tmux as a dependency and gives the application less control over exact visual composition and accessibility. It is a credible optional "Open externally" path, not a full AO desktop replacement. NTM: exact tmux session plus monitoring-grade event streamVerified: NTM explicitly uses tmux as its execution control plane and exposes REST/SSE/WebSocket surfaces (README, capabilities). Its pane streamer prefers a Inference for AO: NTM's events are monitoring-friendly lines, not a faithful interactive terminal frame. That is a useful product separation: AO's overview can use structured agent status/transcripts and avoid continuously painting every hidden terminal, while an exact terminal remains available on demand. Parser/state library without a painter: libvtermVerified: libvterm exposes terminal state properties, input/output and key/mouse APIs, state callbacks, and a screen-cell/callback API (state and input API, screen API). The inspected Neovim fork explicitly says Neovim currently bundles its own vterm rather than using this fork (fork status). Inference for AO: like Which bugs does replacing xterm actually address?
The user's diagnosis is therefore directionally correct at the painter/host-integration layer. It becomes incorrect if "xterm bug" is used as a catch-all for stream corruption, stale replay, terminal replies, process lifetime, or sizing races. Strategy comparisonKeep xterm as both state engine and painterAdvantages:
Costs:
Ghostty VT core plus a web painter (wterm or ghostty-web)Advantages:
Costs:
Native embedded painter (
|