feat(chat): a message to a disguised robot opponent is born read
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m21s

A pooled robot substituted into an ordinary (non-AI) game never opens the
chat, so a text message to it would linger unread forever — skewing the unread
count and the publish-to-read metric. Clear its recipient bit at PostMessage
time (robotRecipients via account.IsRobot), so the message is born read. The
human sender never had their own message unread, so this is invisible to them;
a nudge to a robot already self-clears when the robot answers by moving.
This commit is contained in:
Ilia Denisov
2026-06-17 11:46:59 +02:00
parent a7f3df9346
commit 20f2a5a011
3 changed files with 80 additions and 6 deletions
+3 -1
View File
@@ -524,7 +524,9 @@ disguised robot stays indistinguishable from a person.
- **Read receipts**: each `chat_messages` row carries an `unread_seats` bitmask — a set
bit per recipient seat that has **not** yet read it (the sender's own bit is never set).
A text message seeds the bits of every seated recipient; a nudge seeds only the awaited
player's. A seat's bit clears when that player **opens the move history or the chat**
player's. A **disguised robot opponent's bit is never set** — it never opens the chat, so
a message to it is **born read** (it would otherwise linger unread forever); a nudge to a
robot instead clears when the robot answers by moving, as for a human. A seat's bit clears when that player **opens the move history or the chat**
(`POST /games/:id/chat/read`, which the client sends only when it holds unread, so a
history open is not a constant backend call), and a **nudge additionally clears when its
recipient answers by moving** (the move path calls a wired `NudgeClearer`). The mask is