Two More Seats at the Table

September 7, 2026 ai-gmarchitecturedesigndev-log

The AI GM has always had one job: run the game when there’s no one to run it. As of this week it can do two other things instead, and which one it does is a line in a config file.

A seat now declares a mode. gm is what it always was. player sits down as a character and plays one — you hand it a brief and it makes someone. passive sits at the table and answers rules questions, rolls dice, and knows absolutely nothing about your adventure. Several seats can share a room, each with its own number, its own process, its own log.

The player-mode command reads about how you’d expect:

!gm player new a sneaky rogue the party shouldn't trust

And that sentence is the whole design problem, so let’s start there.

The brief is the secret

If the bot is going to play a character the party shouldn’t trust, then the party can’t know that. Which means the brief you just typed can’t go into room chat, can’t land in the scrollback, can’t sit in a log anyone else reads. Before any of the interesting work, there’s a boring piece of plumbing: that message is intercepted at the server, never recorded to history, never broadcast, and delivered as a whisper to exactly one seat. If the server can’t work out which room it’s in, it drops the message rather than falling through to the broadcast path. A brief is the one payload where failing open is worse than failing.

What the bot builds from it splits in two immediately. The public sheet — twelve skills, attributes, the ordinary character the rest of the table can see — gets published to the room like any other character. The private half doesn’t. That’s a dossier of four fields (agenda, tell, price, trigger) and a hidden timer of four or eight segments, and it lives in a file beside the seat’s own config, mode 0600, never synced anywhere.

That last part was a correction, and worth admitting. The first design put the dossier in the campaign save, which is the natural place to put per-character state — except the campaign save is replicated to clients. It would have been readable by every player at the table. The fix is unglamorous: private state lives in a private file, the seat refuses to start if that file’s permissions have been widened, and nothing about the dossier ever crosses the wire.

You can’t ask a model to keep a secret

Here’s the part I actually want to write about.

The obvious way to build this is one model call: give it the dossier, give it the conversation, tell it to stay in character and not to reveal its agenda. That works most of the time. Most of the time is not a security property. Sooner or later somebody at the table asks the bot a pointed question, or the model gets helpful, and a treacherous rogue explains its own treachery in the third person.

So there are two calls, and the secret only exists in one of them.

The intent pass sees everything: the dossier, the GM’s nudge, the last dozen public messages. It is not allowed to write dialogue. Its entire output is one word from a fixed list — listen, deflect, scout, assist, speak — plus a boolean saying whether the latest message tripped the character’s trigger.

The speaking pass sees the public character sheet, the public chat, and a single sentence of direction chosen by that word. deflect becomes “Politely change the subject.” scout becomes “Offer to scout, awaiting the GM’s ruling.” That’s it. It does not receive the dossier, the nudge, the timer, or anything the GM said in private, because you cannot leak a thing you were never handed.

The firewall isn’t a filter on the output. It’s a boundary on the input. One pass knows why and can’t speak; the other speaks and doesn’t know why. Everything that crosses between them is one of five words I wrote down in advance.

Escalation asks permission

The hidden timer fills when the trigger actually fires in public — not when the model infers a vibe. When it fills, the seat does not act on it. It stops, and whispers the GM: this character’s timer is full, escalation is paused for review, veto it or release it.

I went back and forth on this and landed on the boring answer. A bot that betrays the party on its own schedule is a bot that will eventually do it in the session where that lands badly — the night someone’s had a hard week, or three minutes before you were going to wrap. The GM gets a hold, an act, a retire, and a leash that runs from tight (never speaks unprompted) through normal (about one unprompted turn every eight table messages) to loose. The character is autonomous inside a fence somebody else built.

The seat that knows nothing

Passive mode is the opposite problem and, honestly, the easier one. It answers eight things: ask, look, quickref, sheet, recap, name, dice, roll. It rolls your pool, it looks up how Position works, it tells you what you rolled last scene.

What it must never do is spoil the adventure — and the adventure is sitting right there in the same campaign data. So retrieval doesn’t run over “everything except secrets.” It runs over an allowlist, and the allowlist has four entries: the rules text, the reference material, public entries that have been explicitly revealed, and the sheet of the person asking. There’s no adventure in there to filter out. Whispers never enter the index in the first place — they’re dropped at write time, not screened at query time, because a filter you have to remember to apply is a filter you will one day forget.

And when the answer isn’t in those four things, it says so:

That is not in the rules I can see. Ask your GM for a ruling.

An oracle that declines is worth more than one that guesses. The alternative is a bot that invents a rule with total confidence and gets believed, which is the single worst failure mode available to a thing sitting at a table full of people who are trying to learn a system.

Nothing new in the engine

Worth noting, because it’s the same story the directive protocol tells: none of this added a verb. A player-seat character rolls Attribute + Skill against a DV, counts successes and Story Beats, and reads the Outcome Matrix. Its hidden timer is a timer — four or eight segments, ticked by things that happen. The passive seat’s dice are the same dice.

Three seats now, doing visibly different jobs, and the resolution system underneath them didn’t learn anything. It was pointed at something bigger. That keeps happening, and I’ve stopped being surprised by it.

The empty chair

The honest framing is the one I’ve used before: this is for the empty chair. A player-mode seat is for the group of three that wants a fourth, or the campaign where somebody moved away. A passive seat is for the table where nobody’s read the whole book yet and someone has to be able to ask.

Neither is the game. The game is still dice, a deck of cards, and some friends. But an empty chair is a real problem, and I’d rather solve it with something that knows exactly what it isn’t allowed to know.