fix(client-runtime): reopening a running thread shows its cached messages right away#24210
Refs #24192
Problem
On mobile, reopening a thread whose agent is still working showed a blank view with "Loading messages..." until the server answered. The app keeps a copy of each thread on the phone so it can show messages right away, but it skipped any thread with a running turn. The threads people check while an agent works never got saved, so every reopen after a relaunch waited on the network. On a slow server (the issue's trace shows a 5.1 s snapshot request right after a desktop restart) that is 5+ seconds of nothing.
Change
When the thread view closes, save the latest copy once, even if a turn is running. Streaming updates still skip the save, so the reason for the original skip (#4006: no cache encoding on every streamed update) still holds. The next open shows that copy at once and catches up from where it left off.
The change is in packages/client-runtime, so web reads its IndexedDB copy the same way. Desktop already keeps running threads subscribed.
Scope and approval
A small, focused fix for an obvious bug: one condition in the thread cache's close-time save, plus a regression test. The bug is reported in #24192, which isn't triaged yet. The change keeps the existing rule that streaming updates don't write to the cache.
Verification
Web recordings (current head fff4d8c4ea)
Each run: start a Codex turn that runs sleep 240, open it, leave it, close the browser and start it again (a relaunch), then reopen the running thread from the sidebar, leave it, reload the page, and reopen it again. The server's thread snapshot request was delayed by 5 s in both runs (the same temporary local change as below). A stopwatch in the top right starts at the sidebar click and stops when the thread's messages show.
Before (head with packages/client-runtime/src/state/threads.ts from the PR base): "Loading messages..." both times, messages after 5.46 s and 5.48 s. IndexedDB held no copy of the running thread after leaving it.
https://github.com/user-attachments/assets/f575ac5c-79b2-452a-8b7c-6c125e1ff054
After (head fff4d8c4ea): messages after 0.48 s and 0.64 s, read from the browser's copy, while the turn is still running. IndexedDB held the running thread after leaving it.
https://github.com/user-attachments/assets/3ba7f66d-9f7c-4c48-b679-23ad464024e7
Testing for the web recordings: corex (Linux), vp run dev --no-seed from the PR worktree with its own state dir and a throwaway CODEX_HOME, headless Chromium with a persistent profile driven by Playwright, video converted to MP4.
Android (POCO F4, recorded on 3369733075)
POCO F4, Android dev client. Each run: open a running thread, leave it, force-stop the app, relaunch, open the thread again. The clips start at that last open. To match the issue's trace, I delayed the server's thread snapshot request by 5 s for both runs (a temporary local change, not in this PR). Both clips use the slow dev-mode JS bundle. The delay, the force-stop and the server request counts come from my run's server trace and commands; the clips don't show them.
Before (main): blank with only "Thinking" and "Loading messages..." for about 6.5 s. The server trace logged a 5 s snapshot request for this open.
https://github.com/user-attachments/assets/10c1107e-5d3f-40e3-973e-9b1e0a16eeac
After: messages show in about 3 s, read from the phone. The server trace logged no snapshot request for this open.
https://github.com/user-attachments/assets/6aaf0e36-36d7-4d19-a1d7-a3dde38a7011
Mobile recordings (current head fff4d8c4ea)
Same steps on both phones: start a Codex turn that runs sleep 240, open the thread, go back, kill the app, launch it again, then open the running thread again. Each clip starts just before that last tap. The server's thread snapshot request was delayed by 5 s in every run (the same temporary local change as above; mobile reads snapshots over HTTP, so the delay went on the /api/orchestration/threads/:id and /bounded handlers as well as the socket path). "Before" is the head with packages/client-runtime/src/state/threads.ts from the PR base.
iOS before (iPhone 17 simulator): "Thinking" and "Loading messages..." for about 5.3 s.
https://github.com/user-attachments/assets/4504bb7d-7687-4123-a668-1fa6295c9af8
iOS after: messages about 0.5 s after the tap, while the turn is still running.
https://github.com/user-attachments/assets/a39e82e7-25af-4a06-a414-cd16dc10799e
Android before (POCO F4): blank with only "Thinking" for about 6.5 s.
https://github.com/user-attachments/assets/babca6e9-b0c2-43af-8512-62458434a0b8
Android after: messages about 1.75 s after the tap (dev-mode JS bundle reading the phone's copy), then "Syncing messages..." while it catches up.
https://github.com/user-attachments/assets/2f6547aa-d24d-4618-a6ef-c96c96f21589
Testing for the mobile recordings: head fff4d8c4ea, backend (node apps/server/src/bin.ts serve --base-dir <throwaway home> with a throwaway CODEX_HOME) and Metro (APP_VARIANT=development expo start --dev-client --port 8261) from the PR worktree on corex (Linux). Android: POCO F4 (Android 14) with the existing T3 Code Dev client over adb reverse, recorded on the phone. iOS: iPhone 17 simulator on shiv (macOS) with the existing T3 Code Dev client, backend and Metro reached through ssh -R, recorded with xcrun simctl io recordVideo. Threads started with t3 thread start --provider codex --model gpt-6.1-sol.
New test in threads-sync.test.ts: a running thread isn't saved while it streams, and is saved once, at its latest sequence, when its view closes. It fails on main.
The clips were recorded on 3369733075 (base 171f8d880f). The branch is now rebased onto 5b35402623 as fff4d8c4ea; the fix lines didn't change, only a test import conflicted. At fff4d8c4ea the new test fails with main's threads.ts and passes with the fix, and vp run check:changed passes. The web and mobile recordings above are from fff4d8c4ea.
Not covered
- The first open of a thread this device has never loaded still waits on the server. There's no copy to show yet.
- Not measured: how long the one save on close takes for a very large running thread on Hermes.
- The slow server reads after a restart in the issue's trace (
ensureLegacyTranscriptat 3.7 s on a 20.9 GB database) are a separate server problem.
Opus 5.5, Claude Code.
🤖 Generated with Claude Code
Checks
Approvability
Verdict: Approved at 3369733
Macroscope's review found this PR approvable — This focused client-runtime fix adds one final cache write when an active thread view closes, while preserving the existing no-write-during-streaming behavior. A regression test verifies the cached snapshot contains the latest running state without repeated saves.
You can add or adjust custom eligibility rules. Learn more.
No actionable comments were generated in the recent review.
External image Recent review info
External image Run configuration
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: main
- File: .coderabbit.config.ts
- Commit: 5b35402
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Advanced
- Run ID:
66517898-0029-4a7d-b908-de8e9e999a3b
External image Commits
Reviewing files that changed from the base of the PR and between 33697330751dbe6fc6955955aa047b36ae0967fe and fff4d8c4ea63124714c5c737f1ef13814978a537.
External image Files selected for processing: 2
packages/client-runtime/src/state/threads-sync.test.tspackages/client-runtime/src/state/threads.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.
Walkthrough
The finalizer now persists data-bearing thread projections when history is not expanded, including projections with active runs. A regression test verifies that closing the scope saves one snapshot with the latest sequence and title while preserving the running status.
Changes: Running thread cache persistence
Layer / File(s) Summary Finalizer persistence and regression coverage
packages/client-runtime/src/state/threads.ts,packages/client-runtime/src/state/threads-sync.test.tsThe finalizer persists projections when history is not expanded. The test verifies that no cache write occurs while the scope remains open, then checks that closing it saves one snapshot with the latest sequence, title, and running status. Priority: External image Normal
Estimated code review effort: 2 (Simple) | ~10 minutes
Possibly related issues
- pingdotgg/t3code#24192: Persisting the latest running-thread snapshot relates to the issue’s cached thread display on reopen. The change does not cover a first open without a cached copy.
- pingdotgg/t3code#24305: Persisting the latest running-thread snapshot relates to the issue’s cached thread display on reopen. The change does not address the reported provider-switch or navigation failures.
Suggested reviewers:
maria-rcks,macroscopeapp
Merge Risk: External image Minimal · up to fff4d
Reopening a running thread can show its cached messages while the server responds. No actionable merge-blocking risk remains after normal checks.
Pre-merge checks: Passed 4
Check name Status Explanation Title check Passed The title names the main change: reopening a running thread now shows its cached messages right away. It matches the objectives, which save the latest thread copy when the view closes so the next open… 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 pull request description includes all required sections. It clearly explains the problem, change, scope and approval basis, focused verification with observed results, limitations, recordings, reg… Full details: Title check
Explanation
The title names the main change: reopening a running thread now shows its cached messages right away. It matches the objectives, which save the latest thread copy when the view closes so the next open can render from cache. It uses a conventional commit prefix and scope, and it is short and specific.
Full details: Description check
Explanation
The pull request description includes all required sections. It clearly explains the problem, change, scope and approval basis, focused verification with observed results, limitations, recordings, regression testing, and the agent model and harness.
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.
Added 1 commit
- fix(client-runtime): reopening a running thread shows its cached messages right awayfff4d8c