fix(agent-ui): derive staleness from a direct check w/scheduled rerender#115344
Merged
Conversation
The refetchInterval callback called isTimestampStale directly (absolute wall-clock check) while the rendered isPolling/isTimedOut used the isStale state set by a useTimeout fired 90s after React first observed updated_at. When the observed timestamp was already old, the callback stopped polling before the state-based timeout fired, leaving isPolling=true and isTimedOut=false while no fetches were actually running. Have both call sites read from the same isStale state, and cancel the timeout when transitioning into the already-stale branch.
Contributor
📊 Type Coverage Diff✅ No new type safety issues introduced. Coverage: 93.49% |
trevor-e
approved these changes
May 12, 2026
`useSeerExplorerPolling` computed staleness two ways: the `refetchInterval` callback called `isTimestampStale(updated_at)` directly against wall-clock time, while the rendered `isPolling` / `isTimedOut` used an `isStale` state set by a `useTimeout` fired 90s after React first observed the timestamp. The two clocks could disagree, leaving the UI showing `isPolling=true` after polling had already halted. Drop the `isStale` state. Both the callback and the render-side `getPollingState` now evaluate `isTimestampStale(updated_at)` directly at their respective evaluation times, so they can't diverge. To handle the case where `updated_at` doesn't change across polls (so React Query's structural sharing suppresses re-renders), schedule a single timer for the exact moment the timestamp would cross `STALE_TIME_MS` — its only job is to force a re-render so the returned state catches up. Extend `useTimeout` so `start()` accepts an optional `overrideTimeMs`, used to fire the timer at the correct calibrated remaining time. Other callers ignore the new argument.
Replace `isTimestampStale` (boolean) with `getTimestampAge` (number | null) and pull the staleness comparison into `getPollingState`. The `refetchInterval` callback and the render-side call now invoke `getPollingState` with the same four arguments, making divergence structurally impossible. Add a +1ms buffer to the stale-timer scheduling to absorb sub-millisecond setTimeout drift, matching React Query's own approach for its stale-time timer.
Opt in to `refetchOnWindowFocus` for the polling query so that returning to the tab triggers an immediate refetch instead of waiting for the next poll tick (or for polling to resume after focus). Project-wide default is `false`, so this only affects this query.
nikkikapadia
pushed a commit
that referenced
this pull request
May 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
useSeerExplorerPollingderivesisPolling/isTimedOutfor callers and separately drives theuseApiQueryrefetchInterval. Previously these two paths used different staleness signals and could disagree — the renderedisPollingstayedtrueafter polling had already halted.Old setup:
refetchIntervalcalledisTimestampStale(updated_at)directly — a wall-clock check.isPolling/isTimedOutderived from anisStaleReact state set by auseTimeoutfired 90s after React first observedupdated_at.When the observed timestamp was already partially old (e.g., server stops bumping
updated_at), the callback's wall-clock check tripped before the React-timeuseTimeoutfired, leaving a window where the network had stopped polling but the UI still rendered as if it were.This change drops the
isStalestate entirely. Both the callback and the render-sidegetPollingStatenow evaluateisTimestampStale(updated_at)againstDate.now()at their respective call sites, so they can't diverge.The one wrinkle: when
updated_atdoesn't change across polls, React Query'sstructuralSharingkeeps thedatareference identical and suppresses re-renders, so a pure render-time check would never re-evaluate. To force the render-side transition at the right moment, schedule a single timer calibrated to fire when the timestamp would crossSTALE_TIME_MS. Its only job is to bump a render-counter — no state coupling.Extended
useTimeoutto letstart()take an optionaloverrideTimeMs, so we can pass the dynamically computed remaining time. Other callers are unaffected.Also opts the polling query into
refetchOnWindowFocus: trueso the session refetches immediately when the user returns to the tab, instead of waiting for the next poll tick. Project-wide default isfalseso this only affects this query.