Skip to content

fix(native-chat): mark a chat whose start failed on its tab and workspace row - #24904

Merged
brennanb2025 merged 8 commits into
mainfrom
brennanb2025/chat-failed-start-mark
Oct 5, 2026
Merged

brennanb2025 merged 8 commits into
mainfrom
brennanb2025/chat-failed-start-mark

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​918 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​917
Prod 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​147 $\color{#cf222e}{\Huge{\mathbf{−}}}$​5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​142

ELI5

If a native chat can't start while you are looking at another tab or workspace, Orca now marks that chat's tab, and its workspace in the sidebar, with the same red "failed" mark it already uses when an agent's turn fails. Before, nothing outside the chat told you.

A start Orca could not confirm (its reply was lost, it timed out, or the app reloaded mid-start) is now checked again automatically when its host can be reached again, instead of waiting until you press Retry in that chat. It then ends as started, or as failed with the same mark.

For Claude, this does not yet cover the most common cases: a signed-out Claude, or a Claude CLI that quits during startup (for example one that is too old or crashes). Those are still reported only inside the chat until #24450 makes a chat start its agent on the first message. Codex is covered: when it quits during startup, its chat's start fails and gets the mark.

Follow-up to #24588. Merge after #24917 (or together with it): see "Merge order" below.

What Changed

The problem. A native (structured) chat starts by asking the host to create its session. When that create is refused (for example the agent cannot be started at all because it is not installed or not runnable, the host refuses the request, or the prompt could not be saved locally), the chat says so inside its own tab: "Chat could not be started" with a Retry button. #24588 (now merged) removed the toast that also said it, so the failure is reported once, in the chat. That leaves a gap: if you were not looking at the chat, nothing outside it tells you it failed. That happens when a workspace is created in the background (the chat opens in a tab you never see, and a green "Worktree ready" toast even arrives first), or when you switch away during a slow start.

Why nothing showed: the tab's status mark and the sidebar's workspace status read the chat's status row, and that row comes from the host. A refused create leaves the host with no status to publish for that chat (the agent never ran), so no row appears, and the failure was invisible to every status surface.

Not in this PR: after a failed start, the + menu keeps that agent disabled with a spinner, as if it were still starting. Re-enabling it is not a one-line fix. Picking the agent would then join the failed launch and retry it, and any prompt carried by the pick (for example notes sent to a new agent) would be silently dropped. The fix needs a new chat to stop joining a failed one, which changes how launches are shared, so it has its own PR: #24917.

Before / after (what you see)

Case Before After
A chat's start fails while you are on another tab or workspace nothing outside the chat the chat's tab and its workspace in the sidebar show the red "failed" mark, and Activity counts it as unread
You open the failed chat the "Chat could not be started" line with Retry unchanged; viewing it marks it read
You retry (the Retry button, or sending a message in the chat) the chat starts the mark goes away while it starts; if it fails again, it comes back and is unread again
You close the failed chat's tab — the mark goes away
A chat's start could not be confirmed (the reply was lost, it timed out, or Orca reloaded mid-start), while you were elsewhere it stayed unconfirmed until you opened that chat and pressed Retry; nothing outside the chat showed anything (#24588 removed the toast that used to) no mark while it is unconfirmed; when its host can be reached again (Orca starting up, or the connection to a paired server coming back), Orca checks again by itself: if the chat was in fact created it simply starts, and if the host refuses it the failed mark appears
You restart Orca with the failed chat still open — the mark is dated by when the start failed, not by the restart: a failure you already saw stays read, one you had not seen is still unread, and the tab and workspace marks age out on the same clock as any failed chat (see below)

No new text, toast, banner or status line: the mark is the existing failed verdict on the existing surfaces.

Which failures get the mark today: any start whose create fails, which covers a Codex that quits during startup, an agent that cannot be run at all (not installed or not runnable), the host refusing the request, and a prompt that could not be saved locally. Not yet: a Claude that is signed out or quits during startup (see "What this does not cover yet").

The mechanism.

  • The launch record already knows the start failed: its lifecycle is failed, and it is persisted so it survives a restart. The status bridge (the renderer component that turns each chat's host status into its tab and sidebar row) now reads that lifecycle too. When the host has published nothing for the chat and its launch failed, the bridge writes the chat's row as the failed verdict the host itself publishes when an agent's start refuses a message: finished, with the main agent's outcome failure. The tab mark, the workspace status, the dashboard rows and the Activity feed already render that verdict as failed.
  • The row is re-derived whenever the launch changes. A retry puts the launch back to pending, and the bridge removes the row. When the chat starts, the host's own status takes over: none until the first message, as for any new chat. Closing the tab unmounts the bridge's projection, which already removes the row.
  • The row is dated by when the attempt failed, the way a host-published row is dated by the chat's own history. The launch now records that time on the failed attempt, and saves it with the launch record it already persists, so it survives a restart. A retry starts a new attempt, so the time never outlives its failure. Launch records saved by older builds have no time; for those the tab's creation stands in, which falls before any earlier acknowledgement, so a failure already seen stays read.
  • Unconfirmed starts are re-checked automatically. The signal is one the app already has: each time a host's session-tab stream opens (at startup, and again after a dropped connection or a restarted runtime), it begins with one full inventory. On that event, every launch in this window whose outcome is unconfirmed and whose owning host is that host is re-checked through the existing Retry path, with the same operation id, so the host answers idempotently: published (the host's status takes over), refused (the failed mark), or still unconfirmed (it waits for that host's next opening inventory). There is no timer and no retry loop: one re-check per launch per event. A chat you closed or cancelled is never re-created, and a refused start is not asked again automatically; only your Retry does that. A launch restored after a reload is found through its tab, because the saved record has no workspace.
  • docs/reference/agent-status-store.md gains one row in its list of renderer status writers: this one stays, because a refused create leaves the host no session to publish from.

Why

  • Not the host's status store. The rule is that the host that runs an agent owns its status. Here the host refused the create: the agent never ran, so the host holds no status for it to own. Having it keep a row for a session it refused would add a second copy of a fact the client's launch record already holds durably. The bridge is already the renderer's single writer for chat rows, so the failure joins it as one more input rather than adding a writer.
  • Not a rule in each reader. Teaching the tab bar and the sidebar to check the launch registry would add two precedence rules beside the status store, and the dashboard, Cmd+J and Activity would still miss the failure. Writing the existing verdict reaches all of them through code they already have.
  • Not a toast or notice. A failure is said once, in the chat. The mark is a status on surfaces that already exist, which is how the common pattern marks a failed thread's row.
  • Composes with fix(native-chat): a new chat starts at rest; its first message starts the agent #24450. That open PR makes a new chat start at rest, so most start failures move onto the first message, where the host's own failed verdict already marks the row. What remains (a create the host refuses) gets the same verdict from this change, so the two read the same.

What this does not cover yet

  • Temporary: a Claude chat whose Claude is signed out, too old, or quits during startup, before any message is sent. For Claude the create succeeds as soon as the process is spawned, and "not signed in" is only detected later, at the startup handshake. So the launch never becomes "failed": the chat shows "Claude stopped before it finished starting. Send your message to try again." (or the sign-in problem), and the host publishes no status because nothing was sent. No mark appears. Follow-up: fix(native-chat): a new chat starts at rest; its first message starts the agent #24450 makes a new chat start its agent on the first message, which moves these failures onto that message, where the host already publishes a failed verdict and the same mark appears. Until then these cases are reported only inside the chat. Codex does not have this gap: when it quits during startup, its create fails and the chat is marked.
  • Not a stopgap: after fix(native-chat): a new chat starts at rest; its first message starts the agent #24450 lands, this change still covers what remains: the host refusing a create, an agent that cannot be run at all, and a prompt that could not be saved locally.

Merge order

Merge this after #24917. On its own, the automatic re-check here loads a launch saved before a restart back into memory. If that re-check ends refused, today's launch code would let the next new request for that agent and workspace (for example "Send notes to > New agent") join and retry the old chat and drop its text. #24917 removes exactly that joining, and with it this case. The two PRs also touch the same per-attempt record (failedAt here, attempt there), so whichever merges second keeps both fields.

Differences from the common pattern

  • How an unconfirmed start is resolved. The common pattern re-reads the host's truth when the connection comes back and shows no row mark for a create it has not confirmed. Here too: Orca re-checks on the host's reconnect and shows no mark until the answer arrives. Intended difference in reach: an unconfirmed start on a host whose connection never dropped is re-checked at the next reconnect or app start, not immediately, because by the time Orca calls a start unconfirmed it has already re-checked it and re-sent the same create once during that start.

  • Where the failed state lives. In the common pattern the host keeps the thread record and marks it failed; the client derives the row mark from it. Here the host refused the create, so the fact lives on the client's launch record, and the client derives the row from it. Intended: the host never ran the agent and has no status for it; some failures (a prompt that could not be saved locally) never reach the host at all. As a result, orca worktree ps and the mobile app do not list a chat whose create was refused; they never listed it before either.

  • How long the tab and workspace marks last. The common pattern keeps a failed thread's mark until the next start, or until it is read. Here the tab and workspace marks follow Orca's existing display rule for any finished row whose agent is no longer running: they stop showing 30 minutes after the failure. The Activity unread count and the chat's own Retry line stay until you act. Intended: this is exactly how a failed first message's mark already behaves in Orca, so the two kinds of failed start read the same; a longer-lived mark would be a change to that shared rule for every agent row, not to this one.

  • No OS notification. Intended: desktop notifications for chats are driven by the host's turn completions, and a refused create has none. The unread count and the marks carry it instead.

Linked Issue

Follow-up to #24588 (its review deferred "a start that fails while you are looking elsewhere is reported nowhere outside the chat" and "the + menu shows a failed start as still starting").

Visual Proof

Added: macOS (M4Air), on this PR's final commit. Hidden, isolated test builds on a Mac. Before: 3284b4c70c9 (this branch's base on main). After: ba208fdcb58 (this PR). A stand-in Claude program was used, never the real one. These cover the automatic re-check of an unconfirmed start, which the Windows run below did not cover.
How the states were made: the chat's start request was lost on its way to the app's back end, and the window was then reloaded, the way the app comes back. Nobody pressed Retry. A log of every start request shows which chat each one belonged to.

An unconfirmed start after a reload. Before: 40 s after the reload the chat still says "Chat connection could not be confirmed." with Retry, and no second start request was sent. After: the app re-checked the same chat by itself, with the same request id, and the chat started. A message sent afterwards got the stand-in's reply (right).

Before, 40 s after the reload After, 10 s after the reload After: a message gets a reply
Before: Chat connection could not be confirmed. with Retry, 40 seconds after the reload After: a normal started chat, no Retry line After: QA-24904 hello and the stand-in reply

No failed mark while a start is unconfirmed or being re-checked. The app's own re-check was held. Seen from another tab, the chat's tab has no red mark and the probe workspace row stays green, in both builds. The after build shows the plain empty chat while the re-check runs (middle). After release it started.

Before (unconfirmed) After: the chat while its re-check runs After: from another tab
Before: Terminal 1 active, Claude Chat tab with no failed mark, probe row green After: empty chat while the re-check is held, no Retry, no mark After: Terminal 1 active, no red mark on the Claude Chat tab, probe row green

The re-check is refused. Before: the chat stays "Chat connection could not be confirmed.", with no status row and no mark. After: "Chat could not be started." with Retry. The chat's tab has the red mark, and the probe row is red with "Claude Chat - Failed", also seen from another workspace.

Before After
Before: Chat connection could not be confirmed. with Retry, probe row green After: Chat could not be started. with Retry, red dot on the tab, probe row red with Claude Chat - Failed
Before: from the Terminal 1 tab, no failed mark After: from the Terminal 1 tab, red dot on Claude Chat, probe row red
Before: from the demo workspace, probe row green After: from the demo workspace, probe row red with Claude Chat - Failed

How the refusal was made: the re-check request was refused at the back end, the way a host without that request answers. On macOS a Claude program that cannot be run does not refuse a start: Claude runs under a small supervisor, so it shows "Claude stopped before it finished starting." instead. In that case the after build's re-check was accepted, and the chat then showed "Claude stopped before it finished starting. Send your message to try again." with no mark. That is the Claude startup-exit case this PR lists under "What this does not cover yet"; the before build stayed unconfirmed.
Timing note: in this refused run, the after build sent its re-check 0.16 s after the lost request, before the reload. Both requests carried the same session and request id. In the other after runs, the re-check came only after the reload. The end state matched either way.
The earlier Windows run saw an unexplained second start request in the before build. It did not reproduce here: with every request logged by chat, the before build never sent a second request in any case.
No new pop-up, toast or banner appeared in the after build. The "Enjoying Orca?" card in one after capture is Orca's existing star prompt, shown after the first finished chat turn.
Not captured on macOS: a paired server's connection coming back, Codex, and the 30-minute fade.

Earlier: Windows captures (still hold).

Windows, in hidden, isolated test builds. Before: de77c4b065a (this branch's base on main). After: 355fdc0ce2a (this PR). A stand-in Claude program was used, never the real one.
How the failure was made: the Claude program the app finds first cannot be run, so the start fails before any process exists. Both builds then show "Chat could not be started." with Retry. A stand-in that starts and then quits right away gives a different line in both builds ("Claude stopped before it finished starting. Send your message to try again.") and no mark in either build, because that chat's create already succeeded.

The failed chat itself. Both builds show the same in-chat line; no pop-up appeared in either. After, the chat's tab and its workspace row also show the red failed mark.

Before After
Before: failed chat shows "Chat could not be started." with Retry, no mark on its tab or the demo row After: same line, plus a red dot on the chat's tab and a red "Claude Chat - Failed" row under demo

Seen from another tab in the same workspace. Before: the "Claude Chat" tab has no mark. After: it has a red dot.

Before After
Before: terminal tab active, Claude Chat tab with no mark After: terminal tab active, red dot on the Claude Chat tab

Seen from another workspace. Before: the demo row is green with nothing under it. After: the demo row is red, with "Claude Chat - Failed" under it.

Before After
Before: sidebar from the other workspace, demo row green After: sidebar from the other workspace, demo row red with "Claude Chat - Failed"

Activity. Before: "No agent activity matches these filters." After: the failed chats are listed under "FAILED". A chat whose failure you have not looked at keeps its unread state (orange time, "Mark thread as read"). A chat you watched fail is read once you switch to another tab.

Before After
Before: Activity shows no agent activity After: Activity lists two Claude chats under FAILED

Retry clears it (after only). With the cause removed, Retry started the chat in the same tab. The red dot left the tab and the demo row went back to green. The other red row in these shots is a separate chat from the earlier failure check in a third workspace ("probe"), which was not retried.

Tab after Retry Sidebar after Retry
After Retry: Claude Chat tab with no mark After Retry: demo row green; only the separate probe chat is still failed

Known uncovered case (not a regression). An agent that starts and then quits during startup is not covered yet; see "What this does not cover yet". In the after build such a chat shows "Claude stopped before it finished starting. Send your message to try again." Its tab and workspace row have no mark, the same as before this PR.

After build, startup-exit case: in-chat "Claude stopped before it finished starting. Send your message to try again.", no mark on the tab or the probe row

Unchanged, for the record: in both builds the + menu still shows Claude greyed out with a spinner after a failed start. A separate PR handles that.

Before After
Before: + menu with Claude disabled and spinning After: same

Not captured:

  • Closing the failed chat's tab.
  • A restart with the failed chat still open.
  • The 30-minute fade of the tab and workspace marks.
  • An OS notification (none is expected).
  • The macOS and Linux builds. This run was Windows only; the change has no platform code.

Testing

  • structured-agent-session-status-bridge-failed-start.test.tsx (new) drives a real launch whose create is refused, through the real bridge and test store: the row, the tab and the workspace status read failed; a retry clears the mark and the started chat is left to the host; closing the tab clears it; a retry that fails again is dated by the new failure and is unread again; after a restart a failure already seen stays read and one not yet seen is unread; the mark ages out 30 minutes after the failure, like a host-reported failed chat; a failed row already at that pane key is re-dated.

  • structured-agent-session-status-bridge-unconfirmed-start.test.tsx (new): no mark while a start is unconfirmed; when the host turns out to have had the chat, the re-check settles it as started with no second create; when the host refuses on the re-check it is marked failed and a later event does not ask again; a start interrupted by a reload is restored and re-checked without Retry; a closed chat is never re-created; another host's reconnect does nothing, and a still-unconfirmed result waits for the next reconnect instead of looping.

  • The two tab-stream tests: the re-check runs on a stream's opening inventory only (not on ordinary updates or reads), again after a dropped stream resubscribes, and for a paired server when contact comes back.

  • structured-agent-session-launch-persistence.test.ts: the failure time survives a reload; records without it still load.

  • Ablations, each piece removed alone: the failed-start projection; failure-dated freshness; the saved failure time; the date check; each hook-up of the re-check (this machine, paired server); the tab scan after a reload; the owning-host check; re-checking only unconfirmed launches. Each makes its test fail.

  • 123 test files (1018 tests) covering the launch modules, the status bridge, the session-tab syncs and everything that mocks them pass. The web typecheck (from scratch, and tc:node for the merge with main), oxlint, the changed-code quality gate and React Doctor are clean.

  • Platforms: renderer-only logic with no platform or path code, and no wire change. Native chats run in local Git worktrees and folder workspaces, and now also on paired servers; SSH workspaces use the older chat. A refused create on a paired server goes through the same launch record and status bridge, so it is marked the same way, but that case was not tested in the app.

  • I manually tested these changes locally

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

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

Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

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)

…pace row

A chat whose start the host refused said so only inside its own tab. Its
row now carries the failed verdict the host publishes for a refused start,
so the tab and the workspace row show it from elsewhere until a retry
starts it or the tab closes. The + menu no longer shows a failed start as
still starting, so launching the agent again retries it.
… + menu change for a separate PR

The failed-start mark on a chat's tab and workspace row is now dated by when
the start failed, the way a failure the host reports is dated: it ages out on
the same schedule, a restart does not bring an old mark back, and a failure
the user already saw stays read. The failure time is saved with the launch
record so a restart keeps it; records saved without it fall back to the tab's
creation time.

The + menu change (letting a failed agent be picked again) moves to its own
PR: its correct form makes a new chat instead of retrying the failed one,
which changes how launches join.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary

The problem. When a native chat can't start (the host refuses to create it), Orca says so only inside that chat, with a Retry line. #24588 removes the extra toast so the failure is reported once. That leaves a gap: if you were on another tab or workspace when it failed (for example a workspace created in the background), nothing outside the chat told you. The chat's tab and its workspace row read their status from the host, and the host has no status for a chat it refused to start.

What changes for you. The failed chat's tab, and its workspace in the sidebar, now show the same red "failed" mark Orca already shows when an agent's turn fails, and Activity counts it as unread. Opening the chat marks it read. Retrying it clears the mark (and brings it back if it fails again); closing the tab clears it. There is no new text, toast or banner.

What the review changed.

  • The first version also re-enabled the agent in the + menu after a failed start. Review showed that picking it would join and restart the failed chat and silently drop anything the pick carried, such as notes sent to a new agent. That part was split out into fix(native-chat): start a new chat after an earlier start failed #24917, which makes a new pick always open a new chat.
  • The mark is now dated by when the start failed, the way a host-reported failure is dated by the chat's history. That date is saved with the launch record so it survives a restart. Before, a restart re-dated it. That brought an already-seen failure back as unread, and it re-showed the mark on every launch.
  • One row was added to the status-store reference doc saying this renderer-side writer stays.

Deferred (not fixed here).

  • Not covered yet (found in QA): an agent that starts and then exits during startup, before any message is sent. On today's main that chat's create has already succeeded, so it shows " stopped before it finished starting. Send your message to try again." and gets no mark, because the host publishes no status for a chat with no message. fix(native-chat): a new chat starts at rest; its first message starts the agent #24450 (open) makes a new chat start at rest, which moves this failure onto the first message, where the host already marks the row failed. Until then this case is reported only inside the chat.
  • The tab and workspace marks stop showing 30 minutes after the failure. That is Orca's existing display rule for every failed agent row, and a failed first message behaves the same; the Activity unread count and the chat's Retry line stay until you act. A longer-lived mark would change that rule for every agent, so it is out of scope.
  • No desktop notification for a refused start: chat notifications come from the host's turn completions, and a refused create has none.
  • P3s: one test proves only half of a date check; a malformed saved date drops the whole saved launch (only possible with corrupted local storage); a wording nit in the doc row.

What was verified.

  • New tests drive a real refused launch through the real status bridge: the mark, retry, close, re-failure, restart (seen vs unseen), and the 30-minute ageing. Each fix was removed in turn to show its test fails.
  • 22 neighbouring test files (190 tests) pass. The web typecheck, lint, code-quality gate and React Doctor are clean. CI is green.
  • Review round 1 found two P2 issues caused by the PR (the + menu change dropping notes, and the restart re-dating the mark), both resolved as described above. Round 2 and a readiness check found no P0–P2 issues.
  • Real app on Windows (isolated hidden test builds, stand-in agent, before = base, after = this PR; screenshots in the PR body): with a start that fails before the agent can run, both builds show "Chat could not be started." with Retry; only the after build marks the chat's tab and its workspace row red and lists it under Failed in Activity, and Retry clears the mark.

Not verified. In the app, opening a chat that failed while you were elsewhere and seeing it turn read was not exercised (tests and a code read cover it; a chat you watch fail becomes read, as captured); closing the tab, a restart, and the 30-minute fade were covered by tests but not captured; the app was checked on Windows only (the change has no platform code). Native chat runs only in local Git worktrees and folder workspaces, so SSH and paired remote workspaces are not affected and were not tested. The mark is not shown to a paired web client viewing a desktop chat; that gap is older than this PR.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 3, 2026 01:37
@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d7af8d5b-88ea-471b-ab62-8fc515d4536c
📥 Commits

Reviewing files that changed from the base of the PR and between 74d65a0 and a0cdc9e.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: da267d20-51b5-4a51-a4b7-2208b241fb7e
📥 Commits

Reviewing files that changed from the base of the PR and between ba208fd and 74d65a0.

📒 Files selected for processing (5)
  • src/renderer/src/components/native-chat/structured-agent-session-status-bridge-failed-start.test.tsx
  • src/renderer/src/components/native-chat/structured-agent-session-status-bridge-unconfirmed-start.test.tsx
  • src/renderer/src/lib/structured-agent-session-launch-callers.ts
  • src/renderer/src/lib/structured-agent-session-launch-registry.ts
  • src/renderer/src/lib/structured-agent-session-launch-reload.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The change records and persists a timestamp when a structured launch fails. When the host has no session status, the bridge uses that timestamp to project a completed failed status. Legacy failed records without a timestamp use the tab creation time. Host census events trigger rechecks for launches with unknown visibility that belong to the returning host. Tests cover status projection, retry and recovery behavior, tab closure, unread state, restart behavior, and stale status aging. The reference table documents the bridge’s failed-start row.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 74d65

The failed-chat status and host-return recovery changes have no established merge-blocking issue. Merge after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 74d65

Automatic recovery can reissue a previously requested chat start when its host returns. It preserves the recorded host and request identity, and cancelled chats are excluded. No introduced security defect was established, but reconnect identity continuity and coordinated merge requirements remain incompletely verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The incremental security-relevant exposure is automatic resumption of an existing unknown-outcome launch on its recorded execution host. Recovery can reach session creation and therefore agent execution in that worktree, but the inspected selection logic does not broaden recovery to other hosts or definitive failures.

Trust Boundaries and Controls

  • observed — Recovery preserves the original operation envelope for unknown outcomes and validates host, worktree, and session identity. The create handler applies the structured-client capability gate and fingerprint check, deriving the caller namespace from RPC context rather than persisted renderer data. Stability of that namespace across paired reconnects remains unverified.

Resilience and Maintainability Implications

  • observed — The paired census can trigger recovery before asynchronous snapshot application finishes. Recovery independently reads host inventory and, if necessary, retries the original operation; it does not rely solely on the renderer mirror to decide whether creation landed.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.32% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 37 functions across 14 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: marking a native chat’s tab and workspace row when its start fails.
Description check ✅ Passed The description is detailed and covers the user impact, changes, rationale, testing, and visual proof. It identifies the linked follow-up issue and relevant limitations. Some template items remain inc…
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.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@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

  • Failed-start projection. StructuredAgentSessionStatusBridge.tsx now reads the launch lifecycle and failure time; when the host has published nothing and the launch is failed, it writes the host's own failed verdict (done + failure) onto the chat's pane key so the tab, workspace row, dashboard, and Activity mark it failed.
  • Failure timestamp. settleStructuredLaunchCallers records failedAt; the persisted launch record gains an optional failedAt; the reload path restores it. The mark survives a restart dated by the failure, and a retry starts a fresh caller group so the time never outlives its failure.
  • Tests. A new bridge integration test drives a real refused launch through the real store and the real bridge (mark, retry, tab close, re-failure, restart seen/unseen, 30-minute ageing). The persistence test covers the new field plus legacy and malformed records. The two suites pass (12 tests).

Verification. createTestStore composes the production createAgentStatusSlice, so the new test exercises the real setAgentStatus/buildAgentStatusLiveEntry; no production-only guard silently drops the write (allowOlderTimestamp bypasses the only stale rejection, the retirement/recently-closed maps are empty for a mounted tab, and identity suppression requires a different agentType). The projection is gated on !summary?.status, and a refused create leaves no host session, so it cannot overwrite a live host row; a successful retry flips the group to pending/published before any host row appears.

Deferred items are documented by the author (+ menu spinner → #24917; startup-exit case → #24450) and are not regressions from this PR.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

… reachable again

A chat create whose reply was lost, or that a reload interrupted, stayed unknown until the
user pressed Retry in its tab. Each host subscription opens with one full inventory; on it,
every launch that host owns whose outcome is unknown (including ones restored from the saved
launch record) is re-checked through the existing Retry path, so it settles to started or
failed. One still unknown waits for the host's next return; cancelled launches are skipped.
brennanb2025 added a commit that referenced this pull request Oct 4, 2026
…ent to it

A blank chat that is still starting (a + pick, the empty-workspace default
chat) is empty, so the first request with text, such as notes sent to a new
agent, now goes into it instead of opening a second chat beside it. That
request becomes the chat's own request: an identical repeat joins and is sent
once, and any other request opens a new chat.

Move the logic that decides which launch a start joins out of the launch
registry into its own module, so the registry stays under the line limit
once #24904 lands beside it. Pin that a repeat arriving while the opening
text is still sending is sent once.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Follow-up: starts Orca couldn't confirm

The problem. A chat start whose result Orca never got — the reply was lost, it timed out, or Orca reloaded mid-start — stayed "Chat connection could not be confirmed." until you opened that chat and pressed Retry. With the failure toast removed by #24588, a background start in that state showed nothing anywhere outside its tab.

What changes for you. When the chat's host can be reached again (Orca starting up, or the connection to a paired server coming back), Orca checks that start again by itself. If the chat was in fact created, it just starts; if the host refuses it, the red failed mark from this PR appears on its tab and workspace row. While it is still unconfirmed there is no mark, which is how the common pattern treats a create it has not heard back about. A chat you closed is never re-created, and a refused start is not asked again until you press Retry.

How. The trigger is a signal the app already has: each time a host's session-tab stream opens, it starts with one full inventory. On that event, each unconfirmed launch owned by that host is re-checked once, through the same path Retry uses and with the same operation id, so the host answers idempotently. There is no timer and no retry loop. An unconfirmed start on a host whose connection never dropped is re-checked at the next reconnect or app start (Intended: by the time Orca calls a start unconfirmed, it has already re-checked and re-sent it once).

Also in this round. The PR body now says plainly that for Claude, a signed-out Claude or a Claude CLI that quits during startup is not marked yet; #24450 (chats start on the first message) covers that. Codex is covered.

Merge order. Merge #24917 first, or together. On its own, this re-check could put a restored failed chat back where a new "Send notes to a new agent" would join it and drop its text; #24917 removes that joining. The two PRs conflict only in one type (keep both new fields). I merged them together locally: lint, 126 test files (1,286 tests) and the web typecheck pass.

Review. Round 3 and a final readiness check found no P0–P2 issues. Deferred P3s: a comment that misstates why one ordering is safe, and one test assertion that cannot fail (production behaviour verified by reading the code).

Verified / not verified. Captured on macOS (isolated test builds, stand-in Claude, before = base, after = ba208fdcb58; screenshots in the body):

  • an unconfirmed start after a reload stays unconfirmed in the before build, and the after build re-checks it by itself and the chat starts;
  • no mark while it is unconfirmed;
  • a refused re-check shows the red failed mark.

The body's timing note (one after run re-sent 0.16 s after the lost request, before the reload) is the launch's existing in-session recovery: when a create gets no answer it re-sends the same request once, with the same operation id, in both builds. Not captured: a paired server reconnecting, Codex, and the 30-minute fade. The earlier Windows captures still hold.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 4, 2026 10:28

@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: 87e01669-461c-4c0a-a261-0ab9ca14b16e
📥 Commits

Reviewing files that changed from the base of the PR and between 9d1e3d5 and ba208fd.

📒 Files selected for processing (6)
  • src/renderer/src/components/native-chat/structured-agent-session-status-bridge-unconfirmed-start.test.tsx
  • src/renderer/src/lib/structured-agent-session-launch-unconfirmed-recheck.ts
  • src/renderer/src/runtime/local-structured-session-tabs-sync.test.ts
  • src/renderer/src/runtime/local-structured-session-tabs-sync/subscription.ts
  • src/renderer/src/runtime/web-session-tabs-sync-reconnect-resubscribe.test.tsx
  • src/renderer/src/runtime/web-session-tabs-sync/global-session-inventory-event.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.

Comment on lines 175 to 181
settleHydration?.()
}
})
// Each subscription opens with one census: the host is reachable again. Chats it lists were
// already settled as published above, before any recovery await.
recheckUnconfirmedStructuredAgentLaunches(toRuntimeExecutionHostId(environmentId))
}

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 | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '115,190p' src/renderer/src/runtime/web-session-tabs-sync/global-session-inventory-event.ts
sed -n '265,315p' src/renderer/src/lib/structured-agent-session-launch.ts

