Skip to content

[Bug]: Mobile image thread creation stays on Starting and becomes unavailable until app restart #15605

Description

@nekohasekai

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/mobile

Steps to reproduce

  1. Open the iOS mobile app connected to a T3 server.
  2. Start a new task in a project using the current checkout.
  3. Add a PNG from Photo Library and enter a short prompt (for example, "Describe this demo screenshot in one sentence. Do not modify any files.").
  4. Send the task. Observe that the thread remains on "Starting..." even after the server finishes the response.
  5. Return to the thread list and reopen the new thread. It shows "Thread unavailable".
  6. Fully quit and reopen the mobile app, then open the same thread. The image and completed response now load.

The recording uses an isolated project and test image; no real conversation data is shown.

Expected behavior

The newly created thread should load and stream its response without restarting the app. Returning to the thread list and reopening it should show the existing conversation.

Actual behavior

Creating a new thread with an image on mobile leaves the thread stuck on "Starting...", even though the server creates the thread and completes the response. Going back to the thread list and reopening it shows "Thread unavailable" / "This thread was deleted or is no longer available." Fully restarting the mobile app makes the same thread and its completed response appear.

The user reports this consistently when creating threads with images. I reproduced the complete sequence in an isolated iOS simulator with a demo image.

Investigation

The client loads the new thread before the server has created it. The bounded snapshot GET returned 404, then thread.create began 47.6 ms later and succeeded. The provider run completed normally; the mobile detail view remained stuck. After restarting the app, the same bounded GET returned 200.

Source at eac52f0 explains the observed sequence: use-thread-selection.ts holds selectedThreadDetailRef while creation is pending, but the composer queue-workflow subscription and ThreadDetailScreen subscriptions for reported model, queued count, and subagents use the optimistic thread ID directly. They can start the shared detail loader during attachment preparation, before startTurn/launchThread.

packages/client-runtime/src/state/threads.ts treats the initial missing/404 snapshot as a terminal deleted state and stops the subscription. Reopening the thread reuses that in-memory state; restarting clears it.

Source references: https://github.com/pingdotgg/t3code/blob/eac52f0087/apps/mobile/src/state/use-thread-selection.ts#L161 ; https://github.com/pingdotgg/t3code/blob/eac52f0087/apps/mobile/src/state/use-thread-composer-state.ts#L306 ; https://github.com/pingdotgg/t3code/blob/eac52f0087/apps/mobile/src/features/threads/ThreadDetailScreen.tsx#L309 ; https://github.com/pingdotgg/t3code/blob/eac52f0087/packages/client-runtime/src/state/threads.ts#L894

Related reports checked

#10888: older iOS deep-link hydration race, fixed in #11502; this repro uses new-task image creation. #14689: web cached-shell routing, not this mobile detail state. No matching duplicate found.

Impact

Major degradation or frequent failure

Version or commit

Reproduced at eac52f0 (server reports 0.0.45); unmodified mobile and server checkout.

Environment

iOS 26.5 Simulator (iPhone 17 Pro); macOS 27.0.1 (26A434), Apple Silicon; local isolated dev server over loopback; Node 26.9.0; Codex 0.160.0 / GPT-6-Astra. Android not tested.

Logs or stack traces

2026-10-04 UTC; same test thread throughout:
11:06:41.380721  GET /api/orchestration/threads/<test-thread>/bounded -> 404
11:06:41.428330  thread.create begins (47.609 ms after the 404 completed)
11:06:41.442482  thread.create succeeds
11:06:41.484496  message.dispatch succeeds
11:06:48.868     provider run completed
                Mobile still shows Starting, then Thread unavailable on reopen
11:07:52.938322  After app restart: same bounded GET -> 200

Database: the thread exists and deleted_at is null.
Only two bounded GETs for this thread appear in the captured interval: the initial 404 and the 200 after restart.
Paths, credentials, and unrelated conversation content are omitted.

Screenshots, recordings, or supporting files

Recording (22 seconds, idle waits removed; isolated demo data only):

mobile-image-thread-repro-trimmed.mp4

Workaround

Fully quit and reopen the mobile app, then reopen the thread. The existing conversation and completed response load normally. No source changes were made.

Filed by: Codex harness / gpt-6-astra (ultra) in T3 Code.

