Skip to content

fix(native-chat): one path restarts a chat whose agent failed to start - #22741

Closed
brennanb2025 wants to merge 2 commits into
mainfrom
brennanb2025/chat-one-restart-path
Closed

brennanb2025 wants to merge 2 commits into
mainfrom
brennanb2025/chat-one-restart-path

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​315 $\color{#cf222e}{\Huge{\mathbf{−}}}$​9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​306
Prod 13 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​282 $\color{#cf222e}{\Huge{\mathbf{−}}}$​121 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​161

ELI5

When a new chat's agent (Claude or Codex) crashed while starting, Orca used to have two separate ways to try again, depending on exactly when it crashed. Now a chat whose agent failed to start is just an ordinary chat that says why. Sending a message restarts the agent the same way it does for any chat whose agent stopped.

What Changed

Before, for the user: if the agent died while Orca was still starting it (for example, the CLI isn't signed in), the chat showed "Chat could not be started. " with a Retry button. Sending a message quietly re-ran the whole "create chat" step. If the agent died a moment later, after the chat had opened, you got a normal chat with a status row, and sending a message restarted the agent through a different mechanism on the host. Same failure, two different screens and two restart paths depending on timing.

After, for the user: in both cases you get the chat with the row "The provider stopped before it finished starting: ." Sending a message restarts the agent on the host and delivers it. If it fails again (still signed out), the send is refused with the cause, and the next send tries again. Mobile gets the same chat. Before, it treated this failure as "unknown".

Mechanism:

  • The conversation now exists from the moment its session is reserved. The attach creates the session's journal before it spawns anything (structured-agent-session-conversation-founding.ts). Before, the journal was opened only after the agent process was up. A create whose agent died therefore left a session record with nothing to read, nothing to publish as a tab, and nothing a send could be admitted into.
  • An adopted conversation's history import moved to the same step. A chat resumed from a history row imports that transcript before the first spawn, not after. Otherwise a failed first start would leave the chat without its history, and a later restart (which runs from the record alone) would never import it.
  • The create's answer comes from the host. host.create runs the attach. When the attach is refused and the host proved the agent process is gone, it writes the startup-failure row, keyed by the create's operation id so a replay adds no second row. It then answers the readable chat, and the create route publishes its tab as usual. The row writer is the one the send path already used, moved out of send-preparation so both callers share it.
  • Deleted: the renderer's relaunch-on-send (sendThroughRelaunch in use-native-chat-provisional-launch.ts and its use in NativeChatStructuredSession.tsx). The host's ensure-owner step in send is the only thing that restarts a chat now.
  • Kept on purpose: launch Retry, which is a fresh create. It still covers two cases where the host made nothing: a create refused before reservation (host doesn't support structured chat for that worktree, the worktree can't be resolved yet, the launch prompt couldn't be staged), and a host that predates this change. A message typed while a launch reads "failed" waits in the outbox until Retry publishes the chat. It no longer starts a create by itself.

What still answers a refusal: a definitive refusal (structured_agent_session_unsupported), so the client still falls back to a terminal, and a failure where the host could not prove the process gone, so the client still reconciles instead of assuming.

Why

Only one mechanism should restart a chat, and it should sit where the facts are: the execution host, which knows whether a process is alive. The old renderer path existed only because a failed create left nothing readable. Fixing that removes the reason for the second path, so it is deleted rather than kept alongside.

Alternatives considered:

  • Keep answering the create with a refusal and have the renderer adopt the session anyway. The renderer can't tell a readable failed create on a new host from a journal-less one on an old host without a new wire field. The host also wouldn't publish a tab for a refused create, so the chat could be pruned.
  • Stop starting the agent at create at all, and start it only on the first send or hold. This would be the purest single path, but it changes every create caller and the hold/release lifetime. That is much larger than this fix.

Differences from the common pattern. In the usual design, the conversation (thread) is a record that exists independently of the agent process. Every turn start makes sure a process exists, the first turn included, and a failed start lands as an error entry in the conversation. This PR matches that ownership (the journal exists from reservation, independent of the child) and that delivery (a send goes through the one ensure-owner step). It deviates in lifecycle: Orca still starts the agent eagerly at create, and an open chat still takes a hold that starts one. So after a failed create, opening the chat makes one more start attempt before the user sends anything (one extra short-lived process for a persistent failure such as "not signed in"). That is covered by a follow-up that makes a chat surface's hold leave a failed start for the next send to restart; this PR adds no hold logic. A second design keeps "start" (which carries the first message) separate from "submit". Orca's create doesn't carry the first message, so this follows the first design.

Remote / mixed versions. No wire field was added or removed. What changed is content on an existing reply: a create that used to be refused now answers ok with a chat.

  • Old client against a new host: older desktop and web clients treat it as a published chat. Their sends go through the host's restart, which already exists. Older mobile builds open the chat instead of showing "could not be confirmed".
  • New client against an old host: the old host still refuses the create with the proven-exit verdict. The new client shows the failed launch with Retry, and Retry recovers it. Only the auto-relaunch on send is gone for that pairing. No separate fallback code remains: the Retry path is the same one kept for creates refused before reservation, so there is nothing to delete later.
  • The cross-version agent-session stub gained create as a member that forwards to attach, so both builds still reach the host through attach.

Other create callers: a chat surface gets a readable conversation, and a worker launch needs a running agent. So the chat agentSession.create route answers through host.create, while orchestration workers and agent.launch (the in-process create path) still answer through host.attach and keep the start's refusal, reason and all, after a single spawn.

Known and left for later: a chat launched with a prompt whose agent keeps failing shows two rows: the startup row, then "couldn't restart: " from the prompt's send.

Persisted state: no new record field. The only new durable thing is a journal file created earlier in a session's life (at reservation, before the spawn). Journals are never enumerated for listings: history lists provider transcripts, tabs come from the host's open sessions, and startup restore uses the persisted visible-tab index. So a failed create that the user closes doesn't show up anywhere as a phantom chat.

Linked Issue

Follow-up to #22364. No separate issue.

Visual Proof

N/A: no layout change. The failed-start chat reuses the existing status row. Covered by host tests at the reply boundary.

Testing

  • New structured-agent-session-failed-create-readable.test.ts (real host, real record store and journal):

    • An agent that dies while being acquired: the create answers ok with the startup row and a published tab. A replay of the same operation answers the same chat with no second row. A send at the returned fence restarts the agent and is admitted.
    • A sign-in failure: the create answers the chat, a send while still signed out is refused agent_session_owner_restart_failed with the cause, and a send after signing in restarts and delivers.
    • An unverifiable exit is not turned into a chat.
    • A create refused before reservation stays a refusal with no record, and a fresh create then succeeds.
  • orchestration-structured-worker-failed-start.test.ts (real host): a worker whose agent fails to start is still refused with "…for this worker was refused: ", after one spawn, with no tab published.

  • structured-agent-session-adopted-import.test.ts: an adopted history stays readable after the first start fails. An import write failure now fails before any spawn.

  • NativeChatStructuredSession.launch-lifecycle.test.tsx: a send into a failed launch does not relaunch. It waits in the outbox and is delivered once launch Retry publishes. structured-agent-session-launch-exited-owner.test.ts still covers the old-host refusal shape and Retry.

  • Ablations against the committed tree: sending every create through host.create reddens the worker test (it gets the bare code agent_session_operation_invalid). Removing the founding call reddens the two failed-create tests and the adopt tests. Removing the create's failed-start answer reddens the two failed-create tests. The pre-change renderer reddens the new lifecycle test (relaunch called on send).

  • tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts passes. Broad sweep of src/main/native-chat, src/main/runtime/rpc/methods, structured runtime tests and the renderer native-chat/launch tests passes. The only failures under full parallel load were timeouts that pass when run alone.

  • tsc --noEmit -p config/tsconfig.node.json passes.

  • Not run: the mobile suite (no mobile code changed) and Electron UI validation.

  • I manually tested these changes locally

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

AI Disclosure

Review

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

Author X handle: @BrennanKB5

Checklist

  • This PR is small and focused
  • 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)

A create whose agent died while being acquired used to leave a record and no
journal: nothing to read, no tab, and nothing a send could be admitted into.
The renderer covered it with a second restart path — a send into a failed
launch re-ran the create under a new operation.

The conversation now exists from its reservation. The attach founds the
session's journal, and imports an adopted transcript into it, before it spawns
anything. A create whose first child is proven gone answers that conversation
as a readable chat whose first row says why the agent stopped; a send restarts
the agent through the host's ensure-owner step like any chat whose child ended.
The renderer's relaunch-on-send is deleted. Launch Retry stays for a create
refused before anything was reserved, and for a host that predates this answer.
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The attach flow records adopted conversation history before owner acquisition. A qualifying create failure can now return a readable session with a startup status row. The RPC create route calls the host’s create method. Renderer sends no longer trigger launch relaunch; users can retry the launch explicitly.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 0e847

Failed creates remain readable, but a repeated create reports the wrong replay state. Correct that response field; the remaining established risk is bounded.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 17 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: fixing the chat restart path after an agent fails during startup.
Description check ✅ Passed The description is detailed and covers the user impact, mechanism, rationale, compatibility behavior, testing, and checklist. It includes the required sections and identifies the related issue, althou…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 15c87f25-9852-496d-b3dd-0fa28feb2cef

📥 Commits

Reviewing files that changed from the base of the PR and between edbb2a9 and 0e8477f.

📒 Files selected for processing (18)
  • src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-conversation-founding.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-readable.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-failed-create.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.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-send-preparation.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-row.ts
  • src/main/runtime/rpc/methods/structured-agent-session-create.ts
  • src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.ts
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
  • src/renderer/src/components/native-chat/use-native-chat-provisional-launch.ts
  • src/renderer/src/lib/structured-agent-session-launch-exited-owner.test.ts
  • tests/e2e/cross-version-wire/structured-agent-session-host-fixture.ts
💤 Files with no reviewable changes (1)
  • src/renderer/src/components/native-chat/use-native-chat-provisional-launch.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.

const tabId = context.deps.store.getRecord(sessionId)?.surfaceTabId
return {
ok: true,
replayed: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

set -eu
printf '%s\n' '--- changed file diff ---'
git diff --unified=80 29480cd8cd35a1cf12f36bf056a7e280670c8c52 0e8477f126a20c9d1d89755dc8cfdf9823d15dd6 -- src/main/native-chat/agent-session-wire/structured-agent-session-failed-create.ts
printf '%s\n' '--- implementation ---'
cat -n src/main/native-chat/agent-session-wire/structured-agent-session-failed-create.ts
printf '%s\n' '--- related test files ---'
find . -name 'structured-agent-session-failed-create-readable.test.ts' -o -name '*agent-session*attach*' | sort
printf '%s\n' '--- test references ---'
rg -n -C 8 'replayed|repeated|attach|failed create|failedCreate' --glob '*structured-agent-session-failed-create-readable.test.ts' --glob '*.ts' src/main/native-chat/agent-session-wire

Repository: stablyai/orca

Length of output: 45649


🏁 Script executed:

set -eu
printf '%s\n' '--- failed-create readable test ---'
wc -l src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-readable.test.ts
cat -n src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-readable.test.ts
printf '%s\n' '--- attach orchestration replay references ---'
rg -n -C 12 'replayed|clientOperationId|operationId|idempot|attach\(' src/main/native-chat/agent-session-wire/structured-agent-session-attach-orchestration.ts src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts src/main/native-chat/agent-session-wire/structured-agent-session-attach-failure.ts src/main/native-chat/agent-session-wire/structured-agent-session-attach.ts
printf '%s\n' '--- failure-row recorder ---'
cat -n src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-row.ts
printf '%s\n' '--- shared result and operation types ---'
rg -n -C 12 'AgentSessionMutationResult|AgentSessionAttachResult|replayed|clientOperationId' src/shared/agent-session-wire.ts src/main/native-chat/agent-session-wire

Repository: stablyai/orca

Length of output: 45661


🏁 Script executed:

set -eu
printf '%s\n' '--- attach flow bounded source ---'
sed -n '80,280p' src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts
printf '%s\n' '--- attach flow remaining result construction ---'
sed -n '280,520p' src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts
printf '%s\n' '--- reserveOwner definitions and dispositions ---'
rg -n -C 20 'reserveOwner|disposition:.*replayed|disposition.*replayed|type.*Reserve|replayed' src/main/runtime src/main/shared src/shared src/main/native-chat/agent-session-wire --glob '*.ts' | head -n 1200

Repository: stablyai/orca

Length of output: 42440


🏁 Script executed:

set -eu
printf '%s\n' '--- direct reserveOwner files ---'
rg -l 'async reserveOwner|reserveOwner\s*=' src/main --glob '*.ts' | sort
printf '%s\n' '--- host create path ---'
rg -n -C 20 'answerStructuredAgentSessionCreate|async create|create\(' src/main/native-chat/agent-session-wire/structured-agent-session-host.ts src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-row.ts
printf '%s\n' '--- start-failure recorder ---'
cat -n src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-row.ts
printf '%s\n' '--- reserve implementation excerpts ---'
for f in $(rg -l 'async reserveOwner|reserveOwner\s*=' src/main --glob '*.ts' | sort); do
  echo "--- $f"
  rg -n -C 35 'async reserveOwner|reserveOwner\s*=|disposition|operationRow' "$f" | head -n 500
done

Repository: stablyai/orca

Length of output: 16616


🏁 Script executed:

set -eu
printf '%s\n' '--- reservation commit implementation locations ---'
rg -l 'commitAgentSessionReservation' . --glob '*.ts' | sort
printf '%s\n' '--- reservation commit implementation ---'
for f in $(rg -l 'commitAgentSessionReservation' . --glob '*.ts' | sort); do
  echo "--- $f"
  rg -n -C 45 'commitAgentSessionReservation|disposition|operationRow|outcome' "$f" | head -n 1000
done
printf '%s\n' '--- host create delegation ---'
rg -n -C 15 'answerCreate|create\s*[:=]|create\(' src/main/native-chat/agent-session-wire/structured-agent-session-host.ts src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts

Repository: stablyai/orca

Length of output: 32938


Preserve the replay state for failed creates.

A repeated create replays the stored failure, but answerStructuredAgentSessionCreate converts that refusal into a readable result with replayed: false. Propagate the attach operation's replay state into this conversion and assert replayed: true in the repeated-create test.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

pnpm tc:node is red on this branch — a typing error in the new test-fixture line blocks CI. Everything else reads sound; the only inline comment is the fix for that.

Reviewed changes

  • Journal founded at reservation, before the spawn. New structured-agent-session-conversation-founding.ts opens the journal and applies the adopted-transcript import right before acquireOwner in structured-agent-session-attach-flow.ts, then closes it in a finally; failures wrap in AgentSessionPreSpawnError so the reservation settles processless.
  • Adopted import moved pre-spawn. importAdoptedTranscript now takes the journal directly and no longer closes it on failure; its sole caller (the founding step) owns the close.
  • Create answers a proven-exit failed start as a readable chat. New structured-agent-session-failed-create.ts (answerStructuredAgentSessionCreate) writes a startup-failure row keyed by the create operation and returns {ok:true} with the hydration page; host.create is the new host entrypoint and commitStructuredAgentSessionCreate calls it. Replays re-derive the same chat with no second row.
  • Renderer relaunch-on-send deleted. sendThroughRelaunch and its use in NativeChatStructuredSession.tsx are gone; a send into a still-failed launch parks in the outbox until launch Retry.
  • Refactor: settleLateDispatch / releaseUnansweredDispatches moved onto the mutation delegates; host/test stubs gained create.

I ran the new and adjacent suites: structured-agent-session-failed-create-readable.test.ts (4), structured-agent-session-adopted-import.test.ts (10), NativeChatStructuredSession.launch-lifecycle.test.tsx (11), and structured-agent-session-precommit-refusal.test.ts (9) all pass. The cross-version wire unit test fails only because the CI checkout is shallow with no release tags.

ℹ️ Nitpicks

  • A failed create writes failed-start:<createOpId>, and the next send that also fails writes an identical failed-restart:<sendOpId> row via recordFailedRestart (structured-agent-session-send-preparation.ts), so the user can see two identical "The provider stopped before it finished starting" rows for one persistent failure. Worth confirming that is intended rather than reusing the create's key.
  • readFailedCreate (structured-agent-session-failed-create.ts) always returns replayed: false, even when the answer comes from a replay of the create operation.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.ts Outdated
…nt fails to start

A chat surface gets a readable conversation when its agent fails to start and
restarts it by sending; an orchestration worker or agent.launch needs a
running agent, so its create still answers the start's refusal with the reason
after one spawn. Also types the RPC fixture's forwarding create.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Reviewed the delta since the prior pullfrog review (197f72a on top of 0e8477f):

  • Routed the create answer by caller. commitStructuredAgentSessionCreate now takes answer: 'conversation' | 'running-agent': the chat agentSession.create route passes 'conversation', so a proven-exit failed start still answers a readable chat, while createStructuredAgentSessionForWorktree hardcodes 'running-agent' so orchestration workers and agent.launch keep the start's refusal (with reason) after one spawn.
  • Covered the worker path with a real-host test. orchestration-structured-worker-failed-start.test.ts proves a worker whose agent dies starting is refused with the reason, spawns once, publishes no tab, and leaves no session tab. It would fail against the prior host.create path, so it is genuine coverage rather than a tautology.
  • Cleared the typecheck failure from the prior review. The test-fixture hostStub now binds a well-typed local attach mock and forwards create to it, and the precommit-refusal stub returns { attach, create: attach }.

I re-ran pnpm tc:node (green), oxlint on the changed files (0 warnings/0 errors), and the relevant suites — structured-agent-session.test.ts (58), NativeChatStructuredSession.launch-lifecycle.test.tsx (11), structured-agent-session-adopted-import.test.ts (10), orchestration-structured-worker-session.test.ts (12), structured-agent-session-failed-create-readable.test.ts (4), structured-agent-session-precommit-refusal.test.ts (9), and the two worker/start-failure tests (1 each) — all pass.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing without merging. It's superseded by the native-chat redesign: a failed start becoming a readable chat, and one restart path, are handled by #22821 (accept then deliver) and a later PR where creating a chat founds the conversation without starting the agent.

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