Repository: stablyai/orca

Length of output: 3618


🏁 Script executed:

set -e
printf '%s\n' '--- relevant symbols ---'
rg -n -C 5 'recheckUnconfirmedStructuredAgentLaunches|retryStructuredAgentSessionLaunch|recover.*inventory|inventory.*recover|queueAcceptedWebSessionTerminalSnapshot|recordInventory|settle.*Structured|published' src/renderer/src/runtime src/renderer/src/lib/structured-agent-session-launch.ts
printf '%s\n' '--- target outline ---'
ast-grep outline src/renderer/src/runtime/web-session-tabs-sync/global-session-inventory-event.ts --view compact
printf '%s\n' '--- launch outline ---'
ast-grep outline src/renderer/src/lib/structured-agent-session-launch.ts --view compact

Repository: stablyai/orca

Length of output: 42045


🏁 Script executed:

set -e
printf '%s\n' '--- exact definitions ---'
rg -n '^(export )?(async )?function (recheckUnconfirmedStructuredAgentLaunches|applyWebSessionTabsStorePatch|queueAcceptedWebSessionTerminalSnapshot)|recheckUnconfirmedStructuredAgentLaunches|settlesHostMirror|publishedSnapshotCount' src/renderer/src/lib src/renderer/src/runtime/web-session-tabs-sync src/renderer/src/runtime
printf '%s\n' '--- target handler ---'
cat -n src/renderer/src/runtime/web-session-tabs-sync/global-session-inventory-event.ts | sed -n '1,205p'
printf '%s\n' '--- recheck implementation ---'
files=$(rg -l 'function recheckUnconfirmedStructuredAgentLaunches|recheckUnconfirmedStructuredAgentLaunches' src/renderer/src/lib src/renderer/src/runtime --glob '*.ts' --glob '*.tsx' | head -5)
for f in $files; do
  echo "### $f"
  rg -n -C 35 'recheckUnconfirmedStructuredAgentLaunches' "$f"
done

Repository: stablyai/orca

Length of output: 31973


🏁 Script executed:

set -e
printf '%s\n' '--- structured launch restart and retry ---'
cat -n src/renderer/src/lib/structured-agent-session-launch.ts | sed -n '115,170p'
cat -n src/renderer/src/lib/structured-agent-session-launch.ts | sed -n '250,282p'
printf '%s\n' '--- launch intent and create callers ---'
rg -n -C 8 'createStructuredAgentSessionLaunchIntent|retryStructuredAgentSessionLaunchIntent|restartStructuredLaunchState|callers.outcome|visibilityUnknown' src/renderer/src/lib/launch-structured-agent-session.ts src/renderer/src/lib/structured-agent-session-launch.ts
printf '%s\n' '--- store patch and mirror settlement ---'
cat -n src/renderer/src/runtime/web-session-tabs-sync/store-patch.ts | sed -n '1,180p'
cat -n src/renderer/src/runtime/web-session-tabs-sync/mirror-settle.ts | sed -n '90,165p'

Repository: stablyai/orca

Length of output: 33227


Run the launch recheck after applying the recovered census.

handleGlobalSessionInventoryEvent starts orphan recovery and calls recheckUnconfirmedStructuredAgentLaunches(...) synchronously. The census snapshot is applied only later in the promise’s .then(...) callback. The recheck can therefore restart a visibility-unknown launch before the listed published session is visible, causing a second reconcileUnknownLaunch attempt.

Suggested fix
       }
       coordinator.recordInventory(environmentId, visibilityGeneration, inventoryFrame, missing)
+      recheckUnconfirmedStructuredAgentLaunches(toRuntimeExecutionHostId(environmentId))
     })
@@
-  // Each subscription opens with one census: the host is reachable again. Chats it lists were
-  // already settled as published above, before any recovery await.
-  recheckUnconfirmedStructuredAgentLaunches(toRuntimeExecutionHostId(environmentId))
 }

@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

  • Failed-start mark. StructuredAgentSessionStatusBridge.tsx now reads the launch lifecycle and failure time; when the host has published no status and the launch is failed, projectFailedStart writes the host's own failed verdict (done + mainAgent.outcome: 'failure') onto the chat's pane key, so the tab, workspace row, dashboard and Activity all mark it failed. It is dated by failedAt with allowOlderTimestamp: true, so an already-seen failure stays read across a restart and a retry that fails again is unread again.
  • Failure timestamp. settleStructuredLaunchCallers records failedAt; the persisted launch record gains an optional, finite-checked failedAt; the registry persists it only on failed; the reload path restores it; the legacy fallback is tab.createdAt.
  • Unconfirmed-start re-check. New structured-agent-session-launch-unconfirmed-recheck.ts re-runs the existing Retry path (same operation id) for every visibility-unknown launch a host owns when that host's subscription opens with its one census: local-structured-session-tabs-sync/subscription.ts for the local host and web-session-tabs-sync/global-session-inventory-event.ts for a paired host. In-memory launches and launches restored from a saved record (found through their tab) are both covered.
  • Tests. Two new bridge suites (refused start; unconfirmed start), a persistence-failedAt row, and a re-check row in each sync suite.

Verification. The 5 touched suites pass (42 tests). The host emits type: 'snapshots' exactly once per session.tabs.subscribeAll subscription (session-tabs-inventory.ts:268), so the re-check is one per host return, not per frame. A launch enumerated twice (in memory and via its tab) is retried once: reconcileUnknownLaunch clears visibilityUnknown synchronously before its first await, and restartStructuredLaunchState sets outcome = 'pending' synchronously. Closing an unconfirmed chat records a cancellation tombstone (shouldRetainStructuredAgentSessionLaunchTab covers visibility-unknown), so the re-check skips it. The paired-path comment holds: acceptPairedHostStructuredSessions runs synchronously at the top of recoverWebSessionTerminalOrphansBeforeApply, before the re-check call. Legacy records without executionHostId load as local, so owner matching still works for pre-paired launches.

The author's documented deferrals (+ menu spinner → #24917; Claude startup-exit / signed-out → #24450) are not regressions from this PR.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

brennanb2025 added a commit that referenced this pull request Oct 5, 2026
)

* fix(native-chat): start a new chat after an earlier start failed

A chat whose start the host refused stayed registered under its agent and
workspace, so the + menu and new-tab search kept that agent disabled with a
spinner, and any later "new chat" for that agent (for example "Send notes to
> New agent") restarted the failed chat instead and dropped its own prompt.

Only launches in flight now hold the agent/workspace slot that later starts
join. The registry keeps every launch by session id and derives the holder,
so a failed chat keeps its own Retry while a new start makes a new chat.

* test: import the launch status from its own module

* test: name the owning host on the launch intents the new tests build

* fix(native-chat): a new start never joins an unconfirmed or retried chat

A new start for an agent joined a chat whose create answer was lost, or a
failed chat whose Retry was in flight, and dropped its own text: the
source-control "Fix with AI" action reported success with nothing sent,
"Send notes to > New agent" sent nothing, and after a reload the text became
an unsent draft in the old chat.

Only a new start's own first create now coalesces later starts (double click,
two callers at once). Each attempt records whether it is that first create or
a Retry/re-check of an existing chat; an unconfirmed or retried blank chat
keeps its own Retry and a new start opens a new chat with its text. A resume
still re-checks or joins its conversation's launch, since the host refuses a
second adoption.

* fix(native-chat): a different new request always opens its own chat

A new start used to join any blank chat still in its first create or still
sending its opening text, so a second "Fix with AI" for another check, or
notes sent to a new agent, landed in that first chat. Each first attempt now
keeps the request it was started with (its text and whether it is sent or
drafted). Only a repeat of that request (a double click, a retried call)
joins it, and it shares the text already staged, so the text is sent once.
Any other request opens a new chat with its own text. The + menu, new-tab
search and send-notes menus disable an agent only when their own pick would
join. Resume launches are unchanged.

* fix(native-chat): an empty chat still starting takes the first text sent to it

A blank chat that is still starting (a + pick, the empty-workspace default
chat) is empty, so the first request with text, such as notes sent to a new
agent, now goes into it instead of opening a second chat beside it. That
request becomes the chat's own request: an identical repeat joins and is sent
once, and any other request opens a new chat.

Move the logic that decides which launch a start joins out of the launch
registry into its own module, so the registry stays under the line limit
once #24904 lands beside it. Pin that a repeat arriving while the opening
text is still sending is sent once.

* test: an empty chat delivers the claiming text the way its request asked

* fix(native-chat): only a chat its user has not used yet takes another request's text

A blank chat that is still starting was claimable by the first request with
text even after its user had sent a message into it or typed into its
composer, so notes or a Fix with AI prompt could land in a conversation the
user had already started. Emptiness is now read from what the chat holds:
nothing in its outbox and no text in its composer draft. A chat its user has
used stays theirs, and the request opens a new chat.

