@@ -173,12 +173,6 @@ onlineGossipTtl = 5400000
173173-- Times to retry a federation forward before giving up.
174174maxForwardRetries : Int
175175maxForwardRetries = 10
176−-- Hop-count ceiling for gossip propagation between peers.
177−maxGossipHops : Int
178−maxGossipHops = 5
179−-- Initial hop budget assigned to gossip events that originate on this server.
180−gossipOriginHops : Int
181−gossipOriginHops = 2
182176
183177-- Base delay for federation retry backoff (linear, not exponential — 30 s).
184178retryBackoffBase : Int<Ms>
@@ -251,13 +245,10 @@ impl ToJSON GossipEventType where
251245impl FromJSON GossipEventType where
252246 parseJson t = (\s -> if s == "online" then GossipOnline {} else if s == "offline" then GossipOffline {} else GossipUnknown {}) (parseJson t : Text)
253247
254−-- One presence change exchanged between federated servers. `seenBy` lists the
255−-- hostnames already aware of the event, used by receivers to break gossip loops.
248+-- One presence change exchanged between federated servers.
256249type GossipEvent = {
257250 eventType: GossipEventType,
258− pubkey: PubkeyHex,
259− server: ServerName,
260− seenBy: [Text]
251+ pubkey: PubkeyHex
261252}
262253
263254-- Outcome classification returned to the sender of /messages and per-entry in
@@ -448,7 +439,7 @@ route ClientApi where
448439 POST {pubkeys: [PubkeyHex]} /lookup/batch headers {authorization: Text, clientIp: Text} -> {results: [{pubkey: PubkeyHex, server: AnyServerName}], authorized: Bool, error: Text} = BatchLookupPresence
449440
450441route FederationApi where
451− POST {events: [GossipEvent], hopsRemaining: Int, fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/gossip headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = RecvGossip
442+ POST {events: [GossipEvent], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/gossip headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = RecvGossip
452443 POST {toKey: PubkeyHex, encryptedBlob: BlobHex, messageId: MessageIdHex, fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = ForwardMessage
453444 POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex, messageId: MessageIdHex}], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward-batch headers {authorization: Text} -> {ok: Bool, error: Text, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int} = FederationBatchForward
454445
@@ -477,7 +468,7 @@ route MaintenanceApi where
477468 GET /maintenance/allowed headers {authorization: Text} -> {peers: [Text], ok: Bool} = ListAllowed
478469 POST /maintenance/reconcile headers {authorization: Text} -> {ok: Bool, messageDrift: Int, presenceDrift: Int, sessionDrift: Int} = Reconcile
479470 POST {newSecret: Text} /maintenance/rotate-secret headers {authorization: Text} -> {ok: Bool, error: Text} = RotateSecret
480− GET /maintenance/config headers {authorization: Text} -> {ok: Bool, maxGossipHops: Int, pollTimeoutMs: Int, ackGracePeriod: Int} = GetConfig
471+ GET /maintenance/config headers {authorization: Text} -> {ok: Bool, pollTimeoutMs: Int, ackGracePeriod: Int} = GetConfig
481472
482473route HealthApi where
483474 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, draining: Bool, degradedPeers: Int} = Health
@@ -1060,18 +1051,15 @@ pruneExpired = \t -> atomic do
10601051-- ============================================
10611052-- Federation gossip
10621053-- ============================================
1063−-- Construction and outbound dispatch of presence gossip events. Each event
1064−-- carries a `seenBy` list to short-circuit loops on the receiver. Failures
1065−-- from a peer record into `forwardFailures` (shared with forward retries) so
1066−-- both subsystems back off together.
1054+-- Construction and outbound dispatch of presence gossip events. Failures from
1055+-- a peer record into `forwardFailures` (shared with forward retries) so both
1056+-- subsystems back off together.
10671057
1068−-- Build a gossip event with this server's identity.
1058+-- Build a gossip event with the originating pubkey's status.
10691059mkGossip : GossipEventType -> PubkeyHex -> GossipEvent
10701060mkGossip = \etype pk -> {
10711061 eventType: etype,
1072− pubkey: pk,
1073− server: serverHostN,
1074− seenBy: [serverHostN]
1062+ pubkey: pk
10751063}
10761064
10771065-- Fan out one bundle of gossip events to each peer, skipping peers in backoff.
@@ -1085,7 +1073,6 @@ sendBatchedGossip = \peers events -> forEach peers (\server -> do
10851073 result <- fetch (federationUrl server) (RecvGossip {
10861074 authorization: auth,
10871075 events,
1088− hopsRemaining: gossipOriginHops,
10891076 fromServer: serverHostN,
10901077 requestTimestamp: t,
10911078 correlationId: ""
@@ -1746,30 +1733,29 @@ handleFederationPubkey = \respond -> do
17461733 hex <- *federationPublicKeyHex
17471734 yield (respond {pubkey: hex, ok: hex != ""})
17481735
1749−-- POST /federation/gossip: ingest peer presence events. Drops loop-back
1750−-- events (origin == this server), then applies each event in arrival order:
1751−-- `GossipOnline` upserts the (pubkey, server) row with a fresh receiver-side
1752−-- TTL, `GossipOffline` deletes it. For keys that transition offline → online,
1753−-- flushes any locally-buffered messages via `forwardQueuedToServer`.
1754−handleRecvGossip : [{eventType: <GossipOffline {} | GossipOnline {} | a>, pubkey: PubkeyHex, server: Text | b}] -> Int -> Text -> Int -> Text -> ({error: Text, ok: Bool, retryAfterMs: Int} -> c) -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, rw *presence, clock, network} c
1755−handleRecvGossip = \events hopsRemaining fromServer requestTimestamp authorization respond -> do
1736+-- POST /federation/gossip: ingest peer presence events. The transport-level
1737+-- `fromServer` is the home server for every event. Applies each event in
1738+-- arrival order: `GossipOnline` upserts the (pubkey, fromServer) row with a
1739+-- fresh receiver-side TTL, `GossipOffline` deletes it. For keys that
1740+-- transition offline → online, flushes any locally-buffered messages via
1741+-- `forwardQueuedToServer`.
1742+handleRecvGossip : [{eventType: <GossipOffline {} | GossipOnline {} | a>, pubkey: PubkeyHex | b}] -> Text -> Int -> Text -> ({error: Text, ok: Bool, retryAfterMs: Int} -> c) -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, rw *presence, clock, network} c
1743+handleRecvGossip = \events fromServer requestTimestamp authorization respond -> do
17561744 t <- now
1757− withFederationAuth authorization t respond fromServer "gossip" requestTimestamp (clamp 0 maxGossipHops hopsRemaining >= 1 && isRemote fromServer && count events <= maxGossipEvents) (do
1745+ withFederationAuth authorization t respond fromServer "gossip" requestTimestamp (isRemote fromServer && count events <= maxGossipEvents) (do
17581746 newlyOnline <- atomic (do
17591747 pres <- *presence
17601748 let final = fold (\state e ->
1761− if not isRemote e.server
1762− then state
1763− else (\matches -> case e.eventType of
1749+ (\matches -> case e.eventType of
17641750 GossipOnline {} -> {
1765− pres: union (removeWhere state.pres matches) [{e.pubkey, e.server, expiresAt: t + onlineGossipTtl}],
1766− newlyOnline: if any state.pres (\p -> matches p && p.expiresAt > t) then state.newlyOnline else union state.newlyOnline [{e.pubkey, e.server}]
1751+ pres: union (removeWhere state.pres matches) [{e.pubkey, server: fromServer, expiresAt: t + onlineGossipTtl}],
1752+ newlyOnline: if any state.pres (\p -> matches p && p.expiresAt > t) then state.newlyOnline else union state.newlyOnline [{e.pubkey, server: fromServer}]
17671753 }
17681754 GossipOffline {} -> {
17691755 pres: removeWhere state.pres matches,
17701756 state.newlyOnline
17711757 }
1772− _ -> state) (\p -> p.pubkey == e.pubkey && p.server == e.server)
1758+ _ -> state) (\p -> p.pubkey == e.pubkey && p.server == fromServer)
17731759 ) {pres, newlyOnline: []} events
17741760 *presence = final.pres
17751761 yield final.newlyOnline)
@@ -2027,7 +2013,6 @@ handlePingPeer = \server authorization respond -> withMaintAuth authorization (y
20272013 result <- fetch (federationUrl server) (RecvGossip {
20282014 authorization: auth,
20292015 events: [],
2030− hopsRemaining: 1,
20312016 fromServer: serverHostN,
20322017 requestTimestamp: startTime,
20332018 correlationId: ""
@@ -2161,14 +2146,13 @@ handleRotateSecret = \newSecret authorization respond -> withMaintAuth authoriza
21612146 yield (respond {ok: True {}, error: ""}))
21622147
21632148-- GET /maintenance/config: expose a small set of operationally-relevant
2164−-- compile-time constants (gossip hops, poll timeout, ack grace).
2165−handleGetConfig : Text -> ({ackGracePeriod: Int, maxGossipHops: Int, ok: Bool, pollTimeoutMs: Int} -> a) -> IO {r *runtimeMaintenanceSecret} a
2149+-- compile-time constants (poll timeout, ack grace).
2150+handleGetConfig : Text -> ({ackGracePeriod: Int, ok: Bool, pollTimeoutMs: Int} -> a) -> IO {r *runtimeMaintenanceSecret} a
21662151handleGetConfig = \authorization respond -> do
21672152 ok <- verifyMaintenanceAuth authorization
21682153 let v = \x -> if ok then x else 0
21692154 yield (respond {
21702155 ok,
2171− maxGossipHops: v maxGossipHops,
21722156 pollTimeoutMs: v pollTimeoutMs,
21732157 ackGracePeriod: v ackGracePeriod
21742158 })
@@ -2545,7 +2529,7 @@ serve = \req -> case req of
25452529 DeleteMessages {messageIds, authorization, clientIp, respond} -> handleDeleteMessages messageIds authorization clientIp respond
25462530 LookupPresence {pubkey, authorization, clientIp, respond} -> handleLookupPresence pubkey authorization clientIp respond
25472531 BatchLookupPresence {pubkeys, authorization, clientIp, respond} -> handleBatchLookupPresence pubkeys authorization clientIp respond
2548− RecvGossip {events, hopsRemaining, fromServer, requestTimestamp, authorization, respond} -> handleRecvGossip events hopsRemaining fromServer requestTimestamp authorization respond
2532+ RecvGossip {events, fromServer, requestTimestamp, authorization, respond} -> handleRecvGossip events fromServer requestTimestamp authorization respond
25492533 ForwardMessage {toKey, encryptedBlob, messageId, fromServer, requestTimestamp, authorization, respond} -> handleForwardMessage toKey encryptedBlob messageId fromServer requestTimestamp authorization respond
25502534 FederationBatchForward {forwards, fromServer, requestTimestamp, authorization, respond} -> handleFederationBatchForward forwards fromServer requestTimestamp authorization respond
25512535 FederationPubkey {respond} -> handleFederationPubkey respond