A stall spanning the 32-bit millis() rollover zero-extends to a value smaller
than the pre-wrap start, so the now_ms >= start_ms guard blocked the bail until
the counter lapped the ~49.7-day start value. Use 32-bit unsigned subtraction,
which measures true elapsed time across the wrap. Regression: a stall starting
near the counter max and wrapping bails in bounded ticks.
Three findings on the transient-stall bail, all valid:
- Reload invalidation: a bail recorded BYPASS, but NomadNetCacheFlow::service()
accepts only MISS as a successful invalidation, so a bail during an admitted
reload reported 'Page cache invalidation failed' instead of falling through
to a live fetch. The bail now records MISS.
- Open-resource leak: the bail cleared read_open_/write_open_ (and abandoned an
open directory enumeration) without calling endRead()/abortWrite()/endList(),
leaking SD handles. The bail now releases each in-flight resource via the
seam's own teardown (bounded best-effort; a still-transient close is
accepted rather than re-pinning the op).
- Tick-vs-time: the not-ready UNAVAILABLE path returns immediately (no bus
wait), so a pure 500-tick budget could expire during a legitimate SD mount
window and disable caching for the whole session. The bail is now gated on
BOTH the tick floor AND a 10s wall-time window (service() takes a monotonic
ms clock; production passes millis(), 0 is a safe default for tests).
Regression tests: reload-invalidation bail -> NEED_LIVE (flow), flat-clock does
not bail, healable transient keeps authority, list-open (RECOVERY_END) stall
bails and releases the handle (cache).
The SD page cache (16af3b5) made the SD card a soft dependency of
NomadNet page loads, but its step machine retries a transient storage
result (BUSY = SPI-mutex timeout, UNAVAILABLE = card not mounted)
forever with no budget. On a persistently unhealthy seam the boot-time
recovery pins operation_ != NONE, the flow stays LOOKUP, and the UI
freezes at 'Checking SD page cache...' (the CACHE state has no
deadline, unlike every other NomadNet state).
NomadNetCache::service() now compares each call's entry state to the
previous call's. Any advance (op, offset, scan/cleanup index, scan
count, open-flags) resets a stall counter, so slow-but-progressing
steps (chunked 64 KB transfers, up to 96-record directory scans) never
false-trip; a no-progress tick is a transient stall. Past 500
consecutive no-progress ticks (far beyond any real SPI contention or
SD mount window) the cache bails: mark the namespace non-authoritative
for the session (lookups/commits bypass) and clear the op, so the flow
falls through to a live fetch -- the pre-cache page-load behavior.
Regression tests: cache-level (persistent UNAVAILABLE recovery bails;
lookup bypasses after bail) and flow-level (a permanently BUSY
beginList no longer parks the flow in LOOKUP; it reaches NEED_LIVE in
bounded ticks). Both fail on the pre-fix code and pass with it.
Verified: tdeck firmware build SUCCESS; tests/native 121 passed (3
pre-existing env failures, fail identically on origin/main baseline);
tests/build_scripts 184 passed.