Activity

  1. added
    bugSomething is broken or behaving incorrectly.
    needs-triageIssue needs maintainer review and initial categorization.
    on Oct 4, 2026
  2. juliusmarminge commented on Oct 4, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks so much for the careful repro, recording, and timing log, @nekohasekai, and for refiling on the bug template (this is the same report as #15603). The diagnosis still matches current main (5a96895a85). The files cited from eac52f0087 are unchanged, and the log sequence lines up with the code.

    What I found

    • A new task opens its thread route as soon as the outbox write finishes (NewTaskDraftScreen), before thread.create. use-thread-selection.ts deliberately withholds selectedThreadDetailRef until the server shell exists or delivery is recorded, but some subscribers use the optimistic id directly:
      • useThreadComposerState subscribes environmentThreadDetails.queueWorkflowAtom to the pending-creation shell.
      • ThreadDetailScreen subscribes reported model, queued count, and turn subagents to props.selectedThread.id.
    • Those atoms share environmentThreads.stateAtom, and the first one issues the bounded snapshot GET. A 404 takes the "missing" branch in packages/client-runtime/src/state/threads.ts, which calls setDeleted() and parks the subscription with no retry. That terminal behavior is covered by a test in threads-sync.test.ts ("marks a cold definitive HTTP miss deleted without socket subscribe or retry").
    • setDeleted() also stores the deleted state in the in-memory resume cache (5-minute idle TTL), so reopening the thread replays deleted without another GET. A restart drops that cache, which is why the next GET returns 200.
    • Images make the ordering reliable: sendQueuedCreation awaits prepareQueuedMessageAttachments before startTurn, so the id stays subscribed but not yet created long enough for the 404 to win. Meanwhile resolvePendingThreadCreation keeps "Starting…" until detail contains the turn, and ThreadRouteScreen forces a ready presentation during creation, so "Thread unavailable" only appears after going back and reopening.
    • This is separate from [Bug]: iOS Live Activity shows agent needs input, but opening the thread shows "Thread unavailable" #10888 (deep-link hydration, fixed in fix(mobile): wait for thread deep link hydration #11502) and [Bug]: Opening a link to a thread created since the web client's last visit redirects to an unrelated thread #14689 (web cached-shell routing). The server side and the provider run look fine.

    Likely fix area

    • One option is to gate the composer queue-workflow and ThreadDetailScreen subscriptions on the same condition selectedThreadDetailRef already uses, so the shared loader doesn't start for a thread that hasn't been created yet.
    • Another is to make the client runtime not treat a 404 as terminal for an id that still has a pending local creation, or to clear the cached deleted state once creation succeeds.

    A maintainer will decide on the fix direction.

  3. added
    via-triageFiled through npx t3 triage
    and removed
    needs-triageIssue needs maintainer review and initial categorization.
    on Oct 4, 2026
  4. Nelglor commented on Oct 4, 2026

    @Nelglor
    Contributor

    Note

    🤖 Claude Fable 5.1 responding on behalf of Nick

    Confirming on a real device and production nightly. Two hits back to back within three minutes, both new threads started from the iOS app with a PNG attached, provider Claude Agent (claude-opus-5-5), server 0.0.46-nightly.20261004.2648 reached over Tailscale rather than loopback.

    Server traces show the same sequence as the report:

    Thread GET .../bounded → 404 dispatch.threadCreate Gap
    1 18:13:54.843Z 18:13:54.922Z 79 ms
    2 18:16:16.162Z 18:16:16.216Z 54 ms

    Both runs completed normally. Thread 2 had exactly one more bounded GET, a 200 at 18:23:17Z, after fully quitting and relaunching the app. Reopening from the thread list in between produced no request, consistent with the cached deleted state. So this is not simulator or Codex specific, and the window is wide enough to hit on every image thread over a real network.

  5. derseitenschneider commented on Oct 8, 2026

    @derseitenschneider

    Note

    🤖 Claude Fable 5.1 responding on behalf of Brian.

    Still reproducing on the latest nightly, with the same signature. New thread started from the iOS app with a PNG attached, provider Claude Agent (claude-opus-5-5), Linux host on 0.0.46-nightly.20261008.2819, reached directly over Tailscale (tailscale serve on :8443, not T3 Connect). iOS user agent T3Code/110.

    Server trace for the affected thread:

    Event Time (UTC)
    GET /api/orchestration/threads/<id>/bounded → 404 thread_not_found (ProjectionStoreThreadNotFoundError in loadThreadSnapshotWindow) 16:20:57.350
    orchestrationV2.dispatch.threadCreate success 16:20:57.392

    Gap: 42 ms. That was the only bounded GET the phone ever sent for this thread; going back to the list and reopening produced no request. The run started 2.6 s later, completed normally, and pushed its commit. The phone showed "Thread unavailable / This thread was deleted or is no longer available" the whole time.

    Happy to test #15870 on this setup once there is a build.

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

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions