Repository navigation
[Bug]: iOS re-downloads every attachment image on each reconnect because signed asset URLs change and expo-image has no stable cache key聽#18812
Description
Note
馃 Claude Fable 5.1 responding on behalf of Nick
Before submitting
- I searched existing issues and did not find a duplicate. Closest are [Bug]: iOS 2.0.0 (103) repeatedly reconnects when opening one existing thread, including after un-settling聽#15365 (reconnect loop on one thread) and [Bug]: Re-embedding an overwritten local image shows the old cached version聽#15676 / fix(clients): chat images show the new file after an agent rewrites it聽#14943 (web shows a stale cached image, the opposite direction). Neither covers images being re-downloaded.
- I included enough detail to reproduce or investigate the problem.
Area
apps/mobile
Steps to reproduce
- From the iOS app, start a thread on a remote environment (Tailscale or T3 Connect) and attach a dozen photos across a few messages.
- Use the phone the way people use it while an agent runs: lock it, unlock to check, lock again. Every unlock reconnects the environment socket.
- Watch the server trace for
GET /api/assets/...from the phone's user agent.
Expected behavior
Each attachment image is downloaded once and served from expo-image's disk cache afterwards. A reconnect, or a new signed URL for the same attachment, should not cost another download.
Actual behavior
Every reconnect re-downloads every visible attachment image in full. In one thread over one afternoon, one 0.5 MB photo (IMG_7302.jpg) was fetched 33 times by the same phone under about 25 different signed URLs. Across the thread: 176 image GETs, 124 distinct URLs, 13 distinct images, all 200, roughly 95 MB over Tailscale for images the phone had already shown.
Three things combine:
- Asset URLs are signed per mint with a one hour expiry (
ASSET_TOKEN_TTL_MS,apps/server/src/assets/AssetAccess.ts), so every mint is a new URL. - The client's
createUrlquery re-executes when the environment connection changes (createEnvironmentQueryAtomFamilyreads the connection atom,packages/client-runtime/src/state/runtime.ts), so each reconnect mints fresh URLs for every mounted image, well inside the 5 min stale / 30 min refresh / 60 min idle windows inpackages/client-runtime/src/state/assets.ts. MessageAttachmentImageinapps/mobile/src/features/threads/ThreadFeed.tsxpasses onlysource={{ uri }}to expo-image with nocacheKey, so a new URL is a disk-cache miss. The server sendscache-control: private, max-age=3600but noETagon bytes assets (apps/server/src/http.ts), so the request is always a full transfer.
Trace excerpt from the host (phone user agent T3Code/119), one three minute window. Each WS-TICKET / SESSION / SHELL group is an unlock; the image fetches that follow are the same photos each time:
19:24:16 WS-TICKET 19:25:01 WS-TICKET 19:26:27 WS-TICKET
19:24:17 SESSION 19:25:01 SESSION 19:26:28 SESSION
19:24:24 SHELL 19:25:07 SHELL 19:26:32 SHELL
19:24:27 IMG_7302.jpg RFs_as 19:25:07 IMG_7302.jpg GMWvJs 19:26:32 IMG_7302.jpg BxhWkU
19:24:27 IMG_7302.jpg GMWvJs 19:25:07 IMG_7302.jpg RFs_as 19:26:32 IMG_7302.jpg mjjzag
19:24:27 IMG_7303.jpg H8vA98 19:25:09 IMG_7302.jpg mjjzag 19:26:34 IMG_7302.jpg YQRung
19:24:27 IMG_7311.jpg XznlSk 19:25:09 IMG_7311.jpg Yab9TE 19:26:33 IMG_7311.jpg pMTNFA
... ... ...
(The suffix is the tail of the signed token. The same image also appears twice in the same second under two tokens, so two subscribers mint independently.)
Impact
Minor bug or occasional failure
It is a data and battery cost rather than a functional failure. On a photo-heavy thread the phone moved ~95 MB of already-seen images in one afternoon, decoded on screen each time. On cellular that is metered data.
Version or commit
Mobile TestFlight 2.0.0 (120). Server 0.0.46-nightly.20261012.3010. Code references checked against main at 511d20c6f2.
Environment
iPhone, iOS 26, Claude Agent provider. Host is Linux (Ubuntu) reached over Tailscale, not T3 Connect. Phone was on home wifi and cellular during the window.
Logs or stack traces
Counts above come from server.trace.ndjson on the host, filtered to the phone's user agent and url.path containing /api/assets/. Happy to share the raw span lines for the thread.
Screenshots, recordings, or supporting files
None needed; the trace is the evidence.
Workaround
None in the app. Not reconnecting is not an option on iOS, since the socket drops on lock.
Fix candidates
- Smallest: give expo-image a stable
cacheKeyderived from the attachment id (and a version if the file can change) inMessageAttachmentImage, so URL churn stops defeating the disk cache. One component. - Also worth doing: keep an unexpired signed URL across a reconnect instead of re-minting it, and send an
ETagon bytes assets so a conditional request can return304.
I can send a PR for the cacheKey change if the diagnosis holds.
Activity