M9: a campaign you can actually get back
A campaign could already be exported and imported. What could not survive the trip was everything that explains it: the state events behind the authoritative document, the prompt each turn was actually given, the passages it was shown, the summaries that carry long-story continuity, and which take belonged to which turn. An imported campaign could be read and could no longer say why it was what it was — and a manual correction, the one state change no narration explains, was indistinguishable from something the story had established. The bundle is now `ai-dnd-adventure-v3`, and the version is the design rather than a side effect. Everything added here could have been another optional key, the way persona, Save Points, narrative state and imported knowledge each were. That mechanism stops working at exactly this addition: a v2 file with no prompt provenance is ambiguous between "written before M9" and "written by M9 from a campaign that has none", and those are different facts about a campaign. A version number is how a recovery file states what it was capable of recording. v1 and v2 still import, and every seam from pre-active-head onward is tested for the rule that an older file is never reinterpreted under a newer assumption. Two categories became three. "Chosen travels, derived is recomputed" was enough until stored prompts had to be decided: they are derived, and they must travel anyway. The test that separates evidence from cache is not "could this be recomputed" but "would a recomputation answer the same question" — a rebuilt search index answers the same question, a rebuilt prompt says what the turn would be told *now*, which is the opposite of what the inspector is for. Also here: a real SQLite backup, through the online backup API rather than a file copy, taken while the application is running and verified before it is kept; story cards settled as compatibility-only legacy data and taken out of the narrator's prompt, because they were the untracked path around knowledge authority that IMPORTED-KNOWLEDGE-DESIGN §73 already forbade; and no schema change at all, proved against a database M8's own code wrote. Three defects, found by running the milestone's own tests rather than by reading them. Deleting a campaign leaked its FTS index rows, and SQLite then handed the freed ids to the next source imported into any campaign, which failed with an integrity error that Reindex could not repair — both ends are closed, and a database already carrying the damage now repairs itself. An imported node with no state snapshot was being stamped with the campaign's head state, so an Undo to turn 2 showed what the story knew at turn 20. And the snapshot relink did not persist at all, because it mutated a dict in place on a column SQLAlchemy tracks by assignment: it looked correct in memory and wrote the wrong ids to disk. Carrying per-turn prompts looked like it would halve the length of campaign that can be restored. Measured — and after compressing them inside the file — everything M9 added costs 12% of it: the import ceiling moves from about 318 turns to about 279, against a 100-turn certification target. The dominant cost is not M9's at all. The per-position narrative state document is 74% of a bundle, and v2 already carried it. Backend 1,102 passed / 14 skipped / 0 failed. Frontend 145 passed. Lint, production build and Docker build clean. Verified across two server processes with two data directories, and in a real browser against a real narrator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qyn3oRd4D6pi72nKBG725B
This commit is contained in:
co-authored by
Claude Opus 5
parent
1ce9972760
commit
44edece67e
@@ -65,11 +65,16 @@ that isn't the live one starts a new branch.
|
||||
change is recorded with what it was before and which turn caused it, so the Story State panel
|
||||
can show what changed and why. You can correct it by hand, and your correction outranks the
|
||||
story.
|
||||
- **AI Dungeon-compatible context engine.** Memory, author's note, and story cards (world
|
||||
info) are triggered by keywords in recent story text, then assembled under a token budget
|
||||
(`backend/app/context/builder.py`). Story cards are the inherited authored-lore primitive and
|
||||
are kept; they are **not** the knowledge library below, which is a first-class subsystem with
|
||||
its own classification, provenance, chunking and index.
|
||||
- **A context engine you can account for.** Memory, the author's note, the campaign's own
|
||||
rules, the authoritative state, the summary that applies here, and the retrieved imported
|
||||
passages are assembled under one token budget, in an order chosen so that a section which
|
||||
changes does not re-price the cached prefix above it (`backend/app/context/builder.py`).
|
||||
|
||||
**Story cards** — AI Dungeon's world-info primitive, inherited with the fork — are kept as
|
||||
legacy data and travel with an export, but they no longer reach the narrator. A keyword-matched
|
||||
card used to arrive in front of it as a world fact with no class, no visibility, no source and
|
||||
nothing to switch it off, competing with your imported Canon for the same budget; the knowledge
|
||||
library below replaces it, and does all of that explicitly.
|
||||
- **Total prompt transparency.** Every turn stores the exact prompt sent to the model.
|
||||
**Inspect context** on any narrator turn opens a readable account of what it was given —
|
||||
what it remembered, what it read, what it believes, and what each part cost — with the
|
||||
@@ -122,13 +127,33 @@ that isn't the live one starts a new branch.
|
||||
no story, and deleting a branch a Save Point is kept on is refused until you
|
||||
remove the Save Point yourself, so nothing takes a named moment away behind
|
||||
your back.
|
||||
- **Import and export.** AI Dungeon-compatible scenario format; JSON for everything else. An adventure exports as `ai-dnd-adventure-v2`, which carries the whole tree:
|
||||
every branch, every take, the fork points, which branches the story has left behind, the Save
|
||||
Points and the position it is being read at — all of them chosen rather than computed, which is
|
||||
the rule for what a bundle carries. A campaign opens where its head says, never at a Save Point
|
||||
merely because it has one. A campaign exported after two Undos imports still undone, with its
|
||||
retained future intact, instead of silently reopening at its newest turn. Files that predate
|
||||
the head position, and files saved in the old single-line format, still import.
|
||||
- **Import and export, as a recovery contract.** A campaign exports as one JSON file,
|
||||
`ai-dnd-adventure-v3`, and imports into a clean install on another machine. It carries the whole
|
||||
tree — every branch, every take, the fork points, which branches the story has left behind, the
|
||||
Save Points and the position it is being read at — and, since it is meant to be *recovery*
|
||||
rather than a copy of the text, everything that explains that story: the authoritative state
|
||||
and the typed events behind it, **the exact prompt each turn was given and the passages it was
|
||||
shown**, the summaries with the coordinates that decide whether they still apply, and your
|
||||
imported files with their classifications. A restored campaign can still answer "why does the
|
||||
state say this?" and "what was the narrator actually told?" — after the source file has been
|
||||
deleted and the canon edited since.
|
||||
|
||||
A campaign opens where its head says, never at a Save Point merely because it has one. Exported
|
||||
after two Undos, it imports still undone, with its retained future intact. Search indexes are
|
||||
not carried: they are rebuilt from the content, before the import returns. Nothing about your
|
||||
machine travels — no endpoint, no model, no path — so importing somebody's campaign never
|
||||
reconfigures your inference, and a campaign imports whether or not you have the model that
|
||||
wrote it. Older files still import: the flat single-line format, files that predate the head
|
||||
position, and files that predate everything above. AI Dungeon-compatible scenario format is
|
||||
still read and written for scenarios and story cards.
|
||||
- **A verified backup of everything, taken while you play.** Settings → *Back up everything on
|
||||
this machine* writes a copy of the whole database through SQLite's online backup API — not a
|
||||
file copy, which of a live database can read one page before a transaction and another after it
|
||||
and produce a file that opens and is quietly missing rows. It is checked with `PRAGMA
|
||||
quick_check` before it is kept, and an existing backup is never overwritten
|
||||
(`backend/app/backup.py`). Restoring one is a documented stop-move-start procedure in
|
||||
`DEVELOPMENT.md`, deliberately not a button: replacing the file the running application has
|
||||
open is how you lose both copies.
|
||||
- **Single user, no accounts.** There is no sign-up, no login, no session and no API key
|
||||
anywhere in the product. The storyteller API binds to loopback and is unauthenticated by
|
||||
design, because the only person who can reach it is the person running it. A new install
|
||||
@@ -234,8 +259,8 @@ leave it there.
|
||||
player input
|
||||
→ assemble context: [narrator prompt] + [world state + stat guide] + [AI instructions]
|
||||
+ [plot essentials] + [story summary] + [retrieved memories]
|
||||
+ [triggered story cards] + [retrieved imported knowledge,
|
||||
framed by class and bounded by its own budget]
|
||||
+ [retrieved imported knowledge, framed by class and
|
||||
bounded by its own budget]
|
||||
+ [history along this branch, token-budgeted]
|
||||
+ [author's note] + [player action]
|
||||
→ snapshot context (Insights)
|
||||
@@ -262,7 +287,8 @@ frontend/ React + Vite SPA ──HTTP/SSE──► backend/ FastAPI
|
||||
├─ worldstate/ the inherited RPG stat engine — legacy, no longer authoritative
|
||||
├─ memorybank.py auto-summarization + embedding retrieval
|
||||
├─ knowledge/ the imported library: import, chunk, FTS5, embed, rank, inject
|
||||
├─ bundle.py the export/import formats, v2 (tree) and a v1 reader
|
||||
├─ bundle.py the export/import formats: v3, and readers for v2 and v1
|
||||
├─ backup.py a verified whole-database copy, via SQLite's backup API
|
||||
├─ providers/ OpenAI-compatible adapter, streaming
|
||||
└─ data.db SQLite (path overridable via AIDND_DB_PATH)
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user