Skip to content
AI Systems9 min read

Building an AI Story-Memory Engine for Long-Form Fiction

Why embeddings and character biographies cannot preserve a novel’s internal history — and what a structured, time-aware memory model looks like instead.

A novel breaks in mundane ways. A character’s eyes change color between chapters. Someone who died in Part I answers a door in Part III. A grief ritual introduced with great weight is quietly forgotten. None of these are failures of prose — they are failures of memory. And they are exactly the failures that current AI writing tools make worse, because the tools that are supposed to remember the book do not model the book at all.

I built Continuity Atlas to test a different shape of memory. This is the engineering behind it: what a story-memory engine actually stores, why a vector index and a static story bible both fail, and how modeling characters as time-dependent states turns continuity from a vibe into a query.

Why a story bible and a vector index both fail

The two default answers to “give the AI context” are a story bible (a document of facts) and a vector index (embeddings of the manuscript, retrieved by similarity). Both are storage strategies. Neither is a model of the narrative.

A story bible freezes truth. It says Rowan is a soldier; the war is over; the brother is dead. But in a real manuscript, truth is contextual and it moves on purpose. What a character knows, what the reader knows, and what only the author knows are three different layers, and they drift apart deliberately — that drift is the plot. A frozen bible collapses those layers and, worse, resolves mysteries the author was holding open. It answers questions the book has not asked yet.

A vector index loses structure. Embeddings are wonderful at “find me passages that feel like this” and useless at “is it currently possible for Jack to know his brother is alive?” Similarity is not validity. Retrieval will happily surface the most relevant paragraph that also happens to contradict chapter four, because nothing in the index encodes when a fact became true or who is allowed to hold it.

Characters are not entities. They are sequences of states.

The core modeling decision is that a character is not a record. A character is a stable spine — the invariants that do not move across the book — plus an ordered sequence of states, each valid for a stretch of the story. Jack in chapter one (avoidant, lucid) and Jack in chapter seven (fractured, in bodily panic) are not the same node with updated fields. They are different states, and they carry different continuity guardrails and different voice rules.

// The unit of memory is a *state*, not a character.
// A character is a stable spine plus an ordered sequence of states.

type Knowledge = "reader" | "character" | "author-only";

interface Fact {
  id: string;
  claim: string;
  knownBy: Knowledge;          // who is allowed to know this
  status: "dormant" | "paid-off" | "contradicted";
  firstSeen: ChapterRef;
}

interface CharacterState {
  chapter: ChapterRef;          // when this state is valid
  disposition: string;          // "avoidant, lucid" -> "fractured, panic"
  knows: string[];              // fact ids in scope for this state
  doesNotKnow: string[];        // explicitly out of scope (guardrail)
  readerKnows: string[];        // dramatic-irony ledger
  authorOnly: string[];         // hidden from generation by default
  activeMotifs: string[];       // e.g. "6:17"
  continuityWarnings: string[]; // fracture flags surfaced to the author
}

interface Character {
  id: string;
  // Invariants — the part that does NOT move across the book:
  surfaceGoal: string;
  hiddenDesire: string;
  fear: string;
  lie: string;                  // the false belief the arc tests
  voiceMarkers: VoiceFingerprint;
  // The part that moves:
  states: CharacterState[];     // ordered by chapter
}
The character-state model from the prototype. The invariants rarely change; the states array is the moving part, and each state carries its own knowledge ledgers.

Everything that makes continuity checkable lives on the state: knows, doesNotKnow, readerKnows, and authorOnly. A fact is not globally true — it is true for a state. That single move is what lets the engine answer questions a bible and an index cannot.

Architecture
Manuscript Parser TYPED MEMORY States Events Claims Relations Validator temporal · contradiction Context Receipt inspect before generate Constrained generation
The manuscript is parsed into a typed memory store — states, events, claims, and relations. Generation never reads the store directly: it passes through a validator and a Context Receipt the author can inspect and edit before anything is written.

Four schemas: state, event, claim, relation

The memory store is small and typed on purpose. Four node kinds carry almost all the weight:

  • State — a character (or place, or institution) at a moment in narrative time, holding its knowledge ledgers and active motifs.
  • Event — something that happened, anchored to a chapter, with participants and a cause that may be withheld from the reader.
  • Claim — a fact as asserted, tagged with who knows it and whether it is dormant, paid off, or contradicted. Claims are where secrets and lies live.
  • Relation — a typed edge between the above: remembers, contradicts, marks, caused-by. Relations are what a graph query traverses; they are not free-text notes.

