Skip to content

Preserve agent chats and drafts through reconnects - #9242

Merged
brennanb2025 merged 10 commits into
mainfrom
brennanb2025/fix-native-agent-chat-reconnect
Jul 19, 2026
Merged

brennanb2025 merged 10 commits into
mainfrom
brennanb2025/fix-native-agent-chat-reconnect

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • keep native agent chat attached to its mounted pane through transient connection, title, and session-evidence gaps
  • scope native composer drafts and attachments to stable pane identity so PTY replacement cannot clear unsent input
  • treat stale mirrored terminal handles as reconnect evidence and resubscribe the regular TUI in place without emitting terminal-exit callbacks
  • retry transient host-inventory failures and preserve unknown liveness when bounded polling ends without removal evidence
  • recover a still-mounted mirror from a later accepted host snapshot after the 15-second polling window, with no ongoing timer or global store subscription
  • share each in-flight host inventory request across reconnecting panes in the same runtime/worktree
  • atomically replace stale PTY store identities, including when the host snapshot publishes the replacement before the transport callback
  • retain explicit terminal exit and missing-host-surface close behavior

Root cause

Native chat routing treated temporary loss of agent eligibility as completion and unmounted the chat surface. Its composer state was also keyed by replaceable PTY identity. Separately, the remote runtime transport grouped terminal_handle_stale with terminal_exited, so reconnect handle rotation invoked the same pane-close lifecycle as a dead terminal.

The initial reconnect path fixed the immediate rotation but could still leave a mounted terminal permanently frozen after its bounded inventory polling expired: later host snapshots updated store state, but nothing rebound the already-mounted transport. Concurrent split panes also issued duplicate full session.tabs.list requests, and snapshot-first PTY replacement could strand state indexed by the old identity.

Validation

  • provider reconnect contract: 10 files / 566 tests passed after merging current main
  • complete native-chat component directory: 54 files / 522 tests passed
  • web-session reconciliation: 3 files / 61 tests passed
  • focused changed reconnect/store paths: 4 files / 63 tests passed
  • pnpm run typecheck
  • changed-file oxlint, React-specific lint, and oxfmt checks
  • max-lines ratchet, reliability-gate manifest, and git diff --check
  • iPhone 17 Pro, iOS 26.5: background/foreground caused a real timeout, reconnect transition, and fresh session snapshots; the regular chat surface and exact unsent draft remained visible

React Doctor still reports the existing NativeChatView state-reset warnings outside this PR's changed lines; changing that state model here would be unrelated risk. The local environment uses Node 26.5.0 while the repository requests Node 24; all commands above passed.

iOS reconnect evidence

Before:

orca-mobile-regular-chat-final-before.png

After reconnect:

orca-mobile-regular-chat-final-after-reconnect.png

Reliability contract

  • Invariant (terminal-provider.ssh-remote-reattach-contract): a mounted web terminal mirror keeps its pane, transcript, composer, and PTY ownership while a stale host handle is replaced. It never subscribes the known-stale handle again; explicit exit or accepted removed-surface evidence still retires the mirror. A replacement atomically supersedes the stale PTY identity.
  • Failure source: terminal_handle_stale was classified as terminal death, and the first reconnect patch could re-subscribe the stale handle, retire a temporarily pending host surface, abort permanently on one inventory failure, freeze after bounded polling, issue one inventory RPC per split pane, or miss stale PTY-indexed state after snapshot-first replacement.
  • Oracle: transport coverage drives inventory failure, stale snapshots, pending handles, replacement, the 15-second polling bound, and later accepted-snapshot recovery. It asserts exact old/new rebind identity, no exit/disconnect callbacks, no post-bound polling, and listener cleanup. Additional tests prove same-tick snapshots deliver only the newest accepted state, concurrent panes share one in-flight request only within their runtime/worktree ownership boundary, and snapshot-first replacement migrates old PTY-indexed state.
  • Gate: experimental terminal-provider.ssh-remote-reattach-contract; the merged provider suite passed 10 files / 566 tests. Live remote-runtime evidence remains partial rather than promoted coverage.
  • Provider/platform coverage: the remote-runtime/web mirror path is deterministically covered and the existing iOS reconnect artifact exercises the user path. Local PTY, daemon PTY, SSH, and WSL behavior are unaffected because stale-handle replacement is gated to web terminal surface ids. Live Linux/Windows desktop mirrors remain a gap.
  • Performance budget: polling starts only after a web-mirror transport close, backs off from 150 ms to 1 second, and stops after 15 seconds or lifecycle cancellation. Split panes share one in-flight inventory RPC per runtime/worktree. After the bound there is no polling timer; one identity-scoped listener remains only for each still-mounted recovery and is released on rebind, removal, attach, detach, disconnect, or destroy. Normal accepted snapshots perform a constant-time empty-listener check and return, adding no work to typing, terminal output, or render paths.
  • Diagnostics: typed stale/gone errors, exact old/new rebind identity, and callback/subscription/listener/request-count assertions distinguish handle rotation from true terminal retirement.
  • Residual gaps: no live Linux/Windows remote-runtime reconnect soak was collected; the manifest keeps live remote-runtime coverage partial until broader provider evidence lands.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Native chat composer state is now keyed by stable pane identity, with shared bounded scope caches and extracted composer types. Session resolution retains prior conversation identity during reconnect metadata gaps. Leaf routing keeps mounted chat ownership through transient eligibility loss and uses active-leaf eligibility for synchronization. PTY lifecycle wiring now handles confirmed agent exits and provider PTY rebinding. Remote PTY handling distinguishes stale handles from terminal removal, reconnects with replacement handles, and preserves rendered TUI and draft content. Tests cover these persistence, routing, and recovery paths.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description covers summary and validation, but it omits required Screenshots, Testing checklist, AI Review Report, Security Audit, and Notes sections. Add the missing template sections, include the required testing checklist items, and summarize the AI review, security audit, and any platform-specific notes.
