Skip to content

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

Merged
Kitenite merged 4 commits into
mainfrom
fix/agent-prompt-1024-truncation
Aug 5, 2026
Merged

Kitenite merged 4 commits into
mainfrom
fix/agent-prompt-1024-truncation

Conversation

@Kitenite

@Kitenite Kitenite commented Aug 5, 2026 •

Copy link
Copy Markdown
Member

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.

Written for commit dc350d7. Summary will update on new commits.

Review in cubic

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.

…X_CANON truncation (#5092)

queueInitialCommand types the agent launch command into the PTY after the
OSC 133;A marker, but that marker fires from precmd — before the line
editor switches the TTY to raw mode. Input written in that gap queues
under the kernel's canonical-mode line discipline, which silently drops
every byte past MAX_CANON (1024 on macOS). A long --prompt lost its
closing quote, wedging the shell at zsh's `quote>` continuation prompt
with the agent never launched.

Commands over 512 bytes are now written to a 0600 temp script and only a
short `. <path>` line is typed (source keeps the interactive shell
context; `source` for fish, which dropped `.` in fish 4). The script
self-deletes on its first line so cleanup never waits on a long-running
agent. Short commands keep the existing type-directly path, and the
shell-ready wait + delayed-Enter semantics are unchanged.

Covers every initialCommand producer (agents, terminal-agents, setup,
command-terminal, teardown) — they all funnel through
createTerminalSessionInternal -> queueInitialCommand.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The terminal service stages initial commands over 512 UTF-8 bytes in temporary self-deleting scripts. It selects shell-specific sourcing, removes cancelled scripts, cleans stale files, falls back to direct typing on staging failure, and adds end-to-end coverage.

Changes

Initial command delivery

Layer / File(s) Summary
Command sizing and shell-aware session state
packages/host-service/src/terminal/terminal.ts
Initial commands use a 512-byte UTF-8 threshold. Sessions store the launch shell basename for shell-specific handling.
Script staging, delivery, cleanup, and validation
packages/host-service/src/terminal/terminal.ts, packages/host-service/src/terminal/terminal.initial-command.node-test.ts
Oversized commands are written to securely created temporary scripts. Scripts self-delete after execution. Startup removes stale launch scripts. Cancellation removes unused files, and staging failures fall back to direct typing. End-to-end tests cover long, fallback, teardown, and short-command behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TerminalSession
  participant TemporaryLaunchScript
  participant PTY
  TerminalSession->>TemporaryLaunchScript: Stage oversized initial command
  TerminalSession->>PTY: Type shell-specific source command
  PTY->>TemporaryLaunchScript: Execute staged command
  TemporaryLaunchScript->>TemporaryLaunchScript: Delete script
  TerminalSession->>TemporaryLaunchScript: Delete script if cancelled
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #5092 by staging oversized initial commands, preserving complete prompts, and adding tests for execution and cleanup behavior.
Out of Scope Changes check ✅ Passed The implementation, cleanup logic, stale-file sweep, fallback behavior, and tests are related to preventing and safely managing oversized initial commands.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title uses conventional commit format and clearly describes staging long initial commands to prevent MAX_CANON truncation.
Description check ✅ Passed The description explains the problem, solution, scope, testing, manual QA, linked issue, and known limitations in sufficient detail.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/agent-prompt-1024-truncation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR avoids canonical-mode truncation by staging oversized initial terminal commands in restrictive, self-deleting temporary scripts while preserving direct PTY delivery for short commands.

  • Adds shell-aware script sourcing for oversized initial commands.
  • Adds cancellation cleanup and exclusive 0600 temporary-file creation.
  • Adds real-PTY coverage for long-command round trips, self-deletion, and unchanged short-command behavior.

Confidence Score: 4/5

The PR appears safe to merge, with a non-blocking cleanup gap for prompt-bearing scripts if the host service terminates during the short pre-execution window.

The normal execution and session-cancellation paths remove staged scripts, but process termination before the delayed Enter loses all in-memory cleanup and can leave the complete launch command in temporary storage.

Files Needing Attention: packages/host-service/src/terminal/terminal.ts

Security Review

Oversized agent commands can contain user prompts, and an abrupt host-service termination during the staging-to-Enter window can leave the temporary script on disk because no restart-time stale-file cleanup exists.

Important Files Changed

Filename Overview
packages/host-service/src/terminal/terminal.ts Stages oversized commands safely and preserves shell selection, but abrupt process termination can leave prompt-bearing scripts in temporary storage.
packages/host-service/src/terminal/terminal.initial-command.node-test.ts Provides focused real-PTY regression coverage for successful long-command staging, self-deletion, and direct delivery of short commands.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Initial command] --> B{UTF-8 length over 512 bytes?}
  B -- No --> C[Write command directly to PTY]
  B -- Yes --> D[Create exclusive 0600 temp script]
  D --> E[Write short source line to PTY]
  C --> F[Delayed Enter]
  E --> F
  F --> G{Session still active?}
  G -- Yes --> H[Execute command; script self-deletes]
  G -- No --> I[Best-effort unlink]
Loading
Prompt To Fix All With AI
### Issue 1
packages/host-service/src/terminal/terminal.ts:901-905
**Prompt-bearing scripts can persist**

If the host-service process terminates after staging an oversized agent command but before the delayed Enter runs, neither cleanup path executes, leaving the complete command and potentially sensitive user prompt in a `superset-launch-*` file until external temporary-directory cleanup removes it.

**How this was verified:** The prompt is written into the staged file, while both deletion paths require the same process to remain alive until the timer or sourced script executes.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(host-service): stage long initialCom..." | Re-trigger Greptile

Comment on lines +901 to +905
writeFileSync(
scriptPath,
`command rm -f -- ${quotedPath}\n${commandText}\n`,
{ mode: 0o600, flag: "wx" },
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security Prompt-bearing scripts can persist

If the host-service process terminates after staging an oversized agent command but before the delayed Enter runs, neither cleanup path executes, leaving the complete command and potentially sensitive user prompt in a superset-launch-* file until external temporary-directory cleanup removes it.

How this was verified: The prompt is written into the staged file, while both deletion paths require the same process to remain alive until the timer or sourced script executes.

Knowledge Base Used: pty-daemon

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/terminal/terminal.ts
Line: 901-905

Comment:
**Prompt-bearing scripts can persist**

If the host-service process terminates after staging an oversized agent command but before the delayed Enter runs, neither cleanup path executes, leaving the complete command and potentially sensitive user prompt in a `superset-launch-*` file until external temporary-directory cleanup removes it.

**How this was verified:** The prompt is written into the staged file, while both deletion paths require the same process to remain alive until the timer or sourced script executes.

**Knowledge Base Used:** [pty-daemon](https://app.greptile.com/personal-org-6810/-/custom-context/knowledge-base/superset-sh/superset/-/docs/pty-daemon.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — a host-service crash between staging and the delayed Enter skips both cleanup paths (self-delete on execution, pre-Enter unlink on teardown), stranding the prompt-bearing file. Addressed in d73651d: an age-gated (1h) boot sweep removes stale superset-launch-* files from tmpdir; the gate keeps a concurrently-running instance's just-staged script safe (normal script lifetime is <1s). Verified live: a backdated script is removed at module init while a fresh one survives. Residual exposure is same-user-only (0600) and bounded to the crash→next-boot window; note the prompt is equally visible in the launched agent's argv while it runs.

@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

A crash between staging and the delayed Enter skips both cleanup paths
(self-delete + pre-Enter unlink), leaving the prompt on disk. Age-gated
1h so concurrent instances' just-staged scripts are never touched.
Addresses greptile review on #6210.
@Kitenite

Kitenite commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

CI note: the Test job failure is pre-existing on main, not from this PR. It fails in @superset/chat with zod scopeId invalid_type errors — code that landed after this branch point (#6205 / #6203; this branch contains no scopeId references). Verified locally: bun run test in packages/chat at origin/main fails 5/23 with the same errors, and passes 18/18 on this branch. Host-service suites, lint, and typecheck are green here. Will re-run the Test job once main is fixed.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/host-service/src/terminal/terminal.initial-command.node-test.ts">

<violation number="1" location="packages/host-service/src/terminal/terminal.initial-command.node-test.ts:59">
P3: The new test covers only the /bin/sh happy path; the fish-shell `source` variant, the staging-failure fallback (tmpdir unwritable -> type full command), and the pre-Enter session-end cleanup path added in terminal.ts all ship untested. Adding a fish-shell case (the `source` vs `.` keyword is a distinct branch) and confirming the fallback still types the full line would lock in the non-default behaviors this PR introduced.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

process.env.HOST_SERVICE_VERSION = "0.0.0-initcmd-e2e";
process.env.NODE_ENV = "development";

__setAccountShellForTesting("/bin/sh");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new test covers only the /bin/sh happy path; the fish-shell source variant, the staging-failure fallback (tmpdir unwritable -> type full command), and the pre-Enter session-end cleanup path added in terminal.ts all ship untested. Adding a fish-shell case (the source vs . keyword is a distinct branch) and confirming the fallback still types the full line would lock in the non-default behaviors this PR introduced.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/terminal/terminal.initial-command.node-test.ts, line 59:

<comment>The new test covers only the /bin/sh happy path; the fish-shell `source` variant, the staging-failure fallback (tmpdir unwritable -> type full command), and the pre-Enter session-end cleanup path added in terminal.ts all ship untested. Adding a fish-shell case (the `source` vs `.` keyword is a distinct branch) and confirming the fallback still types the full line would lock in the non-default behaviors this PR introduced.</comment>

<file context>
@@ -0,0 +1,167 @@
+	process.env.HOST_SERVICE_VERSION = "0.0.0-initcmd-e2e";
+	process.env.NODE_ENV = "development";
+
+	__setAccountShellForTesting("/bin/sh");
+	initTerminalBaseEnv({
+		PATH: process.env.PATH ?? "/usr/bin:/bin",
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 44c3441 — added node-tests for the staging-failure fallback (TMPDIR pointed at a nonexistent dir; 550-byte command still round-trips via direct typing) and the pre-Enter teardown cleanup. The fallback test is mutation-verified (fails when the catch returns the staged line). The cleanup test earned its keep immediately: it caught a real leak — dispose unsubscribes daemon callbacks before the pty dies, so exited/cancelled never flip and the Enter-timer guard missed plain disposes, stranding the script and writing Enter into a dead pty. The guard now also treats eviction from the session registry as defunct. Skipped a fish-shell case deliberately: fish isn't on CI runners, so it'd be a permanently-skipped test; the keyword choice is a single data branch (source vs .).

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

…fallback + cleanup paths

Addresses cubic review on #6210, which surfaced a real leak: dispose
unsubscribes daemon callbacks before the pty dies, so onExit never sets
exited and cancelShellReady no-ops for non-pending states — the Enter
timer's guard missed plain disposes entirely, leaking the staged script
(and writing Enter into a dead pty). Guard now also treats eviction from
the session registry as defunct.

Also: warn (not swallow) when the boot sweep fails, and cover the
staging-failure fallback and pre-Enter cleanup with node-tests. The
fallback test was mutation-verified; the cleanup test is what caught
the dispose leak.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/host-service/src/terminal/terminal.initial-command.node-test.ts`:
- Around line 166-191: Update the test around createTerminalSessionInternal to
make the staged command write a sentinel file after its self-delete step, then
wait through the delayed-Enter interval after disposeSessionAndWait and assert
the sentinel remains absent. Keep the existing staged-file cleanup assertion,
and use a unique temporary-path sentinel tied to terminalId.
- Around line 140-162: Update the finally block in the
createTerminalSessionInternal test to restore TMPDIR correctly: when origTmpdir
is undefined, delete process.env.TMPDIR; otherwise restore its original value.
Preserve the existing session cleanup via disposeSessionAndWait.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 06777a67-9d55-4ada-89e4-0d46d7e84cfe

📥 Commits

Reviewing files that changed from the base of the PR and between d73651d and 44c3441.

📒 Files selected for processing (2)
  • packages/host-service/src/terminal/terminal.initial-command.node-test.ts
  • packages/host-service/src/terminal/terminal.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/host-service/src/terminal/terminal.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Restore an unset TMPDIR by deleting it (assigning undefined stores the
string "undefined"), and add a sentinel to the pre-Enter cleanup test so
it proves the staged command never executed — file absence alone can't
distinguish the teardown unlink from run-and-self-delete.
@Kitenite
Kitenite merged commit f133353 into main Aug 5, 2026
19 checks passed

This branch was previously deployed

1 inactive deployment
preview — dc350d76 Deployed Aug 5, 2026 by Kitenite via Deploy Web #7187
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Long prompt lines hang the terminal when launching a terminal agent (macOS TTY 1024-byte canonical-mode limit)

1 participant