- Shipped
- September 6, 2026 at 12:55 AM UTC
- Author
- Kamo
- Commit
- f68bcce
The last way a member could lose a live call and everything in their tool windows without doing anything wrong. `authenticatedFetch` answered EVERY 401 with `window.location.href = '/logout'` — from seventy-one call sites, with no check that the session had actually ended. That is the heaviest thing this app can do from the client: it tears the document down, which ends the SIP session living in it, and it deletes the shared Redis session that the member's other tabs are still working in. A 401 is simply the commonest way a server says "not this request"; it is not evidence that the session is over. The two code paths either side of it already knew that and this one did not. `sessionMonitor` requires two consecutive 401s before calling a token rejected, and `SessionManager`'s expiry warning probes /api/session/info and signs out only on 404, 401 or a TTL at zero, on the stated grounds that "a transient network blip or 5xx must never sign out an active user". This makes the third agree with them: - A 401 now confirms before it redirects, using that same probe. 404, 401 or TTL<=0 means gone and the logout proceeds exactly as before. A 5xx, an offline probe, or a live session means the endpoint refused this one request, which is handed to the caller as an ordinary `Unauthorized` failure instead of being answered by destroying the tab. - A tab holding no token of its own is never signed out on a 401, for the reason sessionMonitor spells out for its own branch: it was never authenticated HERE, so its 401 means "no credentials in this tab", and redirecting would delete a Redis session its siblings are still using. - The probe is shared while in flight. A page that fires six authenticated requests at once gets six 401s at once, and that is one question, not six. - useEntitlements' `isAbandoned` still reads "Session expired" and therefore still means "the document is leaving"; a live-session 401 now falls through to the loader's ordinary retry and self-heal, which is what should always have happened to it. And pin the call protections, which nothing was holding in place: A SIP session lives in this document and nothing on the server holds a handle to it, so a teardown ends the call outright. Five surfaces stand between a call and that, and every one is an effect inside a large file — the beforeunload confirm, the activity heartbeat that keeps the TTL alive while somebody is listening rather than typing, the pagehide BYE for when a teardown is unavoidable anyway, the tool-window close veto, and now the 401 confirmation. liveCallSurvival.test pins all five, and fails when any is removed (verified by removing three). Source greps rather than behavioural tests, deliberately: vitest.config.ts collects app/lib, app/components, app/patients, app/stores, app/network and app/utils — NOT app/contexts or app/hooks, where three of the five live. A test written beside them would be collected by nothing and pass by never running. 456 test files pass; eslint is clean on every file touched.