Skip to content

fix(watch): keep no-farmable grace alive across transient active-drop blips#69

Open
rxm96 wants to merge 1 commit into
mainfrom
fix/watch-engine-no-farmable-flicker
Open

fix(watch): keep no-farmable grace alive across transient active-drop blips#69
rxm96 wants to merge 1 commit into
mainfrom
fix/watch-engine-no-farmable-flicker

Conversation

@rxm96

@rxm96 rxm96 commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Symptom

The engine status panel could get stuck on "Watching, but target currently not watchable" for a very long time (observed: 1h17m) with SUPPRESSION none, COOLDOWNS none, still actively "Watching", and a frozen no-progress tracker showing 0 recovery attempts. A full app refresh cleared it — i.e. it was a wedged runtime state, not expected parking.

Root cause

Watchability is decided by drop state, not channels, so a target with only claimable/blocked/upcoming drops is correctly canWatchTarget = false. When that happens while watching, decideWatchingNoFarmable is supposed to wait out NO_FARMABLE_DROP_GRACE_MS (30s) and then give up: cooldown + retarget + stop + stall-stop. That pure logic is correct and already tested.

The bug is in the executor useStallRecovery: it cleared the no-farmable grace marker (noFarmableDropRef.current = null) on every pass through the no-progress recovery branch — i.e. whenever activeDropInfo was momentarily present. If the active-drop selection oscillates (e.g. an upcoming/unlinked drop surfacing for a tick while canWatchTarget stays false), the marker is reset on each blip, the 30s grace never completes, the give-up path never fires, and the stale stall tracker keeps showing 0 attempts.

This matches all observed symptoms: no give-up, no cooldown, no suppression, frozen tracker.

Fix

Preserve the grace marker across a transient activeDropInfo while the target stays unwatchable; clear it only once the target is genuinely watchable again (canWatchTarget === true). Extracted as the pure reconcileNoFarmableOnActiveDrop(marker, canWatchTarget) and wired into the executor in place of the unconditional reset.

Entry condition of the no-farmable branch is intentionally left on !activeDropInfo (not !canWatchTarget) so the existing channel-switch recovery — for the case where watchable drops exist but not on the current channel — is preserved.

Tests

TDD: added a failing regression first, then the fix.

  • reconcileNoFarmableOnActiveDrop units: preserve while unwatchable, clear when watchable, null-safe.
  • End-to-end flicker regression: seed grace → active-drop blip (marker must survive) → grace elapsed → give-up (stop-watching + resetStallTracking) fires.

npm test (524 passing), npm run typecheck (both tsconfigs), prettier + eslint on changed files: all clean.

Notes

Found via a user-reported screenshot. Workaround until merged: stop/restart watching (or toggle the master switch) re-initializes the refs.

🤖 Generated with Claude Code

… blips

When watching a target whose drops are not watchable (claimable/blocked/
upcoming), useStallRecovery cleared the no-farmable grace marker on every
pass through the no-progress recovery branch -- i.e. whenever activeDropInfo
was momentarily present. An oscillating active-drop signal therefore kept
restarting the 30s grace, so decideWatchingNoFarmable's give-up path
(cooldown + retarget + stop + stall-stop) never fired and the engine wedged
in "watching, but target currently not watchable" indefinitely, with a
frozen no-progress tracker.

Preserve the grace marker across a transient activeDropInfo while the target
stays unwatchable; clear it only once canWatchTarget is true again. Extracted
as the pure reconcileNoFarmableOnActiveDrop() and unit-tested, including the
flicker regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant