M6: branch-safe context, summaries and long-term story memory
Aligns the inherited AI-DnD memory and context foundation with the history,
authority and state model M3-M5 established. Long stories now reach the narrator
through a bounded, lineage-safe, inspectable context rather than a growing
transcript.
This commit includes the corrective work that followed the independent review in
planning/reports/M6-IMPLEMENTATION-REPORT.md. The first implementation reported
E03 as passing and it was not; the report records that history rather than
hiding it.
What was already correct, and was kept rather than rebuilt
Memory lineage. Memories already carried (branch_id, depth) and retrieval
already filtered through the capped-path clause; the ten-step negative control
was measured passing against b7005e6 before any change here. M6 adds the
regression tests that pin it, plus provenance and authority on the result.
Summary lineage — both halves
A summary is a row carrying the coordinate of the last node it covers, and
eligibility is the same head-capped lineage clause memories use. That alone
was not enough: generation was seeded from adventures.story_summary, a
campaign-global column with no lineage, so after a divergence the summariser
was handed the abandoned line's prose and asked to update it. The row it
produced was correctly anchored and therefore looked safe while its sentences
described a story the reader had left.
Generation is now seeded from summaries.current — the same question the
context builder asks — so the input and the output are scoped by one rule.
adventures.story_summary remains a reader-facing mirror for the Plot panel and
the export bundle, kept in step when a summary is written and when the head
moves, and nothing authoritative reads it.
Retrieval redundancy
With a real embedding model, four near-identical memories crowded out the one
distinctive clue, which survived only because the default memory_top_k is 5.
Retrieval now drops a candidate that repeats one already chosen, never across
authority classes, at a threshold measured against the configured embedding
model. The clue is retrieved at top_k 5, 4 and 3. Ranking itself is unchanged;
the further factors CONTEXT-AND-MEMORY §20 contemplates remain unimplemented
and are recorded as such.
Memory authority, budgeting, observability
Memory.authority is accepted_story or heuristic, classified by the application
and marked in the prompt; retrieval never writes state. The reply is reserved
out of the context budget, and an impossible configuration fails clearly
instead of overflowing. Each derived pass records ok/idle/failed per campaign,
served by GET /adventures/{id}/derived and shown in Insights, so the M2
failure — a dead memory bank with a green suite — is visible if it recurs.
Provider-wiring tests mock no factory.
Also: two pre-existing test-suite leaks fixed; two fixtures that stored one
vector in every memory now use distinct ones, so lineage assertions stay
readable alongside redundancy suppression.
Planning: CONTEXT-AND-MEMORY, TECHNICAL-DESIGN, DATA-MODEL, V1-ACCEPTANCE-TESTS,
BUILD-MILESTONES, VERSION and planning/README updated to describe what exists,
including that a valid E03 test must regenerate a summary after diverging. The
M5 report was rotated to planning/archive/milestone-reports/. No new ADR — every
choice implements a decision the package had already settled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWU4gTfLYY6Qq9U7aa9Qw2
This commit is contained in:
co-authored by
Claude Opus 5
parent
b7005e6fdd
commit
a6e9c7a32b
@@ -453,6 +453,40 @@ story_thread:
|
||||
|
||||
These are narrative continuity tools, not RPG quests.
|
||||
|
||||
## 16A. Derived Context Tables (M6, as implemented)
|
||||
|
||||
Two tables and one column carry M6's derived context. All three are derived
|
||||
data: deleting them changes no accepted history, no authoritative state and no
|
||||
head position.
|
||||
|
||||
```text
|
||||
summaries
|
||||
id, adventure_id
|
||||
text
|
||||
branch_id, depth the coordinate of the last node covered
|
||||
source_start, source_end the stretch of story summarized
|
||||
trigger "interval" (generated) or "manual" (reader-written)
|
||||
model_name, created_at
|
||||
|
||||
derived_status one row per (adventure, kind)
|
||||
kind "memory" | "summary" | "embedding"
|
||||
status "ok" (did work) | "idle" (nothing pending) | "failed"
|
||||
detail, failures
|
||||
last_attempt_at, last_success_at
|
||||
|
||||
memories.authority "accepted_story" | "heuristic"
|
||||
```
|
||||
|
||||
`summaries` mirrors the shape `memories` already had, deliberately: both are
|
||||
derived rows anchored to a coordinate on a path, and both are filtered by the
|
||||
same lineage clause — and both are also the *input* to the next round of derived
|
||||
work, which is why summary generation reads `summaries.current` rather than any
|
||||
campaign-global field.
|
||||
|
||||
`adventures.story_summary` is retained as the reader's edit surface and the
|
||||
export field, mirroring whichever summary is eligible. It carries no lineage of
|
||||
its own and nothing authoritative reads it.
|
||||
|
||||
## 17. State Version
|
||||
|
||||
The system must reconstruct authoritative state at any retained turn.
|
||||
|
||||
Reference in New Issue
Block a user