If the claiming text cannot be saved, the claim is not recorded: the request
falls through to a new launch, whose save failure shows on that chat as for
any new launch, instead of reporting a failure nothing showed.

* fix(notes): notes handed to a send leave the next send until it settles

Notes sent to an agent stayed in the notes shelf until their chat delivered
them, so a second "Send notes" made while the first new chat was still
starting collected the first notes again. With every different request now
opening its own chat, those notes reached two chats.

When notes or browser annotations are handed to a send (a new agent, a
running agent from the menu, or a sidebar agent row), they are held in
memory against that send's own delivery result and left out of the next
send. Delivered notes are removed as before; a failed, refused or
undelivered send releases the hold, so they come back for the next send.
The notes menu now builds each scope's prompt from the notes it will send.

* test: give the annotation tray fixture its hand-off callback

* fix(notes): offer no send when every note is already on its way

When every note or annotation in a send is held by an earlier send still in
flight, the built prompt is empty. The notes menu already disabled its
trigger then, but its New agent rows still started an agent with no text,
the annotation Send buttons still opened, and a sidebar agent row could be
sent an empty prompt. The New agent rows, the annotation Send buttons and the
sidebar send now offer nothing when there is nothing to send.

* fix(notes): a new chat's staged prompt decides when its notes leave the shelf

Notes sent to a new agent were released back to the shelf as soon as the
chat's start failed, while that failed chat kept the same text staged for its
own Retry. Re-sending and pressing Retry put the notes in two chats, and Retry
alone left delivered notes listed as unsent.

For a new chat, the notes now follow the prompt it staged: held while that
message is in the chat's outbox, cleared from the shelf when any dispatch
(Retry or re-check included) sends it on, and back on the shelf when the
chat is closed and its outbox thrown away. A paired server's chat is found
once its start settles. Running-agent sends keep their own result.

While notes are only on their way, the send button reads "Sending…" rather
than "All notes sent".

* fix(notes): a new chat's saved message keeps its notes out of another send across a reload

The hold that keeps notes sent to a new agent out of the next send lived only
in memory. A reload while that chat had not yet sent them put the notes back
on the shelf while the chat's saved message still carried their text, so a
second "Send notes" sent them twice.

The staged message now saves the send keys of the notes it was built from.
The shelf treats a note as on its way while any saved message carries its key,
read from the saved outboxes on first use and kept current by the outbox's one
write funnel. When a message carrying notes is sent on (its own start, a Retry
or the re-check), those notes are cleared from their shelf; when it is thrown
away with its chat, they come back. Browser annotations sent to a new chat use
the same keys while the app runs. Running-agent sends keep their in-memory hold.

