Problem
Reported by HostFat on Matrix (2026-07): a message under 1000 characters was rejected after send with the console showing
WARN ui/src/components/conversation.rs:1720 Message too long: 1006 encoded bytes, max 1000 bytes
and the message was silently lost — no error shown, draft cleared. Earlier the same session: "a message was lost" and "a very long message was't sent :(".
Root cause
Three checks measure different things:
- Input gate + byte counter (
message_input.rs): raw text.len() ≤ max_message_size.
- Client safety net (
conversation.rs handle_send_message): RoomMessageBody::content_len() — the encoded content. CBOR framing adds ~9 bytes for plain text; replies embed the quoted author name + ~100-char preview (~150 bytes); private rooms add the 16-byte AES-GCM tag.
- Contract validation (
message.rs): prunes messages with content_len() > max_message_size — the authoritative limit is encoded bytes.
Any message in the gap between (1) and (2) passes the UI, has its draft cleared by send_message, then is dropped by the safety net with only a console warning. For replies the gap is ~150 bytes, so even ~850-char replies vanish. Multi-byte UTF-8 widens the perceived gap further (users count characters; the limit counts bytes).
Edits have no size gate at all: an over-limit edit action (ActionContentV1 overhead on top of the text) is signed, sent, and silently pruned by contract validation on every peer.
riverctl is not affected — it builds the real body and errors visibly (guard_message_size).
Fix
Measure the exact encoded size in the UI using the same construction code path the senders use (RoomMessageBody::measure_text/measure_reply/measure_edit in river-core, pinned equal to real built bodies by tests), gate Send/Save and the byte counter on that, and keep the draft in place when over the limit.
[AI-assisted - Claude]
Problem
Reported by HostFat on Matrix (2026-07): a message under 1000 characters was rejected after send with the console showing
and the message was silently lost — no error shown, draft cleared. Earlier the same session: "a message was lost" and "a very long message was't sent :(".
Root cause
Three checks measure different things:
message_input.rs): rawtext.len()≤max_message_size.conversation.rshandle_send_message):RoomMessageBody::content_len()— the encoded content. CBOR framing adds ~9 bytes for plain text; replies embed the quoted author name + ~100-char preview (~150 bytes); private rooms add the 16-byte AES-GCM tag.message.rs): prunes messages withcontent_len() > max_message_size— the authoritative limit is encoded bytes.Any message in the gap between (1) and (2) passes the UI, has its draft cleared by
send_message, then is dropped by the safety net with only a console warning. For replies the gap is ~150 bytes, so even ~850-char replies vanish. Multi-byte UTF-8 widens the perceived gap further (users count characters; the limit counts bytes).Edits have no size gate at all: an over-limit edit action (
ActionContentV1overhead on top of the text) is signed, sent, and silently pruned by contract validation on every peer.riverctl is not affected — it builds the real body and errors visibly (
guard_message_size).Fix
Measure the exact encoded size in the UI using the same construction code path the senders use (
RoomMessageBody::measure_text/measure_reply/measure_editin river-core, pinned equal to real built bodies by tests), gate Send/Save and the byte counter on that, and keep the draft in place when over the limit.[AI-assisted - Claude]