Repository navigation
Start orchestration workers with their brief on the launch command line (step 4 of 7) - #24081
brennanb2025 wants to merge 10 commits into
Conversation
cb0d0fa to
de94a21
Compare
8386519 to
f57b243
Compare
cc63203 to
74086ca
Compare
The worker brief was its only producer, and #24081 now routes the brief through the carry rule as ordinary text, so every launch file was minted with sensitive: false. The flag, its wire field, its parse check and the skip in the pane prompt cache are removed; nothing that crossed the wire was released.
8ffdf2f to
193eead
Compare
|
Final live QA of the agent-launch stack, the worker-start rows (2026-10-02). Report only; full report with every number kept by the coordinator. Builds compared: main
|
193eead to
a704428
Compare
|
QA update on
|
a704428 to
94e15be
Compare
|
QA update on
Nothing was typed into any shell in these rows. |
3ad7e2b to
d93c542
Compare
Round-1 review fixes. The worker brief no longer forces its own sensitive launch file past the host checks. Worker start offers the brief as an ordinary startup prompt with main's paste (`once-agent-runs`): it rides the agent's launch line where the line carries it exactly, and is otherwise pasted once the agent is ready, as main pasted it, so a host that cannot take a launch file no longer fails the start. With no sensitive file, the agent's hook reports the brief itself, which status readers compact to its task as on main. - Created worktrees are agent-first again, as on main: the startup terminal keeps the handle the brief names, setup and default tabs follow it, and a wait-for-setup sequence still holds the agent until setup succeeds. - The brief names the CLI command the spawn's own WSL decision will give the terminal. - A launch the shell reports finished, with no agent in front, fails and tears down the start. Where hooks are off or never reach the pane, the launch is judged by main's evidence: a title turn-start edge, or the agent holding its terminal.
…settle With agent hooks off, a worker whose brief rode its launch line is judged by the agent holding its terminal. That verdict used to wait a further 2 s for a startup dialog to settle; it now counts as soon as the foreground read proves the agent and a direct read of the screen shows no startup dialog. The foreground read itself is unchanged.
…n terminal #24257's read now takes the pane's root pid from the session inventory and one ps limited to its terminal, so the launch-observation tests fake those rows instead of the whole-machine scan the read no longer uses.
… its staging folder A host that cannot write the folder staged lines and launch files go in now plans main's delivery up front, so a worker brief offered with main's paste is left for that paste instead of being refused at terminal create.
d93c542 to
414d9ab
Compare
|
Final QA on this head
Not re-run on this head: the macOS worker rows (real Claude with hooks on and off, read-only staging folder), last measured on |
There was a problem hiding this comment.
Important
Two paths can fail a launched worker that is actually fine: the exit test folds a 'unknown' foreground read into exited, and the launch-evidence fallback is gated on "some status row exists" rather than "the prompt-carrying hook was seen". Both are inline with fixes. A third, smaller consistency gap is inline as well.
Reviewed changes
- Worker brief on the launch line (
worker-launch-brief.ts,worker-start-agent-placement.ts,worker-worktree-creation.ts,worker-topology.ts,local-worker-start.ts): the handle is minted before the spawn, the preamble is built early, and the brief is offered to the one carry rule asstartupPromptwith pasteonce-agent-runs;carrieddecides line vs.deliverWorkerDispatchPreamble, and a pre-allocated-handle mismatch closes the adopted terminal and fails the start. - Launched-turn acceptance (
launch-turn-start-observation.ts,worker-start-turn-observation.ts,orca-runtime-agent-prompt-request-correlation.ts): the hook's prompt-carrying turn is the proof; title edge or agent-in-front is the fallback when hooks are off/silent; a shell-finished launch with no agent in front fails asexited; a startup dialog reportsturn_start_blockedand keeps the terminal bound withworker-stoprecommended. - New hook field
explicitPromptStartedAt(server-status-application.ts,server-types.ts,server-persistence.ts,server-status-identity.ts,agent-status-ipc-payload.ts,runtime-hook-agent-row-selection.ts,orca-runtime-serialize-agent-prompt-submission.ts): pinned for aworkingrun, cleared when the turn ends, live-only, never persisted. - Prediction and budget sharing (
spawn-cli-command-prediction.ts,worker-setup-gate.ts,runtime-managed-worktree-create-types.ts, worktree startup plumbing): WSL-aware CLI command predicted for a not-yet-spawned terminal, and setup plus the launched turn share one start budget floored atAGENT_PROMPT_EFFECT_TIMEOUT_MS.
I traced brief delivery across all four worker-start paths (existing worktree, local new worktree, SSH new worktree, reused terminal) and it is delivered exactly once: onStartupPromptCarry fires exactly once and startLocalWorker's paste branch is mutually exclusive with the carried branch. The four new-test files I ran pass.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
| while (!signal.aborted && Date.now() < deadline) { | ||
| if (probe.launchRecorded()) { | ||
| launchSeen = true | ||
| } else if (launchSeen && (await probe.readForeground()) !== 'agent') { |
There was a problem hiding this comment.
!== 'agent' treats 'unknown' as an exit. readLaunchedAgentForeground returns 'unknown' whenever the host cannot prove the foreground — a transient readTerminalProcessRows/getForegroundProcess miss locally, or an SSH relay that cannot answer inspectProcess — and LaunchedAgentForeground documents only 'shell' as "the agent exited". Once the launch command retires (launchRecorded() false) and the next read is 'unknown', this returns exited, observeWorkerLaunchTurnStart throws, and the start fails at turn_observation, tearing down a terminal whose agent is still alive.
Technical details
# `unknown` foreground must not count as an exit
## Affected sites
- `src/main/runtime/launch-turn-start-observation.ts:81` — `else if (launchSeen && (await probe.readForeground()) !== 'agent') return 'exited'`
- `src/main/runtime/launched-agent-foreground.ts:56-61` — `asLaunchedAgentForeground` maps `'other'` to `'agent'`, so `readForeground` is only ever `'agent' | 'shell' | 'unknown'`
- `src/main/runtime/orca-runtime-activate-managed-worktree.ts:231-232` — the established read uses `=== 'shell'` for `isShellInFront`
## Required outcome
- Only a foreground read that positively proves the shell came back (`'shell'`) is an exit; `'unknown'` leaves the launch unproven, so the fallback / hook proof still decides.
## Evidence
- The only `exited` test (`src/main/runtime/launch-turn-start-observation.test.ts`) uses `readForeground: async () => 'shell'`; no test covers `'unknown'`, which is why this passed.| } else if (launchSeen && (await probe.readForeground()) !== 'agent') { | |
| } else if (launchSeen && (await probe.readForeground()) === 'shell') { |
| this.observeLaunchHookTurn(handle, ptyId, launch.launchStartedAt, timeoutMs, stop) | ||
| } | ||
| : {}), | ||
| hookReachedPane: () => this.getFreshExplicitAgentStatusForPty(handle, ptyId) !== null, |
There was a problem hiding this comment.
hookReachedPane means "a fresh status row exists", but the hook proof accepts only a row carrying explicitPromptStartedAt. launch-turn-start-observation.ts:86 suppresses the launch-evidence fallback for the whole window whenever this is true, so a row that carries no prompt — a prompt-less SessionStart, a refresh, a hydrated row, or an older/paired host whose hooks never set hasExplicitPrompt — blocks the fallback and the carried brief settles outcome_unknown after the full timeout. Main accepted exactly this launch from the title edge or the agent-in-front read.
Technical details
# Status-row presence is not prompt-hook proof
## Affected sites
- `src/main/runtime/orca-runtime-agent-prompt-request-correlation.ts:138` — `hookReachedPane: () => this.getFreshExplicitAgentStatusForPty(handle, ptyId) !== null`
- `src/main/runtime/launch-turn-start-observation.ts:84-98` — fallback only runs inside `if (hooksSilent)`, and `hooksSilent` requires `!probe.hookReachedPane()`
- `src/main/runtime/agent-prompt-submission-verification.ts:128-133` — `explicitPromptOnly` accepts only `current.explicitPromptStartedAt > baseline`
- `src/main/agent-hooks/server/server-status-application.ts:295-310` — the field is set only when `payload.hasExplicitPrompt === true`
- `src/main/agent-hooks/ssh/ssh-relay-session.ts:1693` — an old relay may forward no `hasExplicitPrompt`, so newer local main computes no field while rows still flow
## Required outcome
- A launch whose hooks reach the pane but never carry the prompt field should still be judged by main's evidence (title turn-start edge, or the agent holding the terminal) once `LAUNCH_HOOK_SILENCE_MS` has passed, rather than only by the hook proof.
## Suggested approach
- Track whether a row actually carried `explicitPromptStartedAt` (not merely that a row exists), or clear the `hooksSilent` gate on the silence window even when rows are present.
## Open questions for the human
- Is this accepted for agents (e.g. Antigravity) whose hooks publish a session-start row but no prompt-carrying hook for a command-line prompt?| if (sameRun && previous.explicitPromptStartedAt !== undefined) { | ||
| return previous.explicitPromptStartedAt | ||
| } | ||
| return payload.hasExplicitPrompt === true && payload.isReplay !== true ? at : undefined |
There was a problem hiding this comment.
resolveExplicitPromptStartedAt omits the payload.toolAgentId === undefined && payload.providerSessionOnly !== true guards its sibling resolveTurnStartedAt applies (:36-42), so a child-agent or identity-only event that carries hasExplicitPrompt can stamp the main run's field, and the launch observer can accept it as the worker's brief. Add the same guards.
Brings #24257's merge of main (e02adc5) through #23962 and #24080. - Worker start: a structured session or a brief that rode the launch line still waits only on the setup gate; any other worker waits through main's waitForWorkerAgentReady, which routes a fresh launch through #24257's worker-start wait and a reused terminal to plain tui-idle. - startupPromptPaste is threaded through main's resolveWorktreeCreateAgentStartup and its extracted terminal startup builder. - Main's worker agent preflight (configured agent params, the OpenCode model probe) moved into local-worker-agent-launch.ts so local-worker-start stays under the line cap.
…-on-launch-line
…nto brennanb2025/worker-brief-on-launch-line A brief that rode the launch line still skips the paste; a pasted brief takes #24257's new check. The local brief-check test uses Amp, whose brief is pasted here since an argv agent's rides the line.
There was a problem hiding this comment.
ℹ️ No new issues in the incremental changes. The worker-start brief is now wired to the host's agent-in-front write guard, so a pasted brief is never typed into a shell that returned after the agent exited.
Reviewed changes
- Wired the worker brief to the host's agent-in-front write guard:
local-worker-startpasseslaunchedAgent(null for a caller-supplied--terminal) intodeliverAndSettleWorkerStartReadiness, which forwards it todeliverWorkerDispatchPreamble; a fresh-launch brief now refuses the paste unless the host proves the launched agent is in front, while a reused terminal keeps its idle-wait paste. - Narrowed the settlement args
agent/launchedAgentfromstring | nulltoTuiAgent | null, so the guard receives a checked agent. - Retargeted
worker-brief-agent-in-front.test.tsatamp— an agent whose brief is pasted, so the guard actually runs — and added a reused-terminal case asserting no foreground read and one write.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…-on-launch-line
…o the stack is one chain #24934 was stacked on #24257; it now sits on #24081 (24257 -> 23962 -> 24080 -> 24081 -> 24934). The prompt-delivery table keeps #24080's argv row and this branch's unconfirmed row. The restart replay test pins its stub to the paste path, since #24080's fixture now reports an offered prompt carried unless told otherwise.
…/worker-brief-on-launch-line
…/agent-launch-identity-record
|
Parked as a draft: launch-point unification (all agent launches through agent.launch: #24934, #25430, #25624) is being rebuilt directly on main so it doesn't wait on delivery changes. This PR changes how the first prompt reaches the agent, and it comes back after unification lands, as its own PR on top of it. |
… 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.
… written when the surface exists (#24934) 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.
…ed (#25430) * feat(agent-launch): host-assigned caller identity and a launch record 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. * feat(agent-launch): show an agent's tab at once, where the caller asked 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. * style(runtime): one-line the tab-order map so the headless browser-tabs 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.
…agent.launch (#25624) * feat(agent-launch): host-assigned caller identity and a launch record 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. * feat(agent-launch): show an agent's tab at once, where the caller asked 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. * feat(agent-launch): the desktop AI buttons start their agent through agent.launch Part 3 of 3 of the agent-launch unification, on main. The AI buttons (Fix checks, the source-control actions, commit/push recovery, Explain commit, notes and annotation sends, session continuation) started their agent from the window. They now ask the host's agent.launch to start it, with no prompt, into the tab the window makes at the click, under one operation id per click; the window's pane waits for the host's agent instead of spawning a shell. The window then pastes the prompt with main's own paste, moved out verbatim and started only once the host's agent holds the tab, so delivery and follow-ups are exactly main's. A typed new-tab prompt and launches while chat is the default keep main's path. The source-control dialog drops its launch-command preview, which the host builds now. * style(runtime): one-line the tab-order map so the headless browser-tabs 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. * fix(agent-launch): watch an AI button's agent for readiness from its first output The window started watching for the agent's ready signal only after the host answered agent.launch, so an agent that had already turned on bracketed paste by then was never seen ready and its prompt waited out the readiness budget. Readiness is now watched from the moment the tab's terminal exists, as main watches it; the paste is written, and the chat copy seeded, only once the host says its agent started in that tab.

What this fixes
Orchestration's
worker-startlaunched the agent empty, waited for it to look idle, then pasted the dispatch brief (the worker's task and the commands it reports with) into its screen.On a fresh Codex 0.157 home that paste lost or cut the first ~1 KB of the brief five times out of five, and orchestration still recorded the start as "observed", because Codex's startup spinner title counted as a started turn (#23745).
A pasted Enter could also land on a startup dialog such as Codex's "Update available", which defaults to "Update now".
What changes for you
worker-startreports "observed" only after the agent's own hook reports a turn that carried the prompt. A spinner title, a session start with no prompt, or output bytes no longer count while hooks are working.worker-stop. The terminal stays bound to the Dispatch, so answering the dialog yourself runs the task and its report settles normally.Scope
Affected:
orca orchestration worker-startlaunching a terminal worker in a fresh terminal, for every agent that takes its first prompt on its command line (Claude, Codex, Gemini, Cursor, Antigravity, Grok and the others), on macOS, Linux, Windows, WSL and SSH hosts, including worktrees the start creates.Not affected: reused terminals (
--terminal), structured (chat) workers, and agents that take text only after start. Those keep the paste after ready.The plan, and where this PR sits
This is step 4 of 7 of the agent launch plan.
agent.launch.How it works
once-agent-runs). Step 3's rule decides: the exact line, else the paste. The rule reports which throughonStartupPromptCarry.explicitPromptStartedAt(when the current working run first saw an event carrying an explicit prompt); it is kept in memory, never persisted, and reaches the runtime on the existing status rows, SSH rows included.launch-turn-start-observation.ts) takes the hook proof first. If the shell reports the launch command finished and the agent is not in front, the start fails as exited. If no hook event reaches the pane within 10 s, or hooks are off, a title turn-start edge counts as observed, and the agent holding its terminal counts as started with no turn signal (unsupported) as soon as the foreground read proves it and a direct read of the screen shows no startup dialog.Why this approach
Differences from the common pattern:
How this differs from the previously reviewed version (b7ec3502370)
Round-1 review fixes, as a second commit on top of the rebuilt change:
orca-ide, as main does.awaitStructuredWorkerSetupGateis renamedawaitWorkerSetupGate(it now serves both kinds of worker), and the unreachable "no prompt hook" readiness branch is folded into the fallback above.Size
56 files, +2,282/−312 against step 3: production 29 files +785/−97, tests 27 files +1,497/−215.
Testing
runtime-rpc-long-poll-transportkeepalives) failed once at load ~105 and passed alone 3/3, including feat(agent-launch): keep long prompts off the launch line and paste them after readiness (step 1 of 7) #24257's foreground-read tests, which step 3 now carries: every test this PR touches, every test importing a changed module (worker start, worker dispatch, hook status store, prompt verification, worktree startup, terminal create), the two end-to-end unit tests that start a worker, and the runtime spec harnessorca-runtime.test.ts.tc:node,tc:webandtc:clipass; oxlint on the 56 changed files andcheck:code-quality:changedare clean.UserPromptSubmitwith the command-line prompt and the worker read "observed" 0.6–0.7 s after the hook; Codex'sUserPromptSubmitcarried its prompt before "observed", and Codex 0.158 skipped its update dialog when started with a prompt. This version has not been run live; the coordinator schedules QA per PR.Visual Proof
N/A: worker start has no UI of its own; the evidence is the receipt and the hook timeline, which the coordinator's QA will attach.
Notes
Author X handle: @BrennanKB5