Skip to content

fix(client-runtime): threads no longer stay on "Syncing messages..." after a failed resume - #19570

Open
shivamhwp wants to merge 2 commits into
pingdotgg:mainfrom
shivamhwp:t3android/issue-19563
Open

shivamhwp wants to merge 2 commits into
pingdotgg:mainfrom
shivamhwp:t3android/issue-19563

Conversation

@shivamhwp

@shivamhwp shivamhwp commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A saved thread can remain stuck on “Syncing messages…” when replaying events from its saved cursor fails. Relaunching asks for the same failed replay again.

Change

When a resumed stream dies, retry it once using a full thread snapshot. Expected and transport failures keep their existing handling. Keep snapshot failures terminal so recovery cannot loop, and use the new snapshot cursor for later sessions. Report a replay failure only if recovery gives up, avoiding an error banner during successful snapshot recovery.

Scope and approval

This is a maintainer-requested takeover of the focused thread-sync bug fix. The shared client runtime serves web, desktop, and mobile. Refs #19563. Based on the original work by @shivamhwp.

The original report’s precise failing event is unconfirmed. #15937 already handles unknown event types on the server; this change adds client recovery for other replay defects.

Verification

After rebasing onto current main (05b697f6), the 27 sync tests in threads-sync.test.ts and vp run check:changed (format, lint, knip, typecheck, tests) pass. The three regression cases verify snapshot recovery, later cursor-based resumption, terminal snapshot failure, and recovery when a session replacement starts while the failed resume is still buffered. The first two fail against main's threads.ts; the race case fails against the earlier head f5cb98a0, which read the attempt kind after the buffer. All observed recovery states are checked for the absence of a transient error. Reintroducing transient error publication or retrying every defect fails the corresponding test.

Before the rebase, the actual web app was tested against the same temporary one-shot replay failure on baseline 07d38b8d and head e4c9b469. The rebase did not change threads.ts. The later race fix (e9d6e852) only changes when a defect is classified, so the recording still shows the same recovery path. The baseline requested cursor 18 and remained at “Syncing messages…”. The head requested cursor 18, recovered through a full snapshot, then successfully resumed from cursor 18 on a later reconnect. No error banner was observed during recovery. The temporary server probe was removed afterward; no provider turn or database write was needed.

This was a synthetic replay failure, not a reproduction of the original Interrupted / ClientAbort report. Native clients were not run; they share the changed client runtime. The terminal double-failure path was verified by focused tests, not the recording.

Before — cached thread remains syncing

Before: cached thread stuck at Syncing messages after failed replay

After — snapshot recovery clears syncing

After: cached thread recovered with syncing banner cleared

Recovery recording

Taken over with Claude Opus 5.5 (tests and review), Claude Fable 5.1 (implementation), and GPT-6.1 Sol (review) through T3 Code; coordinated with Codex.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 14, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Oct 14, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — This focused bug fix changes shared client-runtime subscription behavior by adding snapshot fallback and race-aware stream recovery after failed resumes. Its normal path is preserved and regression tests are included, but the new stateful stream/error-handling logic is sufficiently nontrivial to merit human review.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: da4c5d54-f471-4609-a1c0-b39797dd2cad
📥 Commits

Reviewing files that changed from the base of the PR and between e4c9b46 and f5cb98a.

📒 Files selected for processing: 1
  • packages/client-runtime/src/state/threads-sync.test.ts

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

📝 Walkthrough

The thread subscription tracks snapshot and resume attempts. A defect during resumed replay triggers a full-snapshot retry. A defect during the snapshot attempt reports a synchronization error and does not trigger another subscription.

Changes: Thread synchronization

Layer / File(s) Summary
Subscription retry behavior
packages/client-runtime/src/state/threads.ts
Subscription attempts track whether they load a snapshot or resume from existing thread data. A defect during resumed replay forces a full-snapshot retry. A snapshot-attempt failure does not use this retry path.
Defect recovery tests
packages/client-runtime/src/state/threads-sync.test.ts
The test harness distinguishes stream defects from typed errors and records each subscription’s afterSequence. Tests cover snapshot recovery after a resumed replay defect, resumption from the reloaded snapshot sequence, and no further subscription after a snapshot defect.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Possibly related issues

  • pingdotgg/t3code#17765 — The change adds a client full-snapshot recovery path, though it does not address the reported server timeout or concurrency limit.
  • pingdotgg/t3code#4589 — The change retries a failed resume stream with a full snapshot.

Suggested reviewers: juliusmarminge

Merge Risk: ⚪ Minimal · up to f5cb9

No actionable issue remains in the reviewed change; it is ready to merge after normal checks.

Security architecture risk: 🔵 Low · up to e4c9b

The recovery remains scoped to the same thread and environment and uses the existing subscription method. A local defect in the fallback snapshot terminates rather than repeatedly retrying. No introduced security issue was established, although interruption during recovery and transport-reported defects have limited direct verification.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A defect-inducing stream item can now cause an additional snapshot request for the affected thread-state instance. The inspected path does not select another thread or environment or add write authority; its direct effect is synchronization and local cached state.

Trust Boundaries and Controls

  • observed — The fallback passes through the existing session-selected RPC subscription. Authorization failures remain excluded from automatic expected-failure retries in that layer. This is client-side control evidence, not verification of server-side authorization.

Resilience and Maintainability Implications

  • inferred — The fallback catch surrounds the complete dynamic subscription rather than one session branch, and interrupt-only causes are not retried. This supports sequential replacement and scoped cancellation rather than intentionally concurrent recovery subscriptions. Runtime interleavings during fallback remain unverified.
Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title is concise, uses conventional commit format, and clearly describes the main client-runtime fix for failed thread resume synchronization.
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.
Description check Passed The description includes all required sections. It clearly states the problem, change, scope, issue references, focused verification, observed results, limitations, screenshots, recording, and agent d…
Full details: Description check

Explanation

The description includes all required sections. It clearly states the problem, change, scope, issue references, focused verification, observed results, limitations, screenshots, recording, and agent details. It does not include the explicit maintainer approval comment requested by the template, but it identifies the change as a maintainer-requested takeover and provides sufficient scope context.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@kvnloo kvnloo 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.

Thanks for the focused recovery path and the Android before/after test; the cached-resume defect case is well covered.

One question about the linked report's failure mode: #19563 records subscribeThread ending with Interrupted / ClientAbort, whereas subscribeDynamicMapped only invokes onDefect for Die or RpcClientDefect (packages/client-runtime/src/rpc/client.ts). This PR's fallback flag is set by onDefect, and the new positive test injects Effect.die. Is the interrupted/aborted subscription in the report expected to pass through that same callback? If not, a focused interruption/reconnect reproduction would help distinguish the verified replay-defect fix from the reported stuck-hydration case before treating #19563 as fully closed.

Not requesting a broader redesign—just checking the causal link between the observed trace and this tested recovery. Credit to @hichish for the original trace and @shivamhwp for the focused fix and mobile evidence.

@juliusmarminge

Copy link
Copy Markdown
Member

Taking over this one. I’ll reduce the tests to the useful coverage, update the implementation against current main, and verify it before publishing the changes.

@juliusmarminge

Copy link
Copy Markdown
Member

@kvnloo An interruption-only client cause does not invoke onDefect and still passes through without retry. We have not established that the original report reaches the replay-defect path.

The ClientAbort trace alone does not establish the original cause. A client-side replay decode defect can terminate the subscription and cause the server to observe that abort. The original reporter’s precise failing event remains unconfirmed.

#15937 already addresses unknown event types server-side. This takeover is scoped as client-side recovery for other replay defects: a failed cursor-based replay retries once from a full snapshot; a snapshot defect reports a terminal error. Ordinary client interruption is passed through, and the PR references #19563 rather than claiming its exact cause was reproduced.

At rebuilt head e4c9b469, 49 focused sync tests pass. The two retained regression cases cover recovery without an intermediate error and terminal fallback failure. Reintroducing the intermediate error or retrying every defect fails the corresponding case.

The actual web app was also checked with the same one-shot synthetic replay failure on baseline 07d38b8d and rebuilt head: baseline stays syncing, while the head retries a full snapshot and successfully resumes at the saved cursor on reconnect. Recovery recording. This verifies that recovery path, not the exact cause of the original report; native clients were not run.

@macroscopeapp
macroscopeapp Bot dismissed their stale review October 19, 2026 00:59

Dismissing prior approval to re-evaluate e4c9b46

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Oct 19, 2026
@juliusmarminge

Copy link
Copy Markdown
Member

The linked-issue warning identifies an unresolved part of #19563, not a claim this PR makes. The body uses “Refs,” explicitly says the original Interrupted/ClientAbort cause is unconfirmed, and labels the live proof as an injected replay defect. It does not claim to close that issue.

The retry branch deliberately passes interruption-only causes through. Retrying cancellation would also restart intentional subscription teardown. Failed resumed replay instead requests a full snapshot; a failed snapshot remains terminal. The two focused regressions and the recorded web transport probe cover that narrower behavior, including the subsequent cursor-based resume and absence of a transient error.

I am declining the request to add ClientAbort recovery to this PR without a demonstrated causal path. The original issue remains open for that investigation; expanding the retry predicate based only on its server-side symptom could introduce unwanted subscriptions during teardown.

…after a failed resume

A saved thread resumes by asking the server for the events after its
cursor. When that replay dies, the same cursor dies again on every
reconnect and relaunch, so the thread stays on "Syncing messages...".
A failed resume now retries once with the full snapshot, which moves the
cursor past the bad events. The sync error is published only where the
attempt is given up, so a recovered resume never flashes it and a defect
on a snapshot attempt stays terminal.

Refs pingdotgg#19563

Co-Authored-By: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shivamhwp
shivamhwp force-pushed the t3android/issue-19563 branch from e4c9b46 to f5cb98a Compare October 22, 2026 03:46
… terminal

The retry decided whether a defect broke a resume by reading the shared
attempt kind when the failure reached it. switchMap can buffer that
failure while a replacement session starts building its input and resets
the attempt to snapshot, so the failed resume was reported as a terminal
snapshot failure. The defect is now classified where it happens, before
it can be buffered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@macroscopeapp
macroscopeapp Bot dismissed their stale review October 22, 2026 04:27

Dismissing prior approval to re-evaluate e9d6e85

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants