Skip to content

Accept messages while any agent starts, and bound each startup step - #28046

Merged
brennanb2025 merged 216 commits into
mainfrom
brennanb2025/agent-start-split-2-codex-start
Oct 19, 2026
Merged

brennanb2025 merged 216 commits into
mainfrom
brennanb2025/agent-start-split-2-codex-start

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 190 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​5015 $\color{#cf222e}{\Huge{\mathbf{−}}}$​11577 $\color{#cf222e}{\Huge{\mathbf{−}}}$​6562
Prod 158 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​5397 $\color{#cf222e}{\Huge{\mathbf{−}}}$​2456 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2941

ELI5

When you sent a message while a chat's agent was still starting, the message sat behind the whole start: ownership checks, crash recovery, launching the agent and its handshake. On a slow machine or a slow agent that could be many seconds, and Stop or closing the chat had to wait too. Startup was also killed after 60 seconds without output, even when the agent was legitimately busy, while some handshake steps had no limit of their own.

Now the host accepts your message the moment you send it and delivers it in order once the agent is ready, for every agent (Claude, Codex, Grok/OpenCode and other ACP agents, Pi). Stop, close and quit cancel a start at once. Each startup step has its own time limit, and output no longer counts as progress.

What Changed

Before → after, as a user sees it

  • Sending while the agent starts. Before: a second message, Stop or close waited for the whole start. After: messages are accepted at once and delivered in order; Stop and close act immediately and a late-starting agent is shut down without receiving anything.
  • Startup time limits. Before: one rule ended a start after 60 s without output, plus a 10-minute ceiling; Codex and Pi handshakes waited on the ceiling only, and ACP handshake requests had no limit. After: each protocol step has its own limit (Codex initialize 15 s and thread start/resume 30 s; ACP initialize 60 s, sign-in 120 s, session open 300 s; Pi state 30 s; Claude handshake 60 s), with one 10-minute overall limit counted from when the start begins. A step that runs out fails the start with the normal "couldn't start" message, naming the agent.
  • Pi. Before: Pi was ready only after its model list and commands loaded, and an empty model list was read as "signed out". After: Pi is ready as soon as it reports its state; the model list and commands arrive after. A signed-out Pi (which reports an unknown provider) still gets the sign-in message.
  • Each accepted message keeps its choices until delivery. The host captures the choices received with the message, using the chat's saved choices for anything omitted, and applies them before the prompt reaches the agent. A Codex model or effort change sent while a turn is running waits until that turn ends. Picks still being saved, or made while a chat is being created, retain the limitations listed below.
  • Behavior change: a choice that cannot be applied now fails that message. Before (main): if switching the agent to the message's model or effort failed, the message silently ran on the chat's current settings. After: that message fails with a plain-words error so you never get a reply from a model you didn't pick. A retired Claude model still switches to the list's default before sending, exactly as on main.
  • Phone "+". Before: tapping "+" for a new chat on the phone started the agent right away. After: it creates the chat and the agent starts on your first message, like desktop. Launches with a prompt, the CLI, orchestration workers and ensure still start the agent at once.

Mechanism

  • The structural cause: one per-chat lock was both where messages are accepted and where the agent is started, so anything the start waited on (process probes, recovery, spawn, handshake, history, handover) also blocked admission.
  • A start is now registered first, in one short step that also arms its cancellation and its overall clock. Everything it waits on runs outside the lock, raced with that cancellation. Every step under the lock is a short write that first re-checks that this start is still the current one and has not been cancelled, so a start that loses a race is settled through the existing failed-start path and its process is closed.
  • The existing decision logic is unchanged; each adjudicator was split into an evidence part (outside) and a commit part (inside), in the same order as before: lease reconcile, crash recovery, release, eligibility.
  • Handing a message to the agent is: write the pending row and arm its cancellation in one step, re-check the agent and cancellation, call the agent, write the result.
  • Codex, ACP and Pi now open their protocol (handshake, thread or session) in a separate host-awaited open, as Claude already did. Every adapter now owns its spawned process and has exactly one host-awaited open.
  • Message choices use main's existing send field, capability and saved columns; the host parses them once into typed choices that each agent applies at dispatch.
  • No new saved field, wire field or capability. The phone reuses the existing "create without starting" capability that desktop already advertises.

Why

  • This removes the class instead of patching one wait: admission and start no longer share a lock, so a new slow step in a start cannot block sends or Stop again.
  • It is the common pattern for agent chat hosts: the message is committed first, and starting the agent is its own cancellable job whose writes are fenced by that job's identity.
  • Per-step limits catch a hung handshake quickly without killing a busy agent that is merely quiet; output was never evidence that a protocol step was progressing.
  • Alternatives considered: another guard in the delivery loop only (leaves ready operations, recovery and closing-child waits blocking); Claude writing its first message before its handshake answers (measured on the real CLI: the CLI holds the message until its ~250 ms handshake is done, so it saves about 6 ms while changing what Stop and crashes do during startup; dropped).

Differences from main

  • A Claude chat now runs with the Fast choice the composer shows, even when Claude's own settings start each session without Fast (follows the common pattern).

Differences from the common pattern

  • INTENDED: the start registration lives in memory. The durable obligation is the queued message plus the lease reservation; the next open re-derives the start from them, so no new saved state is needed.

  • INTENDED: a brand-new chat created eagerly still starts inside its create call (launches with a prompt, CLI, workers, ensure, older clients). Nothing can send to a chat before its create answers.

  • INTENDED: execution-host ownership of start, lease and delivery. Loss of contact is reported as unverifiable, never as exited. Needed for SSH and several hosts.

  • INTENDED: Send stays disabled until the host confirms the chat exists, so controls are disabled until their state is known.

  • INTENDED: the overall startup limit stays 10 minutes, counted from when the start begins; per-step limits end stuck steps sooner, and a long history restore still finishes.

  • INTENDED: Codex keeps its earlier 15 s / 30 s request limits; the mechanism matches, only the values differ.

  • INTENDED: ACP option writes keep the existing 30 s wait that still adopts a late answer.

  • INTENDED: Claude's first message is written after its ~250 ms handshake answers. Measured cost ~6 ms; it keeps Stop, close and crash during startup on the start path.

  • INTENDED: the agent may launch on the chat's saved model and effort and then switch to the message's choices before its first prompt. The turn runs on the message's choices; only the launch command line can show the saved ones.

  • INTENDED: an exit that arrives between launch and publish is held and settled when the agent is published, because the agent is not on record until then.

  • INTENDED: phone "+" is an unprompted launch from a client that can create without starting, so it starts on the first message.

  • INTENDED: choices are optional and gated by the host's existing capability; omitted choices use the saved pick. Older saved messages without choices cannot be reconstructed.

  • INTENDED: the ACP process is owned by the host from launch, before its handshake.

  • TEMPORARY: Codex Stop still interrupts only the turn here; Stop ending every agent's runtime is tracked in Record Stop immediately and settle subagents from exit proof #28051, which sits on this PR.

  • TEMPORARY: a pick made while Send is going through, a displayed pick whose save is still in flight, or a pick made while the chat is waiting to be created can still be missed. These gaps move to follow-up PR: composer-owned selection, where the composer owns the accepted selection and sends it with the message, including normalizing an unpicked Fast to off. No additional guards for these gaps are included here.

Linked Issue

Consolidates the agent-startup drafts #28047 (ACP start), #28048 (Pi start), #28207 (message-carried model and effort) and the startup parts of #28055 (owned spawn, one host-awaited open) into this PR; those drafts will be closed. Stop changes stay in #28051.

Visual Proof

Pending: rendered before/after QA on another machine (slow-start send, Stop during start, phone "+", Pi start, picks made during creation).

Testing

  • Manually tested (pending rendered QA)

  • Automated tests added/updated

  • One whole-flow test holds an agent start at every point it can wait (closing-agent join, lease probe, crash-recovery probe, launch, handshake, publish, recovery, handover) and checks that sends are accepted at once, Stop withdraws and closes a late agent, quit before launch launches nothing, concurrent sends start one agent, and messages arrive once and in order.

  • Deadline tests on a fake clock per agent; Pi readiness while the list is held; a host test that a phone's bare launch creates at rest while prompted and CLI launches start at once.

  • Checks that tests deleted during the cleanup used to pin (stale-exit fencing, failed-create sink release, SSH-only resting command menu, older-client answers in plain words, and others) are restored as single assertions.

  • Named test files ran on macOS; static analysis, typecheck and the full PR aggregate passed on the current head. The two named real-Claude suites had the same three canonical-model assertion failures on main and the branch (11 passed on each, fold coverage passed); test concurrency differed. Windows app QA failed during setup, native phone QA is blocked, and SSH/mixed-version runtime checks remain unrun.

Review

Five correctness rounds and source readiness checks are complete on f9153ba48a2ebf0c6cca00a9503e98006970415c. Remote rendered QA remains incomplete; this is not a readiness claim. The final status comment will distinguish measured comparisons from blocked or unrun flows.

Agent skill upstream boundary

  • Not applicable; no upstream skill-installer source is copied or translated.

Notes

Execution stays on the host that owns the agent, so SSH and remote hosts behave the same. Folder workspaces are unaffected. Mixed client and host versions keep working: older hosts ignore the phone's capability and keep starting at once, and older clients' message choices are still read.

Checklist

  • Problem, user-facing change, mechanism and approach explained
  • Cross-platform, folder, SSH and older-peer behavior considered
  • Review loop, rendered QA and CI on the final head

@brennanb2025
brennanb2025 force-pushed the brennanb2025/agent-start-split-1-host-readiness branch from 6b535a2 to 88c4c5d Compare October 12, 2026 21:31
@brennanb2025
brennanb2025 force-pushed the brennanb2025/agent-start-split-2-codex-start branch from 7be6240 to 1232065 Compare October 12, 2026 21:31
# Conflicts:
#	src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-attach-orchestration.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host-types.ts
#	src/main/runtime/structured-agent-session-runtime.ts
#	src/renderer/src/components/native-chat/use-structured-agent-session-option-state.ts
@brennanb2025

brennanb2025 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

What broke: when an agent took time to start, later messages and Stop could wait behind its startup. This PR accepts messages through a short ordered step while one start attempt continues, keeps the choices sent with each message, and withdraws interrupted work before it can make later provider changes.

Where it stands (head 0223f64c6f6cbc72abc1a0b1657091241607c359)

  • Review: five correctness rounds and the source-readiness review are complete. No P0 or P1 findings are open.
  • Main merged: main was merged once at the end. Main's rewind-at-any-length change and the startup's rewind read outside the session lock now work together: the read uses the journal cut and the handle the start answered with, and the apply refuses evidence read for a different cut.
  • Kept from main: main's "keep a model the Claude CLI stopped listing" behaviour is kept. A model the CLI answers model_not_found for is reported again, so the picker shows what the child runs.
  • Merge check: an independent check of the merge found two problems, both fixed and tested here.
  • Second main merge: main merged again, bringing "Sleep stops a workspace's native chat agents" (Sleep stops a workspace's native chat agents #28445), the Codex identity fix (fix(codex): a Codex chat keeps its Orca identity when the user's Codex config strips command environment variables #30406) and the revert of fix(native-chat): Claude, Pi and ACP chats are Working only while the agent holds the send #30134. Sleep also stops a chat whose agent is still starting, through the same abort a close uses. Once that start settles, Sleep re-reads the chat, so a start caught before Orca owns the process reads as stopped, not "couldn't confirm". An independent check of this merge found that gap; it is fixed here, with a test that fails without the fix.
  • CI: the full PR workflow passed on this head (run), and GitHub reports no conflicts.

User-facing scope (agreed)

  • Intended: a Fast choice the user picked or saved runs as the composer shows it, even when Claude's own settings start each session without Fast. On main, that setting dropped it.
  • Temporary, moving to follow-up PR "composer-owned selection":
    • an unpicked Fast that should normalize to off;
    • choices that can be missed while Send or chat creation is in progress.

QA round 1 (Windows high, real Codex driver, branch 9727ddc vs main eaa1ee1, 37 full-window screenshots)

  • Pass:
    • New git workspace with setup script and default tabs: Server, Notes and Setup, then the empty Claude chat active; the setup ran once.
    • Image in the first message: delivered with its original caption, the agent replied about it, and no queue card was left stuck.
    • Plain folder: Open as Folder, a terminal, then an empty chat, with nothing sent. Both heads report the folder's branch as "" rather than null; that code is unchanged, so it isn't from this PR.
    • Codex chat with only the permission picked: one reply, and the saved model stays empty. The request Codex receives was not captured.
  • Partly verified: the same CLI launch run twice during startup returns one session and one tab. The agent itself didn't finish starting, because Claude was signed out in the test home.
  • Blocked on both heads, by the test rig: the picked-Fast comparison. The test home's copied Claude login came up signed out on main and on the branch alike, so neither head offered Fast.
  • Earlier evidence (still valid): Claude first messages, the held-create sample, Codex start ordering, Stop during startup, and signed-out recovery. Earlier comparisons are below.
  • Not covered: native Pi (the installed version is too old), phone (the emulator couldn't bind its port), SSH, and mixed client/server versions.
  • Config audit: on Windows high the real agent configs and both credential files were unchanged. The real .claude.json changed during the run, at 05:28 local, and the cause is not proven: another session on that machine may have written it, and it wasn't the copied test login. An earlier Windows-low attempt for this PR stopped three unrelated processes by mistake; that is described in the PR's history, and cleanup now stops only recorded launch PIDs after a creation-time check.

QA round 2 (Windows high, real Codex agent and driver, branch 0223f64), for what the second merge touched

  • Codex first message: pass. One reply, and the provider log shows Orca's identity.
  • Sleep on a ready chat: pass. Sleep stopped the agent process with no error toast; sending again started a new process, which replied once in the same chat. The sidebar shows Done rather than an explicit asleep badge, so that badge wasn't checked.
  • Sleep while the agent is still starting, three times: pass. Sleep came 27 to 33 ms after the starting state; each time the aborted process exited, there was no "couldn't confirm stopped" toast, and the message sent before Sleep stayed visible as "Queued · 1" and was delivered once after wake. The wake message, sent after Sleep, is answered first and the queued one second. That is the same order queued messages already follow after a Stop.
  • Image in a plain folder: pass. Delivered with its original caption and the reply.
  • Same on both heads (not this PR): the test home's hook errors hide replies until a turn is expanded, and a plain folder is reported with an empty branch.
  • Config audit: login, auth, settings and hooks files are unchanged. The real .claude.json and Codex config.toml changed while drivers were running; the drivers run from the machine's normal home, and Codex records trusted folders there. This is not proven: only hashes were compared, and other sessions share that machine.

Round 2 screenshots, branch, full window:

Codex first message and the reply after waking from Sleep:
driver-flow2-transcript-expanded

After Sleep on a ready chat, with no error toast:
driver-flow2-asleep-settled

Sleep during startup: the message is kept as "Queued · 1":
driver-attempt4-queued

After wake, the queued message is delivered once:
driver-attempt4-resumed

Image with its original caption in a plain folder:
trusted-image-delivered

For Brennan's review

Round 1 screenshots, all from the branch on Windows high and full window. The red "hook failed" cards in the agent turns come from the throwaway test home: its hooks reference a shell-alias file that isn't there. That affects both heads.

New git workspace, after the setup hooks ran:
branch-flow1-post-hooks

Default tabs in order (Server, Notes, Setup), with the empty Claude chat active:
branch-flow1-empty

Plain folder with its empty chat:
continuation-A-empty

Codex with only the permission picked, and its reply:
branch-flow3-expanded

Image in the first message, delivered with its original caption and the reply:
continuation-B-expanded

Fast blocked by the test-home sign-in, the same on the branch and on main:
branch-flow6-fast-unavailable
main-fast-menu-absent

Earlier comparison of startup ordering. Main admitted two messages and left the third drafted; the branch admitted all three, in order:

MAIN:
main-codex-held.png

Branch:
target-codex-held

Earlier comparison of Stop in the middle of a turn: both heads show Interrupted.

MAIN:
main-aux-midturn-interrupted

Branch:
target-midturn2-interrupted

Four places where human judgement matters:

Main's rewind recovery now cuts the chat history from the journal; the read step outside the
session's serialize takes the journal to find that cut, and the apply step refuses evidence
read for a different cut. The saved-pick display keeps main's refused-model exception.
…ests

A model the Claude CLI retired is reported again, so the picker shows what the child runs and the
next send moves the chat off it. The start reads a rewind in doubt against the handle it answered
with, the record its commit then applies to. Main's tests follow the start's open: the admission
fixture holds a real turn, and a sign-in close during setup fails the open.
…ity, held-send revert

Sleep (#28445) closes a workspace's chats through the same abort a close gives a
registered start, re-aborts a start registered while it waited for the lane, and
judges its receipt once the start it aborted has settled, so only a newer start
reads as later work. Each adapter's owned-acquisition check covers the child it
owns between acquire and open. Main's Sleep cases from the deleted surface
lifetime suite move to the workspace Sleep suite, plus one for a starting agent.

Codex (#30406): the split open sends the chat's command env overrides when it
opens the thread; the adapter test keeps that assertion on the open's request.

Revert of #30134 (#30572): the event sink, exit observation, Pi held-send wrapper
and adapter heldSends overrides go with it; Pi dispatch submits through its
turns again and its idle report is wired back.
A start runs outside the chat's lane, so Sleep's step can run before the start it aborted has
settled; it then re-reads the chat once that start is done, instead of reporting it unverified.
@brennanb2025
brennanb2025 merged commit 5319125 into main Oct 19, 2026
48 checks passed
brennanb2025 added a commit that referenced this pull request Oct 19, 2026
Brings the agent-start branch's final tip into the Stop change: starts register before any await, Stop/close/quit abort a registered start, and serialize steps are pure fenced commits. The Stop's note writes keep the start-abort wiring and write at the fence a Stop-caused child end moved.
brennanb2025 added a commit that referenced this pull request Oct 19, 2026
Main now carries #28046's startup design (squash-merged), so its hunks match the previous merge; this adds main's newer changes. Conflicts took the Stop branch's side, since main's text in each conflicted file equals the startup branch tip already reconciled.

Tests whose premise the Stop change moved: a Codex Stop now ends the app server, so the replayed-Stop race resumes the saved thread on a new child; a Stop before startup now records its request beside the taken-back send's stopped result. The background-task test regains the tracker import its proof cases use.
nwparker added a commit that referenced this pull request Oct 19, 2026
…(fix red main) (#30865)

* test(codex): the stopped-send receipt guarantee lives in the journal since #30403

#28046's dispatch-boundary tests pinned that the Codex adapter drops an echo
for a stopped or refused send. #30403 moved that rule to the journal's queued
late-accept write. Pin it there through the host's Stop path, and keep the
adapter tests asserting what still holds at the adapter: no fallback or second
turn/start write, and the earlier send's receipt survives an abandoned wait.

* test: pin that a receipt after Stop adds no dispatch row
brennanb2025 added a commit that referenced this pull request Oct 19, 2026
Resolve against #28046's host-awaited Codex open: main's structure for the
connection, acquire and thread open, with this PR's change on top (no
deadline on initialize and thread start/resume; the start's signal bounds
them). The handshake drops its own signal (the open wraps it), the scripted
Codex fixture honours a null timeout, and #28046's start-deadline test now
pins a slow initialize delivering and the start ceiling ending a silent one.
The two tests written for the old acquire-owned open are removed.
brennanb2025 added a commit that referenced this pull request Oct 19, 2026
…ecycle-queue-derived-cards

Conflicts, resolved so main's change stands and queued messages stay in
the chat's journal, sent again only on proof the agent never got them:
- #28046 (choices fixed per accepted message): its options type and its
  one reader reach the journal's queue row and the card fold too; the
  queued_messages writers it retyped stay deleted.
- #30403 (Codex late receipt): the late settlement keeps both main's
  accept-at-its-turn path and this branch's unrun proof for a card.
- A drain logging test main deleted stays deleted.
brennanb2025 added a commit that referenced this pull request Oct 19, 2026
#28046's scripted transports read versions through the shared reader: the
Codex override keeps the reader type, and the ACP scripted child answers its
version probe with a release above every floor.
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