A 94.3% hit rate, and it was wrong

For long-term roleplay memory, injection position decides both your cache bill and whether memory works at all. A 24-round experiment told us to append a system block after chat history (94.3% cache hits). A 400-round rerun three weeks later overturned it: DeepSeek's template merges every system message to the top of the prompt, and that 94.3% was cache pollution. Full data inside — seven retrieval strategies, leak detection, and the abstention failure that worries us more than retrieval.

A long manuscript scroll of chat history with tweezers slotting a glowing memory card into its end, beside a stack of crossed-out cards

Some context first. If you build long-term memory for character chat, one concrete question is unavoidable: where do the retrieved memory entries go in the prompt? At the head, every memory change invalidates the prefix cache for the entire history and the bill takes off. At the tail, caching stays happy — provided the tail is actually the tail. Our first experiment (24 rounds) measured a standalone system block appended after history at a 94.3% cache-hit rate. We wrote it into the design doc as the recommended layout.

Three weeks later the number fell apart.

The 400-round rerun: 3.3% versus 96.6%

The rerun used a meaner setup: 400 rounds, 47 memory-change events, full unwindowed history — the final prompt weighed 43k tokens. Two layouts ran on identical scripts, and each session carried a random salt for isolation (that detail is the whole story; more below). Results:

LayoutOverall cache hitsHits on memory-change roundsWasted miss tokens
System block after history (v1 recommendation)90.2%3.3%715k
Merged into the head of the latest user message96.9%96.6%224k

Read the middle column. The moment memory updates, the “tail system” layout loses essentially all cache hits — the exact accident injection placement is supposed to prevent, in the layout our first experiment had ranked best. The user-message layout barely flinches on the same updates.

Root cause: DeepSeek merges every system message to the top

A decisive pair of runs made it plain. Warm the cache with a single system message holding persona plus memory at the head; then send the same content re-arranged as “persona at head, memory as a standalone system block after history” — 97% hits. The two layouts produce the same token stream server-side. DeepSeek's chat template collects every system-role message in the list and concatenates them at the front of the prompt. What you meant as tail injection arrives as head injection.

This is not an inference. The Jinja template in DeepSeek's open model repos (V3.1, V3.2-Exp and R1 agree) starts by iterating all messages and rendering the concatenated system contents right after the BOS token. We ran the same layout against Kimi as a control: 88% cache hits on memory-change rounds — Kimi's template preserves message order, so its tail really is a tail. The principle “volatile content goes last” survives; the trap is that whether a system role stays in place is a per-provider template detail. The portable answer is to inject on the user track: for both template families we verified — merging and order-preserving — it is a true tail.

Where the 94.3% came from

The autopsy on v1 was short. Both layouts shared a persona; group B ran after group A on the same account; nothing isolated the sessions. Group A had already warmed the provider's disk cache with prefixes containing each memory version, and group B — whose post-merge token stream nearly matched A's — collected hits for free, round after round. The 94.3% measured the previous experiment's body heat, not the layout.

Seven retrieval strategies, and a flatter answer than expected

The same rig answered a second question along the way: how much does retrieval strategy matter? At 47–82 stored memories, character-bigram BM25 injecting its top 8 hits scored 79% end-to-end — against 80% for oracle retrieval (relevant entries guaranteed present), a paired difference of 7:6 that no test would call significant. Against the no-memory control the same comparison is 59:1 (p<0.001). In plain words: get “is anything injected” right before touching “how it is retrieved” — at dozens of memories, plain BM25 is enough, and the question deserves a rerun only when stores reach the hundreds.

What actually made us sit up was the abstention probes. Ask about something never mentioned — height, birthday, salary — and under a companion persona DeepSeek almost never admits ignorance; it fabricates with conviction. Twelve probes: the no-memory control passed zero, oracle retrieval passed one. A persona line saying “if unsure, say you don't remember” changed nothing. What works is an explicit fence appended to the memory block — apart from these memories you know no other personal facts, do not invent them — plus keeping abstention probes in the regression suite. No retrieval algorithm substitutes for that sentence.

The three lines that reached the product

Foreverse's memory plugins (a fact store plus session summaries) shipped on this experiment's shape: injection rides the user track at the message-level tail, a true tail across the providers we verified and cache-friendly; retrieval is character-bigram BM25 with room to widen top-k; the injection block ends with the abstention fence. How the caching math itself works is in the earlier field test. The full design — leak detection, what remains unfinished (alias-level leak scanning, larger-store stress runs) — is documented, and the numbers will be updated as the reruns land.

FAQ

Where should a memory block be injected into a roleplay prompt?

Message-level tail, but never as a system role. In our 400-round test, merging memory into the head of the latest user message kept cache hits at 96.6% on rounds where memory changed; the same content as a standalone system block after history scored 3.3% on those rounds — because DeepSeek's chat template merges all system messages to the top of the prompt, so 'tail system' is actually head injection, and every memory update invalidates the whole history's cache.

Why did the first experiment reach the wrong conclusion?

Cache pollution. The two layouts shared one persona, ran back-to-back on the same account, with no session isolation. Group B's requests hit the prefixes group A had already warmed into the provider's disk cache and collected a free 94.3%. The lesson is one line: every caching experiment must salt-isolate its sessions, or you are measuring the previous experiment's leftovers.

How much do retrieval strategies (BM25, vectors, hybrid) differ?

At a scale of dozens of memories, character-bigram BM25 with top-8 injection already sits next to the ceiling: 79% end-to-end accuracy versus 80% for oracle retrieval, while the gap to the no-memory control is overwhelming (59:1 on paired questions, p<0.001). Retrieval choice is not the bottleneck at this scale; revisit it once the memory store grows into the hundreds.

What failed harder than retrieval?

Abstention. Ask about something never mentioned (height, birthday, salary) and, under a companion persona, the model almost never says 'I don't know' — it invents vivid details. Across 12 abstention probes, the no-memory control passed 0 and even oracle retrieval passed 1. The fix is not retrieval: append an explicit line to the memory block — 'apart from the memories above, you do not know any other personal facts; do not invent them' — and keep abstention probes in the regression suite.

Questions or ideas? Join our Discord →

Where Should Memory Go in a Roleplay Prompt? Our First Answer Was a Caching Artifact · Foreverse · Xinmeng