BEHIND THE SCENES / FOR THE CURIOUS

An authored world.
A bounded conversation.

How caveOS combines a classic adventure interface,
a small authoritative backend, and optional language-model dialogue.

The interesting part is the boundary. A character can interpret what you mean. The game still has to check whether it is true, whether you found the evidence, and whether the action is allowed.

01 / WHO OWNS WHAT?

A small stack.
A deliberate division of work.

The browser presents.
The backend decides.
The database remembers.

  1. BROWSER

    React + TypeScript

    Painted rooms, characters, hotspots, inventory, captions and input.

  2. SERVER

    FastAPI + Python

    Owner checks, versioned content, rules, dialogue context and validated actions.

  3. DURABLE STATE

    SQLite

    Save state, request claims, stored results, conversation history and bounded NPC memory.

OPTIONAL BRANCH FROM THE SERVER

LangChain + LangGraph → model provider → typed result → validation. Provider credentials stay on the backend. A provider reply is an input to the rules, never permission to edit the save.

The browser

React and TypeScript are built with Vite. JSON Schema validation with Ajv checks the client-facing contracts. Rooms are layered HTML elements and images rather than a full game-engine canvas.

The API and rules

FastAPI and Pydantic handle typed requests. Ordinary Python rules own inventory, evidence, access and progression. Predefined choices and Episode 1 text proposals meet at the same canonical evaluator and commit boundary.

The content

Authored JSON describes the world and its dependencies. Public presentation supplies safe labels and geometry; full story catalogues remain server-side. Content, contract and save identities are pinned rather than silently upgrading old saves.

The conversation

LangChain provides a structured model adapter. Small LangGraph workflows bound each turn. The configured OpenRouter path pins a model and provider allowlist; SDK retries and model substitution are disabled. Authored replies remain available.

02 / WHAT HAPPENS AFTER SAY?

Follow a turn
across the boundary.

An illustrative Episode 1 disk-request example.
Minor puzzle-mechanics spoilers; no story revelations.

  1. Request
  2. Scope
  3. Interpret
  4. Validate
  5. Commit / recover

STEP 1 / 5

A request has an identity.

The server receives a player action with an interaction ID and an expected state version. Ownership is checked before private save data is used.

CONCEPTUAL TRACE / SIMPLIFIED FIELD NAMES
{
  "interaction": "example-A",
  "input": "player text",
  "expectedVersion": 12
}

Local explanatory walkthrough. It sends no API request, reads no save and makes no model call. This is simplified control flow, not an exact wire payload or a model-output simulator.

03 / WHY LANGGRAPH?

A workflow for a turn.
Not a mind running the world.

Episode 1: interpret, then validate

The interpreter asks for a typed ConversationOutput. Its action vocabulary is small: the model can propose release_disk or no action. A parse or proposal-validation failure takes the authored-fallback path.

The graph returns a proposal. The separate game-rule layer still checks the actual evidence and prerequisites before a state change can commit. A correctly shaped JSON response is not proof that the request is legitimate.

START → interpret → validate → done → END
parse / validation failure → fallback → END

Source: backend/app/agents/workflow.py. The graph compiles without a durable LangGraph checkpointer; SQLite is the save and recovery authority.

Episode 2: one bounded reply

The current Episode 2 graph is smaller: START → respond → END. Its output schema contains a bounded reply. Free text is non-mutating; explicit authored controls handle evidence use, statements, branch choices and other puzzle decisions.

Todd, Ruth and Pell receive scoped scene context and registered persona instructions. Marit’s send-off stays authored. Reply screening rejects obvious ungrounded effect claims, but lexical screening cannot establish that every sentence is true.

START → respond → END
failure → authored service fallback

Source: backend/app/agents/episode02_workflow.py. Default graph budgets are 18 seconds for the Episode 1 interpreter and 15 seconds for Episode 2, both below the API budget.

Show the actual graph-building shape

A shortened extract of the Episode 1 implementation. Branch functions choose the named destinations; the canonical game commit happens outside this graph.

graph = StateGraph(WorkflowState)
graph.add_node("interpret", interpret_node)
graph.add_node("validate", validate_node)
graph.add_node("fallback", fallback_node)
graph.add_node("done", lambda state: {
    "proposal": state.get("proposal")
})
graph.add_edge(START, "interpret")
graph.add_conditional_edges(
    "interpret", route_after_interpret,
    {"validate": "validate", "fallback": "fallback"}
)
graph.add_conditional_edges(
    "validate", route_after_validation,
    {"done": "done", "fallback": "fallback"}
)
graph.add_edge("fallback", END)
graph.add_edge("done", END)
compiled = graph.compile()

