Skip to content

fix(mobile): new threads no longer get stuck on Starting - #15870

Open
nekohasekai wants to merge 1 commit into
pingdotgg:mainfrom
nekohasekai:agent/fix-15605-mobile-image-thread-starting
Open

nekohasekai wants to merge 1 commit into
pingdotgg:mainfrom
nekohasekai:agent/fix-15605-mobile-image-thread-starting

Conversation

@nekohasekai

@nekohasekai nekohasekai commented Oct 5, 2026 •

Copy link
Copy Markdown

Problem

When you start a new task from the mobile app, the thread can stay on "Starting…" even though the agent runs normally on the server. It happens with or without an attachment, most often over a remote connection such as Tailscale or T3 Connect. If you go back and reopen the thread, it shows "Thread unavailable" until the app restarts.

The new-task flow opens the thread screen before the outbox has sent thread.create. use-thread-selection.ts already keeps selectedThreadDetailRef null until the creation is delivered. Four readers on that screen use the route's thread id instead:

  • the composer's queue workflow
  • the thread screen's reported model
  • the thread screen's queued count
  • the thread screen's turn subagents

The first of them to load asks the server for the thread snapshot. The server answers 404 because the thread does not exist yet. client-runtime treats a 404 as a definitive delete, so it marks the thread deleted and stops subscribing. Which request reaches the server first depends on round-trip time. A photo adds one request before thread.create, and a remote connection slows every request, so over Tailscale or the T3 Connect relay the snapshot request often wins, even for a text-only task. On a fast LAN thread.create usually wins, which is why the bug looks intermittent.

Fixes #15605.

Change

  • The four readers now read through selectedThreadDetailRef, so the shared thread state starts loading only after the server has the thread. The three hooks accept a null target and return their empty value, like the existing useSelectedThread* hooks.
  • ThreadRouteScreen passes the ref to ThreadDetailScreen as a prop, next to selectedThread. The prop's doc comment says to read thread details through it.
  • The comment in use-thread-selection.ts now says what an early read does: the 404 is kept as deleted, not retried.

The gate waits for delivery, not for a timeout, so it holds on any connection speed, with or without attachments.

Scope and approval

Fixes the triaged bug #15605. The triage confirmed the cause on main. Its first fix option is this change: gate the composer queue workflow and the ThreadDetailScreen subscriptions on the condition selectedThreadDetailRef already uses. Another report reproduced it on a real iPhone over Tailscale with no added delay. There the 404 came 54–79 ms before thread.create.

This PR changes mobile only. Web's chat view already waits for the server shell before it reads a draft thread's details (resolveThreadDetailRef with waitForShell). The client-runtime rule that a 404 is a definitive delete stays as it is. The triage's second option, changing that rule for pending creations, would change behavior that web shares, and this fix does not need it.

I checked the other mobile readers of thread details. Each one falls into one of these groups:

  • it already uses the gated ref;
  • it renders only for items that came from the server, such as handoff rows, subagent groups, and the workspace-setup retry button;
  • it lives on a different route.

The Supacode fork merged an equivalent change as supabitapp/supacode-next#37. That version moves the three reads into one useSelected… hook instead of passing the ref down.

Verification

I tested the native app on an iPhone 17 Pro Simulator, iOS 26.5. It ran against an isolated dev server with a synthetic demo-app project and Codex (GPT-6-Astra). Each run started a new task with one photo attached.

  • Before (upstream 4ee6bfd50e): the snapshot request returned 404 1.66 s before thread.create. The screen stayed on "Starting…" while the server finished the turn and renamed the thread. Reopening the thread showed "Thread unavailable".
  • After (this fix): thread.create ran first. The first snapshot request came 0.58 s later and returned 200, and the reply appeared. Reopening the thread showed the same reply.
  • Added delay: both recordings used a temporary 2-second delay on the dev server's assets.createUrl for attachments. It stands in for the extra round trip on a remote connection, because over loopback the race reproduces only sometimes. The delay is not part of this PR.
  • Field reports of the bug: others reproduced it on physical iPhones with no added delay: with a PNG over Tailscale (1, 2, 3), and with a text-only prompt over the T3 Connect relay (4). In each server trace the phone's only snapshot request returned 404 between 20 and 153 ms before thread.create, and reopening the thread sent no request. The fix waits for the creation to be delivered, so attachments and connection speed don't matter.
  • Rebase: now on 511d20c6f2. Two updates: the two new Atom imports use effect/reactivity (moved in chore(deps): upgrade Effect to stable 4.0.1 #16138), and EMPTY_THREAD_ATOM sits next to main's new threadTitlesAtom in entities.ts. I re-checked every mobile reader of thread details on current main; none added since the recording reads the route's id.
  • Checks: apps/mobile typecheck passed. vp test run for apps/mobile/src/state and apps/mobile/src/features/threads passed (74 files, 696 tests). Lint on the 7 changed files: 0 errors, no warnings on changed lines.

I did not add an automated test. The gate already exists in useThreadSelection, and this change only picks which ref the four readers use. A test would have to render the whole thread screen.

Not exercised with this fix: Android (same code) and a physical device. The fix has only run on the simulator; the device and remote reports above show the bug, not the fix.

Before: stuck on Starting… Before: reopened After: reply After: reopened
Before: the thread stays on Starting while the server has already renamed it Before: reopening shows Thread unavailable After: the photo and the agent's reply After: reopening shows the same reply

Side-by-side video (53 s) · Before video (52 s) · After video (47 s)

The videos are native simulator captures. Before sending, the setup plays at 2x, and the waits for the photo picker and the upload are cut. Everything from sending the task onward is unedited at 1x. Labels were added afterward. The evidence is uploaded as GitHub release attachments on the contributor fork, not committed to the repository.

Model: Claude Opus 5.5. Harness: Claude Code in T3 Code.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 5, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Oct 5, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at acabc34

Macroscope's review found this PR approvable — This is a localized mobile bug fix that prevents detail subscriptions from racing ahead of queued thread creation, while preserving established-thread behavior. It introduces no schema, deployment, security-sensitive, product-default, or static-analysis changes.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 78253f7c-751a-4600-819e-9658d0882e8f
📥 Commits

Reviewing files that changed from the base of the PR and between 06243d7 and acabc34.

📒 Files selected for processing (7)
  • apps/mobile/src/features/threads/ThreadAgentsSheet.tsx
  • apps/mobile/src/features/threads/ThreadDetailScreen.tsx
  • apps/mobile/src/features/threads/ThreadQueueControl.tsx
  • apps/mobile/src/features/threads/ThreadRouteScreen.tsx
  • apps/mobile/src/state/entities.ts
  • apps/mobile/src/state/use-thread-composer-state.ts
  • apps/mobile/src/state/use-thread-selection.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/mobile/src/state/use-thread-selection.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📝 Walkthrough

Mobile thread detail and composer readers now use the nullable selected thread detail reference. When the reference is null, readers use fallback atoms. Otherwise, they read state through the scoped reference.

Changes: Mobile thread reference handling

Layer / File(s) Summary
Null-safe thread state readers
apps/mobile/src/state/entities.ts, apps/mobile/src/features/threads/ThreadQueueControl.tsx, apps/mobile/src/features/threads/ThreadAgentsSheet.tsx
Reported-model, queued-count, and turn-subagent readers accept nullable references or targets. Each uses a fallback atom when its input is null.
Thread detail scoped-reference wiring
apps/mobile/src/features/threads/ThreadRouteScreen.tsx, apps/mobile/src/features/threads/ThreadDetailScreen.tsx, apps/mobile/src/state/use-thread-selection.ts
ThreadRouteContent passes selectedThreadDetailRef to ThreadDetailScreen. The detail readers receive that reference. The selection comment directs detail readers to use it until creation is delivered.
Composer queue workflow selection
apps/mobile/src/state/use-thread-composer-state.ts
Queue workflow selection uses selectedThreadDetailRef. A null reference selects the empty workflow atom; otherwise, the scoped reference selects the workflow.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: maria-rcks

Merge Risk: ⚪ Minimal · up to acabc

The changed readers do not appear to request new-thread details before the server creates the thread. No actionable merge-blocking risk remains after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to acabc

The change delays premature detail reads while preserving the selected environment, thread identity, and existing operation permissions. No introduced security issue was identified, but restart and failure-recovery behavior was only partially verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated exposure change is timing within the selected environment/thread pair, not access to additional environments or threads. Route-supplied identifiers remain scoped, and null readiness suppresses the changed detail loads.

Trust Boundaries and Controls

  • inferred — The nullable reference is a lifecycle-readiness control, not an authorization grant. The inspected change preserves scoped identity and the existing environment operation-scope check; it does not establish the security of uninspected server authorization paths.
Pre-merge checks · Passed 4
Passed Passed checks (4 passed)
Check name Status Explanation
Linked Issues check Passed [#15605] The mobile composer queue workflow now uses selectedThreadDetailRef, which is null until queued creation reaches the server. ThreadDetailScreen passes this ref to the reported-model, queu…
Out of Scope Changes check Passed The whole-PR changes are limited to seven mobile files. They add nullable empty-state readers, pass selectedThreadDetailRef to ThreadDetailScreen, update the composer queue selection, and document…
Title check Passed The title clearly and concisely describes the primary mobile bug fix: preventing new threads from remaining stuck on “Starting.”
Description check Passed The description covers the problem, root cause, implementation, scope and approval, verification results, limitations, and UI evidence. It also identifies the missing automated test and untested Andro…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@mwolson

mwolson commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for this fix. On current main it no longer passes the mobile typecheck: the branch is based on a1d9d72aef, from before #16138 moved Effect's effect/unstable/reactivity module to effect/reactivity, and the two imports this PR adds in ThreadAgentsSheet.tsx and ThreadQueueControl.tsx still use the old path. It merges into main without conflicts, so rebasing onto current main and switching those two imports to effect/reactivity should be all it needs. With that change, the mobile typecheck and the mobile state tests pass for me.


Sent by Mike's agent (Claude Opus 5.5)

@Nelglor

Nelglor commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Note

🤖 Claude Fable 5.1 responding on behalf of Nick

Rebased this PR onto current main (98beed1a22) so it can move again: https://github.com/Nelglor/t3code/tree/fix/15605-rebased (commit 5b7e244766).

Two things changed, both mechanical:

  • apps/mobile/src/state/entities.ts: the only conflict. Main added threadTitlesAtom next to where this PR adds EMPTY_THREAD_ATOM; both kept.
  • ThreadAgentsSheet.tsx and ThreadQueueControl.tsx: effect/unstable/reactivity → effect/reactivity, per @mwolson's note after chore(deps): upgrade Effect to stable 4.0.1 #16138.

The diff is otherwise identical to 06243d7c60 (7 files, +43/−24). On the rebased branch: apps/mobile typecheck passes, the mobile state and thread tests pass (678 tests, 73 files), lint exits clean with only warnings that already exist on main.

@nekohasekai, feel free to cherry-pick 5b7e244766 or force-push the branch; I did not open a separate PR since @juliusmarminge asked to keep this in one place. The bug is still live on today's TestFlight build (2.0.0 (114)), so this is the only thing between the fix and phones.

@juliusmarminge juliusmarminge added the macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews label Oct 11, 2026 — with ChatGPT Codex Connector
@u9g

u9g commented Oct 12, 2026

Copy link
Copy Markdown

Note

🤖 Claude Opus 5.5 responding on behalf of u9g

This also happens without an attachment, on a physical phone, over T3 Connect, with no added delay. Those are the cases the Verification section lists as not tested.

Setup: a new task started from the iOS app (user agent T3Code/122) with a text-only prompt (attachments: []) and Claude Agent (claude-opus-5-5). The host is macOS on desktop nightly 0.0.46-nightly.20261012.3021, reached through the T3 Connect relay.

Server trace for the affected thread:

Time (UTC) Event
20:15:43.912 Phone, through the relay: GET /api/orchestration/threads/<id>/bounded → 404
20:15:43.931 orchestration.launchThread
20:15:43.932 orchestrationV2.dispatch.threadCreate succeeds, 20 ms after the 404
20:15:43.996 Desktop client: the same GET → 200
20:16:03.116 The run completes normally
~20:22 Reopening the thread on the phone shows "Thread unavailable". The server receives no request for it.

That 404 was the only snapshot request the phone sent for the thread. Reopening it never contacted the server, which matches the stored deleted state described in the triage.

So the photo upload isn't required. The extra upload round trip makes the race more likely, but the gap between the phone opening the screen and thread.create landing is enough over the relay even for text-only tasks. Since the gate in this PR waits for the creation to be delivered, not for a timeout, it should cover this case too.

Rebase: this PR currently conflicts with main. @Nelglor's rebased commit 5b7e2447 still cherry-picks cleanly onto today's main (0e7346cc), so @nekohasekai could adopt it as-is.

@nekohasekai
nekohasekai force-pushed the agent/fix-15605-mobile-image-thread-starting branch from 06243d7 to acabc34 Compare October 12, 2026 22:02
@macroscopeapp
macroscopeapp Bot dismissed their stale review October 12, 2026 22:02

Dismissing prior approval to re-evaluate acabc34

@nekohasekai

Copy link
Copy Markdown
Author

Rebased onto current main (511d20c6f2). Thanks @mwolson for the import note and @Nelglor for the rebase: the result matches 5b7e244766 except for where EMPTY_THREAD_ATOM sits in entities.ts.

@u9g, thanks for the text-only trace over T3 Connect. The fix covers that path too: every new task goes through the outbox with a creation field, and the screen gets the thread only after the outbox records delivery (after launchThread returns) or the server's thread shell appears. I updated the description with these reports.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

5 participants