Repository navigation
Description
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
- Open the iOS mobile app connected to a T3 server.
- Start a new task in a project using the current checkout.
- 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.").
- Send the task. Observe that the thread remains on "Starting..." even after the server finishes the response.
- Return to the thread list and reopen the new thread. It shows "Thread unavailable".
- 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
juliusmarminge commentedon Oct 4, 2026
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 fromeac52f0087are unchanged, and the log sequence lines up with the code.What I found
NewTaskDraftScreen), beforethread.create.use-thread-selection.tsdeliberately withholdsselectedThreadDetailRefuntil the server shell exists or delivery is recorded, but some subscribers use the optimistic id directly:useThreadComposerStatesubscribesenvironmentThreadDetails.queueWorkflowAtomto the pending-creation shell.ThreadDetailScreensubscribes reported model, queued count, and turn subagents toprops.selectedThread.id.environmentThreads.stateAtom, and the first one issues the bounded snapshot GET. A 404 takes the"missing"branch inpackages/client-runtime/src/state/threads.ts, which callssetDeleted()and parks the subscription with no retry. That terminal behavior is covered by a test inthreads-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 replaysdeletedwithout another GET. A restart drops that cache, which is why the next GET returns 200.sendQueuedCreationawaitsprepareQueuedMessageAttachmentsbeforestartTurn, so the id stays subscribed but not yet created long enough for the 404 to win. MeanwhileresolvePendingThreadCreationkeeps "Starting…" until detail contains the turn, andThreadRouteScreenforces a ready presentation during creation, so "Thread unavailable" only appears after going back and reopening.Likely fix area
ThreadDetailScreensubscriptions on the same conditionselectedThreadDetailRefalready uses, so the shared loader doesn't start for a thread that hasn't been created yet.A maintainer will decide on the fix direction.
Nelglor commentedon Oct 4, 2026
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.2648reached over Tailscale rather than loopback.Server traces show the same sequence as the report:
GET .../bounded→ 404dispatch.threadCreateBoth 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.
derseitenschneider commentedon Oct 8, 2026
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 on0.0.46-nightly.20261008.2819, reached directly over Tailscale (tailscale serveon :8443, not T3 Connect). iOS user agentT3Code/110.Server trace for the affected thread:
GET /api/orchestration/threads/<id>/bounded→ 404thread_not_found(ProjectionStoreThreadNotFoundErrorinloadThreadSnapshotWindow)orchestrationV2.dispatch.threadCreatesuccessGap: 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.
Nelglor commentedon Oct 12, 2026
Note
🤖 Claude Fable 5.1 responding on behalf of Nick
Still live today, and a pair of runs minutes apart shows why it looks intermittent: it is the order of two requests the phone sends, and the order flips with round-trip time.
Both runs: TestFlight 2.0.0 (120) per the TestFlight page (the request user agent reported
T3Code/119), iPhone, new thread with a PNG attached, Claude Agent, host0.0.46-nightly.20261012.3010on Linux. Same phone, same app, same prompt flow.GET …/boundeddispatch.threadCreateOn the LAN path the attachment check that gates
thread.createreturns fast enough that create lands first. Over Tailscale that extra round trip is long enough for the screen's snapshot GET to win, the 404 is stored as deleted, and the thread never recovers. That matches the 42–79 ms gaps in the earlier reports, all of which were also over Tailscale or a remote link.So nothing in the 10-11/10-12 builds changed this; a LAN test will pass and a remote test will fail. #15870 removes the early GET, which closes it on both paths. A rebased copy that typechecks and passes the mobile tests on current main is linked on that PR.