This replaces the per-message watch and outcome promise from the previous
change.

* fix(notes): release a gone chat's notes, and clear sent ones in the web client too

Two gaps in keeping notes out of another send while a saved chat message
carries them:

- The web client never installed the clearing that removes notes once a
  chat sends them on, so after a Retry there the notes stayed listed. It is
  now installed, once per renderer, by the background services both the
  desktop and the web client load with App.
- A saved message carrying notes was thrown away only by this window's own
  close. A chat closed from the phone, another window or while Orca was off,
  or closed here without a known host, kept its notes held for good. A chat
  the host stops listing (affirmed, and not a launch still starting or
  failed) now has its queued messages thrown away once that sync lands, as
  does a close that can name no host, so the notes come back.

* Revert "fix(notes): release a gone chat's notes, and clear sent ones in the web client too"

This reverts commit ae0b9f3.

* Revert "fix(notes): a new chat's saved message keeps its notes out of another send across a reload"

This reverts commit b88d0dc.

* fix(native-chat): a new chat joins only a re-delivery of the same user action

"The same request" was recognised by its content (agent, workspace, trimmed
text, sent or drafted), which split one action whose text changes between
deliveries ("Fix with AI" re-fetches logs) and merged two different actions
that happen to carry the same text.

Each user action now mints one request id where it is handled (the click,
menu pick, notes send, shortcut, Fix with AI press, quick command; a worktree
create uses its creation id; programmatic starts mint once at their entry)
and passes it through the launch plan, its verdict and the structured launch
options, where it is required. A new start joins a chat only when its first
attempt carries the same id: a double click or a caller retrying its own call
makes one chat and sends the first delivery's text once; any other action
opens its own chat, whatever its text. An empty chat that is still starting
is still claimed once by the first action with text, and then belongs to that
action's id. Resume launches keep joining their conversation's launch.

The + menu and new-tab search no longer grey out an agent while one of its
chats starts: every pick is a new action and opens its own chat. The id lives
in memory only; nothing reads it after a reload (a restored launch is reached
by its session id through Retry, never joined).

* fix(native-chat): a new chat with no text reuses an empty one instead of stacking another

Two bare "+ > Claude" picks (or new-tab search, the new-agent shortcut, the dashboard) opened two
empty chats. A request with no text now focuses an existing empty chat for that agent in that
workspace: one still starting (joined in the launch, as a re-delivery is) or one that published and
sits idle (its host's journal holds no request, read live from the status feed). Empty also means
nothing queued, no composer text or images, and no launch draft its composer has not taken. Failed,
unconfirmed, resumed and other-agent/workspace chats are never reused. A request with text still
opens its own chat, or claims an empty starting one as before; request-id dedupe is unchanged.

* fix(native-chat): a new chat with no text reuses an empty chat only in the split it was opened from

Pressing + in the right-hand split focused an empty chat sitting in the left-hand split. The reuse
now looks only in the tab group the pick targets (the caller's group, else the workspace's active
group, which is where its tab would open); a pick in another split opens a new chat there. Applies
to both an empty chat still starting and one that published and sits idle.

* fix(native-chat): text sent to a new agent claims an empty starting chat only in its own split

Notes sent from the right-hand split could land in, and focus, an empty chat still starting in the
left-hand one. A request with text now claims an empty starting chat only in the tab group it opens
in, the same rule a request without text follows; elsewhere it opens its own chat there.
…keep both caller-group fields; give the tests' starts their request ids

@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

  • Merged origin/main (#24917, "start a new chat after an earlier start failed") into the branch. The merge touched the launch caller group, registry, reload path, and the two new bridge test suites.
  • Recombined the caller group with both fields. structured-agent-session-launch-callers.ts now carries #24917's attempt: StructuredLaunchAttempt and this PR's failedAt?: number — neither side was dropped.
  • Kept both sides of the launch state. structured-agent-session-launch-registry.ts and -reload.ts auto-merged; per-parent diffs confirm #24917's session-id-only registry and createStructuredLaunchCallerGroup({ kind: 'retry' }) reload survive alongside this PR's failedAt persistence and restore.
  • Updated the new bridge tests to the request-scoped launch API. Each start now passes a requestId, kept unique per failed start so #24917's per-request coalescing cannot silently reuse a launch.
  • Confirmed the re-check still holds under the merged control flow. reconcileUnknownLaunch clears visibilityUnknown synchronously (structured-agent-session-launch-recovery.ts:133) and restartStructuredLaunchState sets outcome = 'pending' synchronously, so one re-check per launch per host-return is preserved; a reloaded visibility-unknown record restores visibilityUnknown = true (structured-agent-session-launch-reload.ts:52) so retryStructuredAgentSessionLaunch's guard admits it and reuses the saved clientOperationId (idempotent re-check).

Verification. pnpm tc:web passes; the five touched suites pass (42 tests). The author's documented deferrals (+ menu spinner → #24917; signed-out / startup-exit Claude → #24450) remain non-regressions.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 merged commit e9ed75a into main Oct 5, 2026
37 checks passed
brennanb2025 added a commit that referenced this pull request Oct 5, 2026
…-history

StructuredAgentSessionStatusBridge: main's failed-start row (#24904) and this
PR's batched status projection are both kept. A chat with no summary drops any
queued projection for its pane before it shows the failed start or clears the
row, so a queued row cannot land after it. Main's failed-start test now waits
for the tick its batched row lands in. The status derivation's inputs changed
only in comments; the golden digest is unchanged, so the rules stay at 3.
brennanb2025 added a commit that referenced this pull request Oct 5, 2026
…ative chat's does

The failed-start row #24904 writes carried no native-chat provenance, so its Failed faded from the
tab and card after 30 minutes while every other native chat's Failed stays until the chat changes.
It now carries the same marker ('held': no host runs its agent), and past the freshness window it
sits in the card's retained tier like the rest.
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