@@ -165,10 +165,6 @@ pollTimeoutMs = 25000
165presenceRefreshInterval : Int<Ms>
165presenceRefreshInterval : Int<Ms>
166presenceRefreshInterval = 300000
166presenceRefreshInterval = 300000
168−-- Human-readable build identifier returned by the /health endpoint.
169−serverBuildVersion : Text
170−serverBuildVersion = "2026-04-27-simple"
172-- Master switch for federation; when False the server runs in single-node mode.
168-- Master switch for federation; when False the server runs in single-node mode.
173federationEnabled : Bool
169federationEnabled : Bool
174federationEnabled = True {}
170federationEnabled = True {}
@@ -283,7 +279,7 @@ type RateEntry = {key: Text, windowStart: Timestamp, count: Int}
283*presence : [PresenceEntry]
279*presence : [PresenceEntry]
284-- Cross-server outbox: messages queued for delivery to a peer, with retry counts.
280-- Cross-server outbox: messages queued for delivery to a peer, with retry counts.
285*forwards : [ForwardEntry]
281*forwards : [ForwardEntry]
286−-- Per-peer failure counters; drives `retryBackoff` and the federation health view.
282+-- Per-peer failure counters; drives `retryBackoff`.
287*forwardFailures : [ForwardFailure]
283*forwardFailures : [ForwardFailure]
289-- Per-key fixed-window counters for /messages send-side rate limiting.
285-- Per-key fixed-window counters for /messages send-side rate limiting.
@@ -310,14 +306,11 @@ type RateEntry = {key: Text, windowStart: Timestamp, count: Int}
310*seqCounter : Timestamp
306*seqCounter : Timestamp
311-- Bumped by send/heartbeat to wake all blocked /poll holders.
307-- Bumped by send/heartbeat to wake all blocked /poll holders.
312*pollWakeup : Timestamp
308*pollWakeup : Timestamp
313−-- Wall time of the last completed prune sweep (for /health and /ready).
309+-- Wall time of the last completed prune sweep (gates /ready).
314*lastPrunedAt : Timestamp
310*lastPrunedAt : Timestamp
315−-- Wall time of the last poll-heartbeat tick (for /health watchdog).
316−*lastHeartbeatAt : Timestamp
317-- Wall time of the last self-presence refresh broadcast to peers.
311-- Wall time of the last self-presence refresh broadcast to peers.
318*lastPresenceRefresh : Timestamp
312*lastPresenceRefresh : Timestamp
319−-- Wall time of process start; underpins the /health warmup window and
313+-- Wall time of process start; gates /ready (must be ≤ lastPrunedAt).
320−-- /metrics uptime gauge.
321*startedAt : Timestamp
314*startedAt : Timestamp
322-- Random 64-bit-ish identity stamped at startup. Clients echo it back on /poll;
315-- Random 64-bit-ish identity stamped at startup. Clients echo it back on /poll;
323-- any mismatch means the server restarted and the client must reset its cursor.
316-- any mismatch means the server restarted and the client must reset its cursor.
@@ -330,7 +323,7 @@ type RateEntry = {key: Text, windowStart: Timestamp, count: Int}
330-- AuthApi -- challenge/verify (client identity)
323-- AuthApi -- challenge/verify (client identity)
331-- ClientApi -- /poll, /messages (send), presence lookup
324-- ClientApi -- /poll, /messages (send), presence lookup
332-- FederationApi -- inter-server gossip, forward (single + batch)
325-- FederationApi -- inter-server gossip, forward (single + batch)
333−-- HealthApi/MetricsApi/ReadyApi -- observability
326+-- ReadyApi -- observability
334-- Response field shapes are kept stable for backwards compatibility even when
327-- Response field shapes are kept stable for backwards compatibility even when
335-- the underlying value is no longer computed (zero-filled).
328-- the underlying value is no longer computed (zero-filled).
@@ -349,16 +342,10 @@ route FederationApi where
349 POST {toKey: PubkeyHex, encryptedBlob: BlobHex, fromServer: ServerName} /federation/forward -> {ok: Bool, error: Text, retryAfterMs: Int} = ForwardMessage
342 POST {toKey: PubkeyHex, encryptedBlob: BlobHex, fromServer: ServerName} /federation/forward -> {ok: Bool, error: Text, retryAfterMs: Int} = ForwardMessage
350 POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex}], fromServer: ServerName} /federation/forward-batch -> {ok: Bool, error: Text, results: [{ok: Bool}], retryAfterMs: Int} = FederationBatchForward
343 POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex}], fromServer: ServerName} /federation/forward-batch -> {ok: Bool, error: Text, results: [{ok: Bool}], retryAfterMs: Int} = FederationBatchForward
352−route HealthApi where
353− GET /health headers {authorization: Text} -> {ok: Bool, version: Text, pruneHealthy: Bool, heartbeatHealthy: Bool, capacityHealthy: Bool, capacityWarning: Bool, federationHealthy: Bool, watchdogHealthy: Bool, nonceHealthy: Bool, forwardQueueHealthy: Bool, requestPressureHealthy: Bool, degradedPeers: Int} = Health
355−route MetricsApi where
356− GET /metrics headers {authorization: Text} -> {metrics: Text, ok: Bool} = Metrics
358route ReadyApi where
345route ReadyApi where
359 GET /ready -> {ready: Bool} = Ready
346 GET /ready -> {ready: Bool} = Ready
361−route Api = AuthApi | ClientApi | FederationApi | HealthApi | MetricsApi | ReadyApi
348+route Api = AuthApi | ClientApi | FederationApi | ReadyApi
363-- ============================================
350-- ============================================
364-- Generic helpers
351-- Generic helpers
@@ -672,7 +659,7 @@ shouldRetryServer : Text -> Timestamp -> [ForwardFailure] -> Bool
672shouldRetryServer = \server t failures -> none failures (\f -> f.server == server && t - f.failedAt < retryBackoff f.failures)
659shouldRetryServer = \server t failures -> none failures (\f -> f.server == server && t - f.failedAt < retryBackoff f.failures)
674-- Bump the failure counter for `server`. Preserves `firstFailedAt` across
661-- Bump the failure counter for `server`. Preserves `firstFailedAt` across
675−-- subsequent failures so health views can show the outage age.
662+-- subsequent failures.
676recordFailure : Text -> IO {rw *forwardFailures, clock} {}
663recordFailure : Text -> IO {rw *forwardFailures, clock} {}
677recordFailure = \server -> do
664recordFailure = \server -> do
@@ -1293,36 +1280,9 @@ handleFederationBatchForward = \forwards _ -> do
1293 yield {ok: True {}, error: "", results, retryAfterMs: 0}
1280 yield {ok: True {}, error: "", results, retryAfterMs: 0}
1295-- ============================================
1282-- ============================================
1296−-- Health / readiness handlers
1283+-- Readiness handlers
1297-- ============================================
1284-- ============================================
1299−-- GET /health: aggregate readiness flags. During an initial warmup window
1300−-- (3 prune intervals after startup) prune/heartbeat checks always pass to
1301−-- avoid false negatives before the background loops have ticked.
1302−handleHealth : a -> IO {r *lastHeartbeatAt, r *lastPrunedAt, r *startedAt, clock} {capacityHealthy: Bool, capacityWarning: Bool, degradedPeers: Int, federationHealthy: Bool, forwardQueueHealthy: Bool, heartbeatHealthy: Bool, nonceHealthy: Bool, ok: Bool, pruneHealthy: Bool, requestPressureHealthy: Bool, version: Text, watchdogHealthy: Bool}
1303−handleHealth = \authorization -> do
1305− lastPrune <- *lastPrunedAt
1306− lastHb <- *lastHeartbeatAt
1308− let warmup = t - sa < backgroundPruneInterval * 3
1309− let pruneOk = warmup || t - lastPrune < backgroundPruneInterval * 3
1310− let heartbeatOk = warmup || t - lastHb < pollTimeoutMs * 3
1312− ok: pruneOk && heartbeatOk,
1313− version: serverBuildVersion,
1314− pruneHealthy: pruneOk,
1315− heartbeatHealthy: heartbeatOk,
1316− capacityHealthy: True {},
1317− capacityWarning: False {},
1318− federationHealthy: True {},
1319− watchdogHealthy: True {},
1320− nonceHealthy: True {},
1321− forwardQueueHealthy: True {},
1322− requestPressureHealthy: True {},
1326-- GET /ready: signals readiness once the first background prune has run
1286-- GET /ready: signals readiness once the first background prune has run
1327-- (load-balancer / orchestrator probe).
1287-- (load-balancer / orchestrator probe).
1328handleReady : IO {r *lastPrunedAt, r *startedAt} {ready: Bool}
1288handleReady : IO {r *lastPrunedAt, r *startedAt} {ready: Bool}
@@ -1331,28 +1291,6 @@ handleReady = do
1331 lastPrune <- *lastPrunedAt
1291 lastPrune <- *lastPrunedAt
1332 yield {ready: lastPrune > 0 && lastPrune >= sa}
1292 yield {ready: lastPrune > 0 && lastPrune >= sa}
1334−-- GET /metrics: Prometheus-style text exposition of the most useful gauges.
1335−-- The `authorization` header is accepted but not validated here — operators
1336−-- relying on auth must front this endpoint with a reverse proxy that enforces it.
1337−handleMetrics : a -> IO {r *forwards, r *messages, r *presence, r *sessions, r *startedAt, clock} {metrics: Text, ok: Bool}
1338−handleMetrics = \authorization -> do
1346− metrics: fold (\acc r -> acc ++ r.k ++ " " ++ show r.v ++ "\n") "" [
1347− {k: "skrepka_messages_total", v: count msgs},
1348− {k: "skrepka_sessions_total", v: count ses},
1349− {k: "skrepka_presence_total", v: count pres},
1350− {k: "skrepka_forwards_total", v: count fwds},
1351− {k: "skrepka_uptime_seconds", v: (t - sa) / 1000}
1356-- ============================================
1294-- ============================================
1357-- Dispatch
1295-- Dispatch
1358-- ============================================
1296-- ============================================
@@ -1373,8 +1311,6 @@ api = serve Api where
1373 RecvGossip = \{events, fromServer} -> handleRecvGossip events fromServer
1311 RecvGossip = \{events, fromServer} -> handleRecvGossip events fromServer
1374 ForwardMessage = \{toKey, encryptedBlob, fromServer} -> handleForwardMessage toKey encryptedBlob fromServer
1312 ForwardMessage = \{toKey, encryptedBlob, fromServer} -> handleForwardMessage toKey encryptedBlob fromServer
1375 FederationBatchForward = \{forwards, fromServer} -> handleFederationBatchForward forwards fromServer
1313 FederationBatchForward = \{forwards, fromServer} -> handleFederationBatchForward forwards fromServer
1376− Health = \{authorization} -> handleHealth authorization
1377− Metrics = \{authorization} -> handleMetrics authorization
1378 Ready = \{} -> handleReady
1314 Ready = \{} -> handleReady
1380-- ============================================
1316-- ============================================
@@ -1395,11 +1331,11 @@ backgroundPrune = do
1396-- Endless tick that bumps `pollWakeup` every `pollTimeoutMs` so any blocked
1332-- Endless tick that bumps `pollWakeup` every `pollTimeoutMs` so any blocked
1397-- /poll holders unblock and return an empty heartbeat response.
1333-- /poll holders unblock and return an empty heartbeat response.
1398−pollHeartbeat : IO {rw *lastHeartbeatAt, rw *pollWakeup, clock} a
1334+pollHeartbeat : IO {rw *pollWakeup, clock} a
1399pollHeartbeat = do
1335pollHeartbeat = do
1400 sleep pollTimeoutMs
1336 sleep pollTimeoutMs
1401 t <- now
1337 t <- now
1402− atomic (do *pollWakeup = t; *lastHeartbeatAt = t)
1338+ *pollWakeup = t
1403 pollHeartbeat
1339 pollHeartbeat
1405-- ============================================
1341-- ============================================