✅ Passed checks (3 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 describes the main user-visible change: preserving chats and drafts through reconnects.

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: Pro

Run ID: a9896164-30b4-4660-948c-85ca5a3b7fe6

📥 Commits

Reviewing files that changed from the base of the PR and between 795b600 and d6e5b01.

📒 Files selected for processing (10)
  • src/renderer/src/components/native-chat/native-chat-leaf-routing.test.ts
  • src/renderer/src/components/native-chat/native-chat-leaf-routing.ts
  • src/renderer/src/components/terminal-pane/TerminalPane.tsx
  • src/renderer/src/components/terminal-pane/pty-connection-types.ts
  • src/renderer/src/components/terminal-pane/pty-connection.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection.ts
  • src/renderer/src/components/terminal-pane/pty-transport-types.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts
  • src/renderer/src/components/native-chat/native-chat-leaf-routing.test.ts

@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: Pro

Run ID: a78e5d00-9f47-4c65-9e5d-f12ff431fda2

📥 Commits

Reviewing files that changed from the base of the PR and between d6e5b01 and 2e4987c.

📒 Files selected for processing (10)
  • config/reliability-gates.jsonc
  • src/renderer/src/components/terminal-pane/pty-connection-types.ts
  • src/renderer/src/components/terminal-pane/pty-connection.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection.ts
  • src/renderer/src/components/terminal-pane/pty-transport-types.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts
  • src/renderer/src/store/slices/terminal-pty-identity-replacement.test.ts
  • src/renderer/src/store/slices/terminals.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/renderer/src/components/terminal-pane/pty-connection-types.ts
  • src/renderer/src/components/terminal-pane/pty-connection.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts

Comment on lines 2075 to 2079
const nextSuppressedPtyExitIds = { ...s.suppressedPtyExitIds }
delete nextSuppressedPtyExitIds[ptyId]
if (legacyRemotePtyId) {
delete nextSuppressedPtyExitIds[legacyRemotePtyId]
if (replacementPtyId) {
delete nextSuppressedPtyExitIds[replacementPtyId]
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Transfer the exit-suppression marker instead of dropping it.

If an intentional shutdown races with handle replacement, deleting the old marker makes the replacement PTY’s exit appear unsuppressed and may trigger normal exit handling. Move an existing marker to ptyId, matching the other replacement-keyed metadata.

Proposed fix
+      const hasReplacementSuppressedExit = replacementPtyId
+        ? replacementPtyId in s.suppressedPtyExitIds
+        : false
       const nextSuppressedPtyExitIds = { ...s.suppressedPtyExitIds }
       delete nextSuppressedPtyExitIds[ptyId]
       if (replacementPtyId) {
         delete nextSuppressedPtyExitIds[replacementPtyId]
+        if (hasReplacementSuppressedExit) {
+          nextSuppressedPtyExitIds[ptyId] = true
+        }
       }

Add a rotation test seeded with suppressedPtyExitIds[firstPtyId] to verify the marker follows successive replacements.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const nextSuppressedPtyExitIds = { ...s.suppressedPtyExitIds }
delete nextSuppressedPtyExitIds[ptyId]
if (legacyRemotePtyId) {
delete nextSuppressedPtyExitIds[legacyRemotePtyId]
if (replacementPtyId) {
delete nextSuppressedPtyExitIds[replacementPtyId]
}
const hasReplacementSuppressedExit = replacementPtyId
? replacementPtyId in s.suppressedPtyExitIds
: false
const nextSuppressedPtyExitIds = { ...s.suppressedPtyExitIds }
delete nextSuppressedPtyExitIds[ptyId]
if (replacementPtyId) {
delete nextSuppressedPtyExitIds[replacementPtyId]
if (hasReplacementSuppressedExit) {
nextSuppressedPtyExitIds[ptyId] = true
}
}

@AmethystLiang AmethystLiang self-assigned this Jul 18, 2026
@AmethystLiang
AmethystLiang self-requested a review July 18, 2026 16:48
@brennanb2025
brennanb2025 merged commit 02ff7c7 into main Jul 19, 2026
5 checks passed
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
* fix(chat): preserve drafts through reconnects

* fix(terminal): bound stale handle reconnect polling

* refactor(chat): extract native view props

* fix(chat): harden reconnect lifecycle

* fix(terminal): replace stale reconnect identities atomically

* fix(terminal): recover mirrors after reconnect polling

* docs(reliability): record merged reconnect coverage

* fix(terminal): preserve reconnect lifecycle state

* fix(terminal): keep reconnect polling truly bounded
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.

2 participants