Skip to content

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

Open

Description

@Pyseph

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

# Desktop server trace (server.trace.ndjson), the minute of the screenshot (19:55 local = 17:55 UTC).
# Note: the desktop app had restarted at 17:52:55 UTC (nightly update), and startup ran a migration
# (CREATE INDEX orchestration_v2_projection_turn_items_stop_request_idx: 25.5 s), so this sample is
# from just after a restart. statev2.sqlite is 20.9 GB on this machine.

17:55:06.604  http.orchestration.loadThreadSnapshotWindow                5159ms
                orchestrationV2.threadManagement.ensureLegacyTranscript  3717ms
                  sqlite: SELECT thread_id, transcript_imported_at
                          FROM orchestration_v2_legacy_imports WHERE thread_id = ? LIMIT 1   3717ms
                sql.transaction > ProjectionStore.readCanonicalProjection 1329ms

# Other single-row lookups on statev2.sqlite in the same few seconds also took 1-3.4 s
# (projection_nodes by node_id, scheduled_tasks, MAX(sequence) FROM orchestration_events),
# so the request was waiting on the database, not doing real work.

# For comparison, in the 18 minutes before the restart the same endpoint took ~10-15 ms
# (a few outliers at ~300-470 ms). So in steady state most of the wait on the phone
# is not explained by the server; the client's load/sync path seems to be the bigger part.
#
# One more steady-state pattern from the same traces: a local client (127.0.0.1, UA "node")
# fetched /api/orchestration/threads/:id/bounded for 30 threads at the same instant, and
# EnvironmentAuth.authenticateHttpRequest -> SessionStore.verify queued behind each other,
# 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:

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.

Activity

  1. Pyseph commented on Oct 24, 2026

    @Pyseph
    ContributorAuthor

    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):

    01:49:58.832  POST /api/auth/websocket-ticket                    11ms
    01:49:59.013  GET  /api/auth/session                             10ms
    01:49:59.647  GET  /api/auth/session                            450ms
    01:49:59.661  GET  /api/orchestration/shell                   10022ms   <- ends 01:50:09.683
      01:50:00.093  http.orchestration.loadShellSnapshot           2228ms   <- ends 01:50:02.321
        sql.transaction > ProjectionStore.readShellSnapshotRows    2129ms
          sqlite  SELECT t.thread_id, t.payload_json, CASE WHEN json_extract(...forkedFrom.type)...   499ms
          sqlite  SELECT counts.thread_id, SUM(CASE WHEN counts.run_id IS NULL OR r.status <> 'rolled_back' ...  1605ms
        ProjectionStore.decodeShellSnapshot                          92ms
      (no child spans after 01:50:02.321; the GET stays open another 7.36 s)
    01:50:10.308  ws.orchestrationV2.subscribeShell                 911ms
    01: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.

  2. Pyseph commented on Oct 24, 2026

    @Pyseph
    ContributorAuthor

    @shivamhwp heads up for #24210: I added a new trace above (#24192 (comment)) 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.

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