@@ -293,9 +293,6 @@ isValidMessageId : Text -> Bool
293293isValidMessageId = isValidBoundedHex maxMessageIdLen
294294-- Hex-encoded message id, length ≤ `maxMessageIdLen`.
295295type MessageIdHex = Text where isValidMessageId
296−-- A `MessageIdHex` or the empty string. Used in send responses, where empty
297−-- means the send was rejected so no id was assigned.
298−type OptionalMessageId = Text where \s -> s == "" || isValidMessageId s
299296-- Normalized hostname (lowercased, no trailing dot, length-bounded).
300297isAnyServerName : Text -> Bool
301298isAnyServerName = \s -> not tooLong s maxServerNameLen && s == normalizeServerName s
@@ -400,7 +397,7 @@ type BatchMessage = {
400397 expiresInMs: Int
401398}
402399-- Per-entry result returned by /messages/batch.
403−type BatchResult = {messageId: OptionalMessageId, status: MessageStatus}
400+type BatchResult = {messageId: MessageIdHex, status: MessageStatus}
404401
405402-- ============================================
406403-- Persisted State (all flat lists, no sharding)
@@ -480,7 +477,7 @@ route AuthApi where
480477
481478route ClientApi where
482479 POST {cursor: Timestamp, generation: Int} /poll headers {authorization: Text, clientIp: Text} -> {events: [PollEvent], cursor: Timestamp, authorized: Bool, rateLimited: Bool, draining: Bool, generation: Int, heartbeat: Bool, retryAfterMs: Int, cursorReset: Bool, hasMore: Bool, cursorStale: Bool, messageExpirations: [{messageId: MessageIdHex, toKey: PubkeyHex, expiredAt: Timestamp}], messageDeliveries: [DeliveredEntry], droppedDeliveryReceipts: Int, droppedExpirationNotifs: Int, loadPct: Int} = Poll
483− POST {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, expiresInMs: Int} /messages headers {authorization: Text, clientIp: Text} -> {status: MessageStatus, messageId: OptionalMessageId, retryAfterMs: Int, error: Text, loadPct: Int} = SendMessage
480+ POST {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, expiresInMs: Int} /messages headers {authorization: Text, clientIp: Text} -> {status: MessageStatus, messageId: MessageIdHex, retryAfterMs: Int, error: Text, loadPct: Int} = SendMessage
484481 POST {to: PubkeyHex, messageId: MessageIdHex} /messages/status headers {authorization: Text, clientIp: Text} -> {status: Text, authorized: Bool} = MessageStatus
485482 POST {messageIds: [MessageIdHex]} /messages/ack headers {authorization: Text, clientIp: Text} -> {ok: Bool, authorized: Bool, ackedCount: Int, error: Text} = AckMessages
486483 POST {messages: [BatchMessage]} /messages/batch headers {authorization: Text, clientIp: Text} -> {results: [BatchResult], authorized: Bool, retryAfterMs: Int} = SendBatch
@@ -661,8 +658,8 @@ emptyPollResp = \cursor authorized rateLimited draining gen retryAfterMs cursorW
661658}
662659
663660-- Shared error shape for /messages send.
664−sendErrResp : MessageStatus -> Text -> {status: MessageStatus, messageId: OptionalMessageId, retryAfterMs: Int, error: Text, loadPct: Int}
665−sendErrResp = \status err -> {status, messageId: "", retryAfterMs: 0, error: err, loadPct: 0}
661+sendErrResp : MessageStatus -> Text -> MessageIdHex -> {status: MessageStatus, messageId: MessageIdHex, retryAfterMs: Int, error: Text, loadPct: Int}
662+sendErrResp = \status err messageId -> {status, messageId, retryAfterMs: 0, error: err, loadPct: 0}
666663
667664-- True when a send produced (or matched) a deliverable message.
668665isSendSuccess : MessageStatus -> Bool
@@ -1612,13 +1609,13 @@ ackPolledMessages = \pubkey cursor t -> do
16121609-- POST /messages: send one ciphertext to `to`. Rejects on draining/rate
16131610-- limit/stale timestamp; otherwise calls `sendMessageCore` inside an atomic
16141611-- block and forks the federation forward (if any) once the local commit lands.
1615−handleSendMessage : PubkeyHex -> BlobHex -> Int<Ms> -> Int -> Text -> Text -> ({error: Text, loadPct: Int, messageId: OptionalMessageId, retryAfterMs: Int, status: MessageStatus} -> a) -> IO {r *drainPhase, rw *globalRateCount, rw *globalRateWindowStart, r *sessions, clock, random} a
1612+handleSendMessage : PubkeyHex -> BlobHex -> Int<Ms> -> Int -> Text -> Text -> ({error: Text, loadPct: Int, messageId: MessageIdHex, retryAfterMs: Int, status: MessageStatus} -> a) -> IO {r *drainPhase, rw *globalRateCount, rw *globalRateWindowStart, r *sessions, clock, random} a
16161613handleSendMessage = \to encryptedBlob timestamp expiresInMs authorization clientIp respond -> do
16171614 t <- now
16181615 dp <- *drainPhase
16191616 g <- checkGlobalRate t
16201617 effMsgId <- genToken
1621− let err = \s e -> yield (respond (sendErrResp s e))
1618+ let err = \s e -> yield (respond (sendErrResp s e effMsgId))
16221619 if dp >= 2
16231620 then err (MsDraining {}) "draining"
16241621 else if g
@@ -1633,7 +1630,7 @@ handleSendMessage = \to encryptedBlob timestamp expiresInMs authorization client
16331630 let ok = isSendSuccess result.status
16341631 yield (respond {
16351632 result.status,
1636− messageId: if ok then effMsgId else "",
1633+ messageId: effMsgId,
16371634 retryAfterMs: 0,
16381635 error: if ok then "" else "rejected",
16391636 loadPct: 0
@@ -1645,7 +1642,7 @@ handleSendMessage = \to encryptedBlob timestamp expiresInMs authorization client
16451642-- `seqCounter`, appends the message, and pre-stages forwards for any remote
16461643-- presence rows that already exist for `to`. Returns the chosen status and
16471644-- the list of remote servers the caller should kick forwards to.
1648−sendMessageCore : PubkeyHex -> PubkeyHex -> BlobHex -> Int<Ms> -> OptionalMessageId -> Int -> Int<Ms> -> IO {r *ackedIds, r *deliveredIds, rw *forwards, rw *messages, rw *pairRates, rw *pollWakeup, r *presence, rw *recvRates, rw *sendRates, rw *seqCounter} {remoteServers: [Text], status: MessageStatus}
1645+sendMessageCore : PubkeyHex -> PubkeyHex -> BlobHex -> Int<Ms> -> MessageIdHex -> Int -> Int<Ms> -> IO {r *ackedIds, r *deliveredIds, rw *forwards, rw *messages, rw *pairRates, rw *pollWakeup, r *presence, rw *recvRates, rw *sendRates, rw *seqCounter} {remoteServers: [Text], status: MessageStatus}
16491646sendMessageCore = \senderKey to encryptedBlob clampedTs effMsgId expiresInMs t -> do
16501647 msgs <- *messages
16511648 aids <- *ackedIds
@@ -1718,7 +1715,7 @@ sendMessageCore = \senderKey to encryptedBlob clampedTs effMsgId expiresInMs t -
17181715-- Fire one /federation/forward call per pre-staged server. On success the
17191716-- corresponding row in `forwards` is pruned (no retry needed); on failure
17201717-- it stays so `retryPendingForwards` can re-attempt with backoff.
1721−forkForwardsTo : OptionalMessageId -> PubkeyHex -> BlobHex -> Int<Ms> -> [Text] -> IO {} {}
1718+forkForwardsTo : MessageIdHex -> PubkeyHex -> BlobHex -> Int<Ms> -> [Text] -> IO {} {}
17221719forkForwardsTo = \effMsgId to encryptedBlob clampedTs servers -> forEach servers (\server -> do
17231720 fwds <- *forwards
17241721 let same = \x -> x.messageId == effMsgId && x.toServer == server