[Bug]: Mobile thread list projection goes stale over a churning remote connection — stuck "Working", missing new threads, fixed only by app restart
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Before submitting
- I searched existing issues. Closest are #4952, #4962 and #5695 — this is a distinct, more severe failure (see "Relation to existing issues"). Not #4901 (fixed) and not #5031 (relay-specific; this is pure Tailnet, no relay).
- I included server-side traces to reproduce/investigate.
Area
apps/mobile
Summary
Over a direct Tailscale tailnet connection from a distant cellular link (no T3 Connect relay, no LAN), the mobile thread list goes stale and never reconciles: rows for threads whose provider session has already stopped keep showing Working, and a newly created thread is missing from the list — even while the WebSocket is connected, RPCs succeed, and the user is actively inside the new thread. Only a full app restart (cold re-hydration) corrects the list.
The server state is correct and current the entire time; the desktop sidebar (same server) shows the right state. This is a mobile thread-list projection/reconciliation bug, made visible by an unstable (churning) remote socket.
Steps to reproduce
- Server: T3 Code desktop 0.0.32 (macOS arm64), embedded server, reached over a direct Tailscale tailnet IP (
http://100.x.x.x:3773). No relay, no Tailscale Serve. - Client: T3 Code Android app, current Play Store build (the one that resolved #4901). Phone on cellular, physically far from the server (high latency, NAT churn) — not on the same Wi-Fi.
- Have two threads whose provider sessions have since stopped (turns completed a while ago).
- Open the app and create a new thread, send one message, get a reply.
- Look at the thread list.
Expected behavior
- The two stopped threads show a quiescent state (timestamp / ready / Done), not Working.
- The newly created thread appears in the list.
- Matches the desktop sidebar for the same server.
Actual behavior
- Both stopped threads keep showing Working indefinitely.
- The new thread does not appear in the list, even though it exists, is in the same project, is not deleted/archived, and the user is actively inside it.
- The composer/stream inside the open thread also did not update live; the sent prompt and the reply only became visible after force-quitting and reopening the app. After that cold restart, the whole list is correct.
Server-side evidence
The backend is event-sourced (orchestration_events, monotonic sequence). Read straight from the server's state.sqlite and server.trace.ndjson at repro time (all UTC):
Authoritative server state — correct and current:
| Thread | Server truth | Mobile showed |
|---|---|---|
| Thread A | session stopped since 20:20:40 (event seq 143721) |
🔴 Working |
| Thread B | session stopped since 20:55:40 (event seq 143776) |
🔴 Working |
| New thread | thread.created at 22:00:32 (event seq 143832), same project, not deleted/archived, turn completed 22:00:37 |
🔴 absent from list |
provider_session_runtime at screenshot time: exactly 2 threads running (the active chat + the new thread's session); A and B are stopped. Latest server event: seq 143945 @ 22:05:04.
Mobile WebSocket sessions (UA okhttp), socket lifetimes:
| Session | Socket lifetime | RPCs dispatched |
|---|---|---|
| 21:58:04 → 21:59:13 | 68.5 s | getConfig, orchestration.subscribeThread, dispatchCommand, vcs.* |
| 22:00:26 → 22:02:46 | 140.1 s | getConfig, several orchestration.subscribeThread, server.probe, dispatchCommand (the new-thread send), vcs.* |
| 22:04:18 → 22:04:28 | 9.9 s | getConfig, 3× subscribeThread, vcs.* |
| 22:11:10 → 22:11:18 | 7.4 s | getConfig, server.probe, vcs.* |
| +5 more | never complete markConnected/markDisconnected | partial |
Two measured facts:
- The socket is not instant-dropping (that was #4901, now fixed): it lives 68 s and 140 s, does real RPC, and establishes per-thread subscriptions. But over the far cellular link it churns — long sessions interleaved with 7–10 s sockets and several reconnect attempts that never complete.
- During the healthy 140 s session the client issued many per-thread
orchestration.subscribeThreadcalls (andvcs.*), but no thread-list-level subscription or refetch (nosearchThreads, no list subscription). The new thread was created mid-session (seq 143832, 22:00:32) yet never appeared, and the two stopped rows never transitioned off Working.
Analysis (measured vs. inferred)
- Measured (server): server state is correct throughout; socket connects and does unary RPC; socket churns on the distant link; per-thread subscriptions are opened, but the thread-list surface receives no live channel during a connected session.
- Inferred (client, cannot measure from the server): the mobile thread list is hydrated once (cold start) and then relies on live deltas that either aren't delivered to the list surface or aren't reconciled after a reconnect gap. A full app restart forces fresh hydration and fixes it. A reconnect after a churn gap should re-fetch / replay from the last applied sequence so list-level events (
thread.created, session-status transitions,thread.settled) can't be silently missed.
Relation to existing issues
- #4952 ("never shows Done after a turn completes") assumes the client learns the session went quiescent but lacks a Done presentation path, falling back to timestamp. Here the row is stuck on Working and never transitions at all — the client never learns the session stopped. Complementary but a different (more severe) failure.
- #4962 is the inverse (idle while subagents still work). Not this.
- #5695 (neutral copy while reconnecting) is related reconnect UX, but this is a data-staleness bug, not copy.
Version or commit
Server: T3 Code desktop 0.0.32 (also reproduced against a 0.0.31 self-hosted Linux server). Client: Android Play Store build as of 2026-08-08 (post-#4901). I don't have the exact mobile build hash.
Environment
Android native app, direct Tailscale tailnet IP over cellular, phone far from the server. No T3 Connect relay and no LAN Wi-Fi involved. The server is a macOS desktop 0.0.32 embedded server.
Workaround
Force-quit and reopen the app to force a cold re-hydration of the thread list.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in apps/mobile by tracing thread-list hydration and reconciliation across WebSocket reconnects, then compare it with the per-thread orchestration.subscribeThread flow described in the report. Reproduce the direct Tailscale cellular scenario if possible and inspect whether list-level events are refetched or replayed after socket churn. Done means stopped threads leave Working, newly created threads appear, and open-thread updates survive reconnects without an app restart.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, typescript
- Domain
- mobile, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100