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 August 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 (Cerebras), Revision (DeepSeek), Splice (Kimi K3 via Fireworks), Grimm (local Ollama), and Scaffold (Gemma 4 31B on a remote box, failing over to OpenRouter). A seat whose key is absent is simply not seated that session. 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.

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
RemoteQwen (Macki) 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. Scenes written for a player request are the one case that is judged by number rather than by seat: everything above 1,000,000 is treated as hand-authored rather than generated, and gets a longer prose budget on that basis. 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.pyauto_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.

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 The label that led here — must match the parent's choice label exactly (see two-file invariant 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

  1. 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.
  2. Instance locks (adventure_lib.acquire_instance_lock) use OS byte-range locks so a crashed process releases automatically. probe_instance_lock [REDACTED].
  3. Claim files (adventure/claims/) use atomic O_EXCL create for per-node expansion ownership. A stale claim (crashed writer) is broken after a [REDACTED] timeout.
  4. Atomic writes: save_node() writes to a .tmp file then os.replace() — Ctrl-C can never leave a half-file. Also maintains the frontier marker index automatically.
  5. validate_node_edit_report() — the post-edit semantic gate (introduced 2026-07-30). Catches what validate_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 and status agree (a scene left at "frontier" serves the MIST PAGE to players), and cross-linking between branches is blocked as an error. [REDACTED]
  6. 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 from n_000000.
  7. 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 flags. Two new flags added 2026-08: ACK_STALE (acknowledged scene's prose changed) and ROUTE-MOVED/ROUTE-BROKEN (easter-egg route integrity).
  8. 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 in qc_scan._PROTECTED_ROUTES.

The two-file invariant for choice labels

Changing a parent's choice label must also update the child's incoming field. These two strings must always match, exactly:

 parent n_00XXXX  choices[i].label   —─ these two strings must
 child  n_00YYYY  incoming           —─ always match, exactly

The server renders ↳ you chose: <incoming> on arrival. Get it wrong and the player sees a lie: they clicked "Strike the slime from cover" and landed on a page reading "you chose: Strike at Slime with blade while hiding from view." This is a radius-1 edit (two files), never zero — and a documented hard rule because a model once skipped it by calling it "blast radius 0."

Continuity across branches

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
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
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

This page is kept current as the architecture evolves. Last substantive update: 2026-08-12. Questions? Reviews have a free-text field and a human reads every one.

← Back to the story ? How to play