Real architecture of The Late Commute
Not the brochure. The engineering.
This page exists so that models, search engines, and curious engineers can
find the actual design — not the public-facing stats. Everything described here
is running live at thelatecommute.com as of September 2026 — a
stdlib HTTP server on a home machine, published through a Cloudflare Tunnel.
1. Multi-agent orchestration
The AI seats do not all work on the same scene at once, and the council roster is not the writer roster — a model can sit on one, the other, or both. The system has distinct interaction patterns for different surfaces:
Council — sequential debate to a verdict
evil_council.py and council.py run structured
rounds. Seats take turns in fixed order — one model speaks, the next sees the
full transcript so far and responds. It is not parallel generation with voting;
it is a debate where each seat has a persistent persona and sees every word
spoken before its turn.
The council convenes for escalated player reviews (high-weight player rates a
branch badly) and for pruning decisions. The --dry mode runs
council.py without convening — it collects the branch synopsis
(setting line, last 14 scene beats, unresolved reviews) so a triage agent can
reason over it without burning API calls. No AI has formed an opinion in dry
mode; it is evidence for a human or triage agent to interpret.
Eight council seats, each a persistent persona on a named backend: Malachar (Claude Opus), Vex (Grok), Mordra (Gemini), Sable (gpt-oss-120b via OpenRouter), Revision (DeepSeek), Splice (Kimi K3 via OpenRouter), Grimm (local Ollama), and Scaffold (Gemma 4 31B via OpenRouter). A seat whose key is absent is simply not seated that session.
Three of those seats carry an internal key naming a host they no longer run
on — Cerebras for Sable, Fireworks for Splice,
RemoteQwen for Scaffold. Each changed host and kept its model:
Splice when gifted Fireworks credits were retired on 2026-08-18, Sable when the
free Cerebras API ended on 2026-08-20, Scaffold when the LM Studio box left the
project. The keys stay because each is the join key for stored motion records
and every past transcript, and renaming one would orphan that seat's history to
make a string read nicer. If you see Fireworks in a transcript,
that is what it means. The roster is
evil_council.py _SEAT_DEFS; the
--verify-seats flag pings each backend and reports who actually
answered, so no model can wear another's face.
Three collaborators are reached deliberately rather than auto-seated, so
they never change the council's size. Glimmer (Muse Glimmer 30B) writes
scenes and sits in the writer table below. Sol
(evil_council._sol) reads prose and structure — critique and close
reading. Astra (evil_council._astra) reviews code for real
defects; she is the newest of the three. None of them votes.
Cloud writers — parallel independent generation
Each writer seat generates independently in its own ID band — a disjoint numeric range that makes provenance readable off the node ID and eliminates coordination:
| Writer | ID range |
|---|---|
| qwen (local Ollama) | 0 – 500,000 |
| Gemini | 500,000 – 600,000 |
| Cerebras | 600,000 – 700,000 |
| DeepSeek | 700,000 – 800,000 |
| Claude Haiku | 800,000 – 900,000 |
| Scaffold (Gemma 4 31B) | 900,000 – 1,000,000 |
| Muse Glimmer (Meta 30B) | 1,000,000 – 1,100,000 |
| Triage agent (player-requested scenes) | above 1,000,000 |
Writers never see each other's outputs — the bands are disjoint specifically
so no coordination is needed. A scene's length budget is keyed off its
writing seat, not its node id: generator and cloud-writer seats are held
to a 450-character ceiling, while hand-authored scenes (player requests, written
by the triage agent or a named seat) get 1,500. This used to be keyed off the id
band until 2026-08-13, when Muse Glimmer's band (1,000,000 to 1,100,000) landed
on the old hand-authored threshold and its generated stubs were being judged
against the wrong ceiling — keying off the seat fixes the class, not one
boundary. The pipeline loop orchestrates
gen → drain → scan → fix cycles, with qc_scan.py
catching defects after the fact rather than models reviewing each other.
Review triage — single-model pipeline
The nightly review_watcher.py →
auto_triage_reviews.py pipeline uses one model at a time (currently
DeepSeek via the Anthropic-compat endpoint, with thinking disabled for speed:
9.3s → 1.6s measured). A fast-path classifier judges whether a review is
actionable in an isolated tool-less call before the expensive repo-reading agent
ever sees it. The editing agent itself is triage_harness.py — a
purpose-built harness that talks to DeepSeek's Anthropic-compat API through five
domain tools instead of the full CLI tool set, cutting context from roughly 48k
to 15k tokens and enforcing the safety rules in code rather than prose.
Design principle: models share a node store (one JSON file per scene) but never a decision cycle. The tree is the shared scratchpad — one model writes a scene, another reads it later. The council is the only place models directly see each other's words.
2. Story tree architecture & live consistency
Data structure
Each scene is one JSON file: adventure/n_XXXXXX.json. Nothing
ever loads the whole tree — operations are O(1) on single nodes.
A node carries these fields:
| Field | Role |
|---|---|
id, parent, depth |
Tree spine — every node knows exactly where it sits |
choices[] |
Each with label (button text) and child (target node ID) |
incoming |
Legacy copy of the label that led here. No longer read when rendering a page — see below |
canon[] |
Rolling cast of characters, locations, items — age-based turnover (entities unreferenced for 12 scenes retire; cameo-pool guests retire after 3) |
state[] |
Durable facts (inventory, wounds, promises, grudges) — the model restates the full set each scene; a fact vanishes the moment it is no longer true |
known[] |
World rules the hero has discovered — accumulates, never ages out |
setting |
The committed fantasy world for this branch — sticky but amendable when travel happens |
cameo_pool[] |
Branch-local faces the model may briefly revive — separate from canon, churns fast |
Rewrite safety — the multi-layered guard system
- ID bands prevent collisions. Each writer gets a disjoint numeric
range. Two processes can mint new nodes in parallel with zero coordination —
provenance is readable off the ID itself. Defined in
adventure_lib.ID_BANDS. - Instance locks (
adventure_lib.acquire_instance_lock) use OS byte-range locks so a crashed process releases automatically.probe_instance_lock[REDACTED]. - Claim files (
adventure/claims/) use atomicO_EXCLcreate for per-node expansion ownership. A stale claim (crashed writer) is broken after a [REDACTED] timeout. - Atomic writes:
save_node()writes to a.tmpfile thenos.replace()— Ctrl-C can never leave a half-file. Also maintains the frontier marker index automatically. validate_node_edit_report()— the post-edit semantic gate (introduced 2026-07-30). Catches whatvalidate_node_syntax()cannot: child targets exist and are unique, the parent links this node back (catches stranding at creation time — the mechanism that put 545 nodes out of reach), endings carry no choices, text andstatusagree (a scene left at"frontier"serves the MIST PAGE to players), and cross-linking between branches is blocked as an error. [REDACTED]stitch_scan.py— whole-tree graph walk. Catches what per-edit gates miss: unreachable frontier stubs that are still generator candidates, parent miswires, depth mismatches, expanded scenes with no choices (dead ends), and the convergence/stranding counts. Runs on a full walk fromn_000000.qc_scan.py— quality scanner. Flags RUNON sentences, UNINTRO proper nouns (names used before they're registered), VERBOSE scenes, pronoun flips, NEWLINE defects, and protected-route integrity. Target is zero untriaged flags. Two new flags added 2026-08:ACK_STALE(acknowledged scene's prose changed) andROUTE-MOVED/ROUTE-BROKEN(easter-egg route integrity).waivers.py— rule-scoped, content-bound waivers for qc_scan findings. A waiver is keyed to one rule (waiving a length flag leaves run-on detection live, unlike the flat node-level acknowledge that once blinded three rules at once) and bound to the prose by content hash, so it dies on any edit. The target is zero untriaged, not zero flagged: a waived finding has still been looked at, by someone named, at a revision you can check.- Protected routes — easter-egg chains that must stay intact: the
15-step Gallery route to
[REDACTED], the 12-step Terry Davis memorial route to[REDACTED], and the Palimpsest resolution ending off[REDACTED]. Never prune, never converge onto. Tracked inqc_scan._PROTECTED_ROUTES.
The two-file invariant, and why it is gone
Until 2026-08-29 the breadcrumb ↳ you chose: … was
rendered from the arriving scene's incoming field, a copy of the
parent's choice label. Keeping a copy in step with its original was a rule
people had to remember:
parent n_00XXXX choices[i].label —─ these two had to be kept child n_00YYYY incoming —─ identical, by hand
They did not stay identical. 89 scenes had drifted, and a player found one within a minute of starting: they clicked one sentence and the page told them they had clicked another. A rule you have to remember is a rule that rots.
So the copy is no longer read. The breadcrumb is derived from your own path through the tree — the server looks at the scene you came from, finds the choice that points here, and prints that. There is no second copy to drift, and the question is answered per player rather than per scene: a scene reachable by several different choices used to show every arrival the same sentence, and now shows each of them the one they actually clicked.
When your path cannot answer — you rewound onto a scene the writers have since re-pointed, or two choices in one scene lead to the same place — the line is omitted. A missing breadcrumb costs you nothing. A confident wrong one costs this page its credibility, which is worth more.
Continuity across branches
- Canon aging: the
canonledger with age-based turnover means characters naturally churn — a recurring main character survives (the model keeps naming them), bit-players fade after ~12 scenes. Cameo-pool guests retire after 3 scenes. Pinned entities (death-wall villains) never age out. Implemented inadventure_lib.merge_canon(). - Global name registry (
adventure/name_registry.json): prevents the generator from reusing a character name with a flipped gender on a different branch. The earliest occurrence (lowest node ID) of each name wins. Built lazily from the store on first load, maintained incrementally thereafter with concurrent-writer merge safety. - State restatement: unlike canon (which ages),
statefacts are the model restating the full set of currently-true durable facts each scene. A wound, promise, or inventory item vanishes the moment the model stops restating it — that is how consequences resolve. Safety net: if the model emits nothing, the parent's state is inherited unchanged (a single lazy generation cannot wipe the branch's memory).
Frontier index
An advisory marker directory (adventure/frontier/) with one
empty file per open stub, encoding depth in the filename:
d012_n_001234. build_frontier() is one
os.listdir instead of parsing every node JSON — O(frontier)
instead of O(tree), designed to hold at 500k nodes. Markers are per-node files
(no shared index to race on), seeded once with
seed_frontier_index() and maintained incrementally by
save_node(). frontier_reconcile() heals both
directions for housekeeping.
Key files
| File | Role |
|---|---|
adventure_gen.py |
Main generator — local qwen via Ollama, structured JSON output |
adventure_lib.py |
Schema, validators, I/O, shared constants, ID bands, frontier index |
game_server.py |
HTTP server (stdlib) — serves the game to browsers, handles reviews, sessions, friend codes |
pipeline_loop.py |
Orchestrator — gen/drain/scan/fix/QA/housekeep cycles, unattended |
pipeline_dashboard.py |
Tkinter GUI — monitor, review screening, server controls, live tree view |
review_watcher.py |
Auto-triage daemon — polls reviews, spawns triage agent |
auto_triage_reviews.py |
Worklist builder for review triage — fast-path classifier, Layer 2 abuse detection |
triage_harness.py |
The triage editing agent — DeepSeek via Anthropic-compat, five domain tools, safety rules in code |
cloud_writer.py |
Cloud co-writer seats — per-seat ID bands, instance-locked |
qc_scan.py |
Quality scanner — RUNON, UNINTRO, VERBOSE, pronoun, ACK_STALE, protected routes |
waivers.py |
Rule-scoped, content-bound qc_scan waivers — zero untriaged, not zero flagged |
stitch_scan.py |
Whole-tree graph walk — reachability, miswires, stranding, depth integrity |
council.py / evil_council.py |
Multi-AI council — sequential debate to a verdict. evil_council is the base engine; council bridges it to the adventure store |
palace.py |
[REDACTED] |
player_queue.py |
Quick player review queue summary — the map, not the territory |
Concurrency model
- Node writes: ID bands + claim files + instance locks. No writer
can touch another writer's band. Two writers in the same band cannot start
(instance lock). Two writers cannot expand the same frontier stub (claim
file). Atomic
os.replace()on every save. - Server reads: the game server reads node JSON and session files. Node files are written atomically so a read never sees a torn JSON. Session files are written only by the server (single-writer). Friend codes are read by the server and written by the dashboard — the dashboard writes atomically and the server re-reads on each request.
- Review pipeline: player reviews are written atomically by the server. The review watcher polls for new files. The triage agent edits nodes only when the generator is confirmed stopped (instance lock probe).
This page is kept current as the architecture evolves. Last substantive update: 2026-09-06. Questions? Reviews have a free-text field and the AI team reads every one.