[Bug]: Mobile: opening a thread shows a blank view for 5+ s ("Loading messages..." then "Syncing messages...") even on fast Wi-Fi#24192

opened4d agobyPPyseph

Before submitting

  • I searched existing issues and found related reports, but this is a distinct case.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/mobile

Steps to reproduce

  1. Have the iOS app connected to a Windows desktop environment, with a few active threads (one of them running a turn is enough).
  2. Open the app (cold launch, or come back to it from another app) and tap into an existing thread.
  3. Watch the thread view.

This happens on good Wi-Fi, not only on a weak connection.

Expected behavior

The thread's messages show right away. If the app has already seen this thread, the cached history shows while fresh data loads in the background, and newer messages come in as they arrive.

Actual behavior

The thread is completely blank for 5+ seconds. A "Loading messages..." pill shows first, then "Syncing messages...", and only after both do the messages show. While a turn is running, the only thing on screen is the "Thinking" row above the composer, with nothing above it (screenshot below). Until it fills in, you can't read the thread, scroll it, or tell whether anything is wrong, so the app can't be used for those seconds every time a thread is opened.

Impact

Major degradation or frequent failure

Version or commit

Desktop: T3 Code (Nightly) 0.1.3-nightly.20261023.3579. iOS app from the App Store (build not captured).

Environment

iPhone (iOS) connected to Windows 11 x64 desktop; provider Claude Code

Logs or stack traces

shell
1# Desktop server trace (server.trace.ndjson), the minute of the screenshot (19:55 local = 17:55 UTC).
2# Note: the desktop app had restarted at 17:52:55 UTC (nightly update), and startup ran a migration
3# (CREATE INDEX orchestration_v2_projection_turn_items_stop_request_idx: 25.5 s), so this sample is
4# from just after a restart. statev2.sqlite is 20.9 GB on this machine.
5
617:55:06.604 http.orchestration.loadThreadSnapshotWindow 5159ms
7 orchestrationV2.threadManagement.ensureLegacyTranscript 3717ms
8 sqlite: SELECT thread_id, transcript_imported_at
9 FROM orchestration_v2_legacy_imports WHERE thread_id = ? LIMIT 1 3717ms
10 sql.transaction > ProjectionStore.readCanonicalProjection 1329ms
11
12# Other single-row lookups on statev2.sqlite in the same few seconds also took 1-3.4 s
13# (projection_nodes by node_id, scheduled_tasks, MAX(sequence) FROM orchestration_events),
14# so the request was waiting on the database, not doing real work.
15
16# For comparison, in the 18 minutes before the restart the same endpoint took ~10-15 ms
17# (a few outliers at ~300-470 ms). So in steady state most of the wait on the phone
18# is not explained by the server; the client's load/sync path seems to be the bigger part.
19#
20# One more steady-state pattern from the same traces: a local client (127.0.0.1, UA "node")
21# fetched /api/orchestration/threads/:id/bounded for 30 threads at the same instant, and
22# EnvironmentAuth.authenticateHttpRequest -> SessionStore.verify queued behind each other,
23# so the later requests spent 1.1-3.4 s just authenticating (a one-row session lookup).

Screenshots, recordings, or supporting files

Screenshot (iPhone, 19:55): thread "help rework th..." in pyseph-portfolio, a blank thread view with only the "Thinking" row and the "Loading messages..." pill above the composer.

Workaround

None; waiting it out.

Notes

Related, not duplicates:

  • #9414 / #10206: "Loading messages..." forever (stuck, not slow)
  • #19563 / #15365: stuck or looping on "Syncing messages..."
  • #14244: "Syncing messages..." shown while the provider boots
  • #16418: reconnect wait when resuming the iOS app over the relay

Ideas that would fix most of it from the user's side:

  • Show cached messages right away while the thread hydrates, instead of a blank view.
  • Don't block the first paint on "Loading messages" followed by "Syncing messages"; render the bounded snapshot as soon as it arrives and sync after.
  • On the server, keep a burst of requests from waiting behind each other on one SQLite connection for auth and single-row lookups.
Synced from GitHub

Happened again just now. Neither the iOS app nor the desktop had been restarted, so the restart in the original report doesn't explain it. The thread list sat on syncing for over 6 s after the app came back to the foreground.

These are the server's spans for the phone's requests (desktop server.trace.ndjson, times in UTC, phone = T3Code/164 CFNetwork/3860.700.2 Darwin/25.6.0 / T3Code/2.1.0 (iPhone; iOS 26.6.2) over the T3 relay, iPhone Air):

shell
101:49:58.832 POST /api/auth/websocket-ticket 11ms
201:49:59.013 GET /api/auth/session 10ms
301:49:59.647 GET /api/auth/session 450ms
401:49:59.661 GET /api/orchestration/shell 10022ms <- ends 01:50:09.683
5 01:50:00.093 http.orchestration.loadShellSnapshot 2228ms <- ends 01:50:02.321
6 sql.transaction > ProjectionStore.readShellSnapshotRows 2129ms
7 sqlite SELECT t.thread_id, t.payload_json, CASE WHEN json_extract(...forkedFrom.type)... 499ms
8 sqlite SELECT counts.thread_id, SUM(CASE WHEN counts.run_id IS NULL OR r.status <> 'rolled_back' ... 1605ms
9 ProjectionStore.decodeShellSnapshot 92ms
10 (no child spans after 01:50:02.321; the GET stays open another 7.36 s)
1101:50:10.308 ws.orchestrationV2.subscribeShell 911ms
1201:50:34.751 GET /api/orchestration/threads/<id>/bounded?compactTurnItems=1 165ms (loadThreadSnapshotWindow 42ms)

So for this sync:

  • ~7.4 s went after the handler had built the shell snapshot, with no server work traced. It looks like serializing the response and sending it through the relay. This machine has 2,157 non-deleted threads (their payload_json in orchestration_v2_projection_threads totals 2.7 MB), and as far as I can tell the phone downloads the whole shell again on every foreground/reconnect.
  • ~2.1 s went building the snapshot, 1.6 s of it in the per-thread item-count aggregate query (statev2.sqlite is ~21 GB here).
  • The thread itself was fast this time (165 ms for the bounded window), so the blank wait comes from shell sync blocking the UI, not from the thread fetch.

Suggestions, beyond the ones in the report:

  • Make reconnect sync incremental (resume from the last seen sequence, or send only threads changed since the client's cached shell) instead of re-sending the full shell.
  • Page or limit the shell to the threads the phone shows (active/recent), and fetch settled/archived ones on demand.
  • Don't block opening a cached thread on the shell sync finishing.

I couldn't get the iOS app's own logs off the phone, so this is the server side only. I'm happy to attach client logs if the app has a way to export them.

Synced from GitHub

@shivamhwp heads up for #24210: I added a new trace above (https://github.com/pingdotgg/t3code/issues/24192#issuecomment-6378419254) from a repeat with no restart on either side. The thread snapshot was fast this time (165 ms). The wait came from the phone's full shell sync on reconnect: GET /api/orchestration/shell took 10 s, 2.2 s building it and about 7.4 s sending it over the relay, for 2,157 threads. Showing the cached thread right away, as your PR does, should still cover the blank view as long as opening it doesn't wait on that shell sync.