fix(host-service): stage long agent launch commands as a script to dodge MAX_CANON truncation
Links
- Fixes #5092
Summary
- Launching a terminal agent with a long
--prompt(>~1KB) silently truncated the typed launch command and wedged the workspace shell at zsh'squote>continuation prompt — the agent never started even though the dispatch reported ok. queueInitialCommandnow stages commands over 512 bytes as a self-deleting0600temp script and types only a short. <path>source line; short commands keep the existing type-directly path.
Why / Context
queueInitialCommand delivers the agent launch command by typing it into the shell's PTY after the OSC 133;A shell-ready marker. That marker fires from precmd, before the line editor switches the TTY to raw mode, so the typed bytes queue under the kernel's canonical-mode line discipline — which caps an input line at MAX_CANON (1024 bytes on macOS) and silently discards the rest. A shell-quoted 'claude' '--dangerously-skip-permissions' '<long prompt>' line loses its closing quote; when the delayed Enter lands, zsh sits at quote> forever.
Reproduced live via superset workspaces create --agent claude --prompt "$(python3 -c 'print("x"*2500)')" against v1.18.x: the typed line survived at exactly 1024 bytes (981 of 2500 prompt chars), shell wedged at quote>, no claude process.
How It Works
- New
MAX_TYPED_INITIAL_COMMAND_BYTES = 512threshold (conservative — other platforms/line disciplines can be tighter than 1024). - Over-threshold commands are written to
$TMPDIR/superset-launch-<terminalId>-<rand>.sh(mode0600,wxflag so it never clobbers) and the typed line becomes. '<path>'— sourcing preserves the interactive shell context so the agent process behaves exactly as if typed. Fish getssource(fish 4 removed.); every other launch shell (zsh/bash/sh/ksh) uses POSIX.. - The script's first line is
command rm -f -- <path>: the shell keeps reading from the already-open fd, so cleanup happens immediately and never waits on a long-running agent. If the session is torn down before the delayed Enter fires, the un-run script is unlinked (asyncrm, best-effort). - If staging fails (e.g. tmpdir not writable), it falls back to typing the full text — today's behavior.
- Shell-ready wait and delayed-Enter semantics are unchanged. All
initialCommandproducers (agents, terminal-agents, setup-terminal, command-terminal, teardown) funnel throughcreateTerminalSessionInternal→queueInitialCommand, so this one site covers them all.
Manual QA Checklist
- Before (prod v1.18.x, unfixed):
superset workspaces create --agent claude --prompt <2500 chars>→ terminal read shows the typed line cut at exactly 1024 bytes, shell atquote>, no claude process - After (this branch, dev desktop stack): identical
workspaces.createcall withagents:[{agent:"claude", prompt:"x".repeat(2500)}]→ claude TUI launches with the full 2500-char prompt and responds to it;terminal_agent_bindingsreachesStop - Staged script self-deletes (no
superset-launch-*leftovers in tmpdir after launch) - Short commands still typed verbatim (screen echo shows the command itself, not a source line)
Testing
- New
terminal.initial-command.node-test.ts(same real-PTY harness asterminal.send-snapshot.node-test.ts): queues a >2KB initialCommand into a real/bin/shsession and asserts the command round-trips byte-for-byte, the staged script self-deletes, and short commands are typed verbatim. Verified it catches the regression: fails (wedged shell, timeout) against unfixedterminal.ts, passes with the fix. bun test(host-service): 950 passnode --test terminal.adoption.node-test.ts: 20/20bun run lint/bun run typecheck: clean
Known Limitations
- The
terminals send/writeInputfollow-up path is intentionally unchanged: by the time follow-ups arrive the foreground program has the TTY in raw mode, so MAX_CANON doesn't apply. Sending >1KB single-line input to a program sitting in canonical mode (bareread,cat) still truncates — same as any real terminal. terminal.send-snapshot.node-test.ts's three tRPC-caller subtests fail under--experimental-strip-typeson baseline too (extensionless-import loader limitation documented in that file's header) — unrelated, use the tsx loader for those.
Summary by cubic
Prevents long agent launch commands from being truncated by the TTY line discipline by staging them as a temp script and typing a short source line instead. Also fixes a cleanup leak on dispose-before-Enter and adds a boot-time sweep for stale scripts.
-
Bug Fixes
- Stage
initialCommand>512 bytes to a0600temp script in$TMPDIR, then type. '<path>'(orsourcefor fish); the script self-deletes on its first line to preserve interactive context without leaks. - On dispose-before-Enter or session eviction, unlink the staged script and skip sending Enter; fall back to typing the full text if staging fails; warn if the boot sweep fails; sweep
superset-launch-*files older than 1h on start. - Applies to all initial command paths via
createTerminalSessionInternal→queueInitialCommand.
- Stage
-
Testing
- Real-PTY node tests cover: a >2KB command executes fully; the staged script self-deletes; short commands are typed verbatim; staging failure falls back (with proper TMPDIR restore); pre-Enter teardown unlinks the staged file with a sentinel proving it never ran.
Summary by CodeRabbit
- New Features
- Improved terminal startup support for long initial commands.
- Commands exceeding terminal input limits now launch reliably without truncation.
- Added shell-aware handling for launching staged commands.
- Temporary launch files are automatically cleaned up after execution or cancellation.
- Bug Fixes
- Added a fallback to preserve command delivery when temporary staging is unavailable.
- Improved cleanup of leftover temporary launch files during startup.