Because these are typed, the interesting operations become ordinary code rather than prompts. “Every scene where Rowan and the priest are both present before chapter eight” is a filter over events. “Every claim the reader believes but a character does not” is a set difference over ledgers. The manuscript becomes queryable.

Temporal validity and contradiction detection

Once facts are typed and time-stamped, contradiction detection stops being a judgment call and becomes a validation pass. Three checks catch most of what a story bible silently allows:

// Contradiction detection is a query over typed facts,
// not a semantic-similarity guess.

function validate(fact: Fact, state: CharacterState): Violation[] {
  const out: Violation[] = [];

  // 1. Knowledge boundary: generation must not use author-only facts.
  if (fact.knownBy === "author-only" && !state.authorOnly.includes(fact.id)) {
    out.push({ kind: "leaked-secret", fact: fact.id });
  }

  // 2. Temporal validity: a state cannot "know" a fact from its future.
  if (state.knows.includes(fact.id) && fact.firstSeen.after(state.chapter)) {
    out.push({ kind: "anachronism", fact: fact.id });
  }

  // 3. Explicit contradiction: the fact is in the doesNotKnow ledger.
  if (state.doesNotKnow.includes(fact.id)) {
    out.push({ kind: "impossible-knowledge", fact: fact.id });
  }

  return out;
}
Validation runs before generation. Each check is a deterministic query over typed facts — no similarity threshold, no model in the loop.

The first check is the one general-purpose tools get wrong most often: a leaked secret. If a fact is marked author-only and it is not explicitly in scope for the current state, generation is not allowed to use it — even if it is, semantically, the single most relevant thing in the book. Relevance is precisely why a vector index would surface it. Validity is why the engine must not.

Structured memory vs. vector retrieval, side by side

The graph below is the shape of the model on a small slice of real data. Nodes are typed; edges are relations. Toggle the author-only layer to reveal the fact the model is not permitted to use yet — the true cause of an event the reader has seen but not understood. That hidden node is the whole argument: a retrieval system cannot hide it, because it does not know it should.

Story-memory graph
drifts intoremembersmarksoccasionsJack · Ch. Iavoidant · lucidSJack · Ch. VIIfractured · bodily panicS6:17recurring motifMThe stair eventCh. IVE“Oren left first”stated, Ch. IIC
Hover a node to see who knows what. Toggle the author-only layer to reveal the fact the model is not allowed to use yet.

Interactive — hover a node for its knowledge status; toggle the author-only layer. Data anonymized from Liminal 6:17.

A worked example from Liminal 6:17

The prototype is built on an actual manuscript — Liminal 6:17, a multi-POV literary-horror novel — rather than an invented demo. One thread makes the model concrete. A motif, the time 6:17, recurs from chapter one; the reader clocks it long before it means anything. In chapter four there is an event on a staircase whose cause is withheld. A character states, in chapter two, that “Oren left first.” Much later that claim is contradicted — but the contradiction is not resolved on the page. The author is holding it open.

In the memory store, the true cause of the staircase event is a single author-only claim, linked to the event by a true-cause relation. The reader-facing claim (“Oren left first”) links to it by a contradicts edge flagged as unresolved. An AI asked to expand chapter five can see the event, can see the reader-facing claim, and is structurally blocked from using the true cause — until the author moves it out of the author-only ledger. The secret survives contact with the machine.

Inspect before generate

The last piece is not a model at all — it is a receipt. Before any rewrite fires, the engine assembles a Context Receipt: what will be preserved, what is forbidden, what is hidden from output, which state is active. The author reads it, edits it, and only then approves generation. That ordering — inspect, then generate — is the product’s entire thesis made physical. The value of a story-memory engine is not that it writes better sentences. It is that its memory is visible, editable, and trustworthy before it is ever used.

The most interesting frontier for AI-assisted fiction isn’t better prose. It’s a collaborator with visible, editable, trustworthy memory.

The working prototype — Story Memory, Character Drift, the Voice Fingerprint, and the full rewrite flow with a Context Receipt — runs on the real Liminal 6:17 data. You can open it here: Continuity Atlas.