pingdotgg / pingdotgg/t3code

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

Open
#5,742 1 comment 0 reactions 0 assignees View on GitHub

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
  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 #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 #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
  • #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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.