Note
Grok responding on behalf of Julius.
Triage
Thanks for the timed captures and the proxy setup, @saphid! This is a real bug. I confirmed how it happens on t3code/rebuild-mobile-app-swift at bb01adbf90, the branch tip you named (open PR #5178). It only affects the experimental SwiftUI client. The React Native app we ship doesn't use this refresh loop.
To answer your question: a slow passive environment shouldn't hold up the others. B's new title should appear as soon as B's own shell request returns, and B's next poll should stay on its own schedule. A batch deadline set to the existing 6s shell timeout wouldn't fix that, since a peer that answers at 4.5s would still hold the batch. Treating a slow but successful response as a failure and backing it off would also be wrong, because it would stop updating a machine that's working.
What happens today
All passive environments share one loop in startAggregateRefresh. On each pass, it asks loadEnvironmentShells for every peer that isn't backing off after a failure. That function doesn't return until every peer's request has finished, and the snapshot is only published after that. Cached rows stay in place for a peer whose shell comes back empty, so the problem isn't missing data. Publishing is delayed, and so is the next pass.
The next pass runs after 5s if a shell changed or has active work, after 10s when the batch is idle, and after 20s once a shell comes back nil. That 20s backoff only applies after the whole batch returns, and only when shell == nil. So a peer that answers just under the 6s timeout never gets backed off and can add that delay to every cycle. The active environment uses its own websocket, so it isn't affected. The delay only hits the passive environments.
Two waits fall outside the 6s cap. I spotted both in the source but didn't measure either:
- For a T3 Connect environment,
refreshManagedCredentialruns beforeHTTP.swiftappliestimeoutInterval, so a slow relay can hold the batch longer than 6s. - The first catalogue probe on an uncached passive environment runs
serverConfig()after the shell, inside the same task.
Suggested fix
- Publish each passive environment as soon as its own shell request finishes.
- Keep the next poll of a peer that already answered on its usual 5s or 10s interval, without adding the slow peer's time.
- If a shell fails or times out, keep its cached rows, mark it disconnected, and use the existing 20s backoff. A successful response stays on the normal interval, no matter how long it took.
- Count the credential refresh toward that same per-environment timeout.
- Leave the active environment's websocket as it is. Cold-start hydration is out of scope.
This fits inside startAggregateRefresh and loadEnvironmentShells. Handle each result as soon as that peer's request finishes, and schedule the next pass based on the peers that have already answered.
Related
#10732 (closed PR) solved this by giving each environment its own worker. It was closed under the prior approval rule because it replaced the whole refresh loop without a triaged issue behind it, and this issue is that report. The fix above fits in the existing loop, so accepting this report doesn't mean adopting the worker split. #10761 (closed PR) covered the cold-start path, which is separate.