Repository navigation
feat(rooms): Phase 11 — chat, wired into existing AI personas - #21
Merged
Merged
Conversation
Personas and AI-side chat generation (AgentController.say, persona-driven
voice) already existed since Phase 5. The actual gap: humans had no way
to send a chat message at all, and AgentController.hear() existed but
nothing ever called it, so an AI never learned about anyone else's chat.
- RoomService.send_chat + POST /rooms/{room_id}/chat: own-seat-only,
rate limited (RATE_LIMIT_CHAT, an already-scaffolded-but-unused
setting), works whether or not a game is running (chat never touches
game state, unlike /moves). Feeds a running game's AI seats via
hear() and tags the event with that game's own correlation.game_id so
it archives alongside the game, matching how an agent's own chat
already does.
- Chat.tsx: message list (seat badge showing an AI's persona instead of
a bare seat number) + compose box, gated on having a seat. Wired into
the room detail page for everyone -- chat is public.
- Fixed a real type inaccuracy found while building this: seat was typed
number | null in the hand-written chat event types; the real schema
never allows null.
- Found and fixed while documenting the route: GET /rooms/{id}/events'
since pattern was still the old ^\d+-\d+$, missed when the websocket
route's identical pattern was widened during PR #20's review round --
breaking transport parity. Widened to match. Also found and fixed a
stale WS route doc row (still showed the pre-review-fix token flow).
AI-to-AI hear() fan-out remains unwired -- a separate, older Phase-5 gap,
found while wiring the human side, deliberately not fixed here.
New tests: backend/tests/rooms/test_chat.py (6), a hear()-fan-out test in
test_room_service.py, and a real two-human chat-exchange Playwright test.
Claude-Session: https://claude.ai/code/session_01UbiPa1rKCyqhqufgwJWkeW
- Chat.tsx: reset draft on room switch (same bug class as useRoomSocket), add aria-live/role=log to the message list, key on event_id instead of ts+seat - send_chat: rename text param (shadowed sqlalchemy's text()), name the 200-char cap _MAX_CHAT_CHARS Claude-Session: https://claude.ai/code/session_01UbiPa1rKCyqhqufgwJWkeW
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 11: chat. Personas and AI-side chat generation (
AgentController.say(), persona-driven voice) already existed since Phase 5 — this phase's actual gap was narrower than the name suggests: humans had no way to send a chat message at all, andAgentController.hear()existed but nothing ever called it, so an AI never learned about anyone else's chat either.RoomService.send_chat+POST /rooms/{room_id}/chat: own-seat-only, rate limited (RATE_LIMIT_CHAT, an already-scaffolded-but-unused setting), works whether or not a game is running (chat never touches game state, unlike/moves). Feeds a running game's AI seats viahear()and tags the event with that game's owncorrelation.game_idso it archives alongside the game — matching how an agent's own chat already does.Chat.tsx: message list (seat badge showing an AI's persona instead of a bare seat number) + compose box, gated on having a seat. Wired into the room detail page for everyone — chat is public.seatwas typednumber | nullin the hand-written chat event types; the real schema never allowsnull.GET /rooms/{id}/events'ssincepattern was still the old^\d+-\d+$, missed when the websocket route's identical pattern was widened during PR feat(frontend): Phase 10 — lobby, live game, focus view, admin #20's review round — breaking transport parity. Widened to match. Also fixed a stale doc row still showing the pre-review-fix token flow.Deliberately not fixed: AI-to-AI
hear()fan-out (one agent's chat reaching a sibling agent'srecent_chat) remains unwired — a separate, older Phase-5 gap, found while wiring the human side, out of scope for this phase.Test plan
ruff check/ruff format --check/mypy app/ty check appcleanbun run build/bun run lintcleanprek run --all-filescleanhttps://claude.ai/code/session_01UbiPa1rKCyqhqufgwJWkeW