From bb569d69a282f1a09426f2a6bd3c9dbf6a7dfede Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Mon, 28 Sep 2026 21:40:02 -0400 Subject: [PATCH 1/2] fix: a stalled server no longer reads as "your clock is ahead" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Incident 2026-09-29: the homeserver's host ran out of memory and stalled for ~2 minutes. The /sync that finally went out carried events whose `age` was computed ~30 s before it arrived, so every client showed "Your computer's clock is 30 seconds ahead of the server" while the real problem was the server (all host clocks were within 0.25 s the whole evening). The skew estimate was the median of the last 5 samples, and a sample is local skew + delivery delay, so one late /sync with a handful of events tripped it. - Estimate = the LOWEST sample of the last 5 minutes: delay only ever adds, so the fastest-delivered event is the truest. - "Behind" (which a delay can't cause) is reported as soon as there are 3 samples, like before. "Ahead" must hold across samples received at least a minute apart, so a single late burst never trips it. - Samples are aged on the monotonic clock, and a change of the local clock (someone fixing it) resets the measurement, so the warning clears at once. - Only events stamped by our own homeserver are sampled: a federated event's origin_server_ts is the other server's clock. - Wording: "This device's clock is … Voice calls and encrypted messages can fail until it's corrected." / call bar "Device clock … : calls may fail" (was "will fail"). Unit tests: the incident (late burst after normal traffic, and a fresh client whose first samples are all late), mixed slow/fast deliveries, ahead only after a minute, behind at once, hysteresis, clock fixed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/features/call-status/CallStatus.tsx | 4 +- src/app/pages/client/ClientNonUIFeatures.tsx | 4 + src/app/pages/client/ClockSkewBanner.tsx | 6 +- src/app/utils/clockSkew.test.ts | 103 ++++++++++++++----- src/app/utils/clockSkew.ts | 67 ++++++++++-- 5 files changed, 141 insertions(+), 43 deletions(-) diff --git a/src/app/features/call-status/CallStatus.tsx b/src/app/features/call-status/CallStatus.tsx index 37bd9cfb4..4b4be1543 100644 --- a/src/app/features/call-status/CallStatus.tsx +++ b/src/app/features/call-status/CallStatus.tsx @@ -72,9 +72,9 @@ export function CallStatus({ callEmbed }: CallStatusProps) { size="T200" truncate style={{ color: color.Warning.Main }} - title="Fix your computer's clock — calls and encryption depend on it" + title="This device's clock is off. Calls and encryption depend on it: turn on automatic time in your system settings." > - Clock {describeSkewVsServer(clockSkew.skewMs)} — calls will fail + Device clock {describeSkewVsServer(clockSkew.skewMs)}: calls may fail )} diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx index 95ea1bc44..79e6a4d73 100644 --- a/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -1071,6 +1071,10 @@ function ClockSkewFeature() { data, ) => { if (!data.liveEvent) return; + // Only events our homeserver stamped: a federated event's + // origin_server_ts is the other server's clock. + const senderServer = mEvent.getSender()?.split(':').slice(1).join(':'); + if (senderServer !== mx.getDomain()) return; monitor.sample(mEvent.getTs(), mEvent.getAge(), mEvent.localTimestamp); }; mx.on(RoomEvent.Timeline, onTimeline); diff --git a/src/app/pages/client/ClockSkewBanner.tsx b/src/app/pages/client/ClockSkewBanner.tsx index 0c39d3f7d..ae86ea8c0 100644 --- a/src/app/pages/client/ClockSkewBanner.tsx +++ b/src/app/pages/client/ClockSkewBanner.tsx @@ -19,7 +19,7 @@ const readDismissedUntil = (): number => { }; /** - * [Gitea #158] "Your computer's clock is 14 minutes ahead of the server." + * [Gitea #158] "This device's clock is 14 minutes ahead of the server." * Same slot and style as the sync banners. Shown while the skew monitor is * over its threshold; the direction matters, so it is said. Dismissable for * 24 h; never auto-corrects anything. @@ -53,8 +53,8 @@ export function ClockSkewBanner() { > - Your computer's clock is {describeSkewVsServer(skewMs)}. Encrypted messages - and voice calls will fail until it is fixed. + This device's clock is {describeSkewVsServer(skewMs)}. Voice calls and + encrypted messages can fail until it's corrected.