Dialogue · Design notes · Room language
← Design notes

Room language — one tongue per room, faithfullyLayers A + B live · 2026-06-21

Every room has a target language — detected from the Smart-selector's Describe box, or chosen in the new-chat picker's more settings (which defaults to your account language). The aim: every persona speaks that language, while staying completely themselves. Most do. A few don't — strongly language-anchored figures (Confucius always reaches for Chinese), or any host in a target the model finds hard (Japanese). This is how we fixed it without making the system prompt more fragile.

Strategy in one line: reduce the conflict so personas usually hit the target on their own, then catch the rare miss with a faithful translation. The invariant: a language slip must DEGRADE, never mislead — a host who can't hold the room's language is shown in their own words plus a faithful translation, never dropped, never silently off-target.
Layers A + B — shipped, validated & deployed 2026-06-21 Both layers are now live in production on chat.xbbapp.com (deployed 2026-06-21). The prompt reframe is live (the "voice ≠ language" principle, the whole-language-anchor override, and the schema fix + lang field). Re-running the two reported failures: a Japanese opening went 1/4 → 4/4 (Plato, Confucius and Stan Lee all now speak Japanese), and in a French room Confucius now speaks French while keeping his 正名 ("rectify the names") doctrine. Layer B is now shipped too — the persisted translate net: cross-script slips get a faithful translation under a hairline that survives reopen, is saveable to Notes, and supports go-to-line (validated end-to-end, ZH→FR).

1 · What actually bites

We measured every room with a target language — 46 rooms, 710 on-stage host lines — grading each line against the room's target. The raw number is 90% compliant, but that hides two things, both important for the fix.

TargetRoomsLinesIn targetNote
English2112485%many "misses" are the panel correctly following a guest who wrote another language
Chinese2256493%the model's strong language; when guest + target agree, ~100%
Japanese2862%even with a native-Japanese peer present, others default to English — looks like model capability
French11450%**under-counted by the detector; an Italian test later ran clean for everyone

First: most "non-compliance" is correct — the spec lets the panel follow the guest, so a host switching to the guest's language is the feature working, not a bug. When target and guest agree, compliance is ~100%. Second: the real defect is small and concentrated:

root cause The prompt has ~6 signals pulling toward each persona's own language — the megaprompt's "immutable soul" and "speak in each host's voice", the full profile loaded per host (Confucius: "the attested unit is the Chinese … never an English sentence as though he said it in English"), the tool schema's text = "in their own voice and language", and the verbatim replay of prior turns — versus ~2 toward the room language (one directive block, buried after the profiles). Piling louder room-language commands onto the weak side just makes the prompt contradict itself → more fragile. So we subtract the conflict first, then add a narrow net.

2 · The target language

Set once, then it tracks the live conversation:

effectiveTarget(turn) = the guest's current language ← if they've clearly switched else room.language ← Describe-box detection, or "more settings"

The Smart selector identifies the Describe box's main language at convene time and stores it as room.language — and that can be any language (Italian and French rooms already work), not only the four the manual dropdown offers. On a convene it's pre-filled into the picker's "more settings" Language dropdown (added as an option when it's outside the four), so what you see matches the room you'll get — still overridable. Every persona is graded against effectiveTarget, so a host who correctly followed the guest is never counted — or treated — as a miss.

3 · Layer A — stop contradicting (prompt reframe)

Three subtractive edits. No persona profiles change; nothing gets louder.

WherePullsChange
system-prompt.md (rule 7)own language+1 principle: voice ≠ language (below)
language_directive()room languageextend the override from dialects to whole-language anchors
panel tool text fieldown languageremove the contradiction + add a lang self-report
Voice is not language. The profile fixes a host's voice — diction, idiom, the lines they'd quote in the original. The language a turn is written in is the room's, and follows the guest if they switch. Keep every bit of the voice in that language; quoting a canonical line in its original script and then carrying it in the room's language is faithful, not a breach. A host's native tongue never overrides the room's language for their own speech.
schema fix The single live contradiction — the panel tool literally tells the model each host speaks in its own language:
text: "what this host says, in their own voice and language"
text: "what this host says — their voice, written in the room's language"
+ lang: "the language you wrote THIS reply in (honest self-report)"
That lang field is the keystone: it turns the schema from a contradiction into the detector, and gives exact compliance numbers.

And language_directive gains a clause for figures whose profile anchors a whole language: "Your profile may pin a native tongue ('quote the Chinese', 'speaks Japanese'). That shapes your voice and what you quote — not the room's language. Quote a canonical line in its original script when your profile calls for it, then carry it in {language}."

4 · Layer B — translate on divergence (the net)

Layer A makes most fragile cases land on their own. For the residual — a host who still slips — the server translates that one line, as a last resort:

1
Model turnllm

The panel tool returns each reply with who, text, and a self-reported lang.

2
Effective targetserver

The guest's current language if they've switched, else the room's stored language.

3
Per-reply checkserver

lang matches the target — or the guest's language? → keep as-is. Otherwise → translate.norm() folds synonyms (Chinese / 中文 / 简体 → zh) + a CJK-script sanity check

4
Translate — only on a missllm

A cheap call renders the line into the target, faithfully: preserving markdown, fenced code, math, and any verbatim original-language quote. Misses are the rare tail, so cost stays bounded.

Render, store, rememberclient

The bubble shows original ─ hairline ─ translation, and the line is stored with both halves — so it reappears intact every time the room is reopened. The model's working memory keeps only the target version, so the room language self-reinforces.

How a translated line renders

ConfuciusTeacher of Lu
客随主便。既然贵客以意大利语相邀,老朽自当从命——名不正则言不顺。

Mi adeguo all'ospite. Poiché il nobile ospite invita in italiano, questo vecchio obbedirà — se i nomi non sono retti, le parole non seguono.

One bubble: the host's own words on top, a hairline, then the faithful translation into the room language below — all formatting (markdown, code, math) preserved on both sides. The hairline is the only divider; the labels above are for this writeup only.

two stores, on purpose The translation is generated once and saved with the line, so it's persistent — it shows on every reopen and is never re-translated (which keeps the text byte-stable). Two separate records: the display record (transcript + event log → replay) keeps both the original and the translation; the model's working memory keeps only the target version, so the other hosts see one room language and the slip doesn't snowball.

Saved lines & "go to line" work on the translation

Because the translation is real, persisted text inside the same bubble — one line, one data-lid — the existing notebook features work on it unchanged:

This is why the translation is generated once and stored, not re-made on load: a re-translation could drift by a word, and a saved note would no longer match. Persist-once keeps the highlight exact.

5 · What changes where

FileChangeLayer
prototype/meta/system-prompt.md+1 "voice ≠ language" principle (rule 7)A
lib/run_room.pylanguage_directive reframe · schema text desc + new lang · panel_tool_descA
lib/run_room.pynew translate_to() (once, server-side) · turn pipeline: effective-target → divergence → translate → attach orig/orig_lang · persist them in the transcript + event log so replay rebuilds the bubble; model history keeps only the translationB
lib/room-ui.htmlrender orig + hairline + text on live turns and replay; .bub-rule CSS · save-line + go-to-line tokenize/flash across both halvesB
persona profilesnone — the reframe handles the anchored figures centrally

6 · Rollout & how we measured it

Two stages, so the lift was attributable and we didn't over-engineer:

The compliance probe lives in rooms-dev/_probe/ (gitignored); the lang field removes its blind spot on Latin-script languages, so before/after is measured, not eyeballed.

7 · Open risks

source records: lib/run_room.py (directive · schema · pipeline) · prototype/meta/system-prompt.md (the megaprompt) · lib/room-ui.html (the bubble render) · rooms-dev/_probe/ (the compliance probe, gitignored)

Dialogue · room language · shipped & deployed 2026-06-21 · see also Harness · Style Guide · Studio