Tell the summarizer who the characters are
`_create_due_memories` sent six actions of second-person prose and nothing else — no protagonist, no cast, no setting, and no instruction about what person to write in. So for `You push the door open. She grabs your arm.` the only honest memory was "You entered a room and she stopped you", which names nobody when it is retrieved forty turns later. The framing wandered too: with no rule, the model picked a person per call, and one bank ended up holding "You entered the crypt", "The player entered the crypt" and "He entered the crypt" for the same kind of event. Both prompts now carry a cast brief and a framing rule: third person, the protagonist by name, other characters named rather than left as bare pronouns. The rule states its reason, because a memory really is read in isolation much later and a model told why complies far more consistently than one handed a bare instruction. The cast comes from the story cards, not from `stat_schema`. Every schema NPC is already turned into a card at adventure creation, deduplicated against the hand-written ones by name, so the cards cover schema NPCs, an author's own cards, and an adventure with no RPG layer at all through one path instead of three. Keyword matching alone was not enough, and finding that out changed the design. Built that way first, the brief for "She grabs your arm" listed the protagonist and nobody else: the block that most needs a cast is exactly the one written in bare pronouns, and Gwen's trigger keys include "her" but the text says "she". So matched cards come first and the remaining slots are filled with the other character cards. Places and items are not topped up — an unmentioned tavern is not who "she" was — though a place that is mentioned still matches normally. The asymmetry with the turn prompt is deliberate: an untriggered card is wrong as lore and right in a roster, because the roster answers "who could these pronouns be" rather than "what is on stage". Fixed descriptions only, never live values. `Gwen: trust 40 (wary)` in the brief would make the same event summarized at two different times come out framed differently, which is the fault this removes. An adventure with no persona still gets the cast and the setting, and the model is told to write "the player". One with nothing to say sends byte for byte the prompt it sent before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPyQN926gkZTAYfgugcaok
This commit is contained in:
@@ -4,8 +4,10 @@ Two changes, in order. Phase 1 gives the adventure a persona. Phase 2 uses it,
|
||||
along with the cast, to fix the memories. Phase 1 is worth shipping on its own;
|
||||
Phase 2 depends on it and is much smaller once it lands.
|
||||
|
||||
**Phase 1 is built, green (593 backend tests), and driven in a browser
|
||||
(21/21 checks). Phase 2 is not started.**
|
||||
**Both phases are built and green (610 backend tests). Phase 1 was driven in a
|
||||
browser (21/21 checks). Phase 2's prompts were verified against the real seeded
|
||||
scenario — see "Phase 2 as built" — but no memory has been generated by a real
|
||||
model yet, because that needs an API key.**
|
||||
|
||||
**Last updated: 2026-08-31.**
|
||||
|
||||
@@ -293,25 +295,6 @@ after: Kaelen bribed Gwen with fifty silver to hold the north door while
|
||||
he went down alone.
|
||||
```
|
||||
|
||||
## Open questions for Phase 2
|
||||
|
||||
**Existing memories.** A bank written under the old prompt will sit alongside
|
||||
new ones, mixing "you" and "Kaelen" in the same context. Options: leave them and
|
||||
let eviction age them out at `memory_bank_capacity` (80), or clear
|
||||
`memory_cursor` and re-summarize from the start. Re-summarizing costs one AI
|
||||
call per 6 actions and duplicates whatever is still in the bank, since nothing
|
||||
deletes the old rows. **Leaning: leave them.** Decide when the framing change is
|
||||
measured, not before.
|
||||
|
||||
**Retrieval framing mismatch.** `retrieve_memories` embeds the last 4 actions
|
||||
raw — second person, "you". New memories will be third person and named.
|
||||
Embeddings handle paraphrase well, so this is probably minor, but if retrieval
|
||||
quality visibly dips after Phase 2 this is the first place to look. A fix would
|
||||
be to prepend the same cast brief to the query text.
|
||||
|
||||
**Token cost.** The brief adds roughly 100–200 tokens to one call per 6 actions
|
||||
and one per 15. Negligible against a turn.
|
||||
|
||||
---
|
||||
|
||||
# Phase 1 as built
|
||||
@@ -385,3 +368,106 @@ decode it, write `<base64> <rank>` per line sorted by rank, and the result
|
||||
matches the SHA-256 that `tiktoken_ext/openai_public.py` hardcodes, so the
|
||||
reconstruction is verifiable rather than trusted. Drop it at
|
||||
`$TIKTOKEN_CACHE_DIR/<sha1 of the URL>`.
|
||||
|
||||
---
|
||||
|
||||
# Phase 2 as built
|
||||
|
||||
## The cast comes from the story cards, not from `stat_schema`
|
||||
|
||||
This is the one thing the sketch above got wrong, and it made the change much
|
||||
smaller. `scenario_text.scenario_card_specs` already turns **every schema NPC
|
||||
into a story card** on the adventure at creation, deduplicated against the
|
||||
hand-written cards by name. So the cards are a single unified cast source that
|
||||
covers schema NPCs, an author's own cards, and an adventure with no RPG layer,
|
||||
through one path instead of three. `memorybank` never reads `stat_schema`.
|
||||
|
||||
## Keyword matching alone was not enough
|
||||
|
||||
The sketch said to run `_match_cards` over the block. Built that way first, and
|
||||
it failed the exact case the change exists for.
|
||||
|
||||
The block `You push the door open. She grabs your arm.` matches **no** card
|
||||
keyword, so the brief listed the protagonist and nobody else — leaving the
|
||||
summarizer guessing at precisely the moment it was handed a brief to stop
|
||||
guessing. Seed 04 gives Gwen the trigger keys `"Gwen, ranger, her"`, and even
|
||||
that does not save it: the text says "she", not "her".
|
||||
|
||||
So the roster is **matched cards first, then topped up with the other
|
||||
`character` cards** to `MAX_CAST_MEMBERS`. Places and items are not topped up —
|
||||
an unmentioned tavern is not who "she" was — but a place that *is* mentioned
|
||||
still matches normally.
|
||||
|
||||
That asymmetry with the turn prompt is deliberate. Including an untriggered card
|
||||
as lore would be wrong: it is not relevant to the next sentence. Including an
|
||||
untriggered character in a roster is right: the question the roster answers is
|
||||
"who could these pronouns be", not "what is on stage".
|
||||
|
||||
## `_match_cards` became `match_cards`
|
||||
|
||||
Two callers now run the same rule, so it is public and exported from
|
||||
`app.context`. One rule, one implementation.
|
||||
|
||||
## What actually gets sent
|
||||
|
||||
Verified against the seeded Bandit Camp scenario, with the real
|
||||
`_create_due_memories` path and a stub provider:
|
||||
|
||||
```
|
||||
Cast:
|
||||
- Kaelen (he/him) — the protagonist. A half-elf ranger, exiled from the
|
||||
northern holds for a killing he still won't explain.
|
||||
- Bandit Camp — A rough camp of bandits in a forest clearing, holding a
|
||||
stolen caravan strongbox.
|
||||
- Gwen — A loyal ranger and the player's ally. Quick with a bow,
|
||||
dry-humoured, fiercely protective. …
|
||||
- Bandit Leader — The scarred leader of the bandit camp, guarding the
|
||||
strongbox. …
|
||||
|
||||
Setting:
|
||||
The player and Gwen, a loyal ranger ally, are raiding a bandit camp to
|
||||
recover a stolen strongbox. …
|
||||
|
||||
Story excerpt:
|
||||
|
||||
… You are six paces from the strongbox when she hisses a warning. …
|
||||
|
||||
Memory:
|
||||
```
|
||||
|
||||
That last line is the case in miniature: "she" is now resolvable.
|
||||
|
||||
**With no persona set**, the roster still lists the NPCs and the setting, and
|
||||
the system prompt tells the model to call the protagonist "the player". Phase 2
|
||||
therefore improves adventures that never set a persona at all.
|
||||
|
||||
**With no persona, no cards and no plot essentials**, the user message is byte
|
||||
for byte what it was before this change — `Story excerpt:` first, no stray blank
|
||||
lines. A test holds that.
|
||||
|
||||
## Decided, from the sketch's open questions
|
||||
|
||||
**Existing memories: left alone.** A bank written under the old prompt will mix
|
||||
"you" and named framing for a while. Re-summarizing costs one call per 6 actions
|
||||
and duplicates whatever is still in the bank, because nothing deletes the old
|
||||
rows. Eviction ages them out at `memory_bank_capacity` (80). Revisit only if the
|
||||
mixture visibly hurts.
|
||||
|
||||
**Retrieval framing mismatch: watched, not fixed.** `retrieve_memories` still
|
||||
embeds the last 4 actions raw, in second person, while new memories are third
|
||||
person and named. Embeddings handle paraphrase well, so this is speculative.
|
||||
If retrieval quality visibly dips, prepending the same brief to the query text
|
||||
is the first thing to try.
|
||||
|
||||
## Still to check with a real model
|
||||
|
||||
Everything above is the prompt, not the output. Nobody has yet run a real
|
||||
provider over it and read the memories that come back. That needs an API key,
|
||||
and it is the only way to know whether the framing rule actually holds across a
|
||||
whole adventure. What to look for:
|
||||
|
||||
1. Every new memory names the protagonist and uses no bare pronouns.
|
||||
2. The framing is the same across memories written many turns apart.
|
||||
3. The rewritten story summary inherits it.
|
||||
4. Memories do not get noticeably longer — the brief is context, not content to
|
||||
be repeated back.
|
||||
|
||||
Reference in New Issue
Block a user