fix(host-service): stage long agent launch commands as a script to dodge MAX_CANON truncation

Merged
KI
3mo ago
+364-5

Links

Summary

  • Launching a terminal agent with a long --prompt (>~1KB) silently truncated the typed launch command and wedged the workspace shell at zsh's quote> continuation prompt — the agent never started even though the dispatch reported ok.
  • queueInitialCommand now stages commands over 512 bytes as a self-deleting 0600 temp 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 = 512 threshold (conservative — other platforms/line disciplines can be tighter than 1024).
  • Over-threshold commands are written to $TMPDIR/superset-launch-<terminalId>-<rand>.sh (mode 0600, wx flag 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 gets source (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 (async rm, 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 initialCommand producers (agents, terminal-agents, setup-terminal, command-terminal, teardown) funnel through createTerminalSessionInternal → 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 at quote>, no claude process
  • After (this branch, dev desktop stack): identical workspaces.create call with agents:[{agent:"claude", prompt:"x".repeat(2500)}] → claude TUI launches with the full 2500-char prompt and responds to it; terminal_agent_bindings reaches Stop
  • 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 as terminal.send-snapshot.node-test.ts): queues a >2KB initialCommand into a real /bin/sh session 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 unfixed terminal.ts, passes with the fix.
  • bun test (host-service): 950 pass
  • node --test terminal.adoption.node-test.ts: 20/20
  • bun run lint / bun run typecheck: clean

Known Limitations

  • The terminals send / writeInput follow-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 (bare read, cat) still truncates — same as any real terminal.
  • terminal.send-snapshot.node-test.ts's three tRPC-caller subtests fail under --experimental-strip-types on 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 a 0600 temp script in $TMPDIR, then type . '<path>' (or source for 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.
  • 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.