Skip to content

fix(native-chat): a new chat starts at rest; its first message starts the agent - #24450

Closed
brennanb2025 wants to merge 87 commits into
brennanb2025/chat-start-failure-on-messagefrom
brennanb2025/chat-start-on-first-message
Closed

brennanb2025 wants to merge 87 commits into
brennanb2025/chat-start-failure-on-messagefrom
brennanb2025/chat-start-on-first-message

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 155 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​4645 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1433 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3212
Prod 108 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2371 $\color{#cf222e}{\Huge{\mathbf{−}}}$​583 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1788

ELI5

When you opened a new chat, Orca started the agent (Claude or Codex) right away, before you typed anything. If that start failed, the new chat showed a chat-level "could not be started" error that had nothing to do with any message, and Orca never retried it.
Now a new chat opens with no agent running, the same way /clear already does. Your first message starts the agent. If it can't start, the reason shows on that message, with Orca's automatic retry or a Retry button.

What Changed

The problem. agentSession.create started the provider child before it answered, so a failed start was the create's failure. It looked different everywhere:

  • the desktop showed a chat-level "Chat could not be started…" line before any message, with no sidebar row and no automatic retry (for example, a new chat opened during a Claude account switch, or with Claude not installed);
  • the phone's agent.launch threw a refused-launch error;
  • an orchestration worker ended failed at its create stage.
    None of these put the failure on a message. /clear (fix(native-chat): /clear starts nothing; the new chat's first message starts its agent #23935) already created its new chat at rest, so Orca had two ways of making a chat.

Before / After (what you see)

Case Before After
New chat from the + menu, quick commands, fork, fix-checks, source control, dashboard the agent starts while the chat opens the chat opens at rest; the first message starts the agent
The agent can't start (signed out, not installed, an account switch in progress, a CLI that exits) a chat-level "could not be started" error before you sent anything; no retry nothing before your first message; then the reason on that message, with #24340's automatic retry for failures that clear by themselves (an account switch) or Retry for ones you must fix
"Fix checks with AI", conflicts, commit/push recovery, review notes, browser annotations sent to a new agent the toast said "Started an AI agent…" and notes cleared once the chat existed notes clear and the toast says "Sent to an AI agent…" once Orca has recorded the message; a failed start shows on the message with Retry
"Resolve comments with AI" (GitHub/GitLab review comments) into a new structured chat the checks panel posted its "Fixing" replies and resolved the threads as soon as the chat existed, even if the agent then failed to start the host that runs the chat posts the replies and resolves only once the agent actually takes the message; the panel refreshes when that's done; if a write fails, the chat says so once: "Orca couldn't mark the review comments sent with this message as being fixed. Resolve or reply to them yourself."
Phone agent.launch with a prompt a failed start threw a refused-launch error the chat is created; the phone says "Sent to an agent in X" once the message is recorded
Resume a past conversation into a new chat the agent started and resumed it during the create the history is imported at create; the first message resumes the provider conversation
Orchestration worker-start a failed start ended the worker failed at create the preamble is the first message: a failure only you can fix ends it failed at the preamble stage; a start still being retried when the start's own budget (at most 60 s) runs out reads outcome_unknown, with the start failure in the reason

Mechanism

  • One at-rest create. host.create (structured-agent-session-create-at-rest.ts) writes the record and its operation row in one transaction (store.createAtRest, agent-session-at-rest-create.ts), then opens the journal at rest and imports an adopted transcript. The record is founded by foundAgentSessionRecord, the same founding /clear uses. Create refuses only admission failures: an unsupported host, a taken tab id, a refused resume source, a replayed failed operation.
  • A create cut short comes back whole. The operation row stays pending until the journal is open and the history imported; a replay with the same operation id re-runs both (the import is idempotent). A replay after the row aged out answers the chat the create made. Quit drains a create in flight.
  • A chat an older host left behind after a failed create-time start can be created again (same identity, no conversation bound, its exit proven). The create first reconciles that record's lease, keeps the lease's fence floor and mints the next fence, so the renderer's existing relaunch works after an upgrade.
  • The record is founded only by its create. The old record-founding branch in the reservation is deleted; the attach is only the work-time start, run by the delivery loop and by operations that need the provider. Tests start agents the same way (startAgentForTests).
  • agentSession.ensure is deleted: the method, its phone allowlist entry, its generated params-catalog row and its cross-version manifest entry. No client in the history of the desktop renderer, phone app, CLI or preload ever called it.
  • Launch prompts count as delivered once the host records the message, as for any other message. Their side effects (notes cleared, the "Sent to an AI agent" toast, the phone's answer) run then. A failed start shows on the message, with fix(native-chat): a failed start is shown on its message and retried, and later messages go ahead #24340's normal Retry and Stop.
  • The review-comment reply rides the message and is run by the host. The checks panel builds a bounded reply spec at launch (which threads to reply on and resolve; GitLab resolves only). It travels as an optional reviewReply on the launch prompt's send and is stored on the message's own journal row, never sent back to clients. A host-side observer posts it once that message is accepted (the agent took it), within 24 h of acceptance, and writes a receipt for the message in the chat's own lane. Nothing is stored per launch: what's owed is derived from the message, its state and the absence of a receipt, on every journal publish and again when a journal opens after a restart. Before posting after a restart, the host reads the PR (each thread's newest comments plus the posting account) and skips replies that account already posted after acceptance; if that read fails it posts anyway and logs a possible duplicate. Retry re-arms the same message, so the reply follows it; a message never accepted (rejected, stopped, closed) owes nothing.
  • The checks panel stays in step. After a successful write the host sends the same refresh notice the panel's own writes send, and the panel refetches once when the chat's receipt arrives (which also covers a chat on a paired server). A launch that is not recorded while its chat tab exists is handed to the chat, whose Retry owns it, so the panel never starts a second writer.
  • Orchestration. sendStructuredWorkerPreamble returns { state, startRetry? } and waits across retried starts for at most 60 s, capped by what the worker start's own timeout leaves, so a short --timeout-ms gets the outcome_unknown receipt instead of a transport timeout. This is the worker's readiness check, the same one terminal workers get. fix(native-chat): a failed start is shown on its message and retried, and later messages go ahead #24340's background watch still fails the start and tells the run if the held preamble is later rejected for good.
  • The agentSession.create timing span keeps its meaning: it wraps only a chat's first agent start (including /clear's new chat and automatic retries of a failed first start), not later starts after idle, a crash or an operation.
  • Model picker at rest. A Claude chat at rest, or still starting, names no model until one is saved or the agent reports the model it runs (settings or env can override the catalog's default). A Codex chat with no model listing keeps the built-in list.

Why

The common pattern creates a chat as a cheap record and starts the agent on the first send; a failed start lands on the message, never on the create; a source counts its prompt as delivered once the message is recorded. Orca already had the pieces: /clear's at-rest chat and the delivery loop's start-on-send. Using them for every chat removes the second create model and the three different failure shapes.
Orca also posts a "Fixing" reply on the review threads it hands to an agent. Running that from the host, derived from the message's own state, lets it wait for the agent to really take the message without the source having to wait, and survives reloads and restarts.
Alternatives considered:

  • Keep the eager start and only change how its failure is shown. That keeps two create models, and a start failure still has no message to land on.
  • Start at create only for launches that carry a prompt. That brings back the create-time failure path for exactly the flows with outward side effects.
  • Have the source wait for the agent to take the prompt before running its side effects. Built and reviewed first; every round found another way the message could move without the source knowing (Retry, Stop, reload, a re-send by the chat), so it was replaced by the host-run reply.
  • Store a per-launch obligation on the host. Larger, and unnecessary once the obligation can be derived from the message.

Differences from the common pattern

  • Intended: Orca posts the GitHub/GitLab "Fixing" reply and resolves the threads it hands to an agent. The common pattern posts nothing on its own; Orca keeps this reply (an open product decision), and runs it from the host once the agent takes the message.
  • Intended: the failure line for that reply. Shown only when a write fails, once, in plain words with what to do; success shows nothing new.
  • Intended: the orchestration preamble waits for the agent's readiness (at most 60 s, capped by the start's timeout), as terminal workers do; answering at record would make every structured worker read outcome_unknown.
  • Temporary: an older paired server that doesn't advertise agent-session.review-reply.v1 gets no reviewReply. For such a chat the checks panel posts the replies itself once the message is recorded, as before this PR. Follow-up: drop the fallback once no supported host lacks the capability.
  • Temporary: the neutral "Model" pill on a Claude chat at rest and while its first start runs. The common pattern names a model when the chat is created. Follow-up: resolve the model at create.
  • Temporary: if another client closes a chat while this desktop's checks panel waits for its reply receipt, and the host keeps the record, that wait keeps one at-rest read open until the receipt, the panel closes or 24 h pass. It starts no agent.
  • Temporary: the desktop still handles a create refused because a start failed (relaunchFailedStructuredAgentSessionForMessage). Only an older host can do that now; the same path also covers creates that fail for other reasons. Follow-up: drop the start-failure case once no supported host starts at create.
  • Known gap (pre-existing): the checks panel keeps offering comments Orca already acknowledged, including its own "Fixing" replies; with "don't show next time" saved, a second click starts another agent and posts again. Tracked in STA-9345.
  • Known gap (pre-existing): an adopted create whose transcript import keeps failing leaves a record without a tab that still holds that provider conversation.

Mixed versions

Cost: first-message latency

Starting on the first message moves the agent's start in front of that message instead of overlapping it with typing.
Measured end to end in Orca on an M4 Air: open a new Claude chat, wait 3 s, send; real Claude through a rig wrapper forced to the smallest model, timed in the page (MutationObserver + animation frame on performance.now()), interleaved batches with fresh profiles, the machine shared with other work (load average 18–22).

send → first assistant frame (warm median) open → first assistant frame (warm median) cold send → first
Base, agent started at create (#24340, n=24) 1020 ms 4108 ms 983–1125 ms
This PR, agent started by the first message (n=24) 1313 ms 4408 ms 1306–1386 ms
This PR, later head (n=6) 1269 ms 4361 ms 1325 ms

So the first reply comes about 0.3 s later and varies more. The chat shows its working state 10–40 ms after Enter on both. Mitigation: none planned; nothing in the common pattern starts a chat's agent ahead of time.

Linked Issue

No issue. Follow-up to #24340, which carries the per-message start failure and retry this builds on.

Visual Proof

Recorded on an M4 Air with hidden windows and CDP screenshots, driven by an Electron QA agent; GitHub was a fake gh that logs every call, so no real PR was touched.

Case Screenshot
A new Claude chat at rest A new Claude chat at rest: the empty state, the composer ready, and the neutral "Model" pill
The first message starts the agent The first message shows "Working for 5s" and "Working…" while Claude starts
An account switch on the first message The first message reads "A Claude account switch is in progress. Orca will try again shortly." and the composer offers Stop Orca retried 15 s after the switch ended, and BRAVO-OK arrived
Claude not installed The first message shows "Claude isn't installed. Install it, then retry." with Retry; the sidebar row reads Failed
GitHub reply flow: the panel The checks panel shows the PR's four unresolved review comments The "Resolve PR Comments With AI" dialog: Orca resolves the selected threads and replies to comments it cannot resolve once the prompt is delivered
GitHub reply flow: the chat, then the reply posted The agent answered with no extra line in the chat; without leaving the tab the panel refreshed to two resolved threads, the "Fixing" reply in the thread and one conversation comment
The writes failed: the failure line Under the settled "Worked for 2s", a red line reads "Orca couldn't mark the review comments sent with this message as being fixed. Resolve or reply to them yourself."
Restart before the writes answered After the relaunch, the interrupted chat reads "Failed after 3s"; the host re-read the PR and posted no duplicate reply and no failure line
Before: a new chat opened during an account switch Before: a chat-level line "Chat could not be started. A Claude account switch is in progress. Try again after it finishes." with Retry; no notice on the message and no sidebar row
Before: a new chat while Claude isn't installed Before: a chat-level line "Chat could not be started. Claude isn't installed." with Retry, shown before any message

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • Manual (M4 Air, hidden Electron rig with an isolated home, real Claude, a fake gh that logs every call so no real PR is touched): a new chat at rest; the first message starting the agent; an account switch on the first message (on-message wait, Stop, sidebar Failed, automatic retry at +15 s); Claude not installed (reason and Retry); "Resolve comments with AI" into a new chat (no writes while the start is held; exactly one set of replies and resolves about 1 s after the agent took the message; the panel refreshed in place; no toast or success line); a failed start then Retry (zero writes, then one set); every write failing (one visible failure line, also after two restarts); a quit after the agent took the message (writes already posted: no duplicates on relaunch; writes never posted: exactly one set); a lost first send while the chat tab exists (the chat re-sent, one set); notes to new and existing chats; Stop on a waiting launch prompt; latency. Not covered: browser annotations (need a real element pick; they share the notes path) and a worktree created with an agent prompt (no CLI in the rig).

  • New and changed tests:

    • create at rest: every create refusal; replay stays at rest; an interrupted import or journal open replays with its history; an aged-out row answers the chat; an older host's failed record is created again, keeping its fence floor and reconciling first; quit drains a create; agentSession.ensure answers method-not-found; an adopted create's first message resumes the adopted conversation.
    • the Codex and Claude integration suites create at rest; env, auth and launch checks moved after the first send, and each start failure is asserted on the first message; fences are exact.
    • the review reply on a real host: owed only once accepted; within the 24 h window; once per message (receipt, written in the chat's lane); after a restart it re-reads the PR and skips what this account already posted; a failed read posts and logs; GitLab resolves only; never held as a draft; the failure line is visible after the turn folds; a success shows nothing.
    • the checks panel: builds the spec; posts nothing itself for a structured chat that carries it; falls back when the host lacks the capability or a paired server declines the chat; hands a not-recorded launch to the chat; refetches once on the receipt, only while it still shows that PR, and keeps the chat's read alive while it waits; releases that on fire, unmount, close, purge, a missing record or 24 h.
    • cross-version: a send with reviewReply is accepted exactly where the host advertises the capability.
    • orchestration: ready, failed at the preamble, a retry that clears comes back ready, still retrying at the budget reads outcome_unknown with the cause, and the budget is capped by the worker start's timeout.
  • Every new test was checked by reverting its fix and watching it fail.

  • CI unit, cross-version wire, static analysis, mobile and packaging. Locally: tc:node, tc:web, tc:cli and mobile tsc, full oxlint on every touched file, the changed-code quality gate, localization checks, and named runs of every affected unit file.

  • Not run: the real Claude CLI test files (claude-structured-real-cli*); the real-Claude QA above covers the start path.

AI Disclosure

Review

Reviewed over nine rounds by independent reviewers (host create path, clients and orchestration, tests, architecture, launch-prompt ownership, the host-run reply, the renderer revert), two readiness checklists and two rounds of M4 Air QA. The first design made each source wait for the agent to take its prompt; review showed that shape kept producing edge cases, and it was replaced by the host-run reply described above. Fixed along the way include: GitHub replies, "Started" toasts and cleared notes for an agent that never started; an empty chat after an interrupted import; a chat an older host failed to start that could never start; an aged-out create; quit not draining a create; the timing span firing on every start; orchestration reporting outcome_unknown too early and outliving the CLI's timeout; the picker naming the wrong model; a second launch posting a second set of replies; the panel staying stale after the host's reply; and a failure line folded out of sight.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Stacked on #24340 (brennanb2025/chat-start-failure-on-message): a failed first-send start needs its per-message failure and retry. This retargets to main once #24340 merges.
The change happens on the host that runs the agent, so it is the same on macOS, Linux, Windows and SSH hosts.

Checklist

  • This PR is small and focused (263 files, most of them tests: the at-rest create, the deleted ensure, the host-run review reply, and the tests that assumed a create starts the agent)
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Author: @BrennanKB5

…s the agent

agentSession.create writes the record at rest (shared with /clear's founding) and opens its
journal; no agent starts. The attach is now only the work-time start, which carries the create's
timing span. agentSession.ensure, which no client ever called, is removed. The orchestration
worker's preamble starts its agent and reports a start that is still being retried.
…start agents the way production does

The reservation no longer founds or takes over a record: a start names the fence of the record
it read, a missing record refuses, and adoption and tab checks live with the at-rest create.
attachForTests now runs host.create at rest, then the delivery loop's start from the stored
record. attachExistingForTests keeps the attach's own guards (stale fence, replayed operation,
a lease that still admits a writer) testable. Store-level suites found records through the
create and reserve at its fence, so fences sit one higher, as in production. New direct tests
cover the create's refusals: sessionExists, recordUnreadable, invalid args and options,
conversationHeldElsewhere, and the tab it reserves.
…n and whose start is proven gone

An older host started the agent at create and left the record behind when that start failed:
released, its exit proven, no conversation bound. The renderer's relaunch of that chat sends the
same session id under a new operation, which the at-rest create refused as sessionExists on every
message. The create now founds such a record again at rest, at its own fence, keeping its name,
so the chat starts on its next message after an upgrade.
…d its history imported

The create used to write its operation row as succeeded in the same transaction as the record,
before it opened the journal and imported an adopted conversation. A create cut short between
the two (the journal open or import throwing, a crash, a quit) replayed as an empty chat whose
first message resumed a conversation the user could not see. The row now stays pending until the
journal is open and the history imported; a replay of a pending row does that work again (the
import is a no-op on a journal that already holds rows) and then settles it. The failure path no
longer rewrites the row to failed, so a transient failure heals on the client's retry.
…onversations

The at-rest create ran under the session's serialize but was not tracked with the host's attaches, so a quit could close every open conversation and then let an in-flight create open one behind the teardown. The create is now tracked from enqueue, as the agent start is.
…t agent start only

Moving the start to the first message put the span around every agent start: idle-eviction resumes, start retries, restart continuations and starts an operation asks for. It now wraps only a start of a record no agent has run in Orca yet (its chain empty, or holding only the conversation it adopted), which is the start a create used to run. A failed first start retried is still a first start.
…ried start for its 60 s budget

The preamble's wait for the worker's agent ended at the first failed start that is retried on its own (an account switch, a lease being settled), so the coordinator read outcome_unknown at once, and a start the 15 s retry cleared never reached ready. The wait now runs through such retries: ready if the agent takes the preamble within the budget, outcome_unknown quoting the start failure if a start is still being retried when the budget runs out, and failed at the preamble when the start is refused for the person to fix. Host-backed tests cover the three outcomes and the reason the dispatch is given.
…ost hands it to the agent

A new chat now starts its agent on its first message, and the host answers the send when it
accepts the message, before that start. A launch prompt counted as delivered on that answer, so
"Resolve comments with AI" posted its GitHub fixing replies and resolved threads, the checks and
fix actions toasted "Started an AI agent", and review notes and browser annotations were cleared,
all for an agent whose start then failed (signed out, not installed, exiting as it started).

The launch prompt now reads the session's own publication until the host hands the message over
(delivered) or rejects it (not delivered: the claimed comments, notes and annotations are handed
back for a retry). A start the host is still retrying keeps the wait open; the retry schedule ends
in a verdict, and a 45-minute cap sits past the longest one. No new wire field: a host that
predates the hand-over record answers a send only once it is handed over, and that answer counts.
…ay, resumes what it adopted, and recovers an older host's failed start

Covers the plan items the first round found missing: a plain create replayed under its own operation id starts nothing and its first send starts the agent; agentSession.ensure answers method_not_found; an adopting create's first message resumes the adopted Codex thread rather than starting one; and a chat whose create-time start an older host lost is created again under a new operation and starts on its next message.
…est what they say

The Codex reconnect pins the fence at each step again (the first start moves the create's fence 1 to 2; the start after a close moves it one past the released fence) and checks the journal survived the dead child. The cross-version stale-fence send is checked against the fence the restarted record held before the send, not a value read after it. The AI Vault ownership setup starts the chat's agent after its create, since a chat at rest owns no thread yet (this failed in the suite once it could run). The tab-table test for a create that stopped before publishing its tab goes through host.create, and the failed-start one now sends the first message and checks that its rejection leaves the tab in place.
… gone

Rewords the comments the first round found false: the structured worker is ready with its preamble, not when an attach returns; the create's pre-commit half ends at the host's create; the gate lists no ensure; the cross-version fixture's supportsCreate stub is explained by the create. Test helpers that started an agent through a client's ensure now start it as the delivery loop does.
…nswers the chat it made

A create whose answer was lost before its tab published leaves the renderer holding the launch as
unknown; reopened more than a day later, its retry under the same operation id found the row
pruned and was refused as operation_expired, and the relaunch under a new id then met
sessionExists, so the chat could never start. A create starts nothing, so an aged-out row proves
nothing more: when the record already there is this create's (same identity, same adopted
conversation or none), the create answers it at rest under a fresh row it settles as its own.
A pending row an older host left when it died mid-create is re-run and settled the same way.
The crash-mid-start cases now replay through host.create.
…tate before it counts as delivered

The wait now follows the first message to its final state rather than to its hand-over, and has
no timer of its own: accepted (the agent took it) counts as delivered and runs the caller's
outward side effects; rejected (a start that failed for good, or a chat closed mid-wait) and
unknown do not. A start the host retries keeps the message queued, so the wait spans the retry
schedule. Each launch prompt has one read, shared by whoever asks and tied to the launch call;
a renderer reload ends it with nothing written. Tests drive a failing first start through the
checks-panel acknowledgement, the notes send and the browser annotation send: a retried start
the agent then takes writes exactly once and only after acceptance, a message rejected after its
tries writes nothing, and a chat closed mid-wait writes nothing and keeps the notes.
Every new chat now sits at rest until its first message, and the at-rest options read named the
model catalog's default ("Opus", with its Effort) as the chat's model. Claude's settings or env
may run another, so the picker flipped to the real model once the agent started. The live
child and the client's own catalog upgrade already refuse to name Claude's default. At rest the
host now names the catalog default only for Codex; a Claude chat with no saved model answers an
unnamed model, and the client shows an unnamed model as unknown (the neutral "Model" pill, no
effort) instead of adding an empty row and naming the listing's default.
…take the launch prompt

The fix-checks actions (PR page, PR dialog, check-run details, checks panel), the fix-checks
dialog on the PR page, and the commit/push recovery launch said "Started an AI agent" as soon as
the chat tab existed. With the agent starting on the chat's first message, that toast could
appear for an agent whose start then failed. They now wait for the launch prompt's delivery (the
chat's first message reaching its final state) and stay in their starting state until then; a
start that fails says nothing extra here, since the failure is shown on the message.
…'s rejection to a reader

Pins what ends a launch prompt's read when its chat closes mid-wait: the host withdraws the queued first message, keeping the start failure it waited out, and publishes that rejection to a subscriber. The renderer's closed-chat scenario now uses that same rejection.
…d waits for reconciliation

Founding again a record an older host's failed start left replaced its lease with a fresh one that copied only the fence. That lost the floor a recovered copy of the records sets (so a fence a lost write may hold is never granted again) and skipped the reconcile gate. The create now founds over the existing lease: the fence moves on through the one mint, past any floor, and a lease this host has not yet adjudicated since load is refused as execution_owner_reconciling.
… as the worker start's timeout allows

The preamble waited a fixed 60 s for the worker's agent whatever the start's own --timeout-ms, so with a short timeout or a setup wait the CLI's transport deadline (the timeout plus a fixed grace) could fire before the outcome_unknown receipt arrived. The wait is now the readiness wait capped by what the start's timeout leaves, plus the allowance a terminal worker's turn observation gets, which keeps the answer inside the CLI's grace.
…d sends at the create's fence

It started the agent before sending and sent at the post-start fence, which is not what production does: the create starts nothing and the launch prompt, sent at the create's fence 1, is what starts the agent. It now creates through host.create and sends at that fence, and its header and title say the first send's start publishes the session before the delivery loop dispatches. Also corrects the settlement wait's doc for throughStartRetries.
…ad of fence args attachForTests ignores

attachForTests reads a non-null fence only as "the record exists" and starts the agent at the record's own fence, so the literal fences these tests passed (2, 13, 14, a fence read beforehand) were misleading. They now call startAgentForTests with the session id.
…ic model seed

A Codex chat at rest before any listing exists (its account's first chat, a WSL record, a failed catalog read) is answered with no models and no model. After the Claude naming fix the client turned that into an empty catalog, so the composer had no model picker until the first start. An empty listing is an unknown one, as the host's catalog contract says: the client now keeps its static seed's models and naming.
…swer; tests open on the host's snapshot

An unknown dispatch is not final on this host: a Codex turn/start that timed out after writing the frame keeps its echo armed, and a late echo settles the message as accepted. The launch prompt's read now ends only on accepted, rejected, or the stream ending, so the caller no longer says the agent could not start (and hands the comments back) for a prompt the agent then runs. The test driver now opens with a snapshot, as the host's subscribe does, with cases where that snapshot already carries accepted or rejected, and the closed-chat case uses the rejection the host writes.
…ordinator test's RPC caller in their own modules

Both files went over their line limits once this PR added the at-rest create and the attach helper.
The store's death-evidence and first-record listeners, and the draft watch that feeds them, move to
AgentSessionRecordStoreCommitListeners; the coordinator-mail suite's numbered RPC request and call
move to a test fixture. No behavior change.
…o chat-start-on-first-message

Launch files keep this PR's review reply on top of #24340's per-host structured route; locale
catalogs merged as JSON.
…wn lane

The receipt append now queues in the host's per-session serialize, so it lands after whatever the
lane is running and at the fence the chat holds then, never under a start or close that moves it.
The run that leads to it was started off a publish, so no task in the lane waits on it.
…plies this account posted

Before posting again, the host now matches the PR's comments on thread, text, time since the agent
took the message, and author: the login the repo's GitHub writes run as (its bound gh account, else
the signed-in gh user), looked up once per repo for the host's life. Where that login can't be told
(SSH, WSL, nobody signed in, a failed lookup) it matches on the rest alone and logs that.
… that advertises running it

With #24340's route, a structured chat now starts on whichever host owns the workspace, a paired
server included, and an older server's strict send params would refuse the whole launch prompt.
The host now advertises agent-session.review-reply.v1, and the checks panel plans the reply only
for a chat whose host does (this client's own always does). A chat on an older paired server gets
the prompt without it, and the panel writes the replies itself once the host has the message, as
it did before this PR.
…t first, through a read that throws

The restart dedupe read the panel's PR comments: one oldest-first page per source, every failure
swallowed as an empty list. A busy PR hid Orca's own replies, and a failed read posted again with
nothing logged. It now reads exactly the reply threads (`node(id:)`, `comments(last: 100)`), the
conversation comments since a minute before the agent took the message (host and GitHub clocks can
disagree), and `viewer { login }` in the same GraphQL call, which replaces the separate account
lookup. A failed read is logged and the replies are posted (at worst once more); an unnamed
account matches by text alone.
…desktop's views of it to refetch

The host writes through the runtime's repo methods, which never sent the `gh:workItemMutated`
notice the desktop's own writes do, so the checks panel kept showing the threads open with no
"Fixing" reply and invited a second launch. After a run with any successful GitHub write, main now
sends that notice for the repo and PR through a new runtime notifier hook. Only this machine's
windows hear it: a paired client's panel refetches on its own triggers (limit).
…aunch prompt owns its review reply

If a structured launch's first send was refused or lost, or its create failed but kept the chat's
outbox for the relaunch, the panel handed the comments back, said "Could not start the selected
agent." and left Start enabled, while the chat still sent the message (or offered its Retry) with
the review reply on it. A second Start then posted a second set of public replies. The launch's
delivery result now says when the chat still holds the prompt, and the panel then treats the
comments as handed off: selection cleared, dialog closed, no toast; the chat's Retry owns it. With
nothing held, the comments still go back to the panel as before.
…ed chat can't reply on the PR

Without the PR's repository the chat's host can resolve the threads but not post the replies, and
the structured route dropped them silently where the panel used to say so. It now shows the
panel's existing "Could not resolve the GitHub PR to reply on." once, as the launch hands off. The
other two cases need no new line: with no resolve target, or a reply too large to carry, nothing
rides the message and the panel writes and reports as before.
…ot that one started

A structured chat now starts at rest and its first message starts the agent, so "Started an AI
agent for ..." fired before any agent had started, and that start can still fail. The fix-checks,
conflicts, recovery and check-details toasts now say "Sent to an AI agent for ...", in all six
catalogs. The checks panel's comments now say who writes the review reply on each route.
…ys the agent started

On a structured chat the launch opens the chat at rest, so "The agent started, but ..." was false.
The messages now say only what is known: "The prompt wasn't sent to the agent.", "The notes weren't
sent to the agent.", and "Couldn't confirm the prompt/notes were sent to the agent."
…ich for a chat is when it records the message
…helper in their own module

The suite went over its line limit with the review-reply case; the two helpers move unchanged.
… the kept prompt without its review reply

R5R1 refinement: a create that failed still hands the comments back to the panel, as before, so
the prompt the chat keeps for a relaunch drops its review reply; only one owner can write it. A
prompt the host refused or never answered after the chat existed is still held by the chat, and
three existing tests now expect the delivery result to say so.
…red chat's message took it

The panel decided the reply rode the message whenever the launch took the structured route. A
paired host that declines the chat opens the same launch as a terminal, whose paste carries no
reply, so nobody resolved the threads or posted. The launch prompt's delivery result now says
whether the chat's message carries the reply, and the panel writes after delivery otherwise.
…ly after a restart; only a failed read is logged
…s host has written its review reply

On a paired server the host's mutation notice never reaches this client, so the checks panel kept
showing the threads open with no "Fixing" reply. When a launch hands its review reply to a chat,
the panel now watches that chat's live stream for the message's receipt (a failure line or a
tombstone) and refetches once. The watch is one-shot and dies with the panel, when the chat
closes, or after the 24 h window in which the host still owes the reply. No new notice or toast.
…open, and refreshes only the review still shown

Three round-7 fixes to the refetch:
- The receipt notice moves from the read transport to the chat's read owner, its layer; the
  owner's older-page reader moves to its own module to make room.
- A chat's read runs only while its pane shows or its outbox owes a delivery, and on a paired host
  the entry retires before the host writes, so a hidden chat heard nothing. An armed watch now
  holds the owner's read open, as owed work does, and lets go when it fires, with the panel, when
  the chat closes, or after the window.
- The deferred refetch used the closure of the launch's render, possibly hours later, with no
  check that the panel still shows that review; a stale fetch could leave "Comments are still
  loading." stuck. It now calls the panel's latest refresh, and only while the panel shows the
  launch's review (head changes allowed). The watch lives in its own hook.
…e launch used, and lets go whenever the chat goes

Round-8 fixes:
- The read transport still noticed receipts after the move to the read owner, so each batch was
  checked twice and the wire module imported the panel's bus. The transport no longer does.
- The hold's host was worked out again after delivery from what the UI showed then, which could
  name this machine for a paired chat and retry a refused read for 24 h. The launch now hands out
  the host it used (this client's own, or the paired server it asked), and the hold reads there.
- The hold was let go on a user's tab close only. It now also goes on a close with no host to
  name, on a worktree purge, and when the host refuses the read because it holds no such chat.
…he agent's turn folds

The host wrote the failure into the agent's live turn with no tone, so once the turn settled it
folded under "Worked for Ns" and the person never learned the resolves and replies didn't happen.
It is now written as Orca's other notes about the conversation are: outside any turn, with the
error tone. The success tombstone still shows nothing.
…e agent's turn runs, and still lands outside it
…h notice's new wording

The phone's notice changed to "The notes weren't sent to the agent."; the golden still held the old
"The agent started, but ..." toast. Re-recorded by the corpus script; no other golden moved.
"Could not resolve the GitHub PR to reply on." read as an internal failure. It now reads
"Couldn't find the GitHub PR to reply on. Reply to those comments yourself.", in every catalog.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status (rebuild): #24450, a new chat starts at rest; its first message starts the agent

The problem. Opening a new chat started the agent before you typed anything. If that start failed (signed out, Claude not installed, an account switch in progress, a CLI that exits), the chat showed a chat-level "Chat could not be started…" line with no message attached, no sidebar row and no automatic retry. The phone and orchestration each showed the same failure in their own shape.

What changes for you. A new chat opens with no agent running, and your first message starts it. If it can't start, the reason shows on that message, with Orca's automatic retry or Retry. Launches with a prompt (fix checks, conflicts, recovery, review notes, annotations) count as sent once Orca records the message: notes clear and the toast says "Sent to an AI agent…". "Resolve comments with AI" now has the host post its GitHub/GitLab "Fixing" replies and resolve the threads only once the agent actually takes the message. The checks panel refreshes when that's done, and a failed write is said once in the chat with what to do. The first reply comes about 0.3 s later (M4 Air: 1020 → 1313 ms warm median).

How the review went. The first version made each source (checks panel, notes, phone) wait for the agent to take its prompt before running its side effects. Review kept finding ways the message could move without the source knowing (Retry, Stop, reload, a re-send by the chat), so the PR was rebuilt:

  • everything counts as delivered once recorded, as in the common pattern;
  • only the GitHub reply waits, and it is run by the host, derived from the message's own state, so it survives reloads and restarts without any stored per-launch record.

Fixed during review (each with a test that fails without the fix):

  • GitHub replies, "Started" toasts and cleared notes for an agent that then failed to start (desktop and phone).
  • A resumed conversation coming back empty after an interrupted import.
  • A chat an older Orca failed to start that could never start again, and an aged-out create.
  • Quit not waiting for a create in flight.
  • The timing span firing on every start.
  • Orchestration reading outcome_unknown too early, and answering after the CLI's own timeout.
  • The model picker naming the wrong model at rest and while starting.
  • After a restart, the reply check skipped only what the posting account itself had already posted, and the read now fails loudly instead of being swallowed.
  • An older paired server would have refused the whole launch because of the new field. The field now goes only to hosts that advertise support, with the old behaviour as a fallback.
  • A paired server declining the chat left the replies unposted.
  • A failed first send plus a second Start could post the replies twice.
  • The checks panel stayed stale after the host's reply, including on paired servers and with the chat hidden.
  • The failure line was folded out of sight inside the agent's turn.
  • A phone test recording still expected the old wording.

Deferred, listed in the description:

  • Temporary: an older paired server without agent-session.review-reply.v1 gets the panel's old post-at-record behaviour.
  • Temporary: the neutral "Model" pill until the model is resolved at create.
  • Temporary: one at-rest read can stay open for up to 24 h if another client closes the chat while the panel waits.
  • Temporary: the desktop's create-refused relaunch path, kept for older hosts.
  • Pre-existing, not changed: the checks panel keeps offering comments Orca already acknowledged, so a second click can post again; an adopted create whose import keeps failing leaves a tab-less record.

Verified:

  • CI at 9217694: all 17 checks pass, including mobile; 16 skipped. Later heads only change copy or this description.
  • Locally: tc:node, tc:web, tc:cli and mobile tsc; full oxlint on every touched file; the changed-code quality gate; localization checks; named runs of every affected unit file.
  • Two readiness checklists: no P0 or P1.
  • M4 Air QA with an Electron QA agent driving a hidden window, real Claude, and a fake gh that logs every call:
    • a new chat at rest, and the first message starting the agent;
    • account-switch and not-installed failures on the message;
    • the reply posted exactly once after the agent took the message, with the panel refreshing in place;
    • nothing posted while a start was held or failed, then one set after Retry;
    • the failure line visible once, also after restarts;
    • quit-and-relaunch with no duplicate replies;
    • a lost first send handled by the chat alone;
    • notes; Stop; latency.

Not verified:

  • Browser annotations: they need a real element pick, but share the notes path.
  • A worktree created with an agent prompt: not reachable in the rig.
  • The real Claude CLI test files: not run in this setup.
  • A real GitHub PR: deliberately; the fake gh stood in.
  • The paired-server paths: covered by tests and code review, not by a live paired setup.

Next: this PR is stacked on #24340. When #24340 merges, main is merged in, the PR is retargeted to main, and it is marked ready.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing the duplicate new-chat startup work: #26332 and #22835 already create a chat without starting its agent until there is work to send. The separate failed-start retry problem remains covered by #24340.

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