Repository navigation
Accept messages while any agent starts, and bound each startup step - #28046
Conversation
6b535a2 to
88c4c5d
Compare
7be6240 to
1232065
Compare
# 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
|
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
User-facing scope (agreed)
QA round 1 (Windows high, real Codex driver, branch
QA round 2 (Windows high, real Codex agent and driver, branch
Round 2 screenshots, branch, full window: Codex first message and the reply after waking from Sleep: After Sleep on a ready chat, with no error toast: Sleep during startup: the message is kept as "Queued · 1": After wake, the queued message is delivered once: Image with its original caption in a plain folder: 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: Default tabs in order (Server, Notes, Setup), with the empty Claude chat active: Plain folder with its empty chat: Codex with only the permission picked, and its reply: Image in the first message, delivered with its original caption and the reply: Fast blocked by the test-home sign-in, the same on the branch and on main: Earlier comparison of startup ordering. Main admitted two messages and left the third drafted; the branch admitted all three, in order: Earlier comparison of Stop in the middle of a turn: both heads show 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.
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.
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.
…(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
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.
…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.
#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.
















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
Mechanism
Why
Differences from main
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
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