Two owner-reported defects from a live contour game.
A. Frequency: the robot's proactive nudge fired hourly for 12h+ (a 12h idle threshold
then the 1h cooldown, uncapped). Replaced with a lengthening, randomized schedule
(proactiveNudgeGap): the first nudge ~60-90 min into the human's turn, each later gap
growing toward 1-6h (uniform sample in [60min, ceil], ceil ramping 90min->6h over 12h
of idle, measured from the previous nudge), so a long wait gets a handful of
increasingly-spaced reminders instead of a stream.
B. Language: out-of-app push routed by the recipient's GLOBAL service_language
(last-login-wins), so after re-logging via the RU bot an English game's nudges came
from the RU bot. Now a game push (your_turn, game_over, nudge, match_found) carries
the game's own language (engine.Variant.Language) on push.Event, and the gateway
routes by it (falling back to service_language for non-game pushes). The New-Game
variant-gating guarantees the game's bot is one the player has started, so delivery is
never blocked.
Tests: proactiveNudgeGap unit + retimed TestRobotProactiveNudge; TestVariantLanguage;
emit your_turn/game_over language; TestNudgeRoutedByGameLanguage integration. Docs:
ARCHITECTURE (§7 nudge, §10/§13 routing), FUNCTIONAL (+ _ru), PLAN tracker.
The hourly nudge cooldown now clears as soon as the sender has moved or posted a chat
since their last nudge — engagement lifts the 'don't spam' limit. Backend: Nudge checks
game.LastMoveAt + the sender's last non-nudge chat against the last nudge time
(GameReader gains LastMoveAt). UI: nudgeOnCooldown mirrors it — a chat reset is read from
the message list, a move is tracked client-side (lastActedAt on commit/pass/exchange; the
backend stays authoritative across a reload). Integration test covers the reset.
PostMessage now rejects a chat sent on a finished game or when it is not the sender's
turn (ErrChatNotYourTurn -> 409 chat_not_your_turn), matching the UI where the message
field is hidden off-turn and only the nudge shows. Existing chat tests post on the
to-move seat and are unaffected; adds an off-turn-rejection integration test + the dto
mapping case + the UI error message.
Engine: multi-player drop-out-and-continue with a per-game tile disposition (remove default / return), resigned seats skipped and excluded from the win, leaver rack never revealed; 2-player behaviour unchanged.
New domains (service/store, no HTTP yet): internal/social (friend request/accept graph, per-user blocks, per-game chat with nudge as a message kind, content filter via mvdan.cc/xurls/v2 + leet/separator normaliser + phone heuristic) and internal/lobby (in-memory variant-keyed matchmaking pool, friend-game invitations invite->accept with lazy 7-day expiry). account gains profile editing and the email confirm-code flow (Mailer seam: SMTP or log mailer).
Migration 00003_social.sql + regenerated jet. main wires the new services into the server (accessors for the Stage 6 handlers); robot substitution stays in Stage 5, REST/stream/push in Stage 6/8. Docs (PLAN, ARCHITECTURE, FUNCTIONAL+ru, TESTING, README) updated.