Skip to content

Governor pause/resume UI state can be silently reverted by a stale poll response racing the action #7791

Description

@JSONbored

Context

LedgersPage (apps/loopover-miner-ui/src/routes/ledgers.tsx:429-449, consumed by GovernorControlSection at :465-468) reads governor pause state from two independent sources with no ordering guard between them:

const { result: polledPauseState } = usePolledFetch(loadGovernorPauseState, pollIntervalMs);
if (polledPauseState !== lastPolledPauseState) {
  setLastPolledPauseState(polledPauseState);
  setPauseState(polledPauseState);          // render-time sync, no ordering guard
}

const runGovernorAction = (action: () => Promise<GovernorPauseStateResult>) => {
  setActionPending(true);
  void action().then((next) => {
    setPauseState(next);                    // independent write, races the poll above
    setActionPending(false);
  });
};

pauseGovernor/resumeGovernor (the Pause/Resume buttons in GovernorControlSection) is a completely independent POST from the usePolledFetch(loadGovernorPauseState, …) GET poll — they don't share usePolledFetch's single-flight guard. If a poll GET is already in flight when the operator clicks Pause, and the POST resolves faster (plausible — it's a small local write vs. a read that may compete for the same SQLite file), the poll's stale pre-pause response can land after the action's fresh result and clobber it. The operator sees the UI revert to "Not paused" even though the governor is actually paused server-side, until the next poll tick self-corrects (up to DEFAULT_POLL_INTERVAL_MS, 10s). Same issue in reverse for Resume.

Verified empirically by porting the exact state-transition logic into a standalone Node script with realistic async interleaving (poll GET takes 180ms, action POST takes 50ms, action fires 20ms after the poll starts):

[action]      pauseState <- action result: paused=true
[render-sync] pauseState <- polled: paused=false
Final client pauseState.paused: false
Actual server paused state:       true
MISMATCH

This is a UI display bug only — it does not change what the governor's own pause/resume logic actually does server-side, only how quickly/reliably the dashboard reflects it.

Untested branch: apps/loopover-miner-ui/src/ledgers.test.tsx has separate tests for clicking Pause and for the polling cadence, but nothing exercises the two concurrently — the exact interleaving that triggers this bug is uncovered.

Requirements

Add an ordering/generation guard so a poll response that started before the most recent action cannot overwrite that action's result — mirror the cancellation/generation discipline use-polled-fetch.ts and use-streaming-text.ts already use internally for their own guards. Do not change how pauseGovernor/resumeGovernor themselves work server-side — this is a client-side state-ordering fix only.

Deliverables

  • LedgersPage's governor pause-state sync in apps/loopover-miner-ui/src/routes/ledgers.tsx ignores a poll response that resolves after a more recent action's result has already been applied.
  • A regression test in ledgers.test.tsx exercising the exact interleaving from the bug report (action resolves before an in-flight poll) and asserting the UI reflects the action's result, not the stale poll.

Test Coverage Requirements

apps/loopover-miner-ui is not covered by the src/** 99% patch gate - the new regression test is this issue's own coverage deliverable.

Expected Outcome

Clicking Pause or Resume on the governor never has the button/state visually revert due to a stale, already-superseded poll response landing after the action's own result.

Links & Resources

apps/loopover-miner-ui/src/routes/ledgers.tsx:429-449,465-468, apps/loopover-miner-ui/src/lib/use-polled-fetch.ts (the cancellation/generation pattern to mirror), apps/loopover-miner-ui/src/routes/portfolio.tsx's runQueueAction (a partial precedent — routes its refresh through the same poll instance rather than a second state write, though not fully race-proof either)

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions