Skip to content

fix(agent-ui): derive staleness from a direct check w/scheduled rerender#115344

Merged
aliu39 merged 7 commits into
masterfrom
aliu/timeout-precision
May 12, 2026
Merged

fix(agent-ui): derive staleness from a direct check w/scheduled rerender#115344
aliu39 merged 7 commits into
masterfrom
aliu/timeout-precision

Conversation

@aliu39
Copy link
Copy Markdown
Member

@aliu39 aliu39 commented May 11, 2026

useSeerExplorerPolling derives isPolling / isTimedOut for callers and separately drives the useApiQuery refetchInterval. Previously these two paths used different staleness signals and could disagree — the rendered isPolling stayed true after polling had already halted.

Old setup:

  • refetchInterval called isTimestampStale(updated_at) directly — a wall-clock check.
  • Returned isPolling / isTimedOut derived from an isStale React state set by a useTimeout fired 90s after React first observed updated_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-time useTimeout fired, leaving a window where the network had stopped polling but the UI still rendered as if it were.

This change drops the isStale state entirely. Both the callback and the render-side getPollingState now evaluate isTimestampStale(updated_at) against Date.now() at their respective call sites, so they can't diverge.

The one wrinkle: when updated_at doesn't change across polls, React Query's structuralSharing keeps the data reference 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 cross STALE_TIME_MS. Its only job is to bump a render-counter — no state coupling.

Extended useTimeout to let start() take an optional overrideTimeMs, so we can pass the dynamically computed remaining time. Other callers are unaffected.


Also opts the polling query into refetchOnWindowFocus: true so the session refetches immediately when the user returns to the tab, instead of waiting for the next poll tick. Project-wide default is false so this only affects this query.

aliu39 added 2 commits May 11, 2026 14:56
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.
@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label May 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 11, 2026

📊 Type Coverage Diff

✅ No new type safety issues introduced. Coverage: 93.49%

@aliu39 aliu39 marked this pull request as ready for review May 11, 2026 22:43
@aliu39 aliu39 requested a review from a team as a code owner May 11, 2026 22:43
@aliu39 aliu39 changed the title fix(seer-explorer): Reconcile mismatched polling states fix(agent-ui): Reconcile mismatched polling states May 11, 2026
@aliu39 aliu39 marked this pull request as draft May 12, 2026 03:37
aliu39 added 2 commits May 11, 2026 20:38
This reverts commit 81d297e and the follow-up comment tweak
f010563. The staleness-mismatch fix didn't account for the prod
3-poll-stop scenario and a different refactor is forthcoming.
`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.
@aliu39 aliu39 changed the title fix(agent-ui): Reconcile mismatched polling states fix(seer-explorer): Drive staleness from a single direct check May 12, 2026
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.
@aliu39 aliu39 marked this pull request as ready for review May 12, 2026 04:07
@aliu39 aliu39 requested a review from a team as a code owner May 12, 2026 04:07
@aliu39 aliu39 changed the title fix(seer-explorer): Drive staleness from a single direct check fix(agent-ui): derive staleness from a direct check w/scheduled rerender May 12, 2026
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.
@aliu39 aliu39 merged commit 7dcc406 into master May 12, 2026
75 checks passed
@aliu39 aliu39 deleted the aliu/timeout-precision branch May 12, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants