Repository navigation
feat(agent-launch): show an agent's tab at once, where the caller asked - #25430
Conversation
There was a problem hiding this comment.
Important
One real edge in the withdrawn-verdict path can close a pane the user still has: when the launch pane's leaf has already left the layout, applyAgentLaunchPaneVerdict mistakes the user's remaining single leaf for the launch pane and closes the whole tab. Two smaller reuse-related stale-state points are inline.
Reviewed changes
Step 6 of 7 of the agent-launch plan, stacked on #24934. It publishes an agent.launch terminal tab to the window before admission, then derives each launch pane's fate from the durable operation ledger and keeps the final answer on the tab. I read the full 98-file diff.
- Main publish/verdict pipeline — new
agent-launch-tab-publication.ts(publish before admission; take back only a tab nothing will run in),agent-launch-pane-attachment.ts(in-process running marker + record-backed verdict), andspawn-begin.tsgating a launch pane's spawn on that verdict. - Wire — tolerant
placement/presentationrequest fields, aplacementreceipt, hostagent.launch.placement.v1and clientagent.launch.unstarted-tab.v1capabilities; both fields excluded from the replay fingerprint, no protocol bump. - Ledger —
ownedPanewritten with the winning claim and kept through settle. - Window — tab creation under host ids with placement/fallback,
agentLaunchPaneon the tab, the pane notice, and the user-close-wins reveal. - Persistence —
agentLaunchPanesurvives restart; malformed values are dropped without failing the session. - Mobile — the
"+"launch sendsafterTabId, advertises the capability, and no longer reads a terminal-less listed tab as a started agent.
Confirmed out of scope after checking: ordinary same-caller retries are coalesced by runReplaySafeAgentLaunch before publishEarlyTab, so the runningLaunchesByPane overwrite is not reachable through that path; agentLaunchPane does persist and restore; placement fallback is correct.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
| case 'withdrawn': { | ||
| forgetAgentLaunchPanePrompt(tabId) | ||
| // The pane alone when the user split the tab while it waited; their split stays. | ||
| if (state.terminalLayoutsByTabId[tabId]?.root?.type === 'leaf') { |
There was a problem hiding this comment.
The withdrawn branch closes the whole tab whenever the current layout root is a single leaf, but it never checks that leaf is leafId. If the user split the tab while the launch waited and then closed the launch pane, the root collapses to their leaf and this closes that tab too — contradicting the comment that their split stays. Closing a pane leaf does not clear agentLaunchPane, so the guard above still passes.
Technical details
# withdrawn closes the user's remaining pane
## Affected sites
- `src/renderer/src/lib/agent-launch-pane-verdict-application.ts:33` — `root.type === 'leaf'` is read as "the tab holds only the launch pane", but after a pane close the single leaf is whichever leaf survived (the user's).
## Required outcome
- Close the whole tab only when the single remaining leaf is `leafId`. When `leafId` is no longer in the layout, clear `agentLaunchPane` and leave the user's pane; when `leafId` is present in a split, keep today's `applyClosedTerminalLeafNotice(tabId, leafId)` behaviour.
## Open questions for the human
- For a `withdrawn` whose `leafId` is already gone from the layout, should anything be emitted at all?| } | ||
| } | ||
|
|
||
| export function wasAgentLaunchPaneClosedByUser(tabId: string, leafId: string): boolean { |
There was a problem hiding this comment.
wasAgentLaunchPaneClosedByUser only reads the set; nothing ever clears a mark, and the key is (tabId, leafId) with no operation scope. A later launch that reuses that pane key — callers may supply a stable paneKey, and the renderer explicitly re-arms agentLaunchPane on reuse — would have its reveal killed as if the user had closed it. Clearing the mark when a new launch arms the pane, or scoping it to the launch, keeps "close wins" from leaking onto a different launch.
| rememberAgentLaunchPanePrompt(tabId, request.prompt) | ||
| } | ||
| // A new launch into this pane: whatever an earlier launch left on it no longer stands. | ||
| store.setTabAgentLaunchPane(tabId, { leafId }) |
There was a problem hiding this comment.
The reuse branch re-arms agentLaunchPane but only overwrites the remembered prompt when the new request carries prompt. A new launch into the same pane with no prompt leaves the previous launch's text in promptsByTabId, so a failed launch can offer to copy another launch's prompt. Clear the remembered prompt here before optionally remembering the new one.
Live QA status: base #24934 (
|
| Row | Base | This PR | Verdict |
|---|---|---|---|
| 1. Tab visible, Windows PowerShell, click 4 s after the first PowerShell started, first launch (n=5) | 122 ms (119–128) | 30 ms (29–33) | faster; not ≤ 2–4 ms on a first launch (~21 ms is the workspace lookup before the publish) |
| 1. Same, warm (n=10) | 56 ms | 7 ms | ✅ |
| 1. Mac, first / warm | 49 / 19 ms | 17 / 5 ms | ✅ |
| 1. Ordering (30 Windows + 15 Mac PR launches) | tab waits on admission, record open, spawn, daemon create | tab requested before admission, admission before spawn, tab in store before the daemon create, every time | ✅ |
| 1. Hooks-on cold window (1.6 s in the earlier measurement) | not reproduced with hooks off (base 119–128 ms) | — | unverified live; the PR's tab is independent of it by the ordering above |
| 2. Placement: group / after-anchor / gone group + anchor / gone group | always the active group | as requested; fallback: 'anchor-group' / 'active-group' in the receipt; every launch ran |
✅ |
| 3. Desktop switched workspace mid-launch; two phones | desktop stays; caller selected | same; the tab is in the target workspace at 19 ms; the other phone is unchanged; no notice | ✅ |
| 4. Agent crashes at start / command not found | plain shell, no notice | plain shell, no notice (verdict proceed) |
❌ against the row's expectation, same as base (F1) |
| 4. Host killed before the spawn answered, restart | no tab | "Couldn't confirm the agent started.", never a shell | ✅, then ❌ after a phone retry (F6) |
| 4. Agent survived a host kill, restart | adopted | adopted | ✅ |
| 5. Same op + pane ×4; op without pane ×3; refusals without pane | 1 tab, 1 agent, no flash | 1 tab, 1 agent, no flash | ✅ |
| 5. Refusal with a fresh pane | no tab | its tab flashes ~37 ms, then goes | by design; visible (F3) |
| 6. User closes the tab during the wait | — | stays closed, no agent ran | ✅, but the reply is agent_session_operation_unknown (F2) |
| 6. Window reload during the wait | — | the tab comes back with its agent | ✅ |
| 7. Refused launch whose tab the user split | — | only the launch pane closed; the user's split and tab stayed | ✅ |
| 8. Record warm-up | opens on the first click (9–33 ms in the click path) | opens after startup once the phone connects: 25 ms, no main-thread stall ≥15 ms (fresh); 3 ms (existing) | ✅ |
| 9. Phone view of an early / failed tab | not listed | listed as pending-handle, terminal: null; a "couldn't confirm" tab stays so indefinitely |
data ✅; phone UI blocked (no PR mobile build on a simulator) |
Findings a user would notice
F1. "The agent couldn't start." never appeared in a live failure.
- On macOS the agent runs inside the pane's shell. An agent that exits at once, or whose command is missing, leaves a plain shell in the tab on both arms.
- The notice covers only failures before the PTY spawn is dispatched, and I found no live trigger for that. A workspace folder removed mid-launch ends as "Couldn't confirm".
- Not a regression, but the row's expectation of "never a plain shell" is not met.
F2. Closing the tab while the agent starts returns agent_session_operation_unknown to the caller.
- The tab stays closed and no agent runs, as described. But the description says the launch still reports the terminal it created.
- The main log shows
failed to persist runtime PTY binding after spawn: terminal_split_source_not_found, and the request fails withunknown. - Retrying that operation also returns
unknownand re-shows the closed tab for ~37 ms.
F6. A phone retry erases the "Couldn't confirm" notice and leaves a blank pane.
- After a host restart, the restored tab correctly says "Couldn't confirm the agent started.".
- When the phone then retries the same operation, the host answers
unknown, but the tab's kept outcome is cleared. The pane is left blank: no notice, no shell, no agent. It was still blank 70 s later. Seen in 3 of 3 runs. - From the code (not traced live): the retry republishes onto the existing tab (
created: false), which resets what the earlier launch left on it. Admission then refuses, and nothing sends a new verdict, because only a tab the request created is taken back.
F3. A refused retry carrying a new pane flashes an empty tab for ~37 ms.
F4. A host crash within ~0.3 s of the publish loses the early tab across a restart, because it had not been saved yet. The user sees no tab and no agent.
Screenshots
Captured from the hidden rig window over the DevTools protocol. "Grok" means an independent read-only driver captured and described it; "CDP" means a scripted capture.
Row 1, spawn held: on base there is no launch tab yet (CDP); on this PR the launch tab ("Terminal 3") is already in the strip, after the anchor (Grok).
Row 2, placement: after-anchor and requested-group launches (Grok).
Row 4:
- A crash at start leaves a plain shell (Grok).
- "Couldn't confirm" after a restart (Grok).
- The same pane after a phone retry, blank (Grok).
- "Couldn't confirm" + Copy prompt before a restart (CDP).
Row 6: the tab was closed while the agent started; no tab came back and no agent ran (Grok).
Not verified
- The hooks-on cold window.
- The phone UI.
- Behaviour rows on Windows (Windows ran timing only).
- SSH, WSL and folder workspaces; real agents; packaged builds.
Safety
- Real agent configs: unchanged on Windows throughout. On this Mac,
~/.codex/config.tomlchanged once at 07:11, outside any rig run. The writer is not established; the file was not read, fixed or restored. - Isolation: every stub recorded a home inside its rig directory.
- Processes: every rig was stopped by its recorded PID; final scans are clean on both machines.
…b is the launch's answer Live QA of #25430. F6: after a restart showed "Couldn't confirm the agent started.", a phone retry republished onto the tab, the window reset its kept verdict and nothing re-sent one, leaving a blank pane. - No early tab for an operation the record already holds: a retry of the same launch, a conflict or a recorded failure shows nothing (also F3's flash when the record is open). - The tab records which operation laid out its pane; only a different operation resets what an earlier launch left on it. - Every shown pane nothing spawned into ends in a verdict: taken back, or what the record now says. F2: closing the tab while the agent starts answered agent_session_operation_unknown, and a retry flashed the tab. - The window reports the explicit close to the host. The launch stops before its spawn, or stops the agent that spawned, and records agent_launch_tab_closed as its answer for the caller and every retry. - Callers advertising agent.launch.tab-closed.v1 (this phone build) get that word; others keep agent_session_operation_unknown. Also moves the execution-outcome helpers out of agent-launch.ts.
|
Live re-check of the QA fixes on Head moved during the run: it is now Two new problems:
Fixed live:
Observation: a close that lands inside terminal creation still lets the agent process start, and it is stopped a moment later. The close check sits just before terminal creation ( Not run: Windows, the phone's UI (still blocked), the hooks-on cold window. The real agent configs on the M4Air were unchanged. |
…ches a "couldn't confirm" pane Live QA re-check of #25430. F7: agent_launch_tab_closed was not in the RPC passthrough codes, so a caller that reads it got runtime_error with the word in the message and the phone showed the raw code. Pass it through; the only new code this stack sends a client. F8: a new operation into a pane that showed "couldn't confirm" started its agent but the pane, whose spawn had been refused, never spawned again. A different launch reusing a pane with an outcome now remounts it (the tab-generation seam), and a pane that waited on a launch here reads the record, not the tab's saved outcome from an earlier launch. Race: a close landing while the agent's terminal is created is checked again right after creation, so the agent is stopped before any prompt is pasted. A prompt carried on the command line has already reached it.
|
Live check of F7, fixed. If you close the tab while the agent is starting, a phone that advertises F8, fixed. Setup: the host is killed mid-launch, then restarted, and the pane shows "Couldn't confirm the agent started." Three phone retries of that operation leave the notice in place (9/9).
Close race. I closed the tab while the launch was held at four points: before
Sanity. A normal launch plus two warm ones all succeeded; Grok's screenshot shows the agent drawn. The tab reached the store in 15 ms (warm: 3 and 3), and the reply came back in 59 / 50 / 50 ms. The re-check's numbers for the same scenario were 103 (warm 7 / 5) and 343 / 263 / 115 ms. This is n=1, and the machine was less loaded this time. Not covered: Windows, real agents, packaged builds, SSH and folder workspaces, and the phone's screen (F5 is still blocked). Real agent configs were hash-checked before and after every run. |
There was a problem hiding this comment.
Important
The four live-QA fixes in the new commits look correct, but the earlier finding that a withdrawn verdict can close a pane the user still has remains unaddressed.
Reviewed changes
This run re-reviewed the delta since the prior pullfrog review at f04dea9dbb: the three authored fix commits (b35fdb3ba4, 2cddd5146d, 648a4d4ac5) plus a base merge of #24934 that carries no PR-authored change.
- A retry never resets its launch's tab —
isRecordedOperationnow suppresses the early tab for any operation the record already holds; the tab stores itsoperationId, and only a different operation resets what an earlier launch left on the pane. - A closed tab is the launch's answer — the window reports a user close over IPC; the host stops the launch (or the agent that spawned) and records
agent_launch_tab_closed, which reaches only callers advertisingagent.launch.tab-closed.v1(and the CLI). Every shown pane that nothing spawns into now ends in a verdict. - A new launch reattaches a "couldn't confirm" pane — a different operation reusing a pane with an outcome remounts it via a
generationbump, andsettleVerdictignores the tab's stale saved outcome once it has waited on a launch here, so it reads the record's fresh row. - Runtime boundary — the close-report listener moved into
register-handlers, keeping electron out of the runtime graph. - Wire capability
agent.launch.tab-closed.v1, the mobile wording of the code, and theRUNTIME_PASSTHROUGH_CODESentry all line up with the existing capability-gated pattern.
The prior review left three open threads (the withdrawn edge below, plus the never-cleared closedPaneKeys and the stale remembered prompt on a promptless reuse); none are touched by this delta.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
| case 'withdrawn': { | ||
| forgetAgentLaunchPanePrompt(tabId) | ||
| // The pane alone when the user split the tab while it waited; their split stays. | ||
| if (state.terminalLayoutsByTabId[tabId]?.root?.type === 'leaf') { |
There was a problem hiding this comment.
The withdrawn branch closes the whole tab whenever the current layout root is a single leaf, but it never checks that leaf is leafId. If the user split the tab while the launch waited and then closed the launch pane, the root collapses to their surviving leaf and this closes that tab too, contradicting the comment that their split stays. Closing a pane leaf does not clear agentLaunchPane, so the guard at line 16 still passes.
Technical details
# withdrawn closes the user's remaining pane
## Affected sites
- `src/renderer/src/lib/agent-launch-pane-verdict-application.ts:33` — `root.type === 'leaf'` is read as "the tab holds only the launch pane", but after a pane close the single leaf is whichever leaf survived (the user's).
## Required outcome
- Close the whole tab only when the single remaining leaf is `leafId`. When `leafId` is no longer in the layout, clear `agentLaunchPane` and leave the user's pane; when `leafId` is present in a split, keep today's `applyClosedTerminalLeafNotice(tabId, leafId)` behaviour.
## Open questions for the human
- For a `withdrawn` whose `leafId` is already gone from the layout, should anything be emitted at all?…top-buttons The host now sends agent_launch_tab_closed as its own code; a launch it refuses because the user closed the tab stays silent, as a closed tab does.
|
Back to draft: live QA of step 7 (#25624) found that a terminal tab shown early by this PR stays blank until the prompt is delivered (in agent-launch-tab-publication.ts, the pane's running.finished resolves only at the end of the whole launch). With status hooks off or over SSH that's 5–60+ s of an empty pane, and a startup dialog the user must answer can't be seen. Main draws the agent at once. The fix (release the pane as soon as the agent's terminal is bound to it) is being made here, with a test, before this goes ready again. |
…top-buttons An early tab's pane now attaches when its agent runs, not after its prompt lands (QA F1 on #25624).
…top-buttons The desktop renderer reads agent_launch_tab_closed, so a launch whose tab the user closed mid-delivery answers that code and the window stays silent.
Live QA (rounds 1–4) and review: summary before readyWhat this PR is for. When you start an agent from the phone or the CLI, Orca used to show its tab only after it had finished a list of startup chores. That took up to 1.7 s on a cold Windows machine. Now the tab appears first, where you asked for it, and connects to the agent when the agent starts. If the agent can't start, or Orca can't tell whether it did, the tab says so instead of turning into a plain shell. What live QA found, and what changedAll rows ran on the M4Air with stub agents (no real agent CLIs), comparing main with this PR. Rounds 1–4 are in order.
What review changed (two Claude review rounds plus a readiness pass)
Verified
Not verified
Known red check
Deferred (tracked, not in this PR)
|
…acts, not on the executor's surface CI lint, after merging #25430's final head: agent-launch-executor.ts was 301 lines. The launch time only travelled through the executor so the startup check could read it back; it is a fact about the terminal the surface factory just created, like the host facts it already keeps per handle. The factory now keeps both in one record, and the executor is #25430's again.
… written when the surface exists Step 1 of the agent-launch unification, on main. - The dispatcher stamps every request's caller from what its connection proved (runtime socket: the local CLI; the desktop's IPC: the desktop; a paired socket: its device). Params never set it. - The launch record is written twice: once when the tab exists (what creation settled: on the launch command, a draft, or a submit still `unconfirmed`), and again once the prompt's fate is known. A restart in between finds the running agent instead of answering "unknown". - A replay re-derives its terminal handle from the pane key in the running host, and shows `unconfirmed` only to callers that advertise agent.launch.prompt-unconfirmed.v1. - The record store opens in its own slot, without building the chat host; the chat host is built on that same store. Rebuilt from this PR's own commits (b9adf88, 0d7d9b5, c83f44d, 39d15d5) onto main, without #24080/#24081. Conflicts: the delivery doc table (main's "line fits" row plus the `unconfirmed` row), and main's journal-database open in install(), which now goes through the record-store slot.
7d98ac4 to
82544cb
Compare
…ed for it An agent.launch now shows its terminal tab before admission and spawn, in the requested placement (group and/or anchor tab), and the pane attaches to the agent as soon as it runs. A pane whose agent can't start, or whose start can't be confirmed, says so instead of becoming a plain shell, and keeps saying so across restarts. A user's close of the tab or its pane during the launch stops it and answers agent_launch_tab_closed. Whose view moves is unchanged from main for every caller. Rebuilt on main (with #24934) from the previous branch head 8f62858.
8f62858 to
ea4a899
Compare
Live re-check on the main-based rebuild (
|
| Row | main | This PR | Verdict |
|---|---|---|---|
| Blank pane, 600-byte prompt (pasted) | tab when the agent starts; pasted once | tab at 0.2 s, the agent drawn by 1 s; pasted exactly once after ready (6.96 s); receipt "handed to terminal" | PASS |
| Close the tab mid-delivery, desktop and CLI callers | "ok", prompt not delivered; agent stopped | both told agent_launch_tab_closed; agent stopped; nothing left running; no error shown |
PASS |
| A phone launch: whose screen moves | the desktop stays on its workspace; the launch's workspace is revealed in the sidebar | identical | PASS (unchanged from main, as intended) |
| Windows tab timing, PowerShell and cmd, 10 runs per arm (tab in store, median) | 156 ms first launch / 62 ms warm | 33 ms / 6 ms; agent start unchanged | PASS |
Placement, the phone landing on its own pending tab, and the phone's quiet close carry over from earlier rounds. CI is green on this head, 28/28.
Not verified: a real paired phone on hardware (an iOS Simulator through orca emulator was used), SSH workspaces, and real agent CLIs (stubs only).
…t-launch-instant-tab Conflicts resolved against #24934's rebuild (82544cb), which this branch was built on, so each keeps main's squashed #24934 plus this branch's change. agent-launch-floating-workspace.test.ts takes main's #22302 routing (a structured preference opens a Codex chat) with this branch's host-derived viewMode on the terminal branch. The record-store slot test gets the resolveLaunchArgs dep #25721 made required, the same line as main's fix.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughThe changes add early tab publication for eligible agent launches, placement and presentation fields, and launch-pane outcome tracking. The host and renderer exchange publication requests and pane verdicts. PTY spawning now checks pane evidence before proceeding. Closed-tab outcomes are handled by the host, renderer, and mobile clients. The changes also add native-chat initial view selection, record-store warmup, and localized launch-pane notices. Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to No actionable merge-blocking risk was identified in the selected change. The author also reports passing checks and live validation for the main agent-launch behavior. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
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. Comment |
…bs runtime stays under its line limit The merge of main (#25724's emitMobileSessionTabsSnapshot metadata) plus this branch's close mark put the file one line over max-lines. Formatting only.












ELI5
When you start an agent from the phone, its tab used to appear only after Orca had finished a list of chores: writing down the launch, finding the workspace, deciding chat or terminal, and starting the process. On a Windows machine whose first PowerShell was still booting, that took about 1.7 seconds. Clicking a button in the desktop app shows its tab in 2–4 ms.
The tab also always landed in whichever split was active, not where you asked.
Now Orca shows the tab first, in the place you asked for, and then does the chores. The tab waits for the agent and connects to it when it starts. If the agent can't start, or Orca can't tell whether it started, the tab says so instead of turning into a plain shell, and it still says so after Orca restarts.
What Changed
Before: an
agent.launchtab appeared only after the host spawned the agent. Measured on Windows (pwsh panes): 174 ms on a first launch, 66–80 ms warm, and 1,668 ms (1,555–1,697, 5/5 runs) when the click came ~4 s after the workspace's first PowerShell started. In that cold window the daemon's PTY create took ~1 s and opening the launch record ~0.55 s, and the tab waited for both. The desktop's own renderer-created tab: 2–4 ms in every case.After: the host asks the window for the tab as its first act, before it admits the launch. In a local harness that replays the measured costs, the tab is shown at 31–33 ms on a first launch, cold window or not, and 9 ms warm (step 5 under the same harness: 1,660 / 172 / 88 ms). Only a launch that carries an operation id gets the early tab (the phone always sends one); see "The pane after its launch" for why.
Before: the tab went to the active split. A launch request had no way to say where.
After: a request can name a group, the tab to follow, or both. If the group was closed meanwhile, the tab goes to the anchor tab's group, then to the active one. Placement never fails a launch: an id that isn't a string, or a key this host doesn't know, is dropped. The receipt says where the tab landed and whether it fell back.
Before (and in this PR's first draft): a pane that mounted before its agent existed was guarded only by a marker in memory. A restart, an unknown outcome, a second launch naming the same pane, or a refused launch could each leave the agent's tab running a plain shell, calling a running agent "not started", or killing a replayed agent.
After — the pane after its launch: the launch record from step 5 names the pane the window showed, written in the same transaction that lets the launch run, and the tab keeps what that pane's launch comes to. Every time the pane's process would start, main works out its fate and tells the window as a typed value:
While the launch's fate is open, the pane reads the launch record. Once it is settled (the agent attached, or the pane is an ordinary terminal) the tab forgets the launch, so a restored tab never opens the record during startup. Once it is final ("couldn't start" or "couldn't confirm") the tab keeps it for as long as the tab exists, past the record's 24 h replay window, until you close the tab or a different launch reuses the pane; after that the pane reads nothing but its tab. A retry of the same launch (a phone resending it after a lost reply) is that launch: it shows no tab and changes nothing the tab keeps. Every shown pane that nothing spawns into ends in a verdict, so it never stays blank. A different launch into a pane that shows an earlier launch's outcome remounts that pane, so it waits for the new launch and attaches to its agent.
The message stays in the pane: it can't be dismissed into a blank pane, has no "file an issue" link, and offers Copy prompt while this session of the app holds the prompt (after a restart the message shows without it). A pane no launch laid out pays nothing.
Before: closing the early tab while its agent was starting didn't stop the launch, and the tab came back when the agent spawned.
After: your close wins. The host records every close you make, of a tab or of one pane in a split, from the window, the phone or the CLI, and a launch still starting or delivering its prompt there is stopped. The launch stops before it spawns the agent, or stops the agent if it already spawned, and its answer, to the caller and to every retry of it, is
agent_launch_tab_closed; a retry shows no tab. The phone (which reads that word) says "The agent's tab was closed on the computer before it started, so it was stopped."; a caller that doesn't read it getsagent_session_operation_unknown, as it always did. A tab that is only missing (a window reload before the session saved it) is not a close: the agent keeps running and its tab comes back. A close that lands while the agent's terminal is being created is checked again as soon as it exists: the agent is stopped before any prompt is pasted. A prompt that rode the agent's command line has already reached it; that agent starts and is stopped a moment later (nothing is typed).Before: a second launch that named a pane another agent was running in could mark that pane as failed.
After: a launch whose pane is already running gets no early tab and never touches that pane; the runtime refuses it as before.
Before: the window decided a launched tab's first view (terminal or chat view) on its own, without knowing whether the prompt was a draft.
After: the host decides it by the same rule the window uses for a tab you open yourself, with the prompt's delivery included, and sends it explicitly. It is never a caller field.
Whose screen moves is unchanged from main:
Before: the launch record was opened on the first launch, on the click path.
After: it opens ahead of the first launch only when it can pay off: once startup has settled (1.5 s after the window's first load, or after the RPC server starts in headless
serve) and a client that can callagent.launch(a paired phone) is connected. A profile no such client uses never opens it early. Measured, the open blocks the main thread for 7–16 ms on a fresh profile and ~4 ms with 600 rows on an idle Mac, and 95–280 ms on the same Mac under heavy load (load average 24 on 18 cores). Admission still opens it on demand if this hasn't happened.Mechanism:
agent.launch(with an operation id) resolves the workspace (cheap), checks the settings-only mode preflight, and, for a terminal launch with a window that owns the layout and a pane nothing is running in, registers the launch as running for that pane and sendsui:publishAgentLaunchTabwith the caller'spaneKey(or one it mints), the operation id, the host-derived view, the placement, the viewer rule and the prompt. An operation the record already holds (a retry, a conflicting reuse of its id, a recorded failure) publishes nothing: nothing new runs for it.agentLaunchPane: { leafId, operationId }(persisted), and replies with the group it landed in.ownedPane: { worktreeId, paneKey }on its ledger row in the same transaction; settling the row keeps it.ui:agentLaunchPaneVerdict), which clears a settled one, keeps a final one on the tab, or takes a withdrawn pane back; a refused spawn fails with one fixed code.agent_launch_tab_closed.Wire: two optional request fields,
placement: { groupId?, afterTabId? }andpresentation: 'focused' | 'background'; one optional receipt field,placement: { groupId, fallback? }; three capabilities, each one promise:agent.launch.placement.v1, host-advertised (readsplacementandpresentation, reports the placement, may publish the tab before admission);agent.launch.unstarted-tab.v1, client-advertised (reads a listed launch tab with no terminal as not started); andagent.launch.tab-closed.v1, client-advertised (reads the new refusal codeagent_launch_tab_closed, which the RPC layer passes through as its own error code; others getagent_session_operation_unknownfor it, as before).presentationisterminal.create's existing field with its meaning; whose view moves is decided by the host per caller and matches main for every existing caller, so no navigation field is added. Both request fields are outside the replay fingerprint and never refused: a value of the wrong type or a word this host doesn't know is dropped. Older hosts drop them. No protocol bump. The ledger row gains an optionalownedPane, which older builds keep as part of the row and ignore; the tab gains an optional persistedagentLaunchPane, which older builds drop.Mixed versions: today's phone treats "my reserved tab is listed" as proof its agent started when a reply is lost. A tab listed before the spawn would break that, so the host shows a paired caller's tab early only when the caller advertises
agent.launch.unstarted-tab.v1. This phone build advertises it, sendsplacement: { afterTabId }for the "+" launch, and treats a listed terminal tab as proof only once it has a terminal. Other phones viewing the workspace (not the caller) do see the early tab with no terminal yet, and a failed launch's tab stays listed with no terminal; neither phone build has its own rendering for "couldn't start" (live QA scheduled).Why
The common pattern creates the surface first, under a caller-supplied or allocated id, starts the process after it, and records a start failure in that surface's own record, so it reads the same on every later view and after a restart, for as long as the surface exists. Only the client that created it moves its own view. This PR follows that for the terminal tab: tab first under the caller's pane id, process second, and the pane's final fate kept on the tab itself.
Creating the tab "alongside the spawn" instead would have left the cold-window admission cost (~0.55 s) in front of the tab; the measurement ruled it out as a fix on its own. Opening the record early alone leaves the ~1 s daemon create in front (harness: 1,117 ms). Keeping the outcome only in the launch record would have expired it with the record's 24 h replay window and made every restored launch tab open the record at startup; keeping the open question in the record and the final answer on the tab avoids both.
Differences from the common pattern:
paneKey(tab + pane) instead of a separatetabIdfield; two copies of one id can disagree, and the phone already sendspaneKey.serve), a launch without an operation id, and a desktop client paired to a remote Orca still get the tab when the agent spawns. Follow-up: the same publish through the headless session-tab model, and step 7's callers send operation ids.worktree.create's startup terminal, timing and view rule. Follow-up: step 7's workspace-create rework.Plan change: plan G1 called for an "agent started in workspace — Go to" notice when you have switched away. It is dropped under Brennan's UI rule (no notice the references don't show): neither reference shows one, and a launch you started elsewhere appears in the workspace's tab strip and sidebar, where the creating client highlights or navigates to it.
This is the second of three PRs that unify agent launching, rebuilt directly on main:
agent.launchPrompt delivery is main's (#24257: a short single-line prompt rides the command line, otherwise a guarded paste once the agent is proven in front). #24080 (the first prompt on the command line) and #24081 (worker briefs on the launch line) are parked and return later as their own PRs; this PR uses nothing from them.
Scope: the host's launch path up to the tab, the window's tab creation, the pane's verdict, the record's opening, and the phone's "+" request. No desktop caller uses
agent.launchuntil step 7, and nothing in theorcaCLI calls it yet.Linked Issue
Part of the agent launch plan; no separate issue.
Visual Proof
N/A for this draft: the change is when and where a tab appears and what a pane says when its agent isn't running. Live before/after on Windows (cold window) and the phone is scheduled as QA; this PR does not launch the app.
Testing
f04dea9dbb4, Mac zsh and Windows PowerShell, stub agents, a scripted paired-phone caller): tab before admission on every launch (Windows first launch 30 ms vs 122 ms on the base, warm 7 vs 56 ms); placement, per-viewer navigation, the user's split surviving a withdraw, a reload during the wait and the warm-up all passed. It found two problems, both fixed inb35fdb3ba43/2cddd5146d7and not yet re-run live: a phone retry blanked a "couldn't confirm" pane (F6), and closing the tab during the wait answered "unknown" with a retry flashing the tab (F2). Phone rendering of an early or failed tab is still unverified (no simulator lane). A re-check on2cddd5146d7(M4Air, the phone's own launch code run headless) passed F6, F3 and F2's retry and found two more: the tab-closed answer reached the phone asruntime_error(F7), and a new launch into a "couldn't confirm" pane started its agent but left the pane blank (F8); both are fixed in648a4d4ac51and not yet re-run live. Live QA of the next step then found an early tab stayed blank until the prompt was delivered, because the pane's spawn waited for the whole launch: a dialog the agent showed at startup was invisible until the delivery timed out. Fixed in6273cc5f508(the pane attaches as soon as the agent runs); a live check on that head passed it (the agent drew at 1 s, a startup question was visible, the prompt landed once, and closing the tab stopped the agent). That check also found that the desktop window was told "unknown" when you closed the tab mid-delivery, where the CLI was told the tab was closed; sinceda3707a1cc0the desktop window is told so too. A CLI launch into a just-created empty split landed in the first group, and the split disappeared (3 of 3 runs; main kept it). The cause: a CLI's focused launch made the window switch to the workspace after choosing the split but before creating the tab, and switching workspaces tidies away empty groups once any tab is live. Since8bba067ff41the window creates the tab in the split first and switches after, so the split stays and holds the tab; a live re-check on that head passed it 3 of 3. A Claude review of the last changes then found the design left a second guard: the window kept a copy of "a launch still runs in this pane" only to decide whether to report a close. Since8c77f31c9d3main hears the close from its own commit of it instead, which also stops a launch when you close just its pane in a split (before, only a whole-tab close did), and since91329fd297ethat covers a pane close the phone or the CLI asks for. The same lost-split order existed in a phone's focused "new terminal in this split"; since05137dcc577it creates first too, and a tab created this way still moves its workspace up in Recent. A live re-check on91329fd297epassed them all (every close answers that the tab was closed and stops the agent, including a split-pane close from the window and from the CLI; the phone-style new terminal keeps its split where main loses it). A readiness pass then added9473e9e0651(a phone landing on the launch's early tab no longer starts a second agent in it) and8f62858e606(the phone's closed-tab wording, a closed pane's outcome, a new local worktree's chat view); both are covered by tests, and a live run on8f62858e606with a paired iOS Simulator passed the phone rows: a phone landing on its own pending tab got exactly one agent and no error, and closing that tab during start stopped the agent with no error shown. The new-worktree chat-view fallback was not reached live (the startup terminal always came up), so it is covered by its test only.Local timing harness (the real
agent.launchReplayhandler and on-disk launch record, with the measured Windows segment costs injected: admission 554 ms cold / 52 first / 18 warm, workspace 23 / 0, spawn 1,063 / 83 / 59, window 7 ms; median of 5):The step-5 column reproduces the live measurement (1,668 / 174 / 80 ms), which is what makes the harness worth reading. It is a model, not a live run: the live cold-window rows still have to be run.
Opening the launch record (real code,
node:sqlite, a loaded 18-core Mac, 5 runs each): 142–282 ms on a fresh profile, 93–106 ms with 600 rows. That is why it now waits for the window's first load.What the tests pin (each was checked by breaking the code and seeing it go red: 14 in review round 1, 14 in round 2, 12 for the live-QA fixes):
agent_launch_tab_closed, answers it to a caller that reads it (the CLI, the phone, and the desktop window, which ships with its host) andagent_session_operation_unknownto one that doesn't, and a retry shows no tab; a tab merely missing after a reload comes back with its agent. A close during terminal creation stops the agent before its prompt is pasted. A pane attaches to its agent while the prompt is still being delivered, and a close then still stops the launch asagent_launch_tab_closed. The code reaches the caller on the wire asagent_launch_tab_closed(notruntime_error), which the phone's classifier reads as a definite failure.Not covered by an automated test: that the notice has no dismiss control (structural), the paired-client
onReadyhook that marks a launching client ready (the gate it feeds is tested), and the pane projection's use of the tab's outcome (the helper it calls is tested).Checks on the rebuilt head (
ea4a899fb2e, one commit on #24934's rebuild82544cbb3df, which is one commit on main842c6c667a8):7d98ac4c634..8f62858e606, whose merges only brought in feat(agent-launch): host-assigned caller identity and a launch record written when the tab exists (unification 1 of 3) #24934 heads) applied onto the new base with a 3-way apply; one conflict, an import line interminal-tab-close.ts(both kept). Plus the move-out above (2 lines and its tests).tc:node,tc:web,tc:clipass.useIpcEventstest,orca-runtime.test.ts, and the terminal store, terminal pane, pty IPC, agent-launch and runtime RPC trees: 2,347 files, 23,931 tests passed, 0 failed.agent_launch_tab_closed, agent stopped), whose screen moves for a phone launch (identical to main) and Windows tab timing (tab in store 33 ms first / 6 ms warm vs main's 156 / 62 ms) all pass. Rounds 1–5 ran on the earlier stack-based heads.AI Disclosure
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
placement, which the old host drops. Phones viewing a workspace see an early or failed launch tab with no terminal (see Mixed versions).Author: @BrennanKB5
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)