Skip to content

fix(runtime): frame stdin-after-start follow-up prompts through the paste protocol - #24689

Open
ggbdpq wants to merge 3 commits into
stablyai:mainfrom
ggbdpq:fix/automation-prompt-submit-cr
Open

ggbdpq wants to merge 3 commits into
stablyai:mainfrom
ggbdpq:fix/automation-prompt-submit-cr

Conversation

@ggbdpq

@ggbdpq ggbdpq commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

ELI5

When an Orca automation or worker hands a prompt to a terminal agent that expects the prompt typed into its composer (Kimi Code and similar TUIs), Orca typed the text and pressed Enter — but the "Enter" reached the agent as a newline keystroke instead. The text just sat in the input box and the run was reported as completed even though nothing happened. This PR delivers those prompts the same way every other agent-dispatch path in Orca already does, so Enter actually submits.

What Changed

sendWorktreeStartupFollowupWhenReady (the funnel every managed-worktree dispatch — local, SSH, folder, serve-mode — uses for stdin-after-start agents) previously wrote the follow-up prompt as one raw prompt + CR write, gated only on the foreground process name. That raw write can land while the TUI is still switching its tty into raw mode, so the kernel line discipline (ICRNL) translates the CR to LF before the child reads it. Kimi-style TUIs bind LF to insert-newline: the prompt stays in the composer with a trailing empty line and is never submitted (#24509).

Now the follow-up uses the same delivery protocol as every other TUI dispatch path:

  1. wait for the composer-ready draft signal (waitForWorktreeStartupDraft for the follow-up's agent — the same gate draft pastes and fix(zcode): wait for composer before first worker dispatch #23374 already use), falling back to the existing process-name gate if the signal never fires;
  2. write the prompt as a single bracketed-paste frame (buildAgentPromptPasteBytes), so multi-line prompts arrive atomically instead of keystroking their embedded LFs;
  3. wait out the per-agent submit settle delay (resolveAgentPromptSubmitDelayForAgent), then write AGENT_PROMPT_SUBMIT ('\r') as its own write.

WorktreeStartupFollowup gains the agent field this gate needs (single construction site updated). The user-visible before/after: an automation targeting Kimi Code now actually starts a session from its prompt instead of silently leaving it in the composer.

Why

The byte that reaches the agent is the contract; where the raw write broke it was timing (pre-raw-mode line discipline), not a wrong literal — every dispatch site already writes \r. Routing the one non-conforming path through the shared paste protocol fixes the root cause at the shared funnel rather than special-casing agents or callers, and brings multi-line prompt atomicity for free. The composer-ready gate is the same mechanism draft pastes already trust, so no new readiness concept is introduced.

Linked Issue

Fixes #24509

Visual Proof

N/A — no Orca UI surface changes; the fix is byte-level behavior inside a third-party TUI session. The before/after at the protocol level is locked by the new test: one bracketed-paste frame ESC[200~…ESC[201~ followed by a lone \r write, and no write ending in \n.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Automated: new regression test runtime-worktree-startup-readiness.test.ts ("frames the follow-up prompt as one bracketed paste and submits it with a lone CR") — fails on the old raw write (no paste frame, no standalone CR), passes after the fix. Updated the magnetic assertions in worktree-setup-and-startup-part-04.spec.ts and terminal-run-facts-input.test.ts to the new protocol. Local matrix: pnpm tc clean; oxlint clean on changed files; the key integration case ("sends follow-up prompts for CLI-created stdin-after-start startup agents") passes; full pnpm test / pnpm build not run locally (CI covers them). Manual end-to-end against a real Kimi CLI was not run in this environment — the byte-level behavior is covered by the tests above.

AI Disclosure

Implemented by GLM-5.3-Flash (ZCode agent) under direction of ggbdpq, who reviewed the diff and re-ran the key tests.

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Cross-platform: the settle delay is platform-aware (ConPTY ingest-rate model on win32, POSIX default otherwise); SSH/folder/serve-mode dispatch all funnel through the changed function and now get the identical protocol. No new dependencies, no persistence or protocol-version impact, backwards compatible for agents whose ready signal fires as before.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: dfbf479c-53ec-4696-8869-dd6759be27af

📥 Commits

Reviewing files that changed from the base of the PR and between 607d4ae071e71da91015b048616d7e85c122e535 and eed718a.


📒 Files selected for processing (3)
  • src/main/runtime/runtime-worktree-agent-startup.ts
  • src/main/runtime/runtime-worktree-startup-readiness.test.ts
  • src/main/runtime/runtime-worktree-startup-readiness.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.



📝 Walkthrough

Walkthrough

Worktree startup follow-ups now include the selected agent. Dispatch checks draft-paste readiness first and falls back to the expected-process readiness check. When a PTY is available, it sends the prompt as a bracketed paste, waits for the resolved delay, and sends a separate carriage return. Tests cover both dispatch paths and the updated startup write count.


Priority: ➖ Normal

Merge Risk | 🔵 Low · up to eed71

Merge Risk: 🔵 Low · up to eed71

Some worktree startup prompts may arrive eight seconds later when the readiness signal is lost. The prompt still has a fallback delivery path, so this is mergeable with awareness of the delay.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to eed71

The new delivery sequence weakens checks that the intended recipient owns the terminal. If startup fails or the recipient exits, text or a submission keystroke could reach a shell instead. The demonstrated scope is the affected workspace terminal; broader access or privilege escalation has not been established.

Retained concerns

  • Medium · security · inferred: The new success path treats terminal readiness output as recipient authority, bypassing the prior process gate, then submits after a delay without revalidating ownership. For Kimi, a shell enabling bracketed paste and becoming quiet can satisfy the readiness predicate. If that shell remains in front, supplied text can be pasted and submitted as shell input. A foreground change during the new delay can also redirect the submission keystroke. These are conditional consequences, not a demonstrated exploitation trace.
Security review details

Security Blast Radius

  • inferred — Each dispatch targets a captured PTY in a created workspace, locally or on its selected SSH host. Unintended shell execution would inherit that terminal session's operating-system authority and accessible assets. The inspected routing does not demonstrate delivery to another tenant, another terminal, or a newly privileged identity.

Security Findings and Attack Paths

  • inferred — A conditional path combines control of startup text with a shell remaining or returning to the foreground while readiness output is accepted. For Kimi, bracketed-paste enablement followed by 1.5 seconds of quiet suffices without process inspection. The runtime then pastes the text and sends carriage return. This supports an ownership-containment concern, but does not establish unauthenticated access, an end-to-end exploit, or a retained verified Security finding.

Trust Boundaries and Controls

  • observed — Selected-agent typing and disabled-agent rejection constrain launch selection, while framed delivery sanitizes escape bytes. The fallback retains process recognition, including its pre-existing non-shell child-process allowance. These controls do not protect scanner-success delivery against a foreground shell.

Resilience and Maintainability Implications

  • observed — The established terminal-text delivery path checks request activity, lifecycle generation, permission state, and write success around both initial input and delayed submission. The startup follow-up does not use these protections. Its new tests cover successful framing and timeout fallback, not foreground takeover or interruption during submission delay.

Hardening Proposals

  • proposed — Bind readiness to foreground-recipient ownership and an active terminal lifecycle generation, then revalidate those conditions before submission. Cancel delayed submission after interruption or paste refusal. Exercise shell startup, recipient exit, and terminal replacement as transition cases.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #24509 requires Kimi follow-up prompts to submit with CR, not LF. When the readiness scanner confirms the composer, sendWorktreeStartupFollowupWhenReady sends one bracketed-paste frame, waits …
Out of Scope Changes check ✅ Passed The agent field, readiness handling, and focused test updates support the fix for issue #24509. The changes remain within the linked issue's scope. No unrelated change is demonstrated.
Title check ✅ Passed The title clearly identifies the main change: framing stdin-after-start follow-up prompts through the paste protocol.
Description check ✅ Passed The description covers the required sections, explains the change and its reason, links issue #24509, documents testing and platform considerations, and marks visual proof as not applicable with a rea…

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 4a52b569-f936-4773-add9-f6c00ea05359

📥 Commits

Reviewing files that changed from the base of the PR and between efbf651 and 2550e3ff0638d3792d3a6a99a7d91896a045c997.

📒 Files selected for processing (5)
  • src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts
  • src/main/runtime/runtime-worktree-agent-startup.ts
  • src/main/runtime/runtime-worktree-startup-readiness.test.ts
  • src/main/runtime/runtime-worktree-startup-readiness.ts
  • src/main/runtime/terminal-run-facts-input.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment thread src/main/runtime/runtime-worktree-startup-readiness.ts Outdated

@pullfrog pullfrog 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.

Important

The runtime funnel fix is correct, but one sibling stdin-after-start follow-up path was not updated: the desktop renderer's sendFollowupPromptWhenAgentReady still writes the raw prompt + \r. That path is live for desktop workspace creation, so the same failure the PR fixes on the runtime path can still occur there. Separately, the new readiness gate cannot resolve on Windows ConPTY panes, so it adds a fixed ~8s delay before the existing process-name fallback. Details below.

Reviewed changes

  • Follow-up paste framing — sendWorktreeStartupFollowupWhenReady now gates on waitForWorktreeStartupDraft, writes the prompt as a single bracketed-paste frame, waits the agent's submit-settle delay, then writes AGENT_PROMPT_SUBMIT as its own write.
  • WorktreeStartupFollowup.agent — new required field, populated at the single construction site in buildWorktreeStartupForAgent. It does not cross a wire schema (RuntimeManagedWorktreeCreateArgs carries only startupDraftPaste, which already had agent), so no protocol-version impact.
  • Tests — new unit regression pinning one paste frame plus a lone CR, and magnetic assertion updates in the integration specs (2 → 3 writes).

⚠️ The desktop renderer follow-up path still raw-writes the prompt

sendWorktreeStartupFollowupWhenReady is only the runtime half of the stdin-after-start delivery. The desktop renderer's sendFollowupPromptWhenAgentReady still writes ${prompt}\r in one raw write, gated only on the foreground process — the exact shape this PR removes. It is reachable: for a stdin-after-start agent startupPlan.followupPrompt is set, which leaves backendStartup undefined, so the runtime never spawns a startup terminal and ensureAgentStartupInTerminal → deliverAgentStartupToTerminal takes the follow-up branch. The same LF-vs-CR and multi-line keystroking failure can therefore still bite desktop workspace creation even though the automation/worker path is fixed.

Technical details
# Desktop renderer follow-up path is unfixed

## Affected sites
- `src/renderer/src/lib/agent-followup-delivery.ts:25` — writes `${prompt}\r` raw: no bracketed-paste frame, no composer-ready gate
- `src/renderer/src/lib/new-workspace.ts:289` — the `startup.followupPrompt` branch that calls it
- `src/renderer/src/hooks/composer-state/full-submit-preparation.ts:215` — leaves `backendStartup` undefined whenever `followupPrompt` is set, forcing the renderer path

## Required outcome
- Decide whether desktop workspace creation with a stdin-after-start agent is in scope for #24509. If it is, route this path through the same bracketed-paste + lone-CR protocol (ideally via one shared helper) so the two funnels cannot drift again.

## Open questions for the human
- Is this path intentionally out of scope (automation/worker dispatch only), or tracked in a separate issue?

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

void waitForWorktreeStartupFollowup(host, handle, followup.expectedProcess)
.then((ptyId) => {
if (!ptyId) {
void waitForWorktreeStartupDraft(host, handle, followup.agent)

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.

On a Windows ConPTY pane the composer-ready signal can never fire — ConPTY swallows the agent's \x1b[?2004h, which the repo's own tests assert (draft-paste-ready-scanner.test.ts:275, agent-paste-draft-readiness-budget.test.ts:99). Because the process-name fallback is sequenced behind this call, every stdin-after-start follow-up on Windows now blocks for the full resolveDraftPasteReadyTimeoutMs (8s) before it is delivered, where the old code delivered as soon as the agent was foreground. Racing the two gates (or skipping the readiness wait when the PTY write host is win32) keeps the fallback without the added latency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verified the mechanism but the proposed race would reintroduce the root cause: the process-name gate fires almost immediately after exec — that early fire is exactly the #26387 measurement (foreground name matching at ~0.03s while the REPL is still forming), so min(readiness, process-name) resolves to process-name in practice and the prompt lands before the composer can submit it. The readiness timeout is the cost of the gate being output-confirmed; the fallback still delivers (bare write) on agents whose ready signal never fires.

The latency only materializes on hosts where the signal truly never arrives. If ConPTY does swallow DECSET 2004h end-to-end, Windows pays the full timeout before fallback — if a maintainer can confirm that, a shorter win32-aware timeout (or a platform-conditional gate skip) looks like the safer lever than racing, and I'm happy to implement it on that evidence. I could not verify the swallow claim from the repo itself: the cited tests (draft-paste-ready-scanner.test.ts:275, agent-paste-draft-readiness-budget.test.ts:99) assert Codex composer-glyph sequencing and readiness budgets, not ConPTY DECSET handling.

@ggbdpq

ggbdpq commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in 607d4ae071: the scanner-confirmed path keeps the paste frame + lone CR; the process-name fallback restores the bare prompt + CR write from before the PR, since bracketed paste may be off wherever the ready signal never fires. Regression tests now pin both byte shapes separately (18/18 readiness + run-facts, integration case untouched).

@pullfrog pullfrog 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.

ℹ️ No new issues in this delta. The fallback path was correctly restored to a bare write, and the tests now pin both branches. The Windows ConPTY readiness latency raised in the previous review is unaffected by this commit and remains open.

Reviewed changes

  • Fallback delivery reverted to a bare write — when the composer-ready scanner times out, sendWorktreeStartupFollowupWhenReady now writes ${prompt}${AGENT_PROMPT_SUBMIT} in a single raw write instead of the bracketed-paste frame, on the reasoning that bracketed paste may be disabled on that path, so the ESC[200~/ESC[201~ delimiters would type in as literal input.
  • Tests updated — the fallback unit case pins the single prompt + CR write, while the scanner-confirmed case keeps pinning one paste frame plus a lone CR.

The scanner-confirmed paste path and the WorktreeStartupFollowup.agent field are unchanged from the previous commit.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@ggbdpq
ggbdpq force-pushed the fix/automation-prompt-submit-cr branch from 607d4ae to eed718a Compare October 5, 2026 22:40
@ggbdpq

ggbdpq commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (990b0ed) to clear the merge conflict in runtime-worktree-startup-readiness.test.ts — our new describe block and the OpenCode submit-readiness block from #25101 now sit side by side, both intact (21 passed in that file). Heads-up for CI: pnpm tc currently fails on structured-agent-session-stop-note-opened-turn.test.ts(24,21) TS2353 'kind' does not exist in type 'AgentSessionProviderHandle' — reproduced identically on a clean upstream/main checkout, so it's pre-existing on main and unrelated to this PR.

@ggbdpq

ggbdpq commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Confirmed the finding against the current head: for a stdin-after-start agent the composer path leaves backendStartup undefined (full-submit-preparation.ts), so desktop workspace creation does reach sendFollowupPromptWhenAgentReady, which still raw-writes ${prompt}\r — the same shape this PR removes from the runtime funnel.

Scope call: this PR fixes the runtime dispatch funnel that #24509 is about (automation/worker). Covering the renderer path means giving it the same output-confirmed readiness seam the runtime funnel uses (the runtime side subscribes to PTY output for the ready scanner; the renderer currently has no equivalent for this path) plus the same bare-write fallback semantics — a behavior change to a live desktop delivery path that I'd rather not fold into an already heavily-reviewed PR. Happy to open a follow-up PR routing sendFollowupPromptWhenAgentReady through the shared paste protocol if that's wanted; leaving the routing decision here.

…aste protocol

Automation and worker dispatch delivered `stdin-after-start` agents'
follow-up prompts as one raw write (`prompt` + CR) gated only on the
foreground process name. The write lands while the TUI is still switching
its tty to raw mode, so the kernel line discipline (ICRNL) translates the
CR to LF before the child reads it. Kimi-style TUIs bind LF to
insert-newline, so the prompt stayed unsubmitted in the composer while the
run was marked completed (stablyai#24509).

Deliver the follow-up through the same protocol every other TUI dispatch
already uses: wait for the composer-ready draft signal (falling back to the
process-name gate when the signal never fires), write the prompt as a
single bracketed-paste frame, wait out the submit settle delay, then write
`AGENT_PROMPT_SUBMIT` ('\r') on its own. Multi-line prompts now arrive
inside the paste frame instead of keystroking their embedded LFs.

Fixes stablyai#24509

Generated-by: GLM-5.3-Flash (ZCode)
Coderabbit review: when the composer-ready signal times out, the
process-name fallback can hit a TUI with bracketed paste disabled, where
paste delimiters type in as literal input and the submit CR can still be
line-discipline translated. The scanner-confirmed path keeps the paste
frame + lone CR; the fallback restores the bare `prompt + CR` write this PR
replaced.

Generated-by: GLM-5.3-Flash (ZCode)
Rebase adaptation for upstream stablyai#26820: WorktreeStartupFollowup now carries
the TUI agent (used to pick the composer-ready signal and the submit settle
delay), so the headless automation's deliverTerminalPrompt passes it and the
launch tests assert the full call shape.

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq
ggbdpq force-pushed the fix/automation-prompt-submit-cr branch from eed718a to 88f8abd Compare October 10, 2026 03:42
@ggbdpq

ggbdpq commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto upstream main (now includes #26820, which extracted the awaitable deliverWorktreeStartupFollowup). One semantic note from the rebase, resolved deliberately:

  • The paste-protocol delivery from this PR now lives inside the extracted deliverWorktreeStartupFollowup, so headless automations' deliverTerminalPrompt (the Start server-run automations' agents through the launch executor #26820 consumer) delivers post-start prompts the same way: composer-ready signal → one bracketed-paste frame + lone submit CR; fallback (signal never fires) → the process-name gate with the bare prompt + CR write, which is byte-identical to Start server-run automations' agents through the launch executor #26820's previous raw write (AGENT_PROMPT_SUBMIT is '\r'). The only behavioral delta for that consumer is waiting out the ready-signal timeout (≤8 s, per-agent) before the process-name gate — delivery is never dropped.
  • WorktreeStartupFollowup now carries the agent (needed to pick the ready signal and the submit settle delay); the headless call site passes it (third commit).

Verification on the rebased head 88f8abdabd: readiness + headless-launch + follow-up suites 34/34 green; terminal-run-facts-input 13/13; typecheck:node, oxlint/oxfmt, and the max-lines ratchet clean. worktree-setup-and-startup-part-04.spec.ts shows 7 failures that reproduce identically on pristine upstream cbc565d804 (worktree fixture/environment, not this PR) — declaring for CI rather than claiming green.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant