Before submitting
Area
apps/server, apps/mobile
Environment
- Server: t3
0.0.29-nightly.20260725.899, headless t3 serve under a systemd user unit, Debian 13 (LXC), Node 24 (NodeSource)
- Client: T3 Code iOS (TestFlight), build
T3Code/18 CFNetwork/3892.100.1 Darwin/27.0.0
- Network for the failing case: iPhone → WireGuard → home LAN, ~15–17 KB/s observed goodput (airplane Wi-Fi). Note the failure also reproduced consistently for 1.5h+ before we measured; LAN retest pending.
- Provider in the affected thread: Claude Code (claudeAgent)
Summary
A Claude Code session in a thread used the Read tool on PNG screenshots. The server stored the entire base64 image payload (~370 KB each) in the thread's activity records — and stored it twice per tool call (tool.updated + tool.completed), plus again in orchestration_events. Four image reads left the thread with ~2.6 MB of inline base64 in projection_thread_activities (~5.9 MB counting the event store).
From the moment the first image activity was written, the iOS app could no longer open that thread over a slow link (~15 KB/s: airplane Wi-Fi → WireGuard): "syncing messages" for ~5 seconds, then "failed to connect", retrying forever. All other (small) threads on the same server opened fine, and the same thread opened instantly once the client was on normal cellular bandwidth. Relaunching the app, re-pairing, and rebooting the server host made no difference while on the slow link.
Timeline evidence (server trace)
ws.rpc.orchestration.subscribeThread subscriptions were healthy all day (one lived 7 hours). The failure loop begins the same minute the first oversized activity is written:
17:42:28 subscribeThread lived 161.8s (healthy)
17:45:17.791Z tool.updated + tool.completed written, 369,292 / 369,270 bytes
(full base64 PNG from Read tool)
17:45:13 subscribeThread dies after 15.6s <- loop begins
17:45:33 dies after 12.0s
... retried roughly every 30s, indefinitely ...
Three more image reads (210 KB, 360 KB, 360 KB pairs) followed by 17:47:24.
Packet capture (who closes the connection)
Captured on the server during a clean repro (relaunch app → use a good thread OK → open bad thread):
- TCP handshake,
/.well-known/t3/environment, auth, /api/auth/websocket-ticket, and the /ws upgrade all succeed (HTTP 200, ws auth verified server-side).
- Server streams the thread sync; after ~8 s and only ~118 KB delivered, the app sends RST on all of its connections simultaneously and starts over. Server-side the subscription fibers show
Interrupted when the dead socket is noticed.
So the server stays healthy; the client hard-aborts during the sync stream.
Confirmed bandwidth-dependent: on a normal cellular connection the same thread
opened immediately with no errors. The app is not choking on the payload itself —
its sync watchdog (~5 s to first render) gives up on a multi-MB uncompressed sync
stream that a slow link cannot deliver in time, then retries from scratch forever
(no resume), so the thread is effectively unopenable for as long as the client is
on a slow link.
Contributing factors
- The websocket has no
permessage-deflate, so ~2.6 MB of base64 (highly compressible) crosses the wire uncompressed. On a slow link the sync cannot finish quickly even when the client is patient.
- Payloads are duplicated: identical base64 in
tool.updated and tool.completed per call, and again in the event store.
Sample activity payload (sanitized)
{
"itemType": "dynamic_tool_call",
"status": "inProgress",
"detail": "Read: {\"file_path\":\".../05-post-login.png\"}",
"data": {
"toolName": "Read",
"input": { "file_path": ".../05-post-login.png" },
"result": {
"tool_use_id": "toolu_01YQ...",
"type": "tool_result",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"data": "iVBORw0KGgoAAAANSUhEUgAABaAA...[TRUNCATED, original length 368,872 chars]",
"media_type": "image/png"
}
}
]
}
}
}
Suggested fixes
- Don't inline image tool results into activity payloads — store as an asset and reference it (the assets/createUrl RPC already exists), or stub/truncate image content in the projection.
- Deduplicate:
tool.completed need not carry the same payload as tool.updated.
- Enable
permessage-deflate on the ws — base64 JSON compresses ~10x.
- Client: make the sync watchdog proportional to progress (data is flowing, just slowly) and/or resume partial syncs instead of restarting from zero on every retry — currently a thread whose sync can't complete inside ~5 s is unopenable on that link.
Related
Happy to provide sanitized trace excerpts or a capture summary, or run a patched nightly to verify.
Before submitting
Area
apps/server, apps/mobile
Environment
0.0.29-nightly.20260725.899, headlesst3 serveunder a systemd user unit, Debian 13 (LXC), Node 24 (NodeSource)T3Code/18 CFNetwork/3892.100.1 Darwin/27.0.0Summary
A Claude Code session in a thread used the
Readtool on PNG screenshots. The server stored the entire base64 image payload (~370 KB each) in the thread's activity records — and stored it twice per tool call (tool.updated+tool.completed), plus again inorchestration_events. Four image reads left the thread with ~2.6 MB of inline base64 inprojection_thread_activities(~5.9 MB counting the event store).From the moment the first image activity was written, the iOS app could no longer open that thread over a slow link (~15 KB/s: airplane Wi-Fi → WireGuard): "syncing messages" for ~5 seconds, then "failed to connect", retrying forever. All other (small) threads on the same server opened fine, and the same thread opened instantly once the client was on normal cellular bandwidth. Relaunching the app, re-pairing, and rebooting the server host made no difference while on the slow link.
Timeline evidence (server trace)
ws.rpc.orchestration.subscribeThreadsubscriptions were healthy all day (one lived 7 hours). The failure loop begins the same minute the first oversized activity is written:Three more image reads (210 KB, 360 KB, 360 KB pairs) followed by 17:47:24.
Packet capture (who closes the connection)
Captured on the server during a clean repro (relaunch app → use a good thread OK → open bad thread):
/.well-known/t3/environment, auth,/api/auth/websocket-ticket, and the/wsupgrade all succeed (HTTP 200, ws auth verified server-side).Interruptedwhen the dead socket is noticed.So the server stays healthy; the client hard-aborts during the sync stream.
Confirmed bandwidth-dependent: on a normal cellular connection the same thread
opened immediately with no errors. The app is not choking on the payload itself —
its sync watchdog (~5 s to first render) gives up on a multi-MB uncompressed sync
stream that a slow link cannot deliver in time, then retries from scratch forever
(no resume), so the thread is effectively unopenable for as long as the client is
on a slow link.
Contributing factors
permessage-deflate, so ~2.6 MB of base64 (highly compressible) crosses the wire uncompressed. On a slow link the sync cannot finish quickly even when the client is patient.tool.updatedandtool.completedper call, and again in the event store.Sample activity payload (sanitized)
{ "itemType": "dynamic_tool_call", "status": "inProgress", "detail": "Read: {\"file_path\":\".../05-post-login.png\"}", "data": { "toolName": "Read", "input": { "file_path": ".../05-post-login.png" }, "result": { "tool_use_id": "toolu_01YQ...", "type": "tool_result", "content": [ { "type": "image", "source": { "type": "base64", "data": "iVBORw0KGgoAAAANSUhEUgAABaAA...[TRUNCATED, original length 368,872 chars]", "media_type": "image/png" } } ] } } }Suggested fixes
tool.completedneed not carry the same payload astool.updated.permessage-deflateon the ws — base64 JSON compresses ~10x.Related
Happy to provide sanitized trace excerpts or a capture summary, or run a patched nightly to verify.