P11 slice 64 — IRCv3 bot message tag#
Date: 2026-06-10
Track: IRCv3 (the thin follow-on that lights up slice 55's deferred bot tag, exactly
as slice 62 server-time consumed the message-tags delivery plane).
Spec: docs/rfcs/ircv3/bot-mode.md (§ "The bot tag").
Scope#
When a +B (bot-mode) user sends a message, the server adds a valueless bot
message tag to the delivered line, only for recipients that negotiated
message-tags. Scoped — exactly like slice 62 — to the message delivery plane:
PRIVMSG / NOTICE / TAGMSG (the fan-out to recipients and the slice-61
echo-message copy back to the sender).
Slice 55 shipped +B mode + ISUPPORT BOT=B + WHOIS 335 + WHO B flag, and
explicitly deferred the tag because no tag-rendering plane existed yet. Slices 56/58
built that plane; this slice spends it.
Locked decisions#
- No new capability. The bot tag is not negotiated by the receiver — it is gated
by the existing
message-tagscap (per the spec: "MUST only be sent to users who have requested themessage-tagscapability") and the sender's+Bmode. Socap.rs/ClientCaps/SUPPORTEDare untouched — this is a pure delivery-plane change. This is the key contrast with slices 59–63, which each added a cap. - Valueless. Servers MUST NOT send a value; clients MUST ignore one. We emit a bare
bottag (("bot", "")). - Sender-driven, not recipient-driven. Whether the tag appears depends on the
sender being a bot; whether it's delivered depends on the recipient's
message-tags. The two are orthogonal and both must hold. - Tag ordering. The bot tag is a server tag → prepended before client-only (
+) tags, liketime. When bothserver-timeand the bot tag apply, order is@time=…;bot;+clienttags…(time first, then bot, then client tags) — deterministic, spec-order-agnostic between the two server tags.
Design#
| Site | Change |
|---|---|
message.rs |
New Message::with_bot_tag(&self) -> Message — clones, inserts ("bot","") at index 0 (mirrors with_server_time). |
command/message.rs |
Compute sender_is_bot = client.modes & UserMode::Bot.bit() != 0 once. The per-recipient render(caps) gains a bot = sender_is_bot && caps.message_tags arm: a 2×2 match over (bot, server_time) so the no-tag common path still avoids a clone. |
command/tagmsg.rs |
Same sender_is_bot + bot = sender_is_bot && caps.message_tags in render. (Fan-out recipients are all message-tags consumers by the gate; the echo correctly gates on the sender's own cap.) |
S2S relay paths are unchanged: a remote member's own server stamps the bot tag
from the network-propagated +B (slice 55 already rides +B over UNICK/UMODE) — the
same single-server local-mailbox stance slices 59–63 take.
Divergences (documented)#
- PRIVMSG/NOTICE/TAGMSG only this slice. The spec's "all commands … (JOIN, MODE,
NOTICE, and all others)" and "numerics directly caused by the bot" are deferred —
each a thin follow-on now that
with_bot_tagexists, mirroring slice 62's scope. - Remote members get no
bottag from this server (their server stamps it). - No value ever; numeric replies untagged.
Tests (TDD, inverse invariants first)#
message.rsunit:with_bot_taginserts a valuelessbotat the front; composes withwith_server_timeastime, bot, +client.command/message.rsunits: a bot's channel PRIVMSG → amessage-tagsconsumer sees@bot; inverse a non-consumer co-member sees none; inverse a non-bot sender → the consumer sees none; bot tag + a client+tag coexist (botbefore+); a bot's NOTICE to a local-nick consumer is tagged; the echo to a bot sender carries@botiff the sender negotiated message-tags (+ the tagless inverse).command/tagmsg.rsunits: a bot's TAGMSG to a consumer carries@bot; inverse a non-bot sender's TAGMSG to the same consumer does not.- Proptest
bot_tag_proptest.rs(fuzz target): drive PRIVMSG through a realSessionwith arbitrary(sender_is_bot, recipient_has_message_tags, text)— the delivered line carries a parseable valuelessbottag iff both bools hold, never otherwise; the tag never has a value; panic-freedom on arbitrary trailing bytes. - Golden
golden_bot_tag.rs: real binary —alicedoesMODE alice +B;dan(message-tags ON) sees@boton alice's channel line;mel(OFF) sees no tag. Valueless tag is fully deterministic → no new canonicalizer rule.
Gate#
cargo test -p leveva green; cargo clippy -p leveva --tests clean; cargo build --workspace 0 warnings.