* feat(terminal): add resumable websocket transport Unify terminal input and stream traffic on `/api/terminal/ws` with a v2 control-frame protocol and advertised transport capabilities. Buffer recent PTY output on the server so rebinding clients can replay missed chunks after reconnects or startup races, while keeping SSE as a fallback stream path. Update the web terminal client and store to negotiate the new transport, track tab lifecycle, and avoid reopening exited sessions when restoring tabs. * fix(terminal): retry rehydrated websocket reconnects * fix(terminal): keep reconnect retries silent * fix(terminal): wire ws stream output and replay in runtime * fix(web): enable ws proxy for /api in dev --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
5.0 KiB
5.0 KiB
Terminal Module Documentation
Purpose
This module provides WebSocket transport utilities for terminal input and output in the web server runtime, including message normalization, control frame parsing, rate limiting, pathname resolution, and short-lived output replay buffering for terminal WebSocket connections.
Entrypoints and structure
packages/web/server/lib/terminal/: Terminal module directory.index.js: Stable module entrypoint that re-exports protocol helpers and replay-buffer helpers.runtime.js: Runtime module that owns terminal session state, WS server setup, and/api/terminal/*route registration.terminal-ws-protocol.js: Single-file module containing terminal WebSocket protocol utilities.output-replay-buffer.js: Helper module for buffering recent terminal output so late subscribers can receive startup prompt data.
packages/web/server/lib/terminal/terminal-ws-protocol.test.js: Test file for protocol utilities.packages/web/server/lib/terminal/output-replay-buffer.test.js: Test file for replay buffer helpers.
Public API entry point: imported by packages/web/server/index.js from ./lib/terminal/index.js.
Public exports
Constants
TERMINAL_WS_PATH: Primary WebSocket endpoint path (/api/terminal/ws).TERMINAL_WS_CONTROL_TAG_JSON: Control frame tag byte (0x01) indicating JSON payload.TERMINAL_WS_MAX_PAYLOAD_BYTES: Maximum inbound WebSocket payload size (64KB).TERMINAL_OUTPUT_REPLAY_MAX_BYTES: Maximum buffered terminal output retained for replay (64KB).
Request Parsing
parseRequestPathname(requestUrl): Extracts pathname from request URL string. Returns empty string for invalid inputs.isTerminalWsPathname(pathname): Returns whether a pathname matches a supported terminal WebSocket route.
Message Normalization
normalizeTerminalWsMessageToBuffer(rawData): Normalizes various data types (Buffer, Uint8Array, ArrayBuffer, string, chunk arrays) to a single Buffer.normalizeTerminalWsMessageToText(rawData): Normalizes data to UTF-8 text string.
Control Frame Handling
readTerminalWsControlFrame(rawData): Parses WebSocket message as control frame. Returns parsed JSON object or null if invalid or malformed.createTerminalWsControlFrame(payload): Creates a control frame with JSON payload and prepends the control tag byte.
Replay Buffer Helpers
createTerminalOutputReplayBuffer(): Creates mutable state for recent terminal output replay.appendTerminalOutputReplayChunk(bufferState, data, maxBytes?): Appends a chunk, trimming older buffered data to stay within the configured byte budget.listTerminalOutputReplayChunksSince(bufferState, lastSeenId): Returns buffered chunks newer than the provided replay cursor.getLatestTerminalOutputReplayChunkId(bufferState): Returns the latest chunk id in the replay buffer, or0when empty.
Rate Limiting
pruneRebindTimestamps(timestamps, now, windowMs): Filters timestamps to keep only those within the active time window.isRebindRateLimited(timestamps, maxPerWindow): Checks if rebind operations have exceeded the configured threshold.
Usage in web server
The terminal helpers are used by packages/web/server/index.js for:
- WebSocket endpoint path definition and matching
- Message normalization for terminal input payloads
- Control frame parsing for session binding, keepalive, and exit signaling
- Rate limiting for session rebind operations
- Request pathname parsing for WebSocket routing
- Replaying startup output such as shell prompts when the client binds after the PTY already emitted data
The web server combines these utilities with bun-pty or node-pty to drive full-duplex PTY sessions.
Notes for contributors
- Keep control frames backward-compatible when possible; use explicit
vvalues for protocol changes. - Always normalize incoming WebSocket messages before processing them.
- Keep replay buffering small and memory-only; it exists to cover startup races, not to implement persistent scrollback.
- Add tests for new control frame types, websocket path changes, malformed payload handling, and replay trimming semantics.
- Keep HTTP input and SSE output fallbacks functional unless the rollout explicitly removes them.
Verification notes
Manual verification
- Start the web server and create a terminal session via
/api/terminal/create. - Wait briefly before binding the client to ensure the shell emits its prompt first.
- Connect to
/api/terminal/wsWebSocket and bind to the session. - Verify the startup prompt and early shell output are replayed before interactive input begins.
- Verify
/api/terminal/input-wsis rejected with404 Not Foundand/api/terminal/:sessionId/streamstill works as a fallback path.
Automated verification
- Run
bun test packages/web/server/lib/terminal/terminal-ws-protocol.test.js - Run
bun test packages/web/server/lib/terminal/output-replay-buffer.test.js - Run
bun run type-check,bun run lint, andbun run buildbefore finalizing changes.