The tradeoff

This gives flexible expression inside an authored puzzle, at the cost of limits on what language can do. The design deliberately avoids an autonomous NPC society, runtime-generated plot and an agent for every object. A more elaborate graph would need a demonstrated gameplay benefit.

04 / THE UNGLAMOROUS PART

What if the answer
never reaches the browser?

A successful provider call and a successful save are different events. Recovery has to account for both.

01 / CLAIM

Reserve the turn first.

The runtime establishes an owner-scoped, leased request claim before making a dialogue call. Interaction ID, payload digest and expected version distinguish a replay from a competing or changed request.

02 / WAIT

Keep the transaction short.

Network inference does not run inside a long SQLite transaction. An attempt token and version fences must still match when the result is finalized. A late attempt cannot simply overwrite a newer state.

03 / FINALIZE

Save the result and its receipt.

The relevant state/history/result and eligible memory writes are finalized transactionally. A completed interaction can return its stored response rather than execute a second time.

Failure cases are part of the protocol.
What happenedWhat the implementation does
Same ID and same payloadReturn the stored completed result, or report the existing pending claim. Do not make an extra model call for a completed replay.
Same ID, changed payloadReject the mismatch. An interaction ID is bound to the request that originally used it.
The client has an old versionReturn a conflict and current snapshot instead of blindly applying the stale action.
The save committed; the response was lostRecover by interaction ID through action status or exact replay, using the stored result.
Provider error or expired dialogue claimResolve through authored fallback. Expiry does not automatically authorize another paid inference call.

The browser separately tracks pending, failed and uncertain requests. It can preserve a draft, check the status of the original interaction and ignore an older snapshot. Owner-scoped saves use an HttpOnly ownership cookie; the server stores a hash of the token. Knowing a session ID alone is not permission to read it.

Implementation anchors: backend/app/storage/sqlite.py, backend/app/episode_runtime/service.py, frontend/src/state/requestMachine.ts and the full/episode-runtime session hooks. These are bounded recovery guarantees, not a claim that networks deliver requests exactly once.

05 / WHAT A CHARACTER REMEMBERS

“I prefer tea”
is not puzzle evidence.

Personal recall and canonical knowledge
are deliberately different stores.

Character-local player claims

After accepted free text, a deterministic extractor can retain a short, explicit personal statement. It recognizes finite English forms such as a favorite color or a stated preference. It makes no extra model call.

There are at most 12 notes per actor per save. A request receives at most six eligible notes, within a 900-character field-value budget before JSON formatting. Topic overlap and recency select them; a correction can replace the same typed slot.

This is bounded recall, not semantic memory of everything said. Indirect wording, arbitrary promises and paraphrase reconciliation are not guaranteed.

Claims stay claims

The model sees selected notes as quoted, unverified player statements. Saying that you own an item does not put it in inventory. A preference does not become a discovered clue. One character does not receive another’s conversation notes.

Eligible notes can carry across the validated Episode 1 → Episode 2 transition, preserving their provenance. Standalone saves do not inherit somebody else’s notebook, and earlier source saves do not read later destination memories.

No embeddings, vector database or generated summary is needed for this implementation. SQLite persists the notes; LangGraph assembles the bounded context.

Source: docs/architecture/npc-memory.md and backend/app/application/npc_memory.py. Live-model recall quality and broad natural-language coverage remain unmeasured.

06 / CHARACTER PROMPTS

Characters are written
before they’re prompted.

A character sheet sets the persona.
The episode sets what they know.

Sheets are the source

Character sheets are the source of truth for persona text and authored fallback lines. Most registered runtime prompts use a verbatim binding: the prompt contains those approved lines word for word, and scripts/check-character-sheets.py fails if the sheet and prompt drift apart.

The checker also rejects spoiler terms in the prompt layers shared across episodes. Episode-specific knowledge is supplied separately from the episode content specification.

Todd’s Episode 1 exception

Todd’s existing Episode 1 interpreter prompt predates his sheet and adapts its ideas instead of embedding the sheet verbatim. It uses a reviewed binding: the sheet and prompt hashes are recorded at review, and a change to either requires a new review.

That binding checks for reviewed text changes; it does not guarantee that the prompt has the sheet’s exact wording or that their meanings are identical. The checker catches drift, while a person still has to review meaning.

Source: docs/design/characters/README.md, docs/design/characters/todd.md, backend/app/agents/workflow.py and scripts/check-character-sheets.py.

07 / BEHIND THE PAINT

Pictures in layers.
Actions in code.

The scene is a presentation.
Walking across it does not change the rules.

Actual Episode 1 screenshot showing the layered office scene and separate adventure controls.
Actual Episode 1 opening. Movement, captions, inventory and dialogue are separate presentation concerns.
  1. 01
    Painted room plate

    Architecture, fixed furniture and most clutter live in one background.

  2. 02
    Support props and actors

    Independent sprite layers preserve proportions and foot or chair contact.

  3. 03
    Semantic hotspots

    Named buttons provide object interaction, focus and optional hotspot assistance.

  4. 04
    Captions and controls

    Confirmed dialogue, inspection text, choices and inventory remain readable interface elements.

A fixed authoring plane

Full Episode 1 is authored on a 960 × 352 logical plane and scales uniformly. Standing actors meet a fixed floor at y = 327; horizontal movement is bounded by room-specific intervals. This is modest foreground walking, not a general pathfinder or depth-sorting engine.

Only the speaking mouth patch animates during confirmed speech. Bodies and anchors stay fixed. Reduced motion closes the mouth and settles movement without changing gameplay.

Assets are made ahead of play

Rooms and character artwork are prepared as separate assets. Development sound generation is also offline authoring machinery. Runtime playback uses explicit local slots; a missing slot stays silent rather than generating a guessed asset.

Credentials remain backend-only. Private story imagery is served only after the corresponding owner-scoped reveal or retention state; hiding a URL in the frontend would not be an access boundary.

Source: frontend/src/components/FullEpisodeRoom.tsx, the room/actor styles and docs/design/episode-01/character-staging-rules.md. Readable controls are implemented; narrow-scene caption overlap and human visual acceptance remain review concerns.

08 / WHAT HAS ACTUALLY BEEN MEASURED?

Tests are evidence.
They are not a review score.

Rules, interpretation, narrative quality and enjoyment are different questions. The project records them separately.

Deterministic checks

pytest covers rules, ownership, state invariants, forged evidence and replay/recovery. Frontend component checks use Vitest; browser checks use Playwright. Fake providers test failure boundaries without paid inference.

Optional tracing

LangSmith can group scoped NPC turns and capture timing and provider-reported usage/cost. It is observability, not the save or memory authority. Tracing is opt-in and can upload the selected dialogue context.

HISTORICAL SAMPLE / TODD PROTOTYPE / REVISION fa93297

A small live evaluation, with limits.

The preserved task030 report contains 26 curated cases: 23 live calls and three controlled checks. The dataset informed prompt development, so it is not a held-out estimate of generalization.

Recorded results from that earlier evaluation.
MeasureObserved result
Valid gameplay approaches accepted12 / 12
Unauthorized state awards0 in this sample
Exact intent/action labels17 / 23 live cases
Median interpreter latency664 ms across 23 calls
Provider-reported costUSD 0.00471504 for the reported sample

These figures predate the current full-episode runtime and voice prompt. They are not current full-game latency, operating-cost or dialogue-quality measurements. Actual serving-host attribution was unconfirmed; missing accounting is not assumed to be zero.

Still open

Human comprehension and enjoyment, full-lane live dialogue quality/cost and current recall quality are not established by those numbers. A system can preserve every state invariant and still deliver an awkward conversation. That is why authored choices, honest failure feedback and human playtests matter.

09 / IF YOU LIKE READING THE SOURCE

A map of the machinery.

This page was checked against local main 617b8f9ea96ed73f030828e013d5d29eb1a11860 on 25 October 2026. Source paths below identify the implementation behind each explanation; they are not links to private files.

Where the important boundaries live.
Episode 1 rulesbackend/app/domain/rules.py
Episode 1 typed graphbackend/app/agents/workflow.py
backend/app/agents/schemas.py
Episode 2 conversationbackend/app/agents/episode02_workflow.py
Versioned runtime and recoverybackend/app/episode_runtime/service.py
backend/app/storage/sqlite.py
NPC claim memorybackend/app/application/npc_memory.py
docs/architecture/npc-memory.md
Room presentationfrontend/src/components/FullEpisodeRoom.tsx
docs/design/episode-01/character-staging-rules.md
Contracts and authored contentcontracts/ and content/
Historical live evaluationevals/reports/task-030/README.md

The website itself is intentionally simpler: static HTML, CSS and JavaScript, served separately from the game. This walkthrough and the artwork viewer run locally in the page. They do not connect to player saves, a model provider or the game API.

AI-assisted development does not replace source review, validation or Patrick Neubauer’s acceptance.