Skip to content

fix(host-service): send initialCommand Enter as a separate delayed write - #6026

Merged
saddlepaddle merged 3 commits into
mainfrom
fix/host-service-initial-command-enter
Jul 29, 2026
Merged

saddlepaddle merged 3 commits into
mainfrom
fix/host-service-initial-command-enter

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator

What

Agent and preset launches could end up typed at the prompt but never executed: the workspace was created, the agent terminal opened, and the full launch command ('claude' '--dangerously-skip-permissions' '<prompt>') sat in the shell's edit buffer with the cursor blinking after it. Reported as "the new workspace page creates a workspace but doesn't start the agent" — but it's page-agnostic and hits every initialCommand consumer (modal creates, automations, setup terminals).

Root cause

The OSC 133;A shell-ready marker is emitted from a precmd hook, which runs before the prompt is drawn and before the line editor starts reading input. Writing command\n in that gap lets shell plugin init (oh-my-zsh vi-mode, syntax-highlighting — worst under first-create load) flush the PTY input queue mid-read: the command text survives into the editor's buffer, the trailing newline is lost.

Confirmed live via CDP against the dev app: on the first workspace create after boot the command was stuck unexecuted; sending one synthetic Enter keypress launched Claude immediately, which then completed the prompt's task — proving only the newline was missing. This is the residual hole in the #4963 → #5774 (first-prompt gate) → #5927 (15s bound) series.

Fix

In queueInitialCommand:

  • strip trailing newlines from the command text and write it alone;
  • 500ms later, send Enter as its own write — and as \r (what a real Enter key sends, bound to accept-line in every keymap) instead of \n (relies on the ^J binding).

Exactly one Enter is ever sent, so double-execution is impossible. If startup eats the text itself, the stray \r is a no-op at an empty prompt — every failure mode is strictly no-worse than before. Same technique VS Code shell integration uses; v1 avoided the class entirely by launching via shell -lc, which is why v1 never hit this.

Verification

  • bun run test:e2e (host-service): 20/20 pass, including the direnv input-consumption simulation and the promptness bound (command executed in 517ms with the new delay).
  • End-to-end via CDP on the dev desktop app: pre-fix, first create after boot reproduced the stuck command (screenshot evidence, before/after Enter). Post-fix, restarted the app and ran the full /new-workspace flow twice with real input events — both creates launched Claude, which replied within ~2s. Setup terminals unaffected.
  • Biome + tsc clean.

https://claude.ai/code/session_01CwWxQTQW2zLeN9hhRkQNxT


Open in Stage

Summary by cubic

Fixes an intermittent issue where the initial launch command was typed but not executed. We now send the command text and Enter separately with a short delay for reliable execution.

  • Bug Fixes
    • In packages/host-service/src/terminal/terminal.ts, strip trailing CR/LF from initialCommand and write only the text.
    • Send Enter as a separate \r 500 ms after shell-ready to avoid plugin init eating it.
    • Update packages/host-service/test/integration/setup-scripts.integration.test.ts to assert split write and delayed \r; tidy assertion formatting.
    • Applies to all initialCommand flows (new workspace, modal creates, automations, setup terminals).

Written for commit 56af206. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of terminal “initial command” execution by sending the command text and the Enter keystroke as two separate, ordered writes.
    • Normalized the command text to remove any trailing CR/LF characters before sending, and added a brief delay for better alignment with shell readiness.
  • Tests
    • Updated the integration test to assert the command is written without \n, and that the Enter (\r) write occurs afterward.

Agent and preset launches intermittently ended up typed at the prompt but
never executed: the workspace was created, the terminal opened, and the
full launch command sat in the shell's edit buffer with no agent running.

The OSC 133;A shell-ready marker fires from precmd, which runs before the
prompt is drawn and before the line editor starts reading input. Writing
`command\n` in that gap lets shell plugin init (vi-mode,
syntax-highlighting — worst under first-create load) flush the PTY input
queue mid-read: the command text survives into the editor's buffer while
the trailing newline is lost. Reproduced live via CDP on a first
workspace create after app boot; a single synthetic Enter keypress
launched the stuck agent, confirming only the newline was missing.

Strip trailing newlines from the command text and send Enter as its own
write 500ms later — as `\r`, what a real Enter key sends, bound to
accept-line in every keymap. Exactly one Enter is ever sent, so a double
run is impossible, and if startup eats the text itself the stray `\r` is
a no-op at an empty prompt — no failure mode is worse than before.

Residual hole in the #4963 → #5774 → #5927 marker-gating series.

Claude-Session: https://claude.ai/code/session_01CwWxQTQW2zLeN9hhRkQNxT
@ghost

ghost commented Jul 28, 2026 •

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 3 individual chapters for you:

Title
1 Define delay for initial command execution
2 Split initial command text and Enter keypress
3 Update integration tests for split command writes
Open in Stage

Chapters generated by Stage for commit 56af206 on Jul 28, 2026 10:22pm UTC.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Initial terminal commands are trimmed, written after shell readiness, and followed by a delayed carriage return. Integration coverage verifies the separate write order.

Changes

Initial command dispatch

Layer / File(s) Summary
Command write timing and validation
packages/host-service/src/terminal/terminal.ts, packages/host-service/test/integration/setup-scripts.integration.test.ts
Adds a fixed Enter delay, trims trailing CR/LF characters from initial commands, writes command text after shell readiness, sends \r in a delayed second write, and verifies the write order.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise, conventional, and accurately summarizes the main change.
Description check ✅ Passed The description covers the problem, root cause, fix, and verification, though the checklist section is not fully filled out.
✨ 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/host-service-initial-command-enter

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.

