Before submitting
Area
apps/mobile
Steps to reproduce
- On iOS, open a long thread (hundreds of activities; a few expanded reasoning rows make it worse) while a Claude turn with subagents is running.
- Stay in the thread for a few minutes, or leave it and open it again.
Expected behavior
The thread stays responsive. A streamed event costs work proportional to what it changed.
Actual behavior
Taps respond late, and sometimes the feed stays empty until the turn ends.
iOS CPU reports. iOS wrote two cpu_resource reports for T3Code 1.3.0 (85), both while a turn was running:
- 90 s of CPU over 175 s (51%);
- 90 s of CPU over 133 s (68%).
In the first report, 22 of 30 samples are on the JS thread (com.facebook.react.runtime.JavaScript):
- 17 of them are inside Yoga layout, called from Hermes;
- at least 14 of those end in a measure function in the app binary, which lays out text with TextKit (UIFoundation / CoreText).
The reports are unsymbolicated. T3MarkdownTextShadowNode is the app's only native text measured through Yoga, which is why I attribute this frame to it.
Server side. The server was fine. The 12-minute turn produced about 0.55 events/s (about 440 B/s). But the phone took 5–8 s to acknowledge stream batches, against 0.2–0.4 s normally.
Cause. On main @ 94f92a7, these files are the same as in the 1.3.0 build (76cc9b0).
- Markdown is measured without a cache.
T3MarkdownTextShadowNode::measureContent (apps/mobile/modules/t3-markdown-text/ios/T3MarkdownTextShadowNode.mm) rebuilds the NSAttributedString and runs a full TextKit layout (NSLayoutManager ensureLayoutForTextContainer) on every call.
- React Native 0.86's own
Text caches this (textMeasureCache_ in TextLayoutManager.mm).
- Every commit that clones a markdown row measures it again. Expanded reasoning rows render through the same component (
AssistantMarkdownContent in ThreadFeed.tsx), so each one adds to the cost.
- Every event rebuilds the feed and re-renders rows that did not change.
getThreadFeedActivityEntries (apps/mobile/src/lib/threadActivity.ts) caches by the identity of the activities array. The reducer creates a new array for each activity event, so the whole work log is derived again.
context-window.updated and re-sent activities take the filter + full sort path in threadReducer.ts.
UserMessageContent (ThreadFeed.tsx) calls useThreadSelection(), so every user message row re-renders on every event.
- A git status refresh runs for each event of the open thread.
- The
useEffect in apps/mobile/src/state/use-selected-thread-git-actions.ts depends on selectedThread. That object is replaced on every shell update of the thread (use-thread-selection.ts).
- So each event triggers
vcs.refreshStatus, then vcs.listRefs through onSettled: invalidateRefs (packages/client-runtime/src/state/vcs.ts), and then another render.
- In one hour the phone made 143
refreshStatus calls, with a median of 660 ms and 144 s of server time in total. Each call runs git and a GitHub PR lookup. The desktop app made none.
Separately, after leaving such a thread, the app sometimes stops reacting to taps until it is force-quit. I'm collecting a sysdiagnose for that and will add it here.
Suggested fix
- Cache the measurement. Cache
measureContent results by text, attributes and width, as TextMeasureCache does, or measure through TextLayoutManager.
- Derive per activity, not per array. Derive work log entries per activity instead of per array, and give
context-window.updated a replace-in-place path.
- Stop re-renders from selection. Don't subscribe
UserMessageContent to the thread selection.
- Refresh git on real changes only. Key the git refresh effect on the thread id, cwd and branch, not on the shell object.
Related: #10847 (the same long-thread freeze, closed as fixed by #8309 and #11302; both are already in 1.3.0), #8118.
Impact
Major degradation or frequent failure
Version or commit
iOS app 1.3.0 (85); desktop server 0.0.43-nightly.20260926.2282; main @ 94f92a7
Environment
iPhone 13 Pro Max, iOS 26.5, connected through the T3 Connect relay; desktop server on Linux (NixOS); Claude Code 2.1.283
Logs or stack traces
From T3Code.cpu_resource (1.3.0 (85)). Image names come from a crash report of the same build (`React`, `hermesvm`). The repeated Yoga recursion frames are collapsed.
Event: cpu usage
CPU: 90 seconds cpu time over 175 seconds (51% cpu average), exceeding limit of 50% cpu over 180 seconds
Hardware model: iPhone14,3
Heaviest stack (samples of 30):
22 Foundation + 583716 (NSThread start)
22 React + 2865780 (+[RCTJSThreadManager runRunLoop])
21 hermesvm + 132496 ... + 1060644
18 React + 1909396 ... + 1308736
17 React + 144056 / 129328 / 139204 (recursion, repeated ~15 levels)
14 React + 1554616
9 T3Code + 15555316 (native text measure)
9 UIFoundation + 825424 ... + 247500
4 CoreText + 769904
4 T3Code + 15553852 -> React + 2395540 (attributed string build)
Screenshots, recordings, or supporting files
No response
Workaround
No response
Before submitting
Area
apps/mobile
Steps to reproduce
Expected behavior
The thread stays responsive. A streamed event costs work proportional to what it changed.
Actual behavior
Taps respond late, and sometimes the feed stays empty until the turn ends.
iOS CPU reports. iOS wrote two
cpu_resourcereports for T3Code 1.3.0 (85), both while a turn was running:In the first report, 22 of 30 samples are on the JS thread (
com.facebook.react.runtime.JavaScript):The reports are unsymbolicated.
T3MarkdownTextShadowNodeis the app's only native text measured through Yoga, which is why I attribute this frame to it.Server side. The server was fine. The 12-minute turn produced about 0.55 events/s (about 440 B/s). But the phone took 5–8 s to acknowledge stream batches, against 0.2–0.4 s normally.
Cause. On
main@ 94f92a7, these files are the same as in the 1.3.0 build (76cc9b0).T3MarkdownTextShadowNode::measureContent(apps/mobile/modules/t3-markdown-text/ios/T3MarkdownTextShadowNode.mm) rebuilds theNSAttributedStringand runs a full TextKit layout (NSLayoutManager ensureLayoutForTextContainer) on every call.Textcaches this (textMeasureCache_inTextLayoutManager.mm).AssistantMarkdownContentinThreadFeed.tsx), so each one adds to the cost.getThreadFeedActivityEntries(apps/mobile/src/lib/threadActivity.ts) caches by the identity of the activities array. The reducer creates a new array for each activity event, so the whole work log is derived again.context-window.updatedand re-sent activities take the filter + full sort path inthreadReducer.ts.UserMessageContent(ThreadFeed.tsx) callsuseThreadSelection(), so every user message row re-renders on every event.useEffectinapps/mobile/src/state/use-selected-thread-git-actions.tsdepends onselectedThread. That object is replaced on every shell update of the thread (use-thread-selection.ts).vcs.refreshStatus, thenvcs.listRefsthroughonSettled: invalidateRefs(packages/client-runtime/src/state/vcs.ts), and then another render.refreshStatuscalls, with a median of 660 ms and 144 s of server time in total. Each call runs git and a GitHub PR lookup. The desktop app made none.Separately, after leaving such a thread, the app sometimes stops reacting to taps until it is force-quit. I'm collecting a sysdiagnose for that and will add it here.
Suggested fix
measureContentresults by text, attributes and width, asTextMeasureCachedoes, or measure throughTextLayoutManager.context-window.updateda replace-in-place path.UserMessageContentto the thread selection.Related: #10847 (the same long-thread freeze, closed as fixed by #8309 and #11302; both are already in 1.3.0), #8118.
Impact
Major degradation or frequent failure
Version or commit
iOS app 1.3.0 (85); desktop server 0.0.43-nightly.20260926.2282; main @ 94f92a7
Environment
iPhone 13 Pro Max, iOS 26.5, connected through the T3 Connect relay; desktop server on Linux (NixOS); Claude Code 2.1.283
Logs or stack traces
Screenshots, recordings, or supporting files
No response
Workaround
No response