The button ran a full sync tick inline on the Tk thread, so the fullscreen
lock was dead for its whole duration -- ~18-27s measured against the live
remote, and unbounded in the worst case.
Two changes, and the second is what makes the first cheap:
* the pull moves to a daemon worker thread, with the result marshalled back
through a queue polled on the Tk thread. The worker touches no widget, by
construction: Tcl is not thread-safe and that includes `after`. Polling
also makes the completion path testable with no thread at all.
* the button now runs the *narrow* pull. Its question is identical to the
gate's -- "did a peer log this slot?" -- and `_reconcile_after_fetch`
reads only `due_slots()`, so the budget and food banks it used to fetch
were never consulted here. The full tick still runs after the window
closes. Measured 84ms against the live remote.
Narrowing also shrinks the write race from tens of seconds to imperceptible:
the pull persists its merge, so a meal logged mid-fetch could be overwritten
by a snapshot taken before it. A second click while a fetch is in flight is
now ignored, which is the reentrancy guard and the race fix in one.
That guard started out as a disabled button, which is how it surfaced a
latent bug worth recording: the fetch button widget is never stored on
`GateWidgets`, so `self._widgets.fetch_button` did not exist and the disable
was a silent no-op -- hidden because the suppression tuple included
`AttributeError`. Coverage would have reported 100% on a guard that did
nothing. Guarding on `_fetch_result` needs no widget at all.
Also adds `INTERACTIVE_TIMEOUT_SECONDS = 2.0` for the paths a user waits on.
`SYNC_TIMEOUT_SECONDS` only ever reached GitHub: crdt_sync v0.6.0's
`firebase_client_for` takes no timeout, so Firebase used its 15s default and
one hung session refresh stalled the lock window -- the journal has five such
refresh failures in five days. Verified to fail closed to the local decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJ5YnA3CERitQ57HjMu2Qz