@github-actions

github-actions Bot commented Jul 28, 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! 🎉

…al test

The setup-scripts integration test asserted the old single `cmd\n` write;
initialCommand now arrives as the command text plus a separate delayed
`\r`. Assert both writes and their ordering.

Claude-Session: https://claude.ai/code/session_01CwWxQTQW2zLeN9hhRkQNxT

@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: 1

🤖 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/test/integration/setup-scripts.integration.test.ts`:
- Around line 126-133: Update the waitFor predicate in the integration test to
count carriage-return writes and require exactly one "\r", with that sole Enter
occurring after the setup command write. Keep the existing diagnostic message
and command-order assertion intact.
🪄 Autofix (Beta)

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: ad3f95d4-7281-4e43-ad99-c9f8fe54932a

📥 Commits

Reviewing files that changed from the base of the PR and between 53c7850 and 9d8a24b.

📒 Files selected for processing (1)
  • packages/host-service/test/integration/setup-scripts.integration.test.ts

Comment on lines +126 to +133
// The command text and its Enter arrive as separate writes (the Enter is
// delayed so shell startup can't eat it) — assert both, in order.
await waitFor(
() => writes.includes("echo setup-a && echo setup-b\n"),
() =>
writes.includes("echo setup-a && echo setup-b") &&
writes.indexOf("\r") > writes.indexOf("echo setup-a && echo setup-b"),
5000,
() => `expected setup command write, got ${JSON.stringify(writes)}`,
() => `expected setup command write + Enter, got ${JSON.stringify(writes)}`,

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that exactly one Enter is written.

This predicate passes if two "\r" writes occur after the command. Count the carriage-return writes and assert that the sole one follows the command, matching the PR’s exactly-one-Enter contract.

Proposed assertion
-			() =>
-				writes.includes("echo setup-a && echo setup-b") &&
-				writes.indexOf("\r") > writes.indexOf("echo setup-a && echo setup-b"),
+			() => {
+				const commandIndex = writes.indexOf("echo setup-a && echo setup-b");
+				return (
+					commandIndex !== -1 &&
+					writes.filter((write) => write === "\r").length === 1 &&
+					writes.indexOf("\r") > commandIndex
+				);
+			},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The command text and its Enter arrive as separate writes (the Enter is
// delayed so shell startup can't eat it) — assert both, in order.
await waitFor(
() => writes.includes("echo setup-a && echo setup-b\n"),
() =>
writes.includes("echo setup-a && echo setup-b") &&
writes.indexOf("\r") > writes.indexOf("echo setup-a && echo setup-b"),
5000,
() => `expected setup command write, got ${JSON.stringify(writes)}`,
() => `expected setup command write + Enter, got ${JSON.stringify(writes)}`,
// The command text and its Enter arrive as separate writes (the Enter is
// delayed so shell startup can't eat it) — assert both, in order.
await waitFor(
() => {
const commandIndex = writes.indexOf("echo setup-a && echo setup-b");
return (
commandIndex !== -1 &&
writes.filter((write) => write === "\r").length === 1 &&
writes.indexOf("\r") > commandIndex
);
},
5000,
() => `expected setup command write + Enter, got ${JSON.stringify(writes)}`,
🤖 Prompt for 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.

In `@packages/host-service/test/integration/setup-scripts.integration.test.ts`
around lines 126 - 133, Update the waitFor predicate in the integration test to
count carriage-return writes and require exactly one "\r", with that sole Enter
occurring after the setup command write. Keep the existing diagnostic message
and command-order assertion intact.

@saddlepaddle
saddlepaddle merged commit 044b8ab into main Jul 29, 2026
18 checks passed
Kitenite added a commit that referenced this pull request Aug 9, 2026
…o terminals send submits reliably (#6284)

* fix(host-service): send follow-up Enter as a separate delayed write

terminals send bundled the submit Enter into the same PTY write as the
bracketed-paste text. TUI agents aggregate bytes that arrive together
into one paste burst, so on a busy or laggy Claude session the Enter
could be folded into the input-box draft instead of submitting it —
text lands but never sends, while the CLI reports submitted: true.

Write the framed text first, then the Enter as its own write 500ms
later (same class of fix as INITIAL_COMMAND_ENTER_DELAY_MS for
initialCommand, #6026). If the session exits during the gap, the send
now reports an error instead of pretending.

Fixes #6243

* fix(host-service): serialize follow-up sends per session; assert split writes in tests

Review follow-ups on #6284: the delayed Enter opened a window where a
concurrent send's text (including submit: false drafts) could land
between another send's text and its Enter and get submitted by it —
chain sends per session so the sequence is atomic. Strengthen the
delay test to record pty writes (a delayed-but-bundled write no longer
passes) and add an overlapping-sends test.

* fix(host-service): guard indexed access in send delay test for typecheck

* fix(host-service): dedupe in-flight headless adoptions so racing sends share one session

Concurrent terminal.send calls right after a host-service restart could
each adopt their own TerminalSession (independent followUpWriteChains,
duplicate daemon subscriptions), bypassing per-session send
serialization. Share the leader's adoption attempt per terminal id;
followers re-resolve and run their own workspace check.

This branch was previously deployed

1 inactive deployment
preview — 56af2063 Deployed Jul 28, 2026 by saddlepaddle via Deploy Docs #6918
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.

1 participant