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
@@ -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&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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user