Skip to content

feat(source-control): start the AI buttons' agents through agent.launch (step 2 of 2) - #23748

Closed
brennanb2025 wants to merge 7 commits into
brennanb2025/agent-launch-host-prompt-deliveryfrom
brennanb2025/desktop-action-buttons-agent-launch-2
Closed

brennanb2025 wants to merge 7 commits into
brennanb2025/agent-launch-host-prompt-deliveryfrom
brennanb2025/desktop-action-buttons-agent-launch-2

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1791 $\color{#cf222e}{\Huge{\mathbf{−}}}$​2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1789
Prod 24 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1041 $\color{#cf222e}{\Huge{\mathbf{−}}}$​104 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​937

ELI5

The problem. The desktop's source-control AI buttons (Fix checks, Resolve conflicts, commit/push-failure recovery, Explain commit) opened their agent tab themselves and pasted their long, generated prompt into it. On main that paste went in blind after 8 s if the agent never looked ready, and was typed into the shell if the agent crashed at startup. Every launch is moving onto the host's agent.launch, and these buttons were the last ones pasting on their own.

What this changes. The four buttons now start their agent through agent.launch, which step 1 taught to keep a prompt on the command line only where Orca measured that the typed line arrives whole, and otherwise to paste it after readiness. The tab still opens in the split you used the button from, focused; the prompt arrives once, submitted, at main's timing; and a prompt the host could not deliver shows a notice with Copy prompt instead of being typed anywhere.

The plan, and where this PR fits. This PR is step 2 of 2. Step 1, #24257, changes the host's agent.launch path and the shared launch code (prompt carry, readiness on the agent's own signal, the shell checks, main's paste bytes, the one-write ledger, Qwen Code's second Enter). This PR moves the desktop buttons onto it and places their tabs. After that, #23962 → #24080 → #24081 put long prompts for Claude and Codex on the agent's own command line through a staged launch file.

What Changed

Terminal mode. The agent tab opens in the tab group you used the button from, focused. If you moved to another workspace or an app view before the tab appeared, you stay where you are: the tab joins that group without switching workspaces, and a toast " started in " offers Go to, which opens the workspace and focuses the tab. This is the rule #23974 set for a finished workspace create, and it reuses that check and toast. On a local zsh host (the macOS default), a prompt whose lines are each 512 bytes or shorter, up to 8 KB, rides the agent's launch line, so the agent starts with it. That rule is step 1's, added after the QA below, and has not been QA'd through these buttons yet. On the phone it was re-run on step 1: review notes now match main. The phone's commit-failure Fix still pastes, because its hook-output line is 2,163 bytes, over the 512-byte per-line size measured safe. That is Intended, per the measurement, and #24080 removes the delay. The desktop's commit-failure recovery also carries hook output, so a line over 512 bytes there pastes the same way. Any other prompt is pasted once, submitted, about 1.6 s after the agent looks ready, as on main. If the agent never looks ready within 60 s, or exits at startup, nothing is typed anywhere and a notice offers Copy prompt. The Fix checks and commit-failure spinner and success toast now follow delivery.

Chat mode. With a native-chat default for Claude or Codex, the chat opens in the split you used, focused, with the prompt as its first message, as on main.

Latest live QA of the buttons, main vs this PR on top of #24257 (the QA'd build is their combined tree; QA status comment):

Row main This PR
macOS Explain, click → prompt (median) 2.30 s 2.19 s
macOS, Enter after the paste 52–54 ms 52–55 ms
SSH, click → submitted 2.36–2.52 s 2.35–2.55 s
Agent exits at startup prompt typed into zsh, Enter pressed (macOS) nothing typed; notice at 60 s (macOS, SSH)
Windows Claude / Qwen Code, Explain never arrived / never pasted submitted once
Click → tab, Windows first launch 0.21 s 2.68 s (later 0.20 s)
Click → tab, SSH 207–272 ms 291–437 ms

Every run: one message, submitted, byte-identical to main. The tab rows are the one gap left.

How it works now

  • Each button sends agent.launchReplay (the replay-safe form of agent.launch) with the pane id it minted (and, for a native chat, the chat session id), after checking the host advertises both agent.launch.replay-required.v1 and agent.launch.prompt-carry.v1; an older host keeps today's tab paste. A paired host always gets an explicit viewMode (terminal when the button would open a terminal), as main's paired path does, so it never applies its own default.
  • The button records one reservation of where its tab goes, under every id the tab can arrive as. Locally, the host's terminal reveal and the session-tabs mirror read it, so a terminal or chat lands in the button's split, focused; on a paired host the terminal is placed by the window's recorded placement, which it polls for 10 s from the click; an empty target group is kept until its tab arrives; the reservation dies when consumed, when the launch settles, or after 120 s.
  • A lost launch reply is re-sent under the same operation id, at most three sends in all, so the host answers from its ledger instead of starting a second agent. If no send gets an answer, the button says "Couldn't confirm the agent started. Check the workspace before trying again." rather than inviting a retry.
  • A prompt the host reports as not delivered shows "The agent started, but your prompt wasn't sent" with Copy prompt. A launch the host refuses shows "Couldn't start the agent." with the host's reason underneath.

Why. The host already owns the agent, its terminal and its readiness, so delivery belongs there; the buttons only decide where the tab goes, which only the window can know.

Deviations from main

  • Temporary: the tab appears once the host has spawned the terminal (Windows first launch after app start +2.5 s, SSH +65–85 ms; main created it at the click). Follow-up PR (not filed yet): a placeholder tab at the click that the host's terminal binds into, plus opening the launch ledger without starting the structured-session host. Step 1 already cut one of the two ledger writes, and click → prompt is no later than main.
  • Intended: the Fix checks and commit-failure spinner and success toast follow delivery rather than the tab opening.
  • Intended: Explain commit reports its own launch source in telemetry instead of posing as a tab-bar quick launch.
  • Intended: a launch whose replies were all lost reports "Couldn't confirm the agent started" instead of "failed", because a retry could start a second agent.
  • Intended: because the tab now appears when the host's terminal does (up to 2.5 s after the click on a Windows first launch), you can leave the workspace before it exists. Then you stay where you are and get the Go-to toast, as fix(worktrees): a finished create no longer pulls you off the workspace you switched to #23974 does for creates. A local terminal is checked when its tab appears. A chat, and any launch on a paired host, is checked when the launch replies, because no earlier signal reaches this window. On main the tab was created at the click.
  • Temporary: on a paired remote Orca server, whether the button opens a chat or a terminal follows that server's settings, not this window's (viewMode only picks the terminal's view). Follow-up not filed yet.
  • Host-level deviations (which prompts are pasted rather than typed, no blind paste, refusing only a proven shell, the older-host fallback) are labelled in feat(agent-launch): keep long prompts off the launch line and paste them after readiness (step 1 of 7) #24257.

Known issues, the same as main

Linked Issue

N/A (internal agent-launch unification series).

Visual Proof

Commit-failure recovery from a focused empty split (macOS): the tab lands in the split and the 21-line prompt goes into Claude, not the shell.

b2-r1-aifix-empty-split.jpg

Chat mode, Explain changes (macOS): the chat opens in the focused split with the prompt as its one user message.

f27-m4-pr-chat-explain-in-split.jpg

An agent that exits at startup (macOS): zsh is clean and the not-sent notice with Copy prompt is up.

r9b2-m4-pr-crash-stub-notice.jpg

History, alternatives, full QA tables and more screenshots: Review and QA history.

Testing

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

  • Mixed versions: a new desktop on an older host keeps today's tab paste; viewMode is sent only to hosts advertising the capability. Folder workspaces work as worktrees do.

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)

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: head 2228786, still a draft, not ready to merge yet

Verdict. The review is clean at the pushed head. It went through six review loops (the last two only verified and came back clean), an architecture review of tab placement, a readiness check at the start and end, and live QA on macOS and Windows. CI is green at 2228786. Two things must happen before this merges; see "Before merge".

What the review changed (all pushed except the last item)

  • Merged current main. The guard that makes every host-built agent startup carry launch attribution now also counts this PR's prompt-carry wrapper.
  • A short prompt containing a TAB, ESC or other control byte no longer rides the typed launch line. Those bytes acted as keystrokes in the shell; the prompt is now pasted instead.
  • A launch into a reused terminal waits for plain idle instead of the fresh-launch composer check.
  • A lost reply from a paired host (timeout or dropped connection) is replayed under the same operation id instead of reported as failed, so a retry click cannot start a second agent.
  • Fix checks shows its success toast only once the prompt is delivered.
  • Inline Grok (no alternate screen) keeps its quiet-window readiness. Before, its button prompts came back "not sent" and worker starts timed out.
  • A chat the host starts for a button is focused.
  • The "prompt wasn't sent" notice stays until you dismiss it. It used to close after 4 s and take its Copy prompt button with it (found in Windows QA).
  • Desktop connected to a remote Orca server: the button's tab now opens in the button's group, in the button's view mode, and focused within about 0.5 s. Before this, it opened unfocused in the server's group and was selected only after the prompt paste. The host now selects the caller's tab when the tab is published, which also helps the phone. Decision (coordinator, overridable): fix it here rather than keep remote servers off agent.launch.
  • The launcher reads the workspace's focused group itself, so buttons no longer pass their own group. Explain commit used to pass none, and Fix checks passed a workspace id. It holds that group through Fix checks' workspace reveal, so launching from an empty split keeps the split.
  • A remote server with no window no longer shows "Orca could not make it discoverable" on every launch.
  • Held locally, not pushed: the phone's prompted AI-button launches are gated on agent.launch.prompt-carry.v1, so a released host never types those prompts into the shell. Decision (coordinator, overridable): gated in this PR. This moves 8 phone recordings, so it lands after test(mobile): record RPC goldens without a pinned commit, and check recorded requests against the desktop's params rules #23732 and those recordings are re-recorded once, without the old pinned-commit stamp.

Before merge

Live QA

  • macOS, separate machine: all 12 rows ran, and 11 passed. The one miss was a Fix checks run from an empty split (1 of 3 runs): focus had already moved to another group in the test rig, and the launch followed it. Rows covered: recovery from an empty split and from a split with a tab, Resolve conflicts, Explain commit, agent with no readiness, the TAB versus plain short prompt, the remote-server desktop (10 launches), and Fix checks locally and on the remote-server desktop.
  • Windows, separate machine: PowerShell and Git Bash typed lines arrive byte for byte. The long prompt is pasted once after readiness, and the no-readiness notice appears. AI Fix from an empty split passed. Real Claude's ready title reaches the host through the Windows console, so the prompt is delivered.
  • Blocked: Grok (quota exhausted on the QA machine), SSH (no throwaway Linux SSH target), Codex and SSH with Codex (the QA rules forbid starting Codex in a test build's terminal until fix(codex): opening a terminal no longer strips Codex hooks from the real ~/.codex #23552 lands), WSL.

Found, not caused by this PR

  • Windows cmd panes: a short one-line prompt on the typed line gets stray ^ characters. The shared cmd quoting in src/shared/tui-agent-startup-shell.ts escapes metacharacters inside double quotes. This is identical on main; the buttons don't hit it because their prompts are multi-line. It matters for the planned change that puts launch prompts on the command line.
  • The Fix checks prompt says "check log tails below" but carried no log tail in QA, although Orca fetched the job log. The prompt builder and log fetch are unchanged here; the cause is not yet known.
  • A TAB in a pasted prompt shows as 4 spaces in Claude. Orca's paste writes the TAB unchanged, so this comes from Claude's input box.

Accepted and disclosed in the body: the three lost-reply replays have no delay between them, and they never start a second agent. Worker start now needs name-only-title agents to go quiet. On a remote server, a chat is focused but not placed in the button's group.

@brennanb2025
brennanb2025 marked this pull request as ready for review September 29, 2026 23:10
@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[Medium risk] Refactors how source control agents are launched through a new abstraction.

The PR does not yet appear safe to merge because paired launches can still lose their target split and the host’s prompt-write guard can still write after an agent exits.

Findings

  1. P1 Paired launch loses target split ▶
  2. P1 Security Stale foreground check permits shell input ▶

Summary

The PR moves source-control AI buttons onto the host’s replay-safe agent launch and adds renderer-side tab placement, prompt-delivery notices, and workspace Go-to notifications.

  • Recent changes keep a user who leaves the launching workspace in their current view and add translated launch notices.
  • No new actionable issue was established in those changes. Two earlier findings remain outstanding.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Source-control AI button] --> B{Host supports launch capabilities?}
  B -- No --> C[Existing tab-paste path]
  B -- Yes --> D[Reserve tab placement and send launchReplay]
  D --> E{Launch result}
  E -- Terminal --> F[Reveal or mirror tab in reserved group]
  E -- Chat --> G[Mirror chat tab]
  E -- Prompt not delivered --> H[Offer Copy prompt]
  F --> I{User still viewing workspace?}
  I -- No --> J[Offer Go to workspace]
Loading

Reviews (3) · Last reviewed commit: "refactor(source-control): import the age..."

Comment on lines +113 to +116
settle: (result) => {
releaseGroup()
if (result?.outcome.kind === 'terminal') {
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Paired launch loses target split

If a paired host's terminal launch reply arrives before the tab appears in the desktop mirror, settle() releases the reservation that keeps the button's empty split alive. Tab reconciliation can then remove that split. Placement also stops waiting after 10 seconds and forgets its record, so a delayed tab can land in a different group instead of the one the user launched from. Keep the reservation until the tab appears or the launch is known not to have created one.

@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 critical issues — one scope note and a minor consistency question.

Reviewed changes

  • Host owns prompt delivery for the source-control buttons. Fix checks, Resolve conflicts, commit/push-failure recovery and Explain commit now launch through agent.launchReplay; the host pastes after readiness, and a prompt it cannot deliver produces a "prompt wasn't sent" notice with a Copy action rather than a blind paste.
  • Typed-line prompt-carry rule. startup-line-prompt-carry.ts measures the quoted line (launcher + args included) and only lets the prompt ride it when it has no control byte and is ≤512 bytes; otherwise the agent starts clean and the surface reports promptRodeLaunchCommand: false so delivery pastes it. Hermes keeps its env transport and starts clean over budget.
  • One shared launch-readiness wait. waitForLaunchedAgentComposer now serves both agent.launch and local worker start; composer-marker agents (ZCode/DSH/Grok) use their captured marker, everyone else uses tui-idle with launchReadiness: true, and blocking dialogs are re-checked for up to 60 s.
  • Tab placement survives the reveal. Callers mint the pane, reserve the tab's group and focus intent, and the local reveal claims/consumes the reservation; reconciliation keeps a reserved empty group until the tab arrives.
  • New capability + replay identity. agent.launch.prompt-carry.v1 gates prompted launches (older host → tab paste), and the desktop renderer gets a host-fixed trustedLocalCallerId so replay-protected launches have an identity.

ℹ️ The typed-line guard doesn't cover the prompt paths outside these buttons

The 512-byte/control-byte rule is applied only on the lanes this PR wires (agent.launch, and createManagedWorktree when the new callback is supplied). worktree.create --prompt (CLI) and headless automations still fold the prompt into the typed startup command with no post-start paste and no notice, so the failure mode this PR fixes — a long single line truncated at MAX_CANON, or a newline written raw on a shell without bracketed paste — is unchanged there. This looks deliberate and is pinned by runtime-worktree-agent-startup.test.ts ("keeps folding the prompt for a caller that delivers nothing afterwards"), so it may simply be out of scope; flagging it so the boundary is an explicit decision rather than an oversight.

Technical details
# The typed-line guard is not reached by CLI/automation prompt paths

## Affected sites
- `src/main/runtime/runtime-worktree-agent-startup.ts:163-171` — when `onPromptCarry` is absent, falls back to `buildAgentStartupPlan({ ...planInputs, prompt, ... })`, folding the prompt into `launchCommand` with no length/control-byte check.
- `src/main/runtime/rpc/methods/worktree.ts` (`buildManagedWorktreeCreateArgs`) / `src/cli/handlers/worktree.ts` — `orca worktree create --prompt` reaches that fallback with no callback.
- `src/main/automations/headless-workspace-create.ts:43` — `startupPrompt: automation.prompt`, same fallback.
- `src/main/runtime/orca-runtime-terminal-create-deduplication.ts:161-171` — automation into an existing workspace spawns a command built by `buildStartupForAgent` with no callback.

## Required outcome
- Confirm whether the buttons' path is the intended scope of the guard, or whether the CLI/automation prompt lanes should also stop folding an uncarriable prompt.

## Open questions for the human
- Is a follow-up planned to route these lanes through `planStartupWithPromptCandidate` (with a paste/notice owner), or is the fold behaviour there intentionally frozen for compatibility?

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

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: 753abb48-1904-4810-b775-cdf08b9a0c6e

📥 Commits

Reviewing files that changed from the base of the PR and between 6889642 and f564d7a.

📒 Files selected for processing (53)
  • mobile/src/session/mobile-existing-agent-launch.ts
  • mobile/src/tasks/agent-launch-request.ts
  • src/main/runtime/__fixtures__/zsh-prompt-runs-command.meta.json
  • src/main/runtime/__fixtures__/zsh-prompt-runs-command.txt
  • src/main/runtime/agent-session-operation-admission.ts
  • src/main/runtime/agent-session-record-store.ts
  • src/main/runtime/agent-terminal-startup-prompt.test.ts
  • src/main/runtime/agent-transcript-pane-test-harness.ts
  • src/main/runtime/launched-agent-composer-readiness-claude.test.ts
  • src/main/runtime/launched-agent-composer-readiness-codex.test.ts
  • src/main/runtime/launched-agent-composer-readiness.test.ts
  • src/main/runtime/launched-agent-composer-readiness.ts
  • src/main/runtime/launched-agent-shell-foreground.ts
  • src/main/runtime/launched-agent-shell-handoff-readiness.test.ts
  • src/main/runtime/launched-agent-write-guard.test.ts
  • src/main/runtime/launched-agent-write-guard.ts
  • src/main/runtime/orca-runtime-activate-managed-worktree.ts
  • src/main/runtime/orca-runtime-controller-knows-pty-is-live.ts
  • src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts
  • src/main/runtime/orca-runtime-tests/agent-launch-prompt-submit.spec.ts
  • src/main/runtime/orca-runtime-write-terminal-agent-prompt.ts
  • src/main/runtime/orca-runtime.test.ts
  • src/main/runtime/rpc/core.ts
  • src/main/runtime/rpc/methods/agent-launch-prompt-carry.test.ts
  • src/main/runtime/rpc/methods/agent-launch-replay.test.ts
  • src/main/runtime/rpc/methods/agent-launch-replay.ts
  • src/main/runtime/rpc/methods/agent-launch-terminal-prompt.test.ts
  • src/main/runtime/rpc/methods/agent-launch-terminal-prompt.ts
  • src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts
  • src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts
  • src/main/runtime/rpc/methods/orchestration/worker/zcode-worker-readiness.test.ts
  • src/main/runtime/rpc/rpc-streaming-dispatcher.ts
  • src/main/runtime/runtime-terminal-contracts.ts
  • src/main/runtime/runtime-worktree-agent-startup.test.ts
  • src/main/runtime/runtime-worktree-agent-startup.ts
  • src/main/runtime/runtime-worktree-startup-readiness.ts
  • src/main/runtime/tui-idle-evidence.ts
  • src/main/runtime/zcode-readiness-transcript.test.ts
  • src/renderer/src/components/terminal-pane/terminal-bracketed-paste.test.ts
  • src/renderer/src/components/terminal-pane/terminal-bracketed-paste.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/lib/agent-launch-tab-placement.ts
  • src/renderer/src/lib/agent-launch-tab-reservations.test.ts
  • src/renderer/src/lib/agent-launch-tab-reservations.ts
  • src/renderer/src/lib/agent-paste-draft.ts
  • src/renderer/src/lib/source-control-agent-launch.test.ts
  • src/renderer/src/runtime/local-structured-session-launch-placement.test.ts
  • src/renderer/src/runtime/web-session-tabs-sync/terminal-surfaces.ts
  • src/shared/agent-launch-replay-refusal.ts
  • src/shared/agent-prompt-injection.ts
  • src/shared/protocol-version.ts
  • src/shared/terminal-bracketed-paste-text.ts
  • src/shared/tui-agent-config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/i18n/locales/en.json

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


📝 Walkthrough

Walkthrough

The change adds host-based source-control agent launches with replay protection and capability checks. Startup planning reports whether it carried a prompt on the launch command. When it did not, terminal delivery waits for agent readiness before sending the prompt. The renderer coordinates tab placement and prompt-delivery results across host and new-tab launch paths. The change also adds terminal view-mode support, launch-readiness handling, caller identity propagation, and translated notices for undelivered prompts.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to f564d

Source-control agent launches now go through the host and paste prompts only after the agent is ready; if readiness is never detected, the prompt is not sent and the user is shown a notice to copy it. The only remaining known item is a potential inconsistency that no current caller reaches. This review found no current issue that should block merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to f564d

The change makes startup safer, but delayed automatic input is not consistently checked against current permissions and lifecycle state. This leaves a bounded risk on the selected computer.

Retained concerns

  • Medium · security · inferred: The new delayed retry Enter crosses a state transition without the first submission’s full permission and lifecycle fence. It checks generation before awaiting beforeWrite, then writes without checking permission activity, cancellation, or generation again. If the first submission has already advanced to an approval dialog, or ownership changes during the guard, the retry may act on a different state rather than resubmit the original input. Desktop delivery already retried Enter; this PR extends that behavior into the shared hosted writer, whose base implementation submitted only once. Actual approval acceptance remains provider-dependent and was not verified.
Security review details

Security Blast Radius

  • inferred — The identified retry risk is bounded to a retry-enabled terminal on the selected host and requires an authorized launch to reach delivery. Its effects inherit the authority of the process currently consuming that terminal input. No cross-device identity bypass or broader account escalation was established.

Security Findings and Attack Paths

  • inferred — A conditional attack path is an authorized launch whose first submission triggers an approval request before the scheduled retry Enter. The retry does not inspect permission activity before writing, so it may interact with that approval state. Source confirms the missing check, but does not establish whether a particular provider accepts approval with Enter or whether an attacker can reliably induce the timing.

Trust Boundaries and Controls

  • observed — Desktop IPC assigns the trusted local caller identity after checking the current main frame. Paired transport validates the bound credential and derives device identity and scope from the registry. Replay admission uses those identities rather than a request-supplied caller name, and outcome settlement retains the same caller key.

Resilience and Maintainability Implications

  • observed — Admission and conditional claim now occur in one durable transaction before launch effects. Claims transition pending operations to unknown; fingerprint conflicts are refused. Successful launches record their result, while possible-effect failures or failed settlement remain uncertain rather than being automatically executed again. The terminal delivery guard is disposed on success and failure.

Hardening Proposals

  • proposed — Give every delayed write, including retry Enter, the same final permission, cancellation, and lifecycle checks as the initial submission. Consider limiting retry to evidence that the original input remains unsubmitted, and distinguish unknown foreground ownership from confirmed ownership rather than relying solely on absence of a proven shell.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 103 functions across 80 files. (3 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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 clearly identifies the primary change: routing source-control AI-button launches through agent.launch. It is concise and specific.
Description check ✅ Passed The description is comprehensive and follows the repository template. It explains the user-facing behavior, implementation, rationale, testing, visual proof, compatibility considerations, and known li…
Full details: Docstring Coverage

Explanation

Docstring coverage is 41.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 103 functions across 80 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

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.

@brennanb2025
brennanb2025 marked this pull request as draft September 30, 2026 00:27
brennanb2025 added a commit that referenced this pull request Sep 30, 2026
Main now ships daemon protocol 38 for pushed host colours, so launch-line
staging moves to 39 and 38 joins the attachable previous versions: a v38
daemon keeps its sessions and never gets a staged line it cannot run.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status at e8a19c7

Verdict: code review clean; not ready to leave draft until the live re-run below passes. Five review loops plus two architecture reviews covered everything since the last clean review (6889642). The last loop found nothing to fix, and the closing release-readiness pass found no release blockers. Its one new finding is fixed in e8a19c7.

What the review found and fixed (all pushed)

The problem these fixes address: the host must paste a button's prompt only once the launched agent, not the shell that started it, is ready, and must never type it into a shell. The ownership logic added after 6889642 had several holes, each with a user-visible effect:

  • 6d4dd8e, SSH launches were about 8 s late. Every foreground read over SSH came back "cannot tell", so the ready signal never settled. Each SSH launch waited out the agent's whole budget before falling back, where main pasted at about 2 s. The crash check never fired on SSH either.
  • 19a0ff0, the prompt could go in before Claude was ready (a hand-off rewrite). Any program run from a shell startup file (a conda or pyenv hook) released the hold early, and so did a second shell prompt before the launch line. The shell's own "ready for paste" signal then counted as the agent's, so the prompt could go in before Claude was ready. The hold, the "agent must be seen in front" requirement and the 250 ms retry are replaced by two rules. First, only output after the shell's last hand-off (the point where it turns bracketed paste off to run a command) counts. Second, a ready signal is dropped only while a shell is proven to be in front. This removes code, and a crashed agent now costs one process check instead of one every 250 ms.
  • 7bada25 and fb5417e, npm agents on Windows and crash protection on the local daemon. On Windows, the process scan names the pane's shell for any agent it cannot recognize (Copilot, Qwen Code or an npm-installed Claude, each running as node.exe). So those agents never got their prompt, where main delivered it. The first fix switched off crash protection on the local terminal daemon, which serves local panes by default. That daemon's shell check reads recovery state, and a plain exit leaves that state unset. Now macOS and Linux use a fresh foreground scan, and Windows uses the job-based shell check.
  • e8a19c7, npm agents on an SSH host running Windows. The same misread happened on an SSH pane whose remote host is Windows. There the relay's name now proves nothing, the same rule a local Windows pane follows.
  • a82b993, dead code. Removed a dialog error constant that nothing read.

Every fix has a test that fails without it (checked by removing each guard). 29 named test files pass (1775 tests), and the node typecheck, oxlint and oxfmt pass. The PR body is updated to match the code and labels each difference from main as Intended or Temporary.

What still needs doing before ready

  • Live re-run at e8a19c7 (required). The launch wait was rewritten at 19a0ff0, so the earlier live pass at e6d62dd no longer covers it. Re-run the four buttons with Claude, Codex and OpenCode on macOS, plus these cases: an agent that exits at startup (it needs a macOS daemon pane), a quiet agent, an agent that never enables bracketed paste, a shell startup file that runs a program, and one orchestration worker start. On the common path (zsh, then Claude, Codex or OpenCode), the timing is expected to match e6d62dd.
  • SSH run if a target is available: Claude pastes on the ready signal, and an agent that exits at startup is refused.
  • Windows smoke run, with an npm agent (Copilot or Qwen Code) as well as Claude.

Known limits, labelled Temporary in the body (each is the same as main)

  • An agent that exits at startup. On WSL, a Windows pane served by the terminal daemon and an SSH pane on a Windows host, nothing proves a shell is in front. There, such an agent can still get the prompt typed into the shell and submitted, as on main.
  • An unrecognized agent behind a bash wrapper. On macOS and Linux, an agent the process scan cannot recognize, run by a bash script that does not exec it, reads as a shell. Its prompt is not sent (Copy prompt is shown), and it is never typed into a shell. The follow-up is to read the shell's own command start and end marks first.
  • Planned change for Claude and Codex. The paste path is temporary for Claude and Codex: Carry an agent's first prompt on its command line instead of pasting it (step 3 of 7) #24080 moves their prompts onto the agent's own command line. It stays for agents without a prompt argument and for worker starts.

@brennanb2025

brennanb2025 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Live QA re-run at 9b2ae19: three of the four "worse than main" items are fixed; the tab still appears later on a first launch and over SSH

What was re-run. The final run at 27d59430d76 (below) found four things worse than main: a worker start submitting its brief ~1.6 s late, Enter sometimes trailing the paste (up to 569 ms locally, a relay round trip over SSH), the new tab appearing later after the click, and Qwen Code on Windows left unsubmitted. Only those rows were re-run: this PR at 9b2ae19ca28 against the same main d74388f8a2d, same machines, rigs and start states.

Result.

  • Worker start now submits at main's time.
  • Enter now follows the paste after about 50 ms, the same as main, both locally and over SSH.
  • Qwen Code on Windows is submitted exactly once.
  • Every prompt was still delivered exactly once, and nothing was ever typed into a shell.
  • Still behind main: how soon the tab appears after the click. On Windows the first launch after an app start takes 2.68 s vs main's 0.21 s, though Claude itself is up at 3.0 s on this PR vs 12.6–14.7 s on main, where the prompt never arrives. Over SSH the tab is about 65–85 ms later.
Machine Row main This PR at 27d5943 This PR at 9b2ae19 Verdict
macOS Orchestration worker start, brief submitted (3 runs each) 8.38–8.45 s 9.96–10.04 s 8.37–8.38 s fixed
macOS Explain, Enter after paste (4 tapped runs each) 52–54 ms 53, 569, 53, 165 ms 52–55 ms fixed
macOS Explain, click to prompt (median) 2.30 s 2.16 s 2.19 s not slower
macOS Explain, prompt after Claude's ready title 1.58–1.62 s 1.57–2.40 s 1.60–1.88 s (3 of 6 runs at 1.82–1.88 s) minor; click to prompt still faster
macOS Crash stub typed into zsh (earlier rounds) nothing typed nothing typed; "prompt wasn't sent" + Copy prompt at 60.2 s PASS
SSH Enter after paste (load-matched) 91–131 ms ~45 ms more than main 92–125 ms fixed, round trip gone
SSH Click to submitted 2.36–2.52 s 2.35–2.43 s 2.35–2.55 s same as main
SSH Click to tab 207–272 ms 269–334 ms 291–437 ms still behind
Windows Qwen Code 0.24.7, Explain (2 runs) never pasted pasted, never submitted (4 of 4) submitted once, 2 of 2 fixed
Windows Claude 2.1.286, Explain prompt never arrived (2 of 2) submitted once submitted once, 1.72 s after ready PASS, better than main
Windows First launch after app start, click to tab 0.21 s 2.77 s 2.68 s (first Qwen launch 1.64 s; later launches 0.20 s) still behind

Open observation, not verified as a product issue: in macOS runs where a timestamping wrapper (script -r) sat in front of Claude, this PR's finished answer showed a stale "paste again to expand" footer and odd wrapping. Runs without the wrapper, on both sides, displayed cleanly.

Windows: Qwen Code with the prompt submitted once and the input box empty afterwards (the error line comes from the fake endpoint the test used).
r9b2-win-pr-qwen-submitted-once.jpg

macOS crash stub at 9b2ae19: nothing typed into zsh, and the Copy prompt notice is up.
r9b2-m4-pr-crash-stub-notice.jpg

SSH: the Explain prompt submitted to Claude on the remote host.
r9b2-ssh-pr-explain-submitted.jpg

Build proof at 9b2ae19: the app reported branch 9b2ae19ca2, and literals only the new commits add (admitAndClaimOperation, shellMayHaveReturned, Qwen's submitRetryDelayMs:1200) appear only in this PR's bundle. Real agent config files: unchanged on every machine. Every rig process was stopped by its recorded process ID.


Earlier: final live QA at 27d5943: prompts delivered once and never typed into a shell; four items worse than main, none lose a prompt

What was tested. This PR at 27d59430d76 against origin/main d74388f8a2d, from the same start state on each machine: macOS (M4Air), SSH from a Mac to an Ubuntu host, and Windows 11 (PowerShell panes). Terminal mode with a focused empty split, plus one chat-mode run and one orchestration worker start. The bar: terminal behaviour the same as main or better, native chat not regressed, nothing ever typed into a shell.

Result. On every machine, every prompt the PR delivered arrived exactly once, and nothing was ever typed into a shell. Chat mode is not regressed. Four timing items are worse than main (below), but none of them loses or doubles a prompt. Three cases are better than main:

  • An agent that crashes at startup no longer gets the prompt typed into its shell.
  • Windows Claude now gets its prompt; main lost it in 2 of 2 runs.
  • The fallback paste for an agent that never shows readiness now goes out in one write, where the last run split it about 1 s apart.

Worse than main (read first)

  1. Orchestration worker start submits its brief about 1.6 s later (macOS, 3 of 3 runs per side). Main submits at 8.38–8.51 s, this PR at 9.96–10.04 s. Main pastes the brief when Claude turns on bracketed paste; this PR pastes 1.6 s after Claude's ready title. Both then wait the full 8 s before Enter, because orchestration keeps the gate that waits for the screen to stop redrawing. So the later paste means a later submit (read from the input tap, not ablated). The brief is still delivered once and submitted.
  2. Enter sometimes trails the paste. This is the "Known, minor" item in the body, now measured.
    • macOS with an input tap: Enter came 50–54 ms after the paste on main (6 of 6 runs); on this PR it came 53, 569, 53 and 165 ms after in 4 Explain runs.
    • SSH: about +45 ms, which is one relay round trip.
    • Click to prompt is still not slower overall, because this PR starts Claude earlier. macOS Explain median: 2.16 s here, 2.32 s on main.
  3. The new tab can appear later after the click.
    • Windows: 2.77 s on the first launch after the app started, and 1.21–1.22 s on the first two Qwen launches; 0.20–0.22 s otherwise. Main took 0.17–0.21 s every time.
    • SSH: 269–334 ms, against 145–213 ms on main.
    • Not root-caused.
  4. New bug found, but main does worse: on Windows, Qwen Code's pasted prompt is never submitted (4 of 4 runs), and no "prompt wasn't sent" notice appears.
    • Cause, checked in Qwen Code 0.24.7's published bundle: on Windows, Qwen ignores Enter for 500 ms after a paste (pasteWorkaround: process.platform==="win32"||…, then setTimeout(()=>{setRecentPasteTime(null)},500)).
    • Orca presses Enter 50 ms after the paste, and only Codex gets a delayed second Enter (submitRetryDelayMs: 1200).
    • Main never pasted on that box at all: Qwen came up at 19 s, after main's paste had given up. Main uses the same 50 ms Enter, so it would fail the same way if it pasted.
    • Suggested fix: give Qwen Code a delayed second Enter, later than 500 ms.

Results

Machine Row main This PR Verdict
macOS Fix checks, Resolve conflicts, commit-failure AI Fix, Explain (Claude 2.1.285) prompt 1.55–1.78 s after ready; toast at click prompt 1.57–1.69 s after ready (two Explain runs at 2.28 and 2.40 s, item 2); toast 0.05–0.22 s after the prompt PASS
macOS Crash stub (agent exits at start) typed into zsh, Enter pressed (earlier rounds) nothing typed; "prompt wasn't sent" + Copy prompt at 60.2 s PASS, better
macOS Stub that never turns on bracketed paste blind paste at ~1.9 s (earlier rounds) one fallback paste at 8.1 s, in one burst PASS
macOS Ready agent under a bash wrapper that doesn't exec it delivered delivered PASS
macOS sleep 2 in .zshrc / a conda-like prompt hook before Claude delivered paste 1.62–1.63 s after ready, always after the shell's hand-off, once PASS
macOS Chat mode, Explain n/a chat in the focused split, focused, prompt once PASS
macOS Orchestration worker start submitted 8.38–8.51 s 9.96–10.04 s delivered; slower (item 1)
SSH Explain + commit-failure AI Fix (Claude 2.1.233), 2 runs each submitted 1.63–1.66 s after ready 1.77–1.80 s after ready (1.5 s quiet window, never the 8 s fallback) PASS
SSH Crash stub not run nothing typed into the remote shell; Copy prompt notice at 60.3 s PASS
Windows Claude 2.1.285, Explain prompt never arrived, 2 of 2 submitted once, 1.73–1.76 s after ready PASS, better
Windows Qwen Code 0.24.7 (npm, runs as node.exe) never pasted not refused; pasted once but not submitted FAIL on both (item 4)

Blocked or not run:

Screenshots

macOS, crash stub: zsh is clean after the stub's error, and the Copy prompt notice is up.
f27-m4-pr-crash-stub-notice.jpg

macOS, a ready agent run from a bash wrapper that doesn't exec it receives the prompt.
f27-m4-pr-nonexec-wrapper-delivered.jpg

macOS, sleep 2 in .zshrc before Claude: the prompt is Claude's first message, submitted.
f27-m4-pr-rc-sleep2-claude-explain.jpg

macOS, chat mode: the chat opens in the focused split with one user message.
f27-m4-pr-chat-explain-in-split.jpg

SSH: the 21-line commit-fix prompt submitted to Claude on the remote host.
f27-ssh-pr-commit-fix.jpg

SSH, crash stub: nothing typed into the remote bash, and the Copy prompt notice is up.
f27-ssh-pr-crash-stub-notice.jpg

Windows, main: Claude is ready and its composer is still empty 90 s after the click.
f27-win-main-claude-prompt-lost.jpg

Windows, this PR: the Explain prompt is Claude's first message.
f27-win-pr-claude-explain.jpg

Windows, this PR: the prompt pasted once into Qwen Code and left unsubmitted (item 4).
f27-win-pr-qwen-pasted-unsubmitted.jpg

How it was run.

  • Build proof on every launch: the app reported the right branch, and agent.launch.prompt-carry.v1 and isLaunchShellInFront appear only in this PR's bundle.
  • Each rig app ran in the background with an isolated home and profile.
  • The real agent config files on all four machines were hashed before and after. They were unchanged, except that the Ubuntu host's ~/.claude.json gained one trust entry for the throwaway fixture repo.
  • Every rig process was stopped by its recorded process ID.

Update: re-run at b57ed11 (launch paste bytes now match main)

Why. At 9b2ae19, runs where a timestamping wrapper (script -r) sat in front of Claude showed this PR's finished answer mis-wrapped, with a stale "paste again to expand" footer. One suspected cause was that this PR's launch paste sent LF line breaks where main's sent CR. b57ed11089a makes the launch paste use the desktop's own paste encoder. Re-run on macOS only, against the same main d74388f8a2d: Explain changes with the wrapper (2 runs on this PR, 1 on main), plus one ordinary commit-failure AI Fix on this PR.

Result.

  • The paste bytes are now byte-identical to main's: 390 B, CR line breaks, no LF, no ESC, same sha on all 3 runs.
  • Enter still follows the paste after 52–53 ms on both.
  • The prompt arrives once and never goes into the shell.
  • Click to prompt is the same as main (2.32–2.39 s vs 2.37 s).
  • The ordinary AI Fix run passed: the prompt went in once, and the toast came 0.13 s after it.
  • Still different: the wrapper-only rendering oddity remains on this PR (2 of 2 runs; main clean), so the LF bytes were not its cause.
    • Claude hooks are ruled out as well: both sides run the same hooks from the same shared settings file.
    • Runs without the wrapper displayed cleanly at 9b2ae19 (2 of 2). A user never runs Claude behind script -r, so there's no evidence users see it, but the cause isn't proven.
    • The one timing difference: this PR starts Claude ~0.3 s earlier and pastes ~0.3 s later after Claude's ready title (1.87–1.93 s vs 1.60 s).

This PR, with the wrapper, after Claude's answer: mis-wrapped text and the stale footer.
rb57-m4-pr-tapped-after-answer.jpg

Main, with the wrapper, after Claude's answer: clean.
rb57-m4-main-tapped-after-answer.jpg

Follow-up: same build without the wrapper. Explain changes with normal Claude (no script -r), 2 runs on this PR and 1 on main at b57ed11089a, screenshotted after Claude finished answering.

  • Both PR runs render cleanly, the same as main: normal wrapping, no stale "paste again to expand" footer, no leftover hook lines.
  • The oddity above appears only with the timestamping wrapper between Orca and Claude, so it is a test-rig artifact, not something users see.
  • Each prompt arrived once and nothing went into the shell.
  • Click to prompt: 2.26–2.34 s on this PR vs 2.29 s on main. The paste still comes 0.2–0.3 s later after Claude's ready title (1.78–1.91 s vs 1.59 s).

This PR, without the wrapper, after Claude's answer (runs 1 and 2):
rb57u-m4-pr-untapped-run1.jpg
rb57u-m4-pr-untapped-run2.jpg

Main, without the wrapper, after Claude's answer:
rb57u-m4-main-untapped.jpg

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review and QA history

The full PR description as it stood at b57ed11 (2026-09-30), moved here so the PR body can stay short. It keeps the round-by-round history, the design alternatives, the per-row QA tables and every screenshot. The current summary is in the PR body; the latest live QA is in the status comment #23748 (comment).


ELI5

The problem. The source-control AI buttons generate long, multi-line prompts (a failing hook's output, CI log tails, a conflict list). They are moving onto the host's agent.launch, which put any prompt on the command line it types into your shell: a multi-line prompt typed into a shell runs line by line (macOS's default bash reads each newline as Enter), a long line can wedge an SSH shell that is still starting, and Windows cmd caps a line at 8191 characters. Separately, on main the buttons' own paste went in blind after 8 s when the agent never showed it was ready, and when the agent crashed at startup the prompt was typed into its shell and submitted.

What this fixes and where. The desktop's source-control AI buttons (Fix checks, Resolve conflicts, commit/push-failure recovery, Explain commit) now start their agent through the host's agent.launch, and the host never types a long or multi-line prompt into your shell. Affects those four buttons on local, SSH, WSL and paired (remote Orca server) desktops, the phone's AI buttons on a host running this version (their long prompts now reach the agent by the same paste after readiness; the phone's own code only moves its launch-refusal rules into a shared file, unchanged), and the first-input readiness wait of local orchestration worker starts (main's cue, plus a quiet-stream check for agents whose only ready sign is a bare name title). Not affected: agent launches from the tab bar, the new-workspace dialog, the CLI, and chats you type into yourself.

The source-control AI buttons (Fix checks, Resolve conflicts and the other source-control actions, commit-failure recovery, Explain commit) used to start their agent by themselves in the desktop window and then paste the long prompt they generate. They now ask the Orca host to start the agent and deliver the prompt, the same way every other launch is moving to. The host is careful about long prompts: it never types them into your shell, and it only pastes once the agent is really ready. If the agent never shows it is ready, the prompt is not pasted anywhere; you get a message with a Copy prompt button instead.

Terminal timing: final live run at 27d5943; its flagged rows fixed and re-run at 9b2ae19; paste bytes matched to main at b57ed11

Final live run at 27d5943 (macOS, SSH to Ubuntu, Windows PowerShell; main at d74388f8a2d as the control): every delivered prompt arrived exactly once and nothing was ever typed into a shell, on every machine; native chat was not regressed. Better than main: an agent that crashes at startup no longer gets the prompt typed into its shell (macOS and SSH), Windows Claude gets its prompt (main lost it 2 of 2), and the no-bracketed-paste fallback writes its paste in one burst. Worse than main, now fixed or explained below: the orchestration worker start's brief (1.6 s later), Enter trailing the paste (up to 0.6 s), the new tab appearing later after the click, and Qwen Code on Windows left unsubmitted.

The first live run of this PR measured a terminal Claude getting its prompt about 6 seconds later than main: 8.4–10.8 s after the click, against 2.4–6.3 s on main. Main's buttons were not broken; this PR had made them slower.

  • Why it was slower (two causes, found in turn). (1) On main, the desktop window pasted as soon as the agent turned on bracketed paste (the terminal mode an agent's input box switches on when it is ready for pasted text) and then either drew its composer marker or went quiet for 1.5 s; this PR had moved the paste to the host, which waited for its general "agent is idle" evidence instead. (2) A re-run at 1be7e04, with (1) fixed, showed the prompt pasted early but submitted only 7.94–8.00 s after Claude's ready title, on every run (main: 1.6–2.0 s): a screenshot at click + 4 s showed it sitting in Claude's composer as [Pasted text #1 +3 lines]. The host's prompt writer (sendTerminalAgentPrompt, main's code, also used by orchestration) presses Enter for Claude and Codex only after their render settles, and a live Claude keeps repainting, so every launch waited out that gate's 8 s cap.
  • What changed (c522fec). The host's launch wait now works like main's desktop paste: it waits for bracketed paste plus the agent's own ready cue (draftPasteReadySignal: its composer marker, or a 1.5 s quiet render), read by the same scanner code the desktop paste uses (src/shared/draft-paste-ready-scanner.ts), within the same per-agent budget (resolveDraftPasteReadyTimeoutMs: 8 s by default). That signal cannot tell a composer from a startup dialog drawn in the same mode, so on the host it counts only while the pane shows no startup dialog and no Codex "model: loading" screen (the protection fix(runtime): wait out Codex 0.157's startup screen, and stop at Codex's startup dialogs, before typing a worker brief #23745 added). A dialog ends the signal's wait at once, so it is reported without waiting out the budget.
  • Intended: after main's budget, a checked fallback instead of main's blind paste. When the signal has not settled by the agent's budget, main pasted anyway once the agent's process existed. This PR instead asks the existing tui-idle evidence ranking (idle titles, known ready screens) for the rest of the 60 s; if that shows no readiness either, nothing is pasted and the user gets "prompt wasn't sent" with Copy prompt. tui-idle is also what reports a dialog left up (trust, update, hooks review), so the launch can wait for the user to answer it. It only ever runs after the signal, never in a race with it: its name-plus-quiet rule can settle before OpenCode 1.x draws its input box, which is earlier than main ever pasted. Correction: an earlier version of this description said Windows ConPTY never passes bracketed paste through; fix(agents): stop claiming an unconfirmed OpenCode handoff succeeded #22546 and docs(tui-agent-config): correct the OpenCode readiness-budget rationale #22593 measured Orca's pinned ConPTY on two Windows hosts (build 10.0.26200) forwarding it in order, so that was wrong. Windows is not re-measured on this commit.
  • Enter after the paste (c88a610). A launch whose composer the host just saw ready now presses Enter the way the desktop's draft paste did: one turn after the paste (the same 50 ms constant, now shared from src/shared/agent-prompt-injection.ts by the renderer and the host), after the existing paste-ingest floor (a few ms on macOS; it only grows on Windows ConPTY for large prompts), plus the agent's configured second Enter (submitRetryDelayMs: Codex, 1.2 s later), as the desktop sent it. Prompts into a running or reused agent (orchestration, terminal send, a launch into an existing pane) keep the render-settle gate, unchanged.
  • The signal comes from the agent, not its shell. A re-run at c88a610 found the launch sometimes pasting before Claude was ready: once, the prompt and its Enter went in 2.7 s before Claude's ready title, the Enter was lost, and the 61-line prompt sat unsent in Claude's input while the success toast said it was delivered. The cause: the interactive shell that runs the launch line turns bracketed paste on at its own prompt (captured from zsh 5.9: zsh-prompt-runs-command.txt), and that was read as the agent's signal; main's desktop paste reads the stream the same way.
  • How the host tells them apart now (19a0ff0). A shell turns bracketed paste off (ESC[?2004l) when it runs the command you typed. The host counts only the output after the most recent such hand-off and starts reading afresh at each one, so the shell's own earlier prompt never counts, nor does a second prompt the shell draws before the launch line runs; a hand-off split across two chunks of output is still seen. When the ready signal fires, the host asks one question: is a shell proven to be in front of the terminal? Only positive proof counts (the launch line has not run yet, or the agent exited); the signal is then dropped, and the agent's own later signal asks again. A check that cannot answer, or that finds any other process in front, proves nothing, so the signal settles, as main's did. If the shell hands over again while that question is being answered, the answer is thrown away. Right before the paste, before Enter and before Codex's second Enter, the same question is asked, and a shell proven in front refuses the write. There is no holding of output, no polling before the signal and no retry timer.
  • How "a shell is in front" is read (97fbf52, 27d5943): the shell's identity first, then its name. The question is whether the pane's own shell (the one the host spawned) holds the terminal, not whether the process in front is named like a shell. The cached foreground name still settles it at once when it names a process that is neither a shell nor the agent itself (a native Claude is named by its version, 2.1.285). Otherwise, on macOS and Linux, the host's existing process inspection (the same call the terminal daemon and the SSH relay already answer for other features, no new request) reports the terminal's foreground process group and the pane's shell process id; when that group is not the shell's own, no shell is in front, and when it is, a fresh foreground read must also name a shell (a shell without job control runs the commands it starts in its own group, so the group alone would read an agent under it as the shell). Identity can therefore only veto the name rule, never widen it, so this check never answers "shell" where the name rule would not. A shell with job control runs every command it starts as its own group, so a bash wrapper that does not exec its agent, a conda or pyenv hook, or a setup script is never read as the shell, even though some are named like one; the shell back at its prompt (the launch line not run yet, or the agent exited) is. Captured on macOS 26 and pinned in the tests: while zsh -f -i ran bash -c 'sleep 4; :', zsh (pid 20894) saw foreground group 20996, the wrapper's; back at its prompt it saw 20894, its own. Why not the name: an earlier version of this PR read a foreground named bash or zsh as the shell, so an agent run by a non-exec bash wrapper was not sent where main delivered it; the identity read removes that. On Windows there is no foreground process group, and the process scan answers with the pane's shell (powershell.exe) whenever it cannot recognize the agent in front, which is every npm agent running as node.exe (Copilot, Qwen Code, an npm-installed Claude); trusting that name (up to 19a0ff0) refused every write for those agents (found by code reading and a unit replay of the Windows scan, not live). Windows therefore asks only the host's shell-foreground check (is the shell alone in the pane's process job). The terminal daemon answers that check from its terminal-recovery state, which a plain exit leaves unset (7bada25 used it on every platform, which switched crash protection off on daemon panes; fb5417e fixed that), and an SSH pane has no such check, so on a Windows daemon pane and an SSH pane on a Windows host nothing proves a shell (see the Temporary notes below). Temporary, older hosts: a terminal daemon or SSH relay that predates the inspection's foreground evidence, and a local pane outside the daemon (the non-default setting), return no identity; there the host falls back to the name (a fresh foreground scan locally, the relay's live name over SSH), which still reads an unrecognized agent under a non-exec bash wrapper as the shell (Copy prompt, never typed into a shell). For an older daemon or relay this ends when it is replaced by a current one; for a pane outside the daemon it stays until that provider returns the same evidence (no ticket yet). SSH was not re-measured live.
  • What the earlier design did, and why it was replaced. From d7d03a9 to e6d62dd the host held output until the shell's first hand-off, required a read naming the agent before settling, and retried a refused signal every 250 ms. Each held a stale answer: a conda or pyenv hook released the hold early, a second shell prompt before the launch line counted as the agent's, SSH launches waited out the 8 s budget (fixed in 6d4dd8e), and a crashed agent drove a scan every 250 ms. 19a0ff0 replaced all three with the two rules above; common-path timing is unchanged.
  • Temporary: WSL gets no shell check. A WSL pane's process is wsl.exe, which neither its name nor the host's shell-foreground check counts as a shell, so on WSL the host relies on the stream hand-off and the checked fallback alone. An agent that exits at startup there can still get the prompt typed into its shell and submitted, exactly as on main. Closing it needs a foreground read inside the distro; no follow-up ticket yet. WSL was not measured.
  • Temporary: a Windows pane served by the terminal daemon gets no shell check. Its process scan cannot tell the shell from an unrecognized agent, and the daemon's shell-foreground check answers from its recovery state, which a plain exit leaves unset, so on such a pane nothing proves a shell and the host relies on the stream hand-off and the checked fallback alone, as main had no check at all. A Windows pane outside the daemon gets the job check. Closing it needs a daemon request that returns the shell-alone-in-its-job proof directly; no follow-up ticket yet. Not measured on Windows.
  • An agent that exits at startup: nothing is typed into the shell (macOS and Linux, local and SSH to a macOS or Linux host; not WSL, a Windows daemon pane or an SSH pane on a Windows host, see above). Main typed the prompt into the pane's shell and pressed Enter (the re-run saw zsh reject it only because it did not parse; the prompt carries untrusted commit text). Verified live at ee3cad5 and e6d62dd: nothing typed, and "prompt wasn't sent" with Copy prompt at about 60 s (the launch's full budget; it is not reported sooner, because a shell in front right after a signal can also mean the launch line has not run yet). Verified again at 9b2ae19 (nothing typed, notice at 60.16 s); the shell's next prompt now makes one check, which refuses it, instead of a check every 250 ms (7bada25 would have failed this row on a daemon pane).
  • Resolve conflicts timing: resolved. At d7d03a9 Resolve matched main; rig timestamps showed Claude starting 0.15–0.25 s earlier than on main. The earlier slowness is not reproduced and was not traced to code.
  • Live result, verified PASS at e6d62dd, affected rows re-run at 9b2ae19 (see the bullets above) (macOS, Claude 2.1.285, terminal mode, focused empty split, main at fa3710df5ab as the control): all four buttons deliver the prompt 1.58–1.86 s after Claude's ready title (1.58–1.65 s for Resolve conflicts, commit-failure Fix with AI and Explain changes); main at the same low load took 1.59–1.66 s. Click to prompt: Resolve 2.25–2.34 s (main 2.49 s), commit fix 2.08–2.12 s (main 2.27 s), Explain 2.10–2.13 s (main 2.30 s), Fix checks 4.73–7.69 s including its CI-log fetch. 8 of 8 PR runs: one user message, never left unsubmitted, never on the command line, byte-identical to main, success toast 0.05–0.19 s after the prompt. An agent that exits at startup: nothing typed, "prompt wasn't sent" with Copy prompt at 60.2 s. An agent that never enables bracketed paste: one paste through the checked fallback at about 10.8 s. Earlier rounds, for the record: d7d03a9 matched main for Claude but pasted into a crashed agent's shell; ee3cad5 fixed that but made Claude wait 6.5–13.6 s after ready (a single ownership check that could not yet answer dropped the signal); e6d62dd fixed that with a retried check, which 19a0ff0 replaces by settling unless a shell is proven in front.
  • Temporary for Claude and Codex; intended for everything else. The stacked Stage long agent launch lines where they are typed, so they arrive whole (step 2 of 7) #23962 → Carry an agent's first prompt on its command line instead of pasting it (step 3 of 7) #24080 → Start orchestration workers with their brief on the launch command line (step 4 of 7) #24081 puts the prompt on the agent's own command line through a staged launch file. Once Carry an agent's first prompt on its command line instead of pasting it (step 3 of 7) #24080 lands, Claude and Codex button prompts ride that line and this paste path stops serving them, so the paste timing work above is temporary for those two agents. The wait stays, permanently, for agents whose CLI takes no prompt argument (they read the prompt only after they start), which the buttons launch when that is your chosen agent, and for every fresh local orchestration worker start, whose first dispatch uses the same wait for every agent.
  • Enter after the paste: back to main's 50 ms (5bc2d3f). The final live run at 27d5943 measured Enter trailing the paste by 53, 569, 53 and 165 ms on a loaded Mac (main: 50–54 ms), and 2 of 4 untapped runs submitting about 0.7 s late; over SSH the submit came about 45 ms later than main, one relay round trip. The cause was that every write (the paste, its Enter, Codex's and Qwen's second Enter) re-ran the full "is a shell in front?" read. Now the read before the paste stands for the writes after it until the terminal shows a shell coming back to its prompt: a bracketed-paste toggle (ESC[?2004) or a shell-integration prompt mark (OSC 133), watched from before the read starts and across chunk splits (src/main/runtime/launched-agent-write-guard.ts). An agent that exits between the paste and Enter hands its shell back, whose prompt shows one of those marks, so the Enter still gets a fresh read and is refused. Temporary, narrow: a shell that at its prompt writes neither mark (no bracketed paste and no Orca shell integration, such as a plain bash 3.2 without Orca's wrapper), when the agent exits in the ~50 ms between the paste and Enter, is not caught by the reused read; the paste itself was still checked. Main checked neither. Verified at 9b2ae19: Enter − paste 52–55 ms on macOS (main 52–54 ms); over SSH the extra round trip is gone.
  • Orchestration worker start: back to main's cue (5bc2d3f). The final live run at 27d5943 measured a Claude worker's brief submitted about 1.6 s later than main (9.96–10.04 s after worker-start, against 8.38–8.51 s). Main pasted the brief on tui-idle as soon as Claude drew its ready title (about 0.4 s); this PR had moved the worker start onto the launch paste's signal (bracketed paste, then a 1.5 s quiet render). Both arms then wait out the 8 s render-settle gate before Enter, so the later cue delayed the submit by the same 1.6 s and bought nothing. A fresh worker start now waits on tui-idle again (waitForWorkerStartComposer), keeping one rule from this PR: a bare name-only title (Gemini, Copilot, Grok) counts only once the stream goes quiet, because a shell's auto-title writes that name before the agent has started. Composer-marker agents (ZCode, DSH, Grok) still wait for their marker. The shell's-own-bracketed-paste problem does not apply here: tui-idle reads titles and screens, not bracketed paste. The button launches keep the desktop paste's signal. Pinned by a Claude capture test that settles before the quiet window (fails with the old cue). Verified at 9b2ae19: brief submitted 8.37–8.38 s after worker-start (main 8.38–8.45 s).
  • Qwen Code on Windows: now submitted (5bc2d3f). The final live run found the Explain prompt pasted once into Qwen Code 0.24.7 on Windows and never submitted (4 of 4). Qwen ignores Enter until 500 ms after a paste on Windows and on Node < 20 (its pasteWorkaround, read in the published 0.24.7 bundle), and Orca presses Enter 50 ms after the paste. Qwen Code now gets the delayed second Enter Codex has (submitRetryDelayMs: 1200 in src/shared/tui-agent-config.ts); where the first Enter already submitted, the second lands on an empty composer, which Qwen ignores (same bundle: Enter acts only when the buffer has text). Main never pasted into Qwen on that machine, and main's desktop paste uses the same 50 ms Enter, so this is a fix for both. Checked the other agents: Gemini CLI 0.61 (Qwen's upstream) has a weaker version, turning an Enter within 40 ms of a paste into a newline when the terminal does not use the kitty keyboard protocol; Orca's Enter comes 50 ms after the paste, and no Orca run has shown it, so Gemini is left unchanged. No other configured agent is a fork of it. Verified at 9b2ae19: submitted once, 2 of 2, composer empty afterwards.
  • Launch paste bytes: now identical to main's desktop paste (b57ed11). The final live run saw the host's paste send line breaks as LF where main's desktop draft paste sent CR. The host had framed a launch prompt with the orchestration encoder (buildAgentPromptPasteBytes: line breaks kept as LF, an embedded ESC written as the text <ESC>); main's draft paste used wrapTerminalBracketedPasteText (every line break as CR, which is what xterm's own paste sends, and an embedded ESC as U+241B). That function and its line-ending and ESC rules moved unchanged from the renderer to src/shared/terminal-bracketed-paste-text.ts; the renderer re-exports it and the host's launch prompt calls it, so both send the same bytes for the same prompt. Orchestration dispatch keeps its own encoder, as on main. A runtime test compares the host's paste bytes for a multi-line prompt with an embedded ESC against that function and against main's literal frame (it failed before this commit), and a renderer test pins that the renderer's export is the same function. Whether the LF caused the stale "paste again to expand" footer seen in tapped runs is not verified; one tapped re-run is pending.
  • The new tab can appear later after the click (one step trimmed; the rest is a known cost of host launch). The final live run at 27d5943 measured click → tab on Windows 2.77 s on the first launch after the app started and 1.21–1.22 s on the first two Qwen launches (0.20–0.22 s otherwise; main 0.17–0.21 s), on SSH 269–334 ms (main 145–213 ms), and on macOS 0.21–0.31 s (main 0.15–0.25 s). Cause: on main the button created the tab in the window at the click and the tab's pane spawned its terminal afterwards. With agent.launch the host creates the terminal, and the window shows the tab when the host reveals it, which is after the host has spawned the terminal. So everything the host does first now sits before the tab: the replay-ledger write, a worktree scan when its 30 s cache is cold (one relay round trip on SSH), spawn preflight (Claude auth, a trust write for Claude, Codex and Copilot), and the spawn itself (ConPTY on Windows, a relay round trip on SSH). On a profile with no saved structured-chat store (the Windows rig's fresh profile), the first launch also starts the structured-session host to reach the replay ledger, which fits the 2.77 s outlier (not measured step by step). None of that work is new; main did it after its tab was already on screen. Re-run at 9b2ae19: Windows first launch 2.68 s (Qwen's first 1.64 s; later launches 0.20 s, main 0.18 s), SSH 291–437 ms against 207–272 ms (about +65–85 ms, was +100 ms), macOS about the same. Trimmed (9b2ae19): the ledger used to admit the launch operation and then claim it in two durable transactions (each a file lock, a re-read and a temp write, fsync and rename); both now run in one transaction, with the same conditional claim deciding the one caller that may run the launch (admitAndClaimAgentSessionOperationInto), so one write sits before the tab instead of two. Not trimmed: reaching the ledger without starting the whole structured-session host. The ledger lives in the structured sessions' record store, which is also their lease authority; opening a second copy of it outside the host marks every saved lease unreconciled and writes that on its first transaction, which could race the host's own lease reconciliation. Doing it correctly means splitting the host's install so the store opens first and is shared, which is a refactor, not a trim. Temporary, follow-up (its own PR, no ticket yet): the window shows a placeholder tab at the click and the host's terminal binds into it, which is the only change that puts the tab back at the click for every host-launched tab; the store-first host install can go with it. The Windows Qwen 1.21–1.22 s launches are not explained: they ran in the same app process as the Claude runs, so the structured-session host was already running, and Qwen has no trust write; one of those runs' timing watcher also failed, so the figure rests on one run's sampler (not pursued further). Prompt timing measured from the agent's ready title is unaffected, and click → prompt was still no later than main in the final run (macOS Explain median 2.16 s against 2.32 s).

Before/after: main vs this PR, terminal mode and native chat

A live run on a separate Mac (2026-09-29): current main against this PR, the same four buttons, the same start state (Terminal 1 on the left, a focused empty split on the right), real Claude 2.1.285, once with native chat off (terminal mode) and once with structured native chat on. Every row that differed was run twice on both sides.

Button Mode main This PR (as QA'd, 6889642) This PR at e6d62dd (re-run pending at 9b2ae19)
Resolve conflicts terminal tab in the split; prompt at 6.3 s tab in the split; prompt at 8.7 s prompt 2.25–2.34 s after the click (main at the same load: 2.49 s)
Commit failure → Fix with AI terminal tab in the split; prompt at 2.4 s; success toast at the click tab in the split; prompt at 8.4–8.9 s; success toast after the prompt prompt 2.08–2.12 s (main 2.27 s); success toast 0.05–0.19 s after the prompt
Explain changes terminal tab in the split; prompt at 2.4–2.6 s tab in the split; prompt at 8.6 s prompt 2.10–2.13 s (main 2.30 s)
Fix checks terminal split removed, tab next to Terminal 1; prompt at 6.3 s same placement; prompt at 10.8 s prompt 4.73–7.69 s including its CI-log fetch; placement unchanged (pre-existing, see below)
Resolve conflicts chat chat in the split, focused chat next to Terminal 1, split left empty (2 of 2) fixed at 90eb540: chat in the split, focused, 2 of 2 (live re-run 2026-09-30)
Commit failure → Fix with AI chat chat in the split, focused chat next to Terminal 1, split left empty (2 of 2) fixed: chat in the split, focused, 2 of 2 (live re-run 2026-09-30)
Explain changes chat chat in the split, focused chat next to Terminal 1, split left empty (2 of 2) fixed: chat in the split, focused, 2 of 2 (live re-run 2026-09-30)
Fix checks chat split removed, chat next to Terminal 1 same unchanged (pre-existing, see below)
Explain changes, focused split holding a terminal chat chat in that split chat in that split chat in that split (re-run)

Same on both sides in every row: the prompt was the agent's first message, arrived exactly once, and was byte-identical between main and this PR. It was never on the agent's command line in terminal mode. Native chat was already working on main (the prompt goes in as the chat's first message and the chat takes focus), so for chat mode the bar is "nothing regressed". The chat rows are not affected by 19a0ff0, which changes only the terminal wait.

Fixed in this PR since that run: a chat from a button left the focused empty split empty. The button reserved its tab group only under the terminal tab id it minted. A chat the host starts arrives through the session-tabs mirror (the renderer code that turns the host's list of tabs into window tabs) under a different id, agent-session:<sessionId>, so the reservation never matched and the mirror filed the chat in the host's group, next to Terminal 1. On main the desktop made the chat tab itself, in the split. Now the launch's one reservation is keyed by every id its tab can arrive as (the terminal's tab and the chat's tab), and the mirror puts a chat it has never seen into the group its launch reserved. A tab already on screen keeps its place, so a chat the user drags elsewhere stays there. Live re-run on the same Mac at 90eb540 (same seed and fixtures): all three buttons put the chat in the focused empty split, focused, with the composer ready, twice each; the prompt arrived once with the same text as before; no group was removed and no warning appeared. Terminal mode and Fix checks were not re-run (this fix does not touch them).

Pre-existing, not caused by this PR: Fix checks from a focused empty split. On both main and this PR, in both modes, the window's focused group moves off the empty split about 0.1 s after the click, before either side reads which group to use. Fix checks then reveals its workspace before launching (the only button that does), and that reveal removes the empty split, so the agent opens next to Terminal 1. The reservation cannot help, because by then the focused group is already the other one. What moves the focus at the click is not yet known (the QA set the split's focus through app state only, which a real click might not reproduce). The earlier "split kept" results for Fix checks below are corrected accordingly.

Intended: the success toast now waits for the prompt. "Started an AI agent for …" after commit-failure recovery and Fix checks now appears once the host reports the prompt delivered, not at the click. Main showed it before the prompt was sent, so it could claim success for a prompt that never arrived; this PR shows it only once it is true, and shows the Copy prompt notice instead when the prompt was not delivered. Measured at e6d62dd: the toast appears 0.05–0.19 s after the prompt, which itself arrives at or slightly before main's time.

What Changed

Before. Each button built a generated prompt (a failing hook's output, CI log tails, a conflict list), opened a tab itself, and pasted the prompt as soon as the agent turned on bracketed paste and drew its screen (2.4–6.3 s for Claude in the before/after run), waiting at most 8 seconds. If the agent had not shown it was ready by then, the desktop pasted anyway once the agent's process existed, readiness or not. Separately, the host's own launch command (agent.launch, which these buttons are moving onto) folded any prompt into the command line it types into your shell, with no size or line limit. Multi-line text typed into a shell breaks there: macOS's default bash reads each newline as Enter, a long single line can wedge an SSH shell that is still starting, and Windows cmd caps a line at 8191 characters.

After, for the user.

  • The buttons start the agent through the host. The new tab still lands in the tab group you used the button from and takes focus as soon as it appears, including on a desktop connected to a remote Orca server, where it opens in the view mode the button chose.
  • Long or multi-line prompts are never typed into your shell. The agent starts clean and the host pastes the prompt once the agent shows it is ready.
  • If the agent shows no sign of being ready within 60 seconds, nothing is pasted. A toast says "The agent started, but your prompt wasn't sent. Copy it and paste it once the agent is ready." with a Copy prompt button. This applies on every platform; there is no blind paste anywhere.
  • If the agent exits at startup and its shell comes back, the prompt is not typed into that shell (macOS and Linux, local and SSH panes; WSL and a Windows pane served by the terminal daemon are the exceptions, see the two "Temporary" notes above), and you get the same Copy prompt notice.
  • If a trust or update dialog is up, the launch waits for you to dismiss it (within the same 60 seconds) instead of giving up at once, and never types into the dialog.
  • Explain commit now reports its own launch source in telemetry instead of posing as a tab-bar quick launch.
  • The Fix checks and commit/push-failure recovery buttons keep their spinner, and stay disabled, until the host reports the prompt pasted (the same moment main pasted, see "Terminal timing" above; up to 60 s when the agent shows no readiness); before, they cleared as soon as the tab opened. The tab itself still appears and takes focus within a fraction of a second. Their success toast appears once the prompt is delivered, on purpose (see "Intended" above).
  • If your default for Claude or Codex is a native chat, the host starts a chat, commits the prompt as its first message and focuses it, as before, in the tab group you used the button from. Arguments saved on the action (for example --model) don't apply to a chat, as before, but now show a warning.

Mechanism.

  • The terminal that builds the launch command now measures the actual typed line (quoted, with the launcher and its arguments). The prompt rides that line only when the line has no line breaks or other control characters (a TAB or ESC typed into a shell acts as a keystroke) and is at most 512 bytes; otherwise the agent starts clean. The terminal reports which happened, and the launch pastes only when the line did not carry the prompt. Before, the launch guessed from the agent type alone. The 512-byte figure is half of macOS's 1024-byte limit on a line typed into a shell that is still starting; in a simulation of Orca's SSH startup write, 1 KB lines wedged the shell and 512 bytes did not.
  • Hermes carries its prompt in an environment variable rather than on the typed line, so it keeps doing that; above its own 24,000-character budget it now starts clean and gets a paste instead of failing the launch.
  • One readiness wait, waitForLaunchedAgentComposer, serves agent.launch; the local orchestration worker start uses its sibling waitForWorkerStartComposer (main's tui-idle cue, see "Orchestration worker start" above), sharing the composer-marker lane. The launch wait waits for the desktop paste's own signal (bracketed paste, then the agent's composer marker or a 1.5 s quiet render) within the agent's desktop budget, reading only the output after the shell's last hand-off, and counts the signal only while the pane shows no startup dialog, no Codex provisional header, and no shell proven in front (the runtime's isLaunchShellInFront, which delegates to isShellInFrontOfLaunchedAgent in the typed module src/main/runtime/launched-agent-shell-foreground.ts). After that budget, or at once when a dialog is up, it falls back to the tui-idle evidence ranking for the rest of the 60 s, which also reports a dialog. ZCode and DSH wait for their composer marker alone, and Grok for its marker or its inline quiet window, since tui-idle cannot read them. An agent that shows neither comes back not ready and keeps its prompt. A launch into a reused terminal waits for plain idle instead, since its composer marker may have scrolled away. A settled composer returns a real wait result, so a worker start records its wait-for-setup outcome whichever way it settled (before, the composer lane returned nothing and skipped that record; on main that affected ZCode workers). worktree.create's draft paste uses the same wait function without the shell rules, and behaves exactly as on main.
  • A shell that sets the window title to the command it runs (zsh preexec auto-titles, common in shell frameworks) writes the agent's bare name before the agent has drawn anything. For a launch, a title that is only the agent's name now counts only after the output has gone quiet. Separately, a bare gemini title was being rewritten into Gemini's explicit "idle" glyph, which made it strong readiness; that rewrite now happens only when Gemini itself put the glyph there. Later waits (not launches) keep settling on the bare name, as agents that never go quiet need.
  • On the desktop, the button mints the pane id (and, for Claude or Codex, the chat session id) it sends to agent.launch and records one reservation of where that tab goes, under both ids the tab can arrive as. The local host's terminal reveal reads it by the pane's tab id and focuses the tab; the session-tabs mirror reads it by the chat's tab id and puts a new chat in that group; the view mode rides the launch as a new optional viewMode, which the host applies to the terminal it creates. A reservation dies when the reveal consumes it, when the launch settles either way, after 120 seconds, or on window reload. On a desktop connected to a remote Orca server, the button records the same focus intent and group placement a paired terminal create already records, so the tab mirror places and focuses the tab as it arrives; a chat reply moves the focus intent to the chat's tab. Decision (coordinator, overridable): the paired-host placement and focus are fixed in this PR rather than keeping paired hosts off agent.launch.
  • The host now selects the new tab for a paired caller (a phone, or a desktop connected to a remote Orca server) as soon as the tab is published, instead of after the prompt is delivered, which could take up to 60 seconds.
  • If the launch reply is lost (timeout, dropped connection), the desktop resends the same request with the same operation id up to three times (also when a paired host's timeout or dropped connection comes back as an error reply), so the host answers from its ledger instead of starting a second agent. After that it says "Couldn't confirm the agent started. Check the workspace before trying again." The rule for what a refusal means is now shared by the desktop and the phone (src/shared/agent-launch-replay-refusal.ts).
  • The desktop renderer talks to its own host over IPC as a client with no paired device, and replay-protected launches key on a caller identity, so the host refused every desktop launch sent with an operation id. The IPC transport now sets a host-fixed trustedLocalCallerId that no network transport can set, and launch replay keys the desktop's operations on it (the CLI keeps its own key; paired devices keep theirs).
  • Orca removes empty tab groups whenever it tidies a workspace's tabs, and opening the new tab activates the workspace, which tidies first. A tab group that a pending launch is aimed at is now kept until the tab is in it, so a button used from an empty split still puts its tab there.
  • New host capability agent.launch.prompt-carry.v1. The desktop uses the host path only when the host advertises it (and the replay-required capability); an older host gets today's tab paste unchanged.

Why

The goal is that every agent launch, terminal or chat, goes through the host's agent.launch, so the plugin system has one entry point. These buttons deliberately launched clean and pasted after readiness, because their prompts are too large and multi-line for a typed command line. Moving them onto agent.launch as it was would have typed those prompts into the shell.

Alternatives considered:

  • A caller flag on the request ("paste after ready"). Rejected: the failure is a property of the typed line, not of who asked, so one host rule protects every caller. A flag an older host silently ignores would also have needed the same capability gate anyway.
  • Staging the prompt in a file and passing a short pointer on the command line. Planned, not rejected: it needs no new relay operation (the relay already writes files, fs.writeFile and fs.writeTerminalArtifact in src/relay/fs-handler.ts), and the stacked Stage long agent launch lines where they are typed, so they arrive whole (step 2 of 7) #23962 → Carry an agent's first prompt on its command line instead of pasting it (step 3 of 7) #24080 does it by staging the launch line where it is typed, in the terminal daemon, the SSH relay and the in-process terminal provider. It is kept out of this PR because it changes how every launch line is delivered, not only these buttons'. Until it lands, the paste after readiness is what these buttons already did, so moving the paste to the host keeps delivery unchanged for local, WSL and SSH.
  • Carrying the prompt in an environment variable. Rejected for now: it needs per-shell quoting work, and it changes how SSH hosts receive prompt text (Hermes already does it within its own budget).
  • Settling only once the agent is positively seen in front (the design before 19a0ff0). Rejected: over SSH and whenever a process scan is slow, no read can say so, which cost every SSH launch the full 8 s budget, and keeping a refused signal alive needed a retry timer that ran a scan every 250 ms after a crash. Refusing only a proven shell keeps the crash protection with one check per signal.
  • Proving a shell with the host's shell-foreground check on every platform (7bada25). Rejected: the terminal daemon, which serves local panes by default, answers that check from its terminal-recovery state, which a plain exit leaves unset, so it never proved the shell after an agent exited at startup. It is kept only on Windows, where no name can prove a shell.
  • Reading the shell's own command start and end marks first. Possible follow-up, not in this PR. Orca's shell integration on local zsh and bash panes writes a "command started" mark and a "command finished" mark (OSC 133;C and 133;D) into the terminal output around each command it runs, and main already treats the finish mark after a launch as the launched agent having ended (it retires the pane's launch record). Reading those marks first would answer "is the launch command still running?" from the stream itself, with no process read, which would remove the inspection before Enter on those panes, and could give a Windows daemon pane crash protection where PowerShell marks the typed launch line. Not taken here because the identity read already answers the question on every macOS and Linux host that returns inspection evidence, and the marks would be a third source beside it rather than a replacement (an SSH pane's zsh writes none). No ticket yet.
  • Where this differs from how other tools handle it:
    • The overflow goes to a paste after readiness rather than onto the agent's command line through a staged script or file. Temporary for Claude and Codex, removed by Carry an agent's first prompt on its command line instead of pasting it (step 3 of 7) #24080; intended for agents whose CLI takes no prompt argument and for orchestration worker starts, where no command line can carry it. The cost is that delivery depends on the agent showing readiness, which is why the no-evidence case is explicit.
    • Readiness is inferred from bracketed paste plus a composer marker or a 1.5 s quiet window. Intended: it is main's own desktop-paste signal, kept so the timing matches main.
    • A write is refused only when a shell is proven in front, not unless the agent is proven in front. Intended: an SSH relay or a slow scan often cannot answer, and Orca's SSH rule treats an unanswered read as no evidence that anything exited (docs/reference/ssh-execution-boundary.md); a shell that is proven still refuses the paste and every Enter.
    • Enter goes 50 ms after the paste, shorter than a fixed delay of several hundred ms. Intended: it is main's desktop-paste constant, and the composer was just seen ready.

Linked Issue

N/A (internal agent-launch unification series).

Visual Proof

All from live builds of this branch, driven through the app over the browser debugging protocol with an isolated home and agent config: macOS on a separate machine, and Windows 11 on a separate machine. The real Codex and Claude settings files on both machines were hashed before and after and did not change. These screenshots predate the timing and ownership changes; the delivery they show is unchanged, and the times in them are from before the timing fix.

Commit-failure recovery launched from a focused, empty split (macOS): the Claude tab lands in that split, the 21-line prompt from the failing hook is pasted into Claude (not typed into the shell), and the success toast follows.

b2-r1-aifix-empty-split.jpg

Resolve conflicts (macOS): the generated prompt reaches Claude and the dialog closes after it.

after-r3-resolve-conflicts.jpg

Fix checks from a focused empty split on a failing test PR (macOS), from an earlier run: the split was kept and the agent opened in it. The later before/after run did not reproduce this (0 of 2 on this PR, 0 of 2 on main); see "Pre-existing" above.

b3-r9-fix-checks-empty-split.jpg

Desktop connected to a remote Orca server: Explain commit from a focused empty split opens the tab in that split, focused, with no warning toast.

b3-r8a-paired-explain-empty.jpg

Same desktop, Fix checks from a focused empty split: split kept, tab in it, prompt delivered on the server.

b3-r10-paired-fix-checks-empty-split.jpg

An agent that never shows readiness: nothing is pasted, and the notice with Copy prompt appears at 60 s and is still up 90 s later.

phaseB-r6-notice-at-90s.jpg

Windows, real Claude 2.1.284 through Explain changes: Claude's ready title reaches the host through the Windows console and the prompt is delivered at about 10.7 s.

w5-claude-explain.png

Windows, AI Fix from a focused empty split: tab in that split at 1.1 s, 21-line prompt pasted at 5.1 s, success toast after it.

w4-3-aider-in-second-group.png

Testing

  • I manually tested these changes (live Electron QA on macOS and Windows; rows below; the terminal wait rewritten at 19a0ff0 was re-run live at 27d5943 and its flagged rows at 9b2ae19)
  • Automated tests added/updated

Automated (each new rule was removed once to show its test fails with an assertion, then restored):

  • src/shared/startup-line-prompt-carry.test.ts: 512-byte boundary, quote expansion, LF/CRLF/CR and other control bytes, long launchers, Hermes in and over budget, stdin agents.
  • src/main/runtime/agent-terminal-startup-prompt.test.ts, runtime-worktree-agent-startup.test.ts, rpc/methods/agent-launch-prompt-carry.test.ts, agent-launch-executor.test.ts: the carry fact on both the existing-workspace and create-worktree paths; a carried prompt is never pasted twice.
  • rpc/methods/agent-launch-terminal-prompt.test.ts, orchestration/worker/zcode-worker-readiness.test.ts, the Grok inline trace replay: one readiness wait, composer markers for ZCode/DSH, Grok marker or inline quiet window, dialogs waited out, reused terminals wait for idle, no paste without evidence.
  • src/main/runtime/tui-idle-launch-readiness-shell-title.test.ts: a shell auto-title alone does not settle a launch; it settles once quiet; later waits unchanged.
  • agent-launch-caller-selection.test.ts: a paired caller's tab is selected before its prompt is delivered, and a paired caller gets no host-reveal warning while an in-process caller still does.
  • Ready signal and dialogs: launched-agent-composer-readiness-codex.test.ts streams each captured Codex 0.157/0.158 startup dialog (update, hooks review, model retired, model announcement) into a fresh launch and requires it is reported as that dialog, never read as the composer, and the 0.157 provisional model: loading screen still never reads ready; launched-agent-composer-readiness-claude.test.ts does the same for Claude's captured trust dialog (reported within 4 s) and reads the answered transcript as ready. Without the dialog/provisional check, all 11 of those fail. launched-agent-composer-readiness.test.ts pins that Claude settles on the 1.5 s quiet window without consulting tui-idle, and that tui-idle is asked only after the 8 s budget, for the remaining 52 s. workers-new-worktree.test.ts pins that a worker start the signal settles still records its wait-for-setup outcome (fails when the composer path returns nothing). runtime-worktree-startup-readiness.test.ts (unchanged) pins worktree.create's draft paste as on main.
  • Shell hand-off (rewritten at 19a0ff0), launched-agent-shell-handoff-readiness.test.ts, replaying the captured zsh prompt and hand-off and Claude's captured start: nothing settles until Claude's own bracketed paste and quiet window, even after 3 s of silence; after an agent that exits, the shell's next prompt never settles it and is checked once, not every 250 ms; a check that cannot prove a shell settles on the signal with one read; a shell without bracketed paste gets no check before the signal; a startup-file program in front before the prompt does not settle it early; with two shell prompts before the launch line only the last hand-off counts; a hand-off split across two chunks is seen; a check still running when the shell hands over never settles the wait; and Codex 0.157, which turns bracketed paste off and on itself while starting, still settles on its marker. Each guard was removed once: without the answer-thrown-away rule, the in-flight check test fails; without the carry, the split test fails; taking the hand-off only once fails the two-prompts test; keeping the quiet timer across a hand-off fails the two-prompts and split tests; keeping the scanner across a hand-off fails those two and the in-flight test; without the proven-shell refusal, the exited-agent test fails.
  • Shell check, launched-agent-composer-readiness-claude.test.ts: on macOS and Linux, with the host's inspection built by the real evidence code from captured ps rows, a bash wrapper under zsh is not the shell (with no name scan) and zsh back at its prompt is (its name scan agrees); an SSH pane reads the relay's inspection the same way; an agent in the shell's own group (a shell without job control), whose fresh read names node, is not the shell; and an inspection that cannot observe proves nothing, without falling back to the name. Removing the identity veto fails 3 of these; removing the name confirmation fails 1. Where the host returns no inspection evidence (the fallback), the name rows still hold: a fresh scan whenever the cached name is a shell, the agent's own name or none (the zsh-pane measurement: cached copilot, scan zsh, proves a shell), and a scan that names zsh proves a shell even when the daemon's shell-foreground check says no (the agent-exited case). On Windows only the shell-foreground check decides: powershell.exe for an npm agent is not a shell unless the check confirms it, a controller without the check proves nothing, and an SSH pane on a Windows host (whose relay has no check) proves nothing. Claude's version-number name and node settle at once, with no read. A Claude launched over SSH settles on its 1.5 s quiet window rather than the 8 s fallback. agent-launch-terminal-prompt.test.ts pins that the write is refused when a shell is proven in front and goes out otherwise, and that it goes out as soon as the signal fires. orca-runtime-tests/agent-launch-prompt-submit.spec.ts pins that a launch prompt into a ready composer gets its Enter 50 ms after the paste even while Claude keeps repainting (fails, waiting out the 8 s cap, without the change), that Codex gets its second Enter 1.2 s later, and that a prompt into a running agent still waits for the render.
  • Chat placement (added after the before/after run): local-structured-session-launch-placement.test.ts applies the host's first frame for a button's chat to a workspace with a focused empty split; the chat lands in the split, stays where the host filed it when no launch reserved it, and stays where the user moved it. Removing the mirror's reservation read makes it fail with the QA's symptom (chat in g-1, not the split). source-control-agent-launch.test.ts pins that the button reserves under the chat's id, locally and on a paired host, and fails if the reservation is keyed by the pane only; agent-launch-tab-reservations.test.ts pins that one reservation under two ids dies as one.
  • Renderer: source-control-agent-launch.test.ts (params, replay under one operation id, paired error replies replayed, refusal classes, focused group read by the launcher and held through the reveal and released on every exit, paired placement and focus records), agent-launch-tab-reservations.test.ts, terminal-presentation-reservation.test.ts, agent-launch-prompt-not-delivered-notice.test.ts, and one test per caller.
  • Mobile: not in this PR, and no phone gate is planned. Released desktops v1.4.208 and later already deliver a phone prompt safely (quoted on the launch line, sent as one bracketed paste), so gating the phone on agent.launch.prompt-carry.v1 would disable working buttons until users update.
  • Typecheck (node, web, cli, mobile, mobile tests), oxlint and the changed-code quality gate passed at ba5eb71; 19a0ff0, 7bada25, fb5417e, e8a19c7, 97fbf52, 27d5943, the three at 5bc2d3f, 9b2ae19 and b57ed11 touch node, web and shared code only; at b57ed11 the node and web typechecks, the named test files, oxlint and the changed-code quality gate against origin/main passed.

Live QA, macOS (separate machine). The first rows are from 6889642, before the timing fix; their "about 8.5 s" times are the slow timing that "Terminal timing" above describes, and the current timing is in that section. The terminal wait rewritten at 19a0ff0 needs a re-run: the four buttons with Claude (and Codex and OpenCode if allowed), an agent that exits at startup, an agent that never enables bracketed paste, a quiet agent, a shell startup file that runs a program, and one orchestration worker start.

Row Result
Commit-failure recovery, Claude, focused empty split / split with a tab (6889642) Pass: tab in that split in about 0.3 s, focused; 21-line 13.5 KB prompt pasted at about 8.5 s (before the timing fix); success toast after the paste
Resolve conflicts dialog, Claude Pass: 24-line prompt pasted; dialog closed after delivery; recipe saved
Explain commit, Claude (6889642) Pass: 4-line prompt pasted at about 8.5 s (before the timing fix)
Agent that never shows readiness (stand-in) Pass: nothing pasted into the agent or the shell; notice with Copy prompt at 60 s, still up at 90 s; no success toast
Short prompt with a TAB / plain short prompt, launcher called directly Pass: TAB prompt kept off the command line and pasted; plain prompt rode the command line
Desktop connected to a remote Orca server: Explain commit and AI Fix from an empty split and from a split with a tab Pass (10 launches): tab in the focused split, focused in 0.3–0.5 s, no warning toast
Fix checks, failing test PR, focused empty split, local Earlier run 2 of 3 split kept; the before/after run 0 of 2, same as main 0 of 2. Pre-existing: the focused group leaves the split at the click on both sides (see "Pre-existing" above)
Fix checks, same, on the desktop connected to a remote server Pass
Terminal timing, crash and no-bracketed-paste rows Pass at e6d62dd (see "Terminal timing"); re-run pending at 9b2ae19
Explain commit, Grok Blocked: Grok quota exhausted on the QA machine (an earlier run of this branch passed: pasted at about 1.4 s)
SSH Blocked: no throwaway Linux SSH target available on the QA machine
Codex, and SSH with Codex Blocked: the QA rules forbid starting Codex in a test build's terminal until #23552 lands

Live QA, Windows 11 (separate machine, build of this branch at aeb975a, before the ownership changes):

Row Result
Short prompt with quotes, $, backticks, %, ^, &, é, 你好 on the typed line, PowerShell and Git Bash panes Pass: arrived byte for byte
Same, cmd pane Fail, older than this PR: cmd's quoting keeps the ^ escapes inside double quotes, so the agent receives stray carets (see Review)
24-line 1.6 KB prompt Pass: not on the command line, pasted exactly once after readiness, line breaks kept
Agent that never shows readiness Pass: nothing typed or pasted; notice at 60 s (it then closed after 4 s, fixed since and verified on macOS)
AI Fix from a focused empty split Pass
Real Claude, Explain changes Pass: Claude's ready title passes through the Windows console; delivered at about 10.7 s

Not measured / not built:

  • WSL was not measured.
  • Windows was not re-measured after the ownership changes (d7d03a9 and later); Windows ran at 27d5943 and 9b2ae19 with Claude and Qwen Code (an npm agent): both submitted once.
  • The test that the desktop's launch-platform guess (still used to check a recipe's arguments before launch) matches the host's own derivation could not be built without running a real host runtime inside a renderer test. The host derives the platform itself for the launch.
  • The old-host fallback's removal condition (delete it once the oldest supported paired host advertises agent.launch.prompt-carry.v1) is stated here but not pinned by a test.

Review

Codex readiness: #23745 (merged, now in this branch) made Orca read Codex 0.157/0.158's live chat and stop at its startup dialogs. The button path's own wait is pinned against the captured 0.157 and 0.158 screens in launched-agent-composer-readiness-codex.test.ts: the live chat reads ready, the 0.157 provisional model: loading screen does not, and each 0.158 startup dialog stops the launch instead of receiving the prompt. With a model that plays Codex 0.158's one-time star animation in the composer, the idle-evidence lane waits for the animation to finish (about 15 s) plus its 3 s quiet window; a launch now reaches that lane only when Codex's › marker has not settled the wait within its budget, and how the marker behaves during that animation was not measured.

Which agents get their prompt pasted: any agent that turns on bracketed paste gets it on the same signal main's desktop paste used, within the same budget, including those with no other readiness evidence in code (Trae, Prime Agent, Command Code, Droid, Autohand, Ante, Goose, Amp, Kilo, Kiro, Crush, Aug, Cline, Freebuff, Codebuff, Continue, Kimi, Mistral Vibe, Qwen Code, Rovo, Copilot, Aider, OpenClaw, OpenClaude, OpenCode 2, Mimo Code). An agent whose signal does not settle in its budget gets the tui-idle check instead of main's blind paste, and "prompt wasn't sent" with Copy prompt if that finds nothing. Unlike main, the signal is read only from the output after the shell's last hand-off, and it is dropped while a shell is proven in front, so a shell's own prompt no longer settles it (the crash case aside on WSL, a Windows pane served by the terminal daemon and an SSH pane on a Windows host, see the Temporary notes above). An agent that turned bracketed paste off during its own startup and never back on would lose its signal and reach the tui-idle check instead; none of the 47 captured agent transcripts in src/main/runtime/__fixtures__ or the Grok traces does that (each agent-side ?2004l before readiness is followed by a new ?2004h; the Antigravity captures end on one because they end at the agent's exit).

Local orchestration worker start waits on tui-idle, as on main (5bc2d3f; at 27d5943 it used the launch paste's signal, which the final live run measured 1.6 s later than main). Two differences from main: a bare name-only title counts only once the stream goes quiet, and DSH and Grok wait for their composer marker (Grok, or its inline quiet window) like ZCode did on main. Federated worker start and worktree.create's draft paste keep their own waits, as on main.

Overlap with a planned PR (agent-status work, not yet open): it moves OpenCode and OpenCode 2 worker starts onto the composer-marker-only lane (#13911), fixes a seam bug in draft-paste-ready-scanner.ts on its revocable-anchor path, and adds a bracketed-paste-off revocation to OpenCode's ready signal. Those three are left to that PR on purpose; it builds on this PR's waitForLaunchedAgentComposer, so the two converge on one function.

Found in QA or review, not caused by this PR: (1) on Windows, a short one-line prompt typed into a cmd pane gets stray ^ characters, because the shared cmd quoting (src/shared/tui-agent-startup-shell.ts) escapes metacharacters inside double quotes; it is identical on main, and none of these buttons' prompts is short and single-line, so the buttons don't hit it. (2) The Fix checks prompt says "check log tails below" but carried no log tail in QA although Orca fetched the job log; the prompt builder and log fetch are unchanged by this PR, and the cause is not yet known. (3) Main's desktop paste (the renderer's own draft paste, still used by other launches) reads the shell's own bracketed paste as the agent's signal and can paste before the agent has drawn anything; this PR fixes that only on the host's launch path. Follow-up: move the hand-off reset into the shared scanner so the desktop paste gets it too.

Known limits: the three replays of a lost launch reply run back to back, so a dropped paired connection that is still down ends at "Couldn't confirm the agent started" (never a second agent). On a paired host, a chat the host starts is focused; the button's group is now reserved under the chat's id as well, which should put it in the button's group while the terminal placement wait (up to 10 s) is running, but this is covered by tests only, not verified live on a paired host; and a terminal the host takes longer than 10 s to create lands in the host's group (still focused). A new worktree whose setup must finish before its agent starts decides prompt carry before the setup wrap; nothing that ships sends a prompt with a create launch today.

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

  • SSH: the host pastes into the remote terminal through the same writer the desktop's paste used, so prompt text still reaches an SSH host only as terminal input, never in a remote command line, environment variable or file. Codex readiness over SSH reads the host's own screen model, which every terminal's output feeds. The shell check over SSH uses the relay's own live foreground read, with no process scan.
  • Mobile: fix(mobile): start AI-button agents through agent.launch, never a bare shell #22762 (merged, not released) sends the phone's AI-button prompts through agent.launch. Checked against the released desktops: v1.4.206–v1.4.207 start a terminal agent without the prompt and the phone says so with Copy prompt; v1.4.208 and later pass it as one quoted argument, so no prompt line can run as a shell command (bash/zsh/fish/PowerShell; a cmd.exe terminal shell is unverified). So this PR adds no phone gate.
  • Mixed versions: a new desktop on an older paired host keeps today's tab paste. An older client on a new host gets the new delivery rule; receipts stay within the existing outcomes, and the only new effect is a possible "not delivered" where the typed line would have been used. viewMode is sent only to hosts advertising agent.launch.prompt-carry.v1, and an older host strips it.
  • Folder workspaces: the launch addresses the workspace by id and works for folders as for worktrees.
  • Merged current origin/main at ba5eb71. Main had added a rule to the phone's launch: a refusal saying the launch's pane or chat reservation is already taken means "failed" on the first send but "unconfirmed" after a replay, since the holder may be this launch's own. This PR moved that refusal logic into the shared classifier (src/shared/agent-launch-replay-refusal.ts) used by the desktop and the phone, so the rule now lives there and the desktop buttons follow it too (pinned in source-control-agent-launch.test.ts); the phone keeps its own wording for the first-send case.

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)

@brennanb2025
brennanb2025 marked this pull request as ready for review September 30, 2026 23:34
Comment on lines +30 to +31
if (cleared?.ptyId === ptyId && !cleared.shellMayHaveReturned) {
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Stale foreground check permits shell input

If an agent exits after the guard checks which process owns the pane, and the returning shell emits neither watched marker, the guard skips its next check. The launch prompt or a later Enter can then be written to the user's shell instead of the agent. A marker arriving during the initial check also does not trigger another check before the first write.

How this was verified: The guard reuses a negative foreground read unless it observes one of two output markers, while the prompt writer calls it immediately before writing to the PTY.

@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 critical issues — one latent consistency note inline.

Reviewed changes — re-read the full diff at f564d7a8ea (55 commits, 105 files) since the earlier snapshot, focusing on the production paths and the commits landed after the prior read.

  • Prompt carry decided on the built line — src/shared/startup-line-prompt-carry.ts measures the quoted typed line and selects the clean plan when the text is multi-line, control-bearing, or over 512 bytes; agent-launch-executor.ts/surfaces report promptRodeLaunchCommand instead of predicting it, and an uncarried prompt is pasted after readiness.
  • One launch readiness wait — launched-agent-composer-readiness.ts is shared by agent.launch and worker start; launchReadiness holds a name-only OSC title to a quiet stream (tui-idle-evidence.ts, agent-title-status.ts), with a tui-idle fallback through trust/update dialogs.
  • Shell-in-front guard — launched-agent-shell-foreground.ts + launched-agent-write-guard.ts refuse the paste/Enter only when a shell is proven in front, reusing the paste's read until a shell-return marker.
  • Byte parity with the desktop paste — wrapTerminalBracketedPasteText moved to src/shared/terminal-bracketed-paste-text.ts; the host's launch prompt now sends the draft paste's exact bytes.
  • Replay hardening — the desktop renderer gets trustedLocalCallerId, admit+claim collapses into one store transaction, and classifyAgentLaunchReplayRefusal is shared with mobile.
  • Tab placement — reservations + placement + terminal-presentation bridge + chat mirror + reconciliation keep a button's launch in the focused group (including an empty split), with the reservation consumed/expired on every exit.
  • Capability gate — agent.launch.prompt-carry.v1 routes older hosts to the existing tab-paste fallback.

I traced the hazards carried forward from the prior review and confirmed each is not a regression: direct-SSH worktrees correctly target { kind: 'local' } (the in-process main runtime is the host that spawns SSH PTYs, matching launchAgentInNewTab); the waitForWorktreeStartupDraft handoff bookkeeping cannot false-settle or permanently drop a signal; and the write guard's mark-less-shell Enter reuse is the narrow deviation the PR body already documents. The remaining inline note is a latent consistency gap with no current caller.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts
The desktop's source-control AI buttons (Fix checks, Resolve conflicts,
commit/push-failure recovery, Explain commit) opened a tab themselves and
pasted their generated prompt. They now ask the host's agent.launch, which
step 1 (#24257) taught to keep a long prompt off the launch line and paste it
after readiness. The buttons record where their tab goes under every id the
tab can arrive as, so the host's terminal reveal and the session-tabs mirror
put the terminal or chat in the split the button was used from; an empty
target group is kept until its tab arrives; a lost reply is replayed under
the same operation id; and a prompt the host could not deliver shows a
notice with Copy prompt.

Rebuilt on step 1; the combined tree is identical to f564d7a
(backup/23748-pre-split).
@brennanb2025
brennanb2025 force-pushed the brennanb2025/desktop-action-buttons-agent-launch-2 branch from f564d7a to 5867ca6 Compare October 1, 2026 01:40
@brennanb2025
brennanb2025 changed the base branch from main to brennanb2025/agent-launch-host-prompt-delivery October 1, 2026 01:40
@brennanb2025 brennanb2025 changed the title fix(source-control): start AI-button agents through agent.launch without typing long prompts into the shell feat(source-control): start the AI buttons' agents through agent.launch (step 2 of 2) Oct 1, 2026
… send paired hosts an explicit terminal view

A refused or unconfirmed source-control launch showed the host's raw error text or a hard-coded English line in place of the translated failure toast. A paired host also got no viewMode for a terminal launch, so it applied its own default instead of this window's, as main's paired path avoids.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status (one pass after the split, 2026-09-30)

No blockers. Fixed in 0ff385f:

  • The hosted launch's failure toasts are now translated. "Couldn't start the agent." shows the host's reason underneath, and "Couldn't confirm the agent started" is in all six catalogs; before, the toast was raw host text or hard-coded English.
  • A paired host now gets an explicit viewMode: 'terminal', as main's paired path sends, instead of applying its own default.

Gates at 0ff385f:

The body is corrected:

  • It now names agent.launchReplay and both capabilities.
  • It says "at most three sends", not three replays.
  • It describes paired-host placement.
  • It labels the new deviations.
  • It states that the paired remote-server path hasn't been QA'd.

Open, not fixed (design calls):

  1. The host's reveal forces focus whenever a placement reservation exists, so a launch pulls you back to its workspace if you switched away before the terminal appeared (terminal-presentation-ipc-bridge.ts). The same class of bug as fix(worktrees): a finished create no longer pulls you off the workspace you switched to #23974. Now labelled Temporary in the body.
  2. On a paired host, the 10 s placement poll starts at the click, not after the create. A slow capability probe, a replay or a slow spawn past 10 s lands the tab in the default group.
  3. On a paired remote Orca server, chat vs terminal follows the server's settings, not this window's.

…kspace you left

The four source-control buttons' agent tab appears when the host spawns its terminal, seconds after the click. The reveal forced focus, so a user who had switched to another workspace or an app view in that gap was pulled back to the launching one. It now follows #23974's rule for finished creates: still on the workspace, the tab is placed and focused as before; moved on, the tab joins its reserved group without switching workspaces, mounts in the background, and a toast offers Go to, which opens the workspace and focuses the tab. The in-view check and the Go-to toast are #23974's, shared rather than copied.
@brennanb2025

brennanb2025 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Re-run on the combined build 6171ba9 (#23748 + #24257 at e463aea), M4Air (zsh), 2026-10-01

What changed. #24257's e463aea makes a local zsh host carry a multi-line prompt on the launch line, as main's agent.launch did, when every line is at most 512 bytes and the whole line at most 8 KB; otherwise it pastes after readiness. #23748 routes the desktop source-control buttons through that host path, so this round checks how the buttons now deliver.

Short answer.

  • Both buttons pass: the prompt was delivered once in every run, no prompt line ever ran as its own shell command, and nothing reached the shell prompt after the agent.
  • Explain changes is now about 2 s faster than main.
  • Commit-failure Fix with AI matches main's timing.
  • One difference to decide on: on main these buttons never put the prompt in the shell, because the renderer pastes it into Claude after it is ready. On the combined build a short-lined prompt like Explain's is now typed into zsh as Claude's quoted argument. It runs once, as part of the claude command, the same way the phone and the CLI already work on main.

Setup.

  • Builds:
    • Head re-checked: 6171ba9e8a9c40fa93be74f332d282b82b9efc0b, which merges e463aea.
    • Main is 1b0ee969bccd6a58f8412dc1b425079c47810e2a.
    • Build proof: devBranch 6171ba9e8a, and the new zsh rule (nameLocalTypedLineShell, zshReadsMultiLineWhole) is in the built main bundle only on this arm.
  • Rigs: SHELL=/bin/zsh and no terminalDefaultShell on both rigs. Same seed, fixture and Claude 2.1.286 shim as before.
  • Method: each run starts from a focused empty split, with arms interleaved. The script -r tap is on the Claude pane in tapped runs.
Button Prompt main: delivery / what zsh received combined: delivery / what zsh received click→submitted, main click→submitted, combined Verdict
Explain changes, 4 runs each 390 B, 4 lines pasted after readiness (Enter 49–50 ms after) / 39 B claude launch line only on the launch line as Claude's quoted argument, no paste / one 432 B, 4-line command (longest line 201 B) 2.58–3.23 s 0.81–1.21 s (4.83 s in one run at load 45) PASS, faster than main
Commit failure → Fix with AI, 2 runs each 13.5 KB, 21 lines (over 8 KB) pasted after readiness / 39 B launch line only pasted after readiness / 39 B launch line only 2.46, 2.60 s 2.20, 3.03 s PASS, same as main (paste−ready 1.87 vs 1.62 s, the same ~0.25 s offset as earlier rounds)
  • Every run on both arms: one submitted user message with the same text as earlier rounds; exactly one zsh command (the launch line); the tab landed in the focused split.
  • On the combined build the Fix toast appears after the prompt; on main it appears at the click.

Screenshots (combined build).

Explain changes: the 4-line prompt is Claude's first message in the right split, and the answer is complete:
6171 explain

Commit failure → Fix with AI: the 198-line lint prompt is pasted once, and Claude is working in the right split:
6171 fix

Safety.

  • Real agent config was unchanged on the M4Air and the laptop.
  • Every rig was stopped by recorded pid.
  • Orca launches Claude with --dangerously-skip-permissions on both arms. In the throwaway fixture Claude ran read-only checks (git status, git show, node --check) before Esc or tab close, and the fixture was unchanged after every run.

Live QA: switching away right after a source-control AI launch, at 48e8dd7

What was tested. #23748 moves the four source-control AI buttons onto the host's agent.launch. On that path the host opens the agent's tab a moment after the click. Before 48e8dd7, opening that tab also forced focus. A user who had already switched to another workspace was pulled back to the one that launched the agent. 48e8dd7 changes this. If you have moved on, the tab joins its group in the launching workspace in the background, and a toast " started in " offers Go to. If you are still there, the tab is placed and focused as before.

Short answer: PASS. You are never pulled back. The prompt arrives once. The toast appears, and Go to opens the workspace with the agent tab focused. Without a switch, the tab lands in the focused split as before.

Build. Head re-checked with gh pr view 23748 = 48e8dd76aa45373de7b39e050bd522556773395a, which contains #24257's d2680896f72. The app reported devBranch 48e8dd76aa. The dev renderer serves agent-launch-started-elsewhere-notice.ts and the {{agent}} started in {{name}} string. The main bundle has agent.launch.prompt-carry.v1. Main control is 1b0ee969bcc (0 hits of all three). Rig: macOS M4Air, Claude 2.1.286 through a rig shim, isolated home and profile, terminal mode, status hooks off. The fixture repo has a staged change and a failing pre-commit hook (a 21-line commit-failure prompt). A second project, other, is the workspace to switch to.

Method. Each run focuses an empty split in the launching workspace, commits, and clicks Fix commit failure with AI. One in-page script then clicks another workspace's sidebar row the way a user would: a pointer/mouse/click sequence, fired 20 ms after the Fix click so it lands before the tab exists. It then samples the active workspace, the agent tab's group, the focused element and the toasts every 50 ms for 10 s. Prompt evidence comes from the rig Claude transcript (user-message count) and zsh preexec (what the shell ran).

(a) Switch to a different workspace right after the click: 4 of 4 valid runs PASS

Times are ms after the Fix click.

Run (1-min load) Switch landed Tab existed at switch? "Claude started in main" toast Stayed on the other workspace Tab in the launching workspace's split group Prompt recorded (count) Shell Go to clicked → landed After Go to
a3 (16.2) +33 no +404, with "Go to worktree" yes, never flipped back yes 4542 (1), while away launch line only not clicked (toast had gone, see note 1) –
a4 (32.7) +43 no +473 yes, until Go to yes 5540 (1) launch line only +3676 → launching workspace by +3883 Claude tab active in its split
a5 (34.1) +23 no +270 yes, until Go to yes 2431 (1), while away launch line only +3473 → by +3604 Claude tab active; cursor solid in its composer
a6 (6.8) +22 no +249 yes, until Go to yes 2896 (1), while away launch line only +3451 → by +3569 Claude tab active, focus inside the Claude pane
  • zsh ran only claude '--dangerously-skip-permissions' in every run. The 21-line prompt was Claude's first message exactly once, with the same text as earlier rounds.
  • Two more runs were not "before the tab" cases:
    • a1: my click hit a non-clickable wrapper, so no switch happened.
    • a2: a 150 ms timer fired at +283 ms because the renderer was busy after the click. The tab was already there (+284 ms). It correctly showed no "started in" toast and never pulled back.

(b) Same click, no switch: 2 of 2 PASS

Run (load) Tab in the focused split Claude tab active and focused Prompt recorded (count) Toasts
b1 (8.9) +281 yes, focus in the Claude pane from +281 3007 (1) "Started an AI agent for the commit failure." at +3002; no "started in"
b2 (10.5) by +136 yes 1997 (1) the same at +2003; no "started in"

Main control (1b0ee96, 1 run of (a))

On main the button's agent tab exists before a user's switch can land: it was already there at +231 ms. So main does not pull you back in this flow either. When you leave, main says nothing about where the agent went beyond "Started an AI agent for the commit failure." I did not build #23748 without 48e8dd7, so I have not reproduced the pull-back itself here. These rows show only that the fix behaves as described.

Worth knowing (not failures)

  1. The "started in" toast goes away after about 4.2 s (the toast library's default). In a3 it was up at +0.40 s and gone by +4.6 s. A user who isn't looking can miss the way back. The agent still shows under the launching workspace in the sidebar.
  2. While you are away, two toasts show: "Claude started in main" (with Go to) and, when the prompt lands 2.4–4.5 s later, "Started an AI agent for the commit failure." (no Go to).
  3. The toast names the workspace, not the project. Both fixture projects' primary workspaces are named after their branch, so "main" alone does not say which project. Two projects on the same branch name would read the same.
  4. Agent behaviour. Orca launches Claude with --dangerously-skip-permissions, and the commit-failure prompt asks for a fix. In the throwaway fixture, Claude ran read-only git status / cat / node --check. The fixture (a7bec65, staged blob unchanged) was re-checked after every run, and each agent was stopped by closing its tab.

Screenshots (full window, right sidebar collapsed)

(a) run a6, 1 s after the toast. You are on other-main (Terminal 1 at other %), the agent row shows under repo › main, and the toast reads "Claude started in main" with Go to worktree:
a6 toast

(a) run a6 after Go to: repo › main is active, with the Claude tab in the right split, focused and working on the delivered prompt:
a6 after Go to

(b) run b2, no switch: the Claude tab is in the right split and focused, with only "Started an AI agent for the commit failure.":
b2 no switch

Main control at 10 s: you are still on other-main, the agent is under repo › main, and there is no "started in" notice:
main control

Safety. The M4Air's real ~/.claude/settings.json, ~/.codex/hooks.json and ~/.codex/config.toml were identical before, at each rig start and after. Every rig process was stopped by its recorded pid. The worktrees qa24257-48e (48e8dd7) and qa24257-mainctl (main) are left built on the M4Air.

…rompt-delivery' into brennanb2025/desktop-action-buttons-agent-launch-2
…rompt-delivery' into brennanb2025/desktop-action-buttons-agent-launch-2
…rompt-delivery' into brennanb2025/desktop-action-buttons-agent-launch-2
@brennanb2025
brennanb2025 marked this pull request as ready for review October 1, 2026 10:22

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

Reviewed changes — initial review of this PR's current scope: it is step 2 of 2, now branched on brennanb2025/agent-launch-host-prompt-delivery, so the 35-file diff is just the caller-side change (8368b0e).

  • Four source-control buttons launch through the host — Fix checks, Resolve conflicts, commit/push-failure recovery and Explain commit call agent.launchReplay via the new launchSourceControlAgent, and fall back to launchAgentInNewTab only when the host is unsupported.
  • Caller-side placement reservations — new agent-launch-tab-reservations + agent-launch-tab-placement record the target group (and the chat focus intent) under every id the tab may arrive as; the terminal reveal claims/consumes it, the chat mirror reads it, and reconciliation keeps a reserved empty split alive.
  • Workspace-switch handling — a tab that appears after the user left keeps their place and offers a Go-to toast, reusing workspace-go-to-toast / workspace-terminal-view, which the worktree-creation ready toast and creation-flow view check now share.
  • Result notices — settleSourceControlAgentLaunch maps the receipt to started / promptDelivered / failureNotified, with a persistent "prompt wasn't sent" Copy-prompt notice and a localized failure/unconfirmed toast.

I traced the reservation lifecycle (local and paired; reveal, reused-tab, reply, refusal and lost-reply exits) and the replay classifier against the host's agent.launchReplay ledger. No exit leaves live placement state keyed to another tab, and {kind:'failed'} is only returned where the host proved no effect — recoverable paired errors replay, agent_session_operation_unknown maps to unknown, and pane/session-already-live is failed on the first send but unknown after a replay, so a retry cannot silently start a second agent. Mixed-version, folder-workspace and SSH-to-local routing are covered. Nothing blocking; the remaining gaps are the temporary deviations the description already calls out (paired-server chat-vs-terminal follows the server's settings, and the paired Orca-server path is not QA'd live).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 marked this pull request as draft October 2, 2026 01:39
brennanb2025 added a commit that referenced this pull request Oct 2, 2026
A launch line that is long or has several lines could lose lines or wedge a
shell that is still starting, because it was typed into the terminal before
the shell could read it whole. The terminal host (local daemon, and the SSH
relay) now writes such a line to a script file and types one short line that
runs it, for every POSIX shell; a shell that cannot read Orca's quoting runs
Orca's own agent lines through /bin/sh. If the script cannot be written, the
terminal says so.

The SSH background launch path no longer types the line from the renderer;
the relay stages it like any other terminal.

Bumps the terminal daemon protocol to 40 (main took 39); a v39 daemon keeps
its sessions and gets no staged line.

Restacked onto #24257 from the version reviewed on top of #23748; carries
none of #23748's changes.
brennanb2025 added a commit that referenced this pull request Oct 2, 2026
A launch prompt for an agent whose CLI takes one could still be pasted into
its screen once it looked ready: the host pasted any prompt whose typed line
was over 512 bytes or held a newline (outside short-lined zsh), and the
desktop's generated-prompt launches started the agent empty and pasted. A
paste can lose text, land its Enter on a startup dialog (Codex's update
prompt), or wait seconds for the agent to look ready.

Where a launch prompt rides is now decided in one place, the startup-plan
builder (carryLaunchPrompt), for every launch path:
- on the agent's command line, which the previous step stages when the line
  is too long or multi-line to type;
- in a private launch file the line points at, when the prompt is past
  16,384 characters, sensitive, or one a Windows shell would damage (a
  control byte; on PowerShell a ", a %NAME% pair or a trailing \; past cmd's
  8,191-character cap). The execution host writes it before typing the line,
  quoted for its shell, and grants Claude a read of its directory. A failed
  write refuses the spawn with a Copy-prompt notice;
- pasted once the agent is ready only for an agent that takes text after
  start, a prompt needing a launch file its agent is not measured reading
  (only Claude and Codex are), a reused terminal, and a paired host, which is
  sent a command and never a file.

This replaces the host's typed-line check (512 B, or zsh with every line
<=512 B and <=8 KiB), which existed because nothing staged a long line.

WSL writes its staged line and launch file into the distro and runs the line
in the account's own login shell (sourced in bash, zsh and sh, eval'd in fish,
through /bin/sh in ksh), with ~/.cache/orca made 0700 first.

The "prompt wasn't sent" notice with Copy prompt lives here, shared by the
new agent tab, background draft, paste timeout, work item and new workspace.

Rebuilt on the restacked staging step; the earlier version sat on a base
that carried #23748.
brennanb2025 added a commit that referenced this pull request Oct 5, 2026
…hem after readiness (step 1 of 7) (#24257)

* feat(agent-launch): host-side prompt delivery for agent.launch

The host's agent.launch typed any launch prompt into the shell as part of the
launch command. A long or multi-line prompt then ran line by line in the
shell, and an agent that never showed readiness or crashed at startup had
nothing guarding where its text went.

agent.launch now carries a prompt on the typed line only when the line stays
one line, control-free and at most 512 bytes; otherwise the agent starts
clean and the host pastes the prompt once the agent's own ready signal fires
(bracketed paste plus its composer marker or a quiet render, read only after
the shell's last hand-off, never while the pane's own shell is proven in
front), with main's draft-paste bytes and an Enter 50 ms later. Orchestration
worker starts wait on tui-idle as before. A replay-safe launch admits and
claims its ledger row in one write, Qwen Code gets a second Enter, the
desktop and phone share one launch-refusal classifier, and hosts advertise
agent.launch.prompt-carry.v1.

Split out of #23748, which moves the desktop source-control buttons onto
this path.

* fix(agent-launch): keep a short-lined multi-line prompt on a local zsh launch line, as main did

#24257 moved every multi-line or over-512-byte prompt off the typed launch line and pasted it after readiness. The phone's AI buttons and review notes, whose multi-line prompts main typed whole into zsh, then reached Claude 0.5-3 s later and their RPC reply waited for the paste.

The host now names the shell a local macOS or Linux line is typed into, the way the spawn picks it, and a multi-line prompt rides a zsh line when every line is at most 512 bytes and the whole line at most 8 KB. A real-zsh test types such a line through Orca's own ready barrier and startup write, including when a slow user config makes the write land early. Elsewhere the measured unsafe cases keep the paste: bash 3.2 runs multi-line lines piecemeal, fish drops an early multi-line write, and any shell loses a line over 1 KB written early.

* test(agent-launch): keep the real-zsh launch-line test out of the Windows lane's gate scan

The Windows lane registration check read `const ZSH_PATH = process.platform === 'win32'` (the head of a multi-line ternary) as a Windows-true flag, so `describe.skipIf(!ZSH_PATH)` looked like a Windows-only suite. The file is POSIX-only; the zsh lookup is now a function.

* refactor(protocol): move the agent.launch capabilities into their own module

Main's protocol-version.ts sits at the 300-line cap, so the prompt-carry capability pushed it over. The four agent.launch capabilities and their doc move to agent-launch-runtime-capability.ts, re-exported by name and spread into RUNTIME_CAPABILITIES at the same position; the advertised lists and every export are unchanged.

* refactor(protocol): import the agent.launch capabilities from their own module

`export *` from protocol-version left the four names undefined under the mobile recording loader, which resolves a relative import through a Proxy with no own keys, so 37 phone recordings lost agent.launchReplay. Importers now name agent-launch-runtime-capability directly; protocol-version only spreads its list.

* refactor(agent-launch): drop the unshipped viewMode field and trusted local caller id

Both were inert in step 1 and existed only for step 2. agent.launch will become a
public plugin API, so every wire field is permanent once shipped; a top-level
viewMode reads as "choose terminal vs chat", which the host decides. Step 2
introduces placement and view intent under a placement object instead.

* fix(agent-launch): read Codex's provisional startup from the rule files' hold anchor

Main (#24375) moved Codex's provisional-header check into codex.json's
provisional_startup hold anchor and deleted codex-terminal-readiness.ts, so the
launch readiness hold now asks showsHoldAnchor, as main's own settled check does.

* fix(agent-launch): hold rule-file name titles to quiet for a launch, and census the zsh fixture

Main (#24375) answers a name-only title from each agent's rule file ahead of the
sustained-title lane, so gemini.json's name_title settled a launch readiness wait
on the shell's auto-title while Gemini was still booting. A launch now asks quiet
of every weak idle verdict, as that lane did.

Main's readiness census requires a recorder for every runtime fixture; the zsh
prompt recording is a non-agent control. Gemini's synthetic baseline is
regenerated for this PR's stated change: a bare gemini title is no longer its
rest mark, so name-only rows settle weak, and a fresh working or blocked status
is no longer overridden.

* fix(agent-launch): paste a launch prompt only when the launched agent is proven in front

A launch pasted its prompt unless a shell was proven in the terminal's
foreground, so any read that could not prove one let the prompt through. After
an agent exited at startup, its shell turned bracketed paste on at the next
prompt, readiness fired on it, and the prompt was typed into the shell:

- macOS: a pane runs its shell under login, so the process-group fence's root
  was never the shell's group and never proved it; the cached foreground name
  could also still name the exited process.
- Windows Git Bash and WSL: the shell-alone-in-its-job check never answers.

Now one fresh read of the terminal's foreground decides: agent, shell or
unknown. Only 'agent' lets a write through (paste, Enter, second Enter, reused
panes too); 'shell' still drops a ready signal. A Windows host never proves the
agent, so there the launch line carries the prompt at any size, as on main.

* test(agent-launch): cover the Windows QA stub, a grok override that exits at once

* fix(agent-launch): keep the local socket alive while a prompted launch waits for its agent

A launch with a prompt now waits up to 60 s for the terminal agent to be
ready before it writes the prompt, and reports not-delivered when the agent
never is. The local runtime socket closes a connection idle for 30 s unless
the request is a long poll, so a launch whose agent exited at startup lost its
reply and the caller saw 'runtime closed the connection' instead of
not-delivered. Classify a prompted agent.launch and agent.launchReplay as a
long poll, as orchestration.workerStart already is for the same wait.

* refactor(agent-launch): narrow the launch params by 'in' instead of a cast

* fix(agent-launch): find a launched agent behind a wrapper that leads its process group

A tcsh or nu launch line runs the agent from /bin/sh '<script>', and a
wrapper script that does not exec its agent does the same: the wrapper leads
the terminal's foreground process group and the agent is a member of it. The
fresh foreground read names the group's leader, sh, so a prompted launch was
refused or pasted late (M4Air tcsh: 2 of 4 not delivered, 2 pasted ~9 s late).

Before that read, take the host's process-group observation as positive proof
when it names the launched agent among the foreground group's members and is
younger than a ready signal's quiet window. It never proves a shell.

* fix(agent-launch): judge the foreground-group proof by when its capture began, not how long ps took

The age the host stamps on a process-group observation runs from the start
of its whole-machine ps, so on a loaded Mac a capture begun after the read
was asked for still read as older than 1 s and the proof was dropped. Count
an observation whose capture began after the read was asked for, less the
window a shared capture is reused across.

* test(agent-launch): keep the crash-guard live test out of the Windows lane's gate scan

The Windows-lane registration scan read the const assigned from a platform
check as a Windows-only gate, though the suite runs everywhere but Windows;
find zsh in a function instead, as the real-zsh typed-line test does.

Under load the fresh foreground scan can fail to answer, which lets the
shell's prompt settle readiness (2 of 4 paired runs). The guard still refuses
that write, so assert the refused write, the property that must always hold.

* perf(agent-launch): read a local pane's foreground from its own terminal, not the whole process table

The foreground read that gates every launch paste ran the daemon's
inspectProcess capture and then a fresh scan, each a whole-machine ps; the
fresh one also waits for any capture already running before it starts its own.
Measured here at load 5: 1.2 s a read (M4Air QA: 3.4-5.0 s, and worker starts
17.6-32 s against main's 9-12 s at load 25-84).

On a local macOS or Linux host, take the pane's root pid from the provider's
session inventory and run one ps limited to that pane's terminal. Its
foreground process group decides: the launched agent or any non-shell member
is the agent (a wrapper that did not exec its agent leads the group), a group
of shells alone is the shell. Same pane, same verdict: 2.7 ms a read. SSH hosts
keep the relay's observation and name.

* test(mobile): re-measure the web app's script sweep after agent.launch's capabilities moved out of protocol-version

The mobile web bundle check failed at 124 assets against a ceiling of 123. Main already sat
exactly on that ceiling: its sweep table read 69 scripts at 16 routes while the tree builds 73,
the whole margin of 4. This branch imports the agent.launch capabilities from their own module,
so protocol-version is no longer pulled into the root layout and four other routes. That moves
which routes share which modules, and the Qoder capability module, imported by protocol-version
and the AI-vault resume path, no longer shares an importer set with anything, so it gets a chunk
of its own: 74 scripts.

The fence says to re-derive the bound rather than raise it, so the sweep is re-measured on this
head (every prefix of the sorted route list). The worst route now adds 10 scripts (session), not
9, which moves the pinned shell crossing from 32 to 30 routes; main re-measured on its own lands
on the same crossing.

* fix(agent-launch): a worker's brief needs its agent found in front, and Grok's start answers on its composer

A paired-server worker start whose agent exited at startup typed its brief into the server's
shell, which ran it: the idle wait can settle on a shell back at its prompt, and the brief was
written with no foreground read. Both worker-start paths now check before each brief write, as a
launch prompt is checked: on a host that can find the agent in front it must be there; on one that
cannot (Windows) a shell proven in front still refuses, and anything else writes as before.

A Grok worker start waited ~10 s more than main: its only rest signal is its bare name, which a
launch holds to quiet output, and Grok animates its logo for ten seconds after its composer glyph.
A worker start for an agent whose rest signal is its bare name and whose composer draws a marker
(Grok, DSH, mimo-code) now also answers on that marker, whichever comes first.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…ole (step 2 of 7) (#23962)

* feat(agent-launch): host-side prompt delivery for agent.launch

The host's agent.launch typed any launch prompt into the shell as part of the
launch command. A long or multi-line prompt then ran line by line in the
shell, and an agent that never showed readiness or crashed at startup had
nothing guarding where its text went.

agent.launch now carries a prompt on the typed line only when the line stays
one line, control-free and at most 512 bytes; otherwise the agent starts
clean and the host pastes the prompt once the agent's own ready signal fires
(bracketed paste plus its composer marker or a quiet render, read only after
the shell's last hand-off, never while the pane's own shell is proven in
front), with main's draft-paste bytes and an Enter 50 ms later. Orchestration
worker starts wait on tui-idle as before. A replay-safe launch admits and
claims its ledger row in one write, Qwen Code gets a second Enter, the
desktop and phone share one launch-refusal classifier, and hosts advertise
agent.launch.prompt-carry.v1.

Split out of #23748, which moves the desktop source-control buttons onto
this path.

* fix(agent-launch): keep a short-lined multi-line prompt on a local zsh launch line, as main did

#24257 moved every multi-line or over-512-byte prompt off the typed launch line and pasted it after readiness. The phone's AI buttons and review notes, whose multi-line prompts main typed whole into zsh, then reached Claude 0.5-3 s later and their RPC reply waited for the paste.

The host now names the shell a local macOS or Linux line is typed into, the way the spawn picks it, and a multi-line prompt rides a zsh line when every line is at most 512 bytes and the whole line at most 8 KB. A real-zsh test types such a line through Orca's own ready barrier and startup write, including when a slow user config makes the write land early. Elsewhere the measured unsafe cases keep the paste: bash 3.2 runs multi-line lines piecemeal, fish drops an early multi-line write, and any shell loses a line over 1 KB written early.

* test(agent-launch): keep the real-zsh launch-line test out of the Windows lane's gate scan

The Windows lane registration check read `const ZSH_PATH = process.platform === 'win32'` (the head of a multi-line ternary) as a Windows-true flag, so `describe.skipIf(!ZSH_PATH)` looked like a Windows-only suite. The file is POSIX-only; the zsh lookup is now a function.

* refactor(protocol): move the agent.launch capabilities into their own module

Main's protocol-version.ts sits at the 300-line cap, so the prompt-carry capability pushed it over. The four agent.launch capabilities and their doc move to agent-launch-runtime-capability.ts, re-exported by name and spread into RUNTIME_CAPABILITIES at the same position; the advertised lists and every export are unchanged.

* refactor(protocol): import the agent.launch capabilities from their own module

`export *` from protocol-version left the four names undefined under the mobile recording loader, which resolves a relative import through a Proxy with no own keys, so 37 phone recordings lost agent.launchReplay. Importers now name agent-launch-runtime-capability directly; protocol-version only spreads its list.

* refactor(agent-launch): drop the unshipped viewMode field and trusted local caller id

Both were inert in step 1 and existed only for step 2. agent.launch will become a
public plugin API, so every wire field is permanent once shipped; a top-level
viewMode reads as "choose terminal vs chat", which the host decides. Step 2
introduces placement and view intent under a placement object instead.

* fix(agent-launch): read Codex's provisional startup from the rule files' hold anchor

Main (#24375) moved Codex's provisional-header check into codex.json's
provisional_startup hold anchor and deleted codex-terminal-readiness.ts, so the
launch readiness hold now asks showsHoldAnchor, as main's own settled check does.

* fix(agent-launch): hold rule-file name titles to quiet for a launch, and census the zsh fixture

Main (#24375) answers a name-only title from each agent's rule file ahead of the
sustained-title lane, so gemini.json's name_title settled a launch readiness wait
on the shell's auto-title while Gemini was still booting. A launch now asks quiet
of every weak idle verdict, as that lane did.

Main's readiness census requires a recorder for every runtime fixture; the zsh
prompt recording is a non-agent control. Gemini's synthetic baseline is
regenerated for this PR's stated change: a bare gemini title is no longer its
rest mark, so name-only rows settle weak, and a fresh working or blocked status
is no longer overridden.

* fix(launch): stage long or multi-line launch lines so they arrive whole

A launch line that is long or has several lines could lose lines or wedge a
shell that is still starting, because it was typed into the terminal before
the shell could read it whole. The terminal host (local daemon, and the SSH
relay) now writes such a line to a script file and types one short line that
runs it, for every POSIX shell; a shell that cannot read Orca's quoting runs
Orca's own agent lines through /bin/sh. If the script cannot be written, the
terminal says so.

The SSH background launch path no longer types the line from the renderer;
the relay stages it like any other terminal.

Bumps the terminal daemon protocol to 40 (main took 39); a v39 daemon keeps
its sessions and gets no staged line.

Restacked onto #24257 from the version reviewed on top of #23748; carries
none of #23748's changes.

* fix(launch): run a staged launch line as its own job in fish and ksh

fish and ksh run the commands of a sourced file in the shell's own process
group, so a staged agent was not a job: Ctrl-Z was dropped (a typed line
stops), and the shell-in-front check saw the shell's group in the foreground
while the agent ran. fish now runs the script with
`eval (string collect < '<path>')`, which runs it as if typed and leaves the
shell's job-control mode alone. ksh and mksh run a long Orca-built line through
`/bin/sh '<path>'`, a real job; a long command the user wrote is typed as
before. The real-shell test now asserts the agent's process group is its own
and is the terminal's foreground group.

The staging rule also reuses hasControlByte and the 512-byte budget from
startup-line-prompt-carry so the carry and staging limits cannot drift.

* fix(agent-launch): paste a launch prompt only when the launched agent is proven in front

A launch pasted its prompt unless a shell was proven in the terminal's
foreground, so any read that could not prove one let the prompt through. After
an agent exited at startup, its shell turned bracketed paste on at the next
prompt, readiness fired on it, and the prompt was typed into the shell:

- macOS: a pane runs its shell under login, so the process-group fence's root
  was never the shell's group and never proved it; the cached foreground name
  could also still name the exited process.
- Windows Git Bash and WSL: the shell-alone-in-its-job check never answers.

Now one fresh read of the terminal's foreground decides: agent, shell or
unknown. Only 'agent' lets a write through (paste, Enter, second Enter, reused
panes too); 'shell' still drops a ready signal. A Windows host never proves the
agent, so there the launch line carries the prompt at any size, as on main.

* test(agent-launch): cover the Windows QA stub, a grok override that exits at once

* fix(agent-launch): keep the local socket alive while a prompted launch waits for its agent

A launch with a prompt now waits up to 60 s for the terminal agent to be
ready before it writes the prompt, and reports not-delivered when the agent
never is. The local runtime socket closes a connection idle for 30 s unless
the request is a long poll, so a launch whose agent exited at startup lost its
reply and the caller saw 'runtime closed the connection' instead of
not-delivered. Classify a prompted agent.launch and agent.launchReplay as a
long poll, as orchestration.workerStart already is for the same wait.

* refactor(agent-launch): narrow the launch params by 'in' instead of a cast

* fix(agent-launch): find a launched agent behind a wrapper that leads its process group

A tcsh or nu launch line runs the agent from /bin/sh '<script>', and a
wrapper script that does not exec its agent does the same: the wrapper leads
the terminal's foreground process group and the agent is a member of it. The
fresh foreground read names the group's leader, sh, so a prompted launch was
refused or pasted late (M4Air tcsh: 2 of 4 not delivered, 2 pasted ~9 s late).

Before that read, take the host's process-group observation as positive proof
when it names the launched agent among the foreground group's members and is
younger than a ready signal's quiet window. It never proves a shell.

* fix(launch): type a plain Orca agent line as is in tcsh and nu

In tcsh and nu, every Orca-built agent line ran through `/bin/sh '<script>'`,
so the agent was a child of sh in sh's process group. When the prompt was
pasted after the agent was ready, the paste guard read sh in front and, on a
loaded Mac, reported the prompt not delivered (4/4 live runs in tcsh).

Now only a line that needs it goes through `/bin/sh`: one over 512 bytes, with
a control byte, or with a character those shells would not read literally
(`!`, backslash, `"`, `$`, backtick). A plain line such as
`claude '--dangerously-skip-permissions'` is typed as on main, so the agent is
the shell's own job.

* fix(agent-launch): judge the foreground-group proof by when its capture began, not how long ps took

The age the host stamps on a process-group observation runs from the start
of its whole-machine ps, so on a loaded Mac a capture begun after the read
was asked for still read as older than 1 s and the proof was dropped. Count
an observation whose capture began after the read was asked for, less the
window a shared capture is reused across.

* test(agent-launch): keep the crash-guard live test out of the Windows lane's gate scan

The Windows-lane registration scan read the const assigned from a platform
check as a Windows-only gate, though the suite runs everywhere but Windows;
find zsh in a function instead, as the real-zsh typed-line test does.

Under load the fresh foreground scan can fail to answer, which lets the
shell's prompt settle readiness (2 of 4 paired runs). The guard still refuses
that write, so assert the refused write, the property that must always hold.

* perf(agent-launch): read a local pane's foreground from its own terminal, not the whole process table

The foreground read that gates every launch paste ran the daemon's
inspectProcess capture and then a fresh scan, each a whole-machine ps; the
fresh one also waits for any capture already running before it starts its own.
Measured here at load 5: 1.2 s a read (M4Air QA: 3.4-5.0 s, and worker starts
17.6-32 s against main's 9-12 s at load 25-84).

On a local macOS or Linux host, take the pane's root pid from the provider's
session inventory and run one ps limited to that pane's terminal. Its
foreground process group decides: the launched agent or any non-shell member
is the agent (a wrapper that did not exec its agent leads the group), a group
of shells alone is the shell. Same pane, same verdict: 2.7 ms a read. SSH hosts
keep the relay's observation and name.

* test(mobile): re-measure the web app's script sweep after agent.launch's capabilities moved out of protocol-version

The mobile web bundle check failed at 124 assets against a ceiling of 123. Main already sat
exactly on that ceiling: its sweep table read 69 scripts at 16 routes while the tree builds 73,
the whole margin of 4. This branch imports the agent.launch capabilities from their own module,
so protocol-version is no longer pulled into the root layout and four other routes. That moves
which routes share which modules, and the Qoder capability module, imported by protocol-version
and the AI-vault resume path, no longer shares an importer set with anything, so it gets a chunk
of its own: 74 scripts.

The fence says to re-derive the bound rather than raise it, so the sweep is re-measured on this
head (every prefix of the sorted route list). The worst route now adds 10 scripts (session), not
9, which moves the pinned shell crossing from 32 to 30 routes; main re-measured on its own lands
on the same crossing.

* fix(agent-launch): a worker's brief needs its agent found in front, and Grok's start answers on its composer

A paired-server worker start whose agent exited at startup typed its brief into the server's
shell, which ran it: the idle wait can settle on a shell back at its prompt, and the brief was
written with no foreground read. Both worker-start paths now check before each brief write, as a
launch prompt is checked: on a host that can find the agent in front it must be there; on one that
cannot (Windows) a shell proven in front still refuses, and anything else writes as before.

A Grok worker start waited ~10 s more than main: its only rest signal is its bare name, which a
launch holds to quiet output, and Grok animates its logo for ten seconds after its composer glyph.
A worker start for an agent whose rest signal is its bare name and whose composer draws a marker
(Grok, DSH, mimo-code) now also answers on that marker, whichever comes first.

* test(startup-staging): expect the CR that submits a typed launch line since #23672
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing as superseded: a merged PR already covers this change. Merged #25624 (47f4d3f) body: 'supersedes #23748, which routed the same buttons with its own tab reservations'; desktop AI buttons now start agents through agent.launch. #23962 also notes it no longer carries

(Stale-PR cleanup 2026-10-20, approved by Brennan. Reopen if something here is still missing on main.)

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