Stage 17: fix the robot-nudge frequency + per-game push language
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s

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.
This commit is contained in:
Ilia Denisov
2026-06-09 08:06:58 +02:00
parent 265e442252
commit bf7dca0a09
21 changed files with 257 additions and 45 deletions
+14 -9
View File
@@ -141,8 +141,10 @@ arrive from a platform rather than completing a mandatory registration).
gate (it is a product affordance, not a trust boundary). The service language is
**persisted** per account (`accounts.service_language`, updated on every Telegram
login — last-login-wins) and routes the user's out-of-app push back through the right
bot (§10); it is distinct from `preferred_language` (the interface language) and from
a game's variant language. Non-Telegram logins (web / email / guest) carry the
bot (§10)**except a game event, which routes by the game's own language** (its variant →
en/ru, Stage 17), so a game's notification always comes from the game's bot rather than the
recipient's latest login bot. The service language is distinct from `preferred_language` (the
interface language) and from a game's variant language. Non-Telegram logins (web / email / guest) carry the
gateway's default set (`GATEWAY_DEFAULT_SUPPORTED_LANGUAGES`, all variants by default).
- The client holds `session_id` in memory for the app session (browser/OS
storage is optional and may be unavailable; losing it means re-login).
@@ -339,8 +341,9 @@ English game the Latin pool.
**sleeps 00:0007:00** anchored to the **opponent's** profile timezone with a
per-game drift of **±3 h** (fallback UTC), so its night overlaps the human's
rather than running anti-phase; on a daytime nudge it replies near the move's lower
band; it proactively nudges the human after **12 hours** idle (subject to the
once-per-hour chat limit).
band; it proactively nudges the idle human on a **lengthening, randomized schedule** the
first ~60-90 min into the turn, each later reminder spaced further out toward 1-6 h — so a long
wait gets a handful of increasingly-spaced nudges rather than an hourly stream (Stage 17).
- **Observability**: robot accounts accrue ordinary statistics (§9) — the
authoritative balance metric (target ≈ 40% robot wins) — and a
`robot_games_finished_total` OTel counter plus a per-finish log give a live view.
@@ -507,11 +510,13 @@ missed while the app was hidden. **Out-of-app platform push** (Stage 9) is a fal
the **gateway** routes from the same firehose: for an event whose recipient has **no
live in-app stream** it resolves the backend `/internal/push-target` (their Telegram
`external_id`, the **service language** — the bot they last signed in through, falling
back to the interface language — and the `notifications_in_app_only` flag) and asks the
**Telegram connector** to deliver a localized message with a Mini App deep-link
button — only when the recipient has a Telegram identity and has not confined
notifications to the app, so the two channels never duplicate. The connector routes by
that language to the matching bot and renders the message in it. The out-of-app set is
back to the interface language — and the `notifications_in_app_only` flag). A **game** event,
however, carries the **game's own language** on the push (Stage 17), and the gateway routes by
that instead of the service language — so a game's notification always comes from the game's bot,
not the recipient's latest-login bot. It then asks the **Telegram connector** to deliver a
localized message with a Mini App deep-link button — only when the recipient has a Telegram
identity and has not confined notifications to the app, so the two channels never duplicate. The
connector routes by that language to the matching bot and renders the message in it. The out-of-app set is
your-turn, game-over, nudge, match-found and the invitation / friend-request notify sub-kinds;
the connector renders the message and skips the rest. Operator broadcasts
(`SendToUser` / `SendToGameChannel`, §10 admin) instead pick the bot by an
+4 -2
View File
@@ -35,8 +35,10 @@ the Telegram colours, and — on first contact — seeds the new account's inter
language from the Telegram client. The sign-in service also declares the **game
languages** it offers (a set of en/ru, at least one), which gate the New Game variant
choice in the lobby. Telegram runs a separate bot per language (an English bot and a
Russian bot, the same player spanning both); the bot a player signed in through both
sets their offered languages and is the bot their out-of-app notifications come from. Guests are session-only with restricted features
Russian bot, the same player spanning both); the bot a player signed in through sets their
offered languages, and their non-game notifications come from it. A **game's** notifications
(your turn, game over, a nudge), though, always come from **that game's** bot — by the game's
language, not whichever bot the player signed in through last. Guests are session-only with restricted features
(auto-match only; no friends, stats or history); an abandoned guest that never
joined a game and has been idle past the retention window is garbage-collected. While the app is open the client
keeps a live stream and receives in-app updates in real time — the opponent's move,
+4 -2
View File
@@ -36,8 +36,10 @@ Mini App** авторизует по подписанным `initData` плат
языку Telegram-клиента. Сервис входа также объявляет **языки игры**, которые он
предлагает (набор из en/ru, минимум один), и они ограничивают выбор типа партии в
лобби. Telegram держит отдельного бота на язык (английский и русский, один игрок
охватывает обоих); бот, через которого игрок вошёл, задаёт его доступные языки и
является тем ботом, от которого приходят его внеприложенческие уведомления. Гость — только сессия, с урезанными функциями (только
охватывает обоих); бот, через которого игрок вошёл, задаёт его доступные языки, и от него
приходят его **внеигровые** уведомления. А уведомления по **партии** (ваш ход, конец партии,
nudge) приходят от бота **этой партии** — по языку партии, а не по тому боту, через которого
игрок входил последним. Гость — только сессия, с урезанными функциями (только
авто-подбор; без друзей, статистики и истории); заброшенный гость, не вошедший ни
в одну игру и простаивавший дольше окна удержания, удаляется сборщиком. Пока приложение открыто, клиент
держит живой стрим и получает обновления в реальном времени — ход соперника, ваш ход,