ActivityAnalyser), using server-authoritative time to decide when a refresh is actually due — not the browser's local clock.rokojori-auth's /api/auth/refresh deletes the used token then issues a new pair (routes/auth.ts → issueTokenPair). When Roject's editor fires several parallel API calls right as the access token expires, each one independently triggers jwtMiddleware's silent refresh (auth-connector/source/server/auth.ts → tryRefresh) using the same refresh-token cookie; the first call wins and rotates it, every other call already in flight gets a 401 against the now-deleted token and force-redirects to login even though the session is fine. Electron: electron/main.ts only ever refreshes once, in the startup setTimeout block. Nothing refreshes the Bearer token again for the rest of the running session, so once ACCESS_TOKEN_TTL (1h default) elapses, every subsequent request silently sends an expired token until the app is restarted.source/auth/TokenUpdater.ts: a periodic timer every 5 minutes plus an immediate check on ActivityAnalyser.onActive (covering tab/window resume), each ping hitting the cheap GET /api/auth/me. Turned out the client-side exp/server-time-offset comparison originally planned here doesn't apply to the browser flow at all — the access token cookie is httpOnly, so client JS can never read its exp in the first place. The proactive decision moved server-side instead: jwtMiddleware (auth-connector/source/server/auth.ts) now rotates the cookie once the token is within PROACTIVE_REFRESH_MARGIN_SEC (15 min default) of its real, server-signed expiry — using the server's own clock, trivially authoritative, no offset math needed. The Updater's only job on the browser side is making sure a request happens often enough for the server to act on; it exposes an EventSlot-driven valid | refreshing | expired | network-error state that editor-shell currently reacts to directly (redirect to login on expired, console.warn on network-error) until Phase 3 routes this through the shared guarded-call wrapper instead. The exp/server-time-offset comparison as originally described still applies as designed — to Electron (Phase 5/7), where the token is a Bearer value actually held in the main process, not hidden behind a cookie. No reactive retry-on-401 fallback — deliberately skipped, tracked as a Nice To Have in the backlog if the 5-minute margin ever proves too wide.rokojori-auth already supports multiple parallel sessions — every login creates an independent refresh-token row (refreshTokens.create) without invalidating others. The problem is that within one session, several writers can share the same refresh token and race each other. Browser tabs in the same profile share one cookie jar, so they're literally the same session; fix via leader election with the Web Locks API — confirmed as the approach — one tab holds an exclusive lock and runs the updater, others follow via BroadcastChannel. Electron deliberately will not get a single-instance lock — multiple projects need to run in parallel, each as its own instance, and each mints its own fully independent session (no shared tokens.json, nothing to race over). To keep this transparent instead of showing a login screen per instance: every running instance writes its current access token plus a timestamp to a shared heartbeat file every 10s (piggybacking on the updater's tick). A newly-starting instance checks that file on launch — if it was written within the last ~30s, it POSTs that token to a new requireAuth-guarded endpoint, /api/auth/new-session, which mints a brand-new independent token pair for the same user via issueTokenPair (the same call /login already uses, just triggered by an existing valid access token instead of a password). The new instance now owns its own refresh token from the start — never shared, never racing. If the heartbeat is stale or the mint call fails, it falls through to the normal login screen. This also replaces the plaintext-password auto-login currently in main.ts (saveLastPassword/loadLastPassword) with something safer — proof of a live session instead of a stored secret.rokojori-auth/routes/auth.ts, so a near-simultaneous second refresh call still succeeds instead of hard-401ing. This is the baseline correctness guarantee; client-side leader election (Web Locks) is only an optimization on top to reduce how often that grace window gets exercised.console.warn so failures stay debuggable instead of vanishing silently. None of the three tiers retry against a confirmed-dead session (expired state) — only against transient refreshing/network-error states. Calls should also check the shared auth/network state proactively before firing, not just react to a failed response.rokojori-auth so concurrent refresh calls stop hard-failing — fixes the browser race outright, independent of any client changes. 2. Build the central Token Updater (periodic loop + ActivityAnalyser.onActive + server-time offset), replacing jwtMiddleware's silent per-request refresh and electron/main.ts's one-shot startup refresh. 3. Refactor apiFetch and Electron's request path into the shared guarded-call function with the three-tier retry policy. 4. Add Web Locks-based leader election across browser tabs, with BroadcastChannel token sharing. 5. Move the Electron updater into the main process, no single-instance lock — each project runs as its own instance with its own independent session. 6. Add POST /api/auth/new-session to rokojori-auth (mints a fresh token pair from an existing valid access token). 7. Electron: write a heartbeat file (current access token + timestamp) every 10s from the updater tick; on startup, if the heartbeat is ≤ 30s old, silently mint a new session from it instead of showing the login screen; retire the plaintext last-password.txt auto-login in favour of this.Date response header once, diff against local Date.now(), cache the offset, use localNow + offset for all expiry comparisons — ties directly into the existing clock-skew task instead of duplicating it. ActivityAnalyser (library-ts/browser/dom/ActivityAnalyser.ts) needs an OnVisibilityChange listener added alongside its existing focus/blur/mouse/touch set, since switching tabs within one window doesn't fire window focus/blur at all. Shared auth state should be an EventSlot-driven enum (valid | refreshing | expired | network-error) that both the updater and the guarded-call wrapper read and write, following existing project convention — no ad-hoc event buses.console.warn; Electron runs multiple concurrent instances with no single-instance lock, each minting its own independent session transparently via the heartbeat + /api/auth/new-session mechanism described under Multi-Session Coordination. One deliberately deferred hardening note for later: the heartbeat bootstrap currently reuses the general-purpose access token rather than a narrow-scope, short-lived bootstrap token — acceptable for now since it's no weaker than the existing on-disk tokens.json, but worth revisiting if the security surface needs tightening later.