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:
JesseMarkowitz
2026-09-07 01:55:45 -04:00
co-authored by Claude Opus 5
parent 1ce9972760
commit 44edece67e
46 changed files with 9227 additions and 178 deletions
+193 -1
View File
@@ -1118,6 +1118,187 @@ Make campaigns portable and recoverable without losing lineage, state, knowledge
A campaign can be safely exported, imported into a clean data directory, and reopened at the exact intended active position with authoritative history/state intact.
## Status: COMPLETE — 2026-09-07, pending independent review
Implemented on `m9-recovery` from the signed M8 commit `1ce9972`, measured
before and after against the same fixture, and verified in a real browser
against a real narrator. `planning/reports/M9-IMPLEMENTATION-REPORT.md` is the
implementer's account, written for a reviewer.
**What it delivered, beyond the scope list above:**
- **The bundle became a version, and the reason is a rule.** `ai-dnd-adventure-v3`.
Everything M9 adds could have been an optional key, the way four earlier
additions were — and that mechanism fails exactly here, because a v2 file with
no prompt provenance is ambiguous between "written before M9" and "written by
M9 from a campaign with none". A version is how a recovery file states what it
was capable of recording. v1 and v2 are still read, and every seam from
pre-active-head onward is tested.
- **A third category of data.** "Chosen travels, derived is recomputed" was
enough until stored prompts had to be decided. They are derived and must
travel, so the rule is now chosen / **evidence** / rebuildable, and the test
separating the last two is not "could this be recomputed" but "would a
recomputation answer the same question".
- **The M8 handoff on prompt provenance is closed.** An old turn in a restored
campaign shows what it was actually given, after the source has been deleted,
the canon edited and the state moved on.
- **State events and proposals travel**, so a moved campaign can still say why
its state is what it is — and a manual correction is still identifiable as
one, which it was not before.
- **Summaries travel with their coordinates**, so an abandoned line's summary is
still ineligible after the move, and a moved campaign resumes with its
long-story continuity instead of behaving like a new one.
- **A verified SQLite backup**, using the online backup API rather than a file
copy, taken while the application is running, with a browser control in
Settings.
- **Take parentage**, memory authority and parser/chunking versions, each
closing a smaller fidelity loss.
**Three defects found by running the milestone's own tests, and fixed here:**
1. **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. Reindex could not repair it either. Pre-existing
since M7; both ends are now closed and an already-damaged database repairs
itself with no migration.
2. **An imported node with no state snapshot was stamped with the campaign's
head state**, so an Undo to turn 2 showed what the story knew at turn 20 —
M5's review finding 3, arriving through the import.
3. **A snapshot's `source_id` was not being translated on import** because the
relink mutated a dict in place, which a non-`MutableDict` column does not
notice. Found by a test asserting the outcome rather than the call.
**One decision the brief asked for, made and recorded:**
**Story cards are compatibility-only legacy data, and no longer enter the
narrator's prompt.** They still travel in both directions, the rows and the API
stay, and `memorybank.cast_brief` still reads them as the summariser's character
roster. What stops is the injection: a keyword-matched card arrived in front of
the narrator as `World Lore: …` with no class, no visibility, no source, no way
to switch it off since M8 removed the editor, and no row in the context
inspector — which is `IMPORTED-KNOWLEDGE-DESIGN.md` §73's "alternate untracked
path around the new knowledge authority/provenance rules" in as many words.
**Debt carried forward, deliberately:**
- **A long campaign's bundle has a measured ceiling: ~279 turns** against the
20 MB import limit. A per-turn prompt contains the story so far, so carrying
one per turn is O(turns²); compressing them inside the file cut that to about
an eighth of what it would have been. **M9's additions account for only 12% of
the ceiling.** The other 88% is the per-position narrative state document,
which is 74% of a bundle and which v2 already carried — so lifting the ceiling
means addressing that, not the evidence. Far beyond M11's 100-turn
certification (14% of the cap), and stated with its measurement rather than
hidden. A streaming or chunked import is the fix if a later milestone needs
one; the asymmetry to know about is that such a campaign can still be exported
and would be refused on import.
- **No discarded-history recovery screen** (§63). M9's job was that retained
history survives correctly so a later screen can use it; it does.
- **No whole-transcript copy and no story search** (§77, §78) — still M11 or
later.
---
# Post-M8 hands-on playtest findings — recorded 2026-09-07, owned by M11
**Not M9 defects, not caused by M9, and they did not block M9 acceptance.**
Recorded here rather than only in the M9 report because a milestone report is
archived when the next one replaces it, and these must not go with it.
They come from a real play session against **accepted, signed M8**: real
browser, real trusted-LAN Ollama, narrator `qwen2.5:3b-instruct-16k`, disposable
isolated campaign database. **That database was deliberately destroyed
afterwards**, so the stored context snapshot for finding D is gone and no root
cause is claimed for it. Full write-up, with the verification behind each
mechanism, is in the M9 report's **§Y** (in `archive/milestone-reports/` once
M10's report replaces it).
**These are M11's, and explicitly not M10's.** M10 is media-readiness
architecture and stays bounded; it inherits them as known carry-forward only.
### A. The browser still calls the product "AI D&D" — M11 release polish
`frontend/index.html` still carries the inherited `<title>AI D&amp;D</title>`.
M8 changed the navigation, the inspector and the screens, and never claimed the
title — so this is an uncovered gap rather than a false claim.
**Do not fix it with a find-and-replace to "Adventure Storyteller".**
`SPECIFICATION.md` requires a genre-agnostic engine, and *Adventure* is narrower
than the product. The naming decision is the repository owner's; a neutral
working name such as **Interactive Story** and a tab form such as
`<Campaign Name> — Interactive Story` are candidates, not decisions.
### B. After Undo, the reader cannot tell where they are — M11 UX polish
Undo behaved correctly (M3 semantics; re-verified throughout M9). The reader
could not tell **which point in the story** they had moved to.
`BROWSER-UX-SPEC.md` §8 said only *"The current endpoint should be clear"*, and
its companion sentence was already true while the reader was lost — so the
requirement could not hold the behaviour. §8 has been strengthened to state the
orientation requirement; **no UI text is prescribed**, because none is ratified.
A `Moment 8` → `Moment 7` style indicator is a candidate. Needs a browser
regression scenario.
### C. Narration length has no measurable effect — M11 realistic-model behaviour
The setup choice becomes **one English sentence** in the campaign's
`ai_instructions` and changes **no generation setting**. Independently,
`length_hint()` derives a numeric word range from the **global**
`Settings.max_output_tokens` and places it after the history — and at the default
800 it reads *"must not exceed 506 words, and it should not stop short of about
177"* **identically for brief, medium and long**.
That is a mechanism, verified by reading and running the code — **not a proven
cause** of what the reader saw; the narrator's instruction following is also in
play. A reproduction must measure what enters the stored prompt, whether the
setting moves any generation budget, and actual word/paragraph counts across
repeated turns, on the reference 3B narrator **and** a stronger local one.
Clearer numeric targets (`Brief ~100-200 words` and so on) are a design
candidate, not ratified. **Do not hard-truncate prose** — the state block is
emitted last and truncation removes it.
### D. Character identity / coreference confusion — M11 diagnostic
Four people in one scene — Bill (protagonist), Roger, John, Alice — and later
narration treated Alice as two different Alices.
**Root cause UNKNOWN and no longer establishable.** Candidates: a model
coreference failure on a correct prompt; duplicate/conflicting state; a
context/summary/memory assembly failure; or a context that is not contradictory
but too implicit for a small model.
**One structural fact to check first**, verified by reading the code: the
narrative state **permits two entities to share a display name and reports
nothing**. Entities are keyed by the model-supplied id; `DUPLICATE_ENTITY`
rejects only a repeated *key*; no check exists on `name`. That is one of this
finding's failure modes, and establishes nothing about what happened.
**M11 must run an explicit diagnostic** with a protagonist and three same-scene
supporting characters, stressing pronouns, dialogue attribution, entrances and
exits, reference by name and by role, and one character speaking about another.
It must detect duplicate creation, same-name duplication, protagonist drift,
misattributed dialogue, self-as-other reference, and state/context disagreement
— and on any failure preserve the pre-generation state, the exact stored prompt
snapshot, history, summaries, memories, imported knowledge, narrator output and
model settings, then classify:
```text
STATE DEFECT / CONTEXT ASSEMBLY DEFECT / DERIVED MEMORY-SUMMARY DEFECT /
MODEL FAILURE WITH CORRECT CONTEXT / AMBIGUOUS
```
**Do not "fix" a model failure by changing authoritative state, and do not blame
the model if the prompt already contained the error.** M9 made all of that
evidence portable, so a failing campaign can be exported and handed over intact.
**The standard fixture does not cover this class.** `TEST-CAMPAIGN-FIXTURE.md`'s
seven traps are knowledge, authority, branch leakage and possession; there is no
identity trap and its on-stage cast is effectively two people. The established
fixture was **not modified** — it is the deterministic baseline earlier results
are compared against. A companion fixture, `Multi-Character Identity Test`, is
proposed in an appendix to that document.
---
# M10 — Future Media Extension Hooks Only
@@ -1126,6 +1307,13 @@ A campaign can be safely exported, imported into a clean data directory, and reo
Preserve the approved future media interfaces without adding a media-generation dependency to v1.
## Note — the post-M8 playtest findings are **not** M10 scope
The four findings recorded above are owned by M11. M10 inherits them as known
carry-forward items only: it should neither implement nor test them, and its
scope below is unchanged by them. They are listed before this milestone rather
than after it only because they were recorded during M9's closeout.
## Scope
- scene snapshots/packets suitable for future providers,
@@ -1178,7 +1366,11 @@ Validate the full product against the release contract after all functional mile
- fantasy and science-fiction fixtures,
- export/import/recovery tests,
- migration tests,
- documentation and packaging.
- documentation and packaging,
- **the four post-M8 hands-on playtest findings above**: the browser product
name, reader orientation after history movement, a narration-length setting
with a measurable effect, and the multi-character identity diagnostic with its
companion fixture.
## Tests / Acceptance