[Bug]: Mobile: opening a new thread from the list while its creation is queued makes it "Thread unavailable"#25586

opened3d agobyWwhoisaldo

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. On mobile, start a new thread and send its first message while the creation is still queued in the outbox. A slow or still-syncing connection makes the window seconds long (the trace below shows 3.6 s).
  2. Before the creation is delivered, tap that thread's row in the thread list: the split-view sidebar on iPad or an unfolded iPhone, or the Home list.
  3. Wait for the server to create the thread and start the run.

Expected behavior

The thread opens once the server has created it.

Actual behavior

The thread shows "Thread unavailable: This thread was deleted or is no longer available." The server has the thread, the run proceeds, and other clients show it. The phone recovers only after the cached state's 5-minute idle TTL or an app restart.

Impact

Minor bug or occasional failure

Version or commit

main @ 210b2e7c6a (includes #25477)

Environment

T3 Code iOS app (build 164) over T3 Connect, macOS host on a nightly server. Found by reading the code after the incident below.

Logs, stack traces, or screenshots

Related: #24344 has the same symptom, but its cause was ThreadFeed reading the route id, which #25477 fixed. This issue covers a second path that #25477 does not guard.

useThreadSelection withholds selectedThreadDetailRef while a thread's creation is queued, because a not-found snapshot is cached as a deleted thread (pending-thread-creation.ts#L43-L49). Opening a thread from a list row skips that guard:

  1. A queued ("pending") task row opens { environmentId, threadId } (usePendingTaskListActions.ts#L21-L28).
  2. Selecting a detail calls startOpeningDetail (AdaptiveWorkspaceLayout.tsx#L505, and the fallback at useSelectWorkspaceDetail.ts#L34).
  3. startOpeningEnvironmentThread mounts environmentThreads.stateAtom(environmentId, threadId) without checking the outbox (threads.ts#L61-L64). That is the same atom the guarded reader uses later.
  4. The snapshot request returns 404, and the thread state goes to deleted and parks without retrying (client-runtime threads.ts#L947-L950). The resume cache keeps that state for a remount.

A focused test against makeEnvironmentThreadState on main confirms step 4. After one missing snapshot, a replacement session, an application-active wakeup and a minute of test clock leave the state deleted, with one loader call and no socket subscription. A new state for the same id with the same resume cache restores deleted with zero loader calls, even though the loader now returns the thread.

Server trace from the incident that led here. The phone was on build 164, which most likely predates #25477, so this run may have taken the #24344 path. It shows how wide the window is in practice:

116:33:53.026 GET /api/orchestration/threads/<id>/bounded 404 (iOS app)
216:33:56.665 thread.create dispatched
316:33:58.055 GET /api/orchestration/threads/<id>/bounded 200 (desktop web)
4 (no further requests for this thread from the phone)

I have not yet reproduced steps 1 to 3 end to end in a running app. I'll follow up here with a simulator run.

Model: Claude Opus 5.5 via Claude Code, in T3 Code.

Synced from GitHub

Reproduced live on main (210b2e7c6a) in iOS simulators, with no client changes. The local server was patched to wait 10 s before committing thread.create, so that tapping during the window was repeatable.

  • iPhone Duo (iOS 27.1, unfolded split view), tapping the queued sidebar row: 4 of 4 runs stuck on "Starting…", and 4 of 4 showed "Thread unavailable" when reopened.
  • iPhone 17 Pro (iOS 26.3), tapping the queued Home row: 4 of 4 stuck, and 3 of 4 showed "Thread unavailable" when reopened. The fourth was reopened after the 5-minute cache expired and loaded normally.
  • Without tapping the row: 4 of 4 runs opened normally.
  • Server: each tapped run produced one GET …/threads/<id>/bounded 404 before thread.create committed. The device sent no further request for that thread while the cache held it.

While a thread is open it stays on "Starting…" even though its row says Working and then Failed. "Thread unavailable" shows when you reopen it. Fix, with before/after recordings: #25632.