Skip to content

[Bug]: Mobile thread list projection goes stale over a churning remote connection — stuck "Working", missing new threads, fixed only by app restart #5742

Description

@coreldh

Before submitting

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

  1. 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.
  2. Client: T3 Code Android app, current Play Store build (the one that resolved [Bug]: Android store app disconnects immediately after server.getConfig — RPC protocol skew with all installable server builds #4901). Phone on cellular, physically far from the server (high latency, NAT churn) — not on the same Wi-Fi.
  3. Have two threads whose provider sessions have since stopped (turns completed a while ago).
  4. Open the app and create a new thread, send one message, get a reply.
  5. 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:

  1. The socket is not instant-dropping (that was [Bug]: Android store app disconnects immediately after server.getConfig — RPC protocol skew with all installable server builds #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.
  2. During the healthy 140 s session the client issued many per-thread orchestration.subscribeThread calls (and vcs.*), but no thread-list-level subscription or refetch (no searchThreads, 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

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions