mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
Tor wedged across app restarts with ~87% of relay connections failing, and
neither existing recovery fired. Captured from the device:
guards.json default: 60 guards, 59 disabled, 1 unlisted -> 1 usable
Arti log AllGuardsDown { n_accepted: 0, n_rejected: 60 } x21,490
sockets 36,007 failures vs 5,276 successful opens
Everything above Tor degraded with it. Concord was the visible casualty: its
control-plane sync drained exactly 12 wraps on every launch and never folded, so
the Messages tab showed zero Concord rows for two restarts running.
Two recoveries exist and this state slipped between both:
- `ArtiGuardState.hasNoUsableGuards` required `usable == 0`. Its own KDoc claimed
"a single usable guard is enough to recover" — the device disproved it. One
survivor that is merely *unreachable* is useless for recovery but sufficient to
veto it, so the wipe never ran.
- `TorManager`'s watchdog only arms while status sits at Connecting. Tor had
bootstrapped: the SOCKS proxy was bound, `hasEverBootstrapped` was true, ~13% of
connections still worked, so status reached Active and the watchdog never fired.
It is built for "Tor never came up"; this is "Tor came up and its guards rotted".
Nothing observed the steady-state failure rate, so all three gates evaluated the
same way on every launch and `guards.json` carried the wedge forward forever.
1. Proportional disk rule. A sample of at least MIN_SAMPLE_TO_JUDGE_RATIO whose
usable guards fall under 1/USABLE_RATIO_DIVISOR of the total is wedged. Below
that size only the strict `usable == 0` rule applies, so a young sample Arti is
still filling is never wiped out from under a legitimate first bootstrap.
2. Runtime detector. `TorService` counts Arti's own AllGuardsDown log lines
(GUARDS_DOWN_THRESHOLD within GUARDS_DOWN_WINDOW_MS) and exposes
`TorBackend.guardsDownSignal`; `TorManager` routes it through the same
rate-limited self-heal to `resetWithCleanState()`. This is the general fix: it
believes Arti when it says every guard was rejected, so it fires even while
`guards.json` still looks healthy and status is Active — precisely the blind
spot between the two older heuristics. The count lives in the log callback
because that is the only place Arti surfaces it; the reset stays in TorManager,
which owns the cadence.
Verified on the wedged device. Next launch, unprompted:
W TorService: No usable Arti guards left on disk — wiping state to rebuild the guard sample
guard sample 60 total / 1 usable -> 20 total / 20 usable / 0 disabled
AllGuardsDown 21,569 -> 0
sockets 36,007 fail / 5,276 -> 444 fail / 232 open
Concord wraps 12, 12, 12 (pinned) -> 12 -> 37 -> 258
Concord rows 0 -> 4 and climbing
Tests cover the 59-of-60 field case, a small-sample false-positive guard, and a
healthy-majority sample. The pre-existing 22-guard "poisoned but not wedged"
fixture still passes, so the ratio does not regress the earlier variant.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>