Cache poisoning is the whole problem
Semantic caching has an obvious failure mode nobody likes to talk about: one bad write, served forever to everyone nearby. This is how Crowkis decides what to trust.
Here is the uncomfortable math of semantic caching. A normal cache serves a bad entry to exactly the requests that match its key. A semantic cache serves a bad entry to every request that lands near it in embedding space. Poison doesn't sit in one cell, it radiates.
flowchart LR
subgraph EX["exact-match cache"]
K["☠ one poisoned key"] --> V1["one victim"]
end
subgraph SC["semantic cache, unguarded"]
P["☠ one poisoned entry"] --> N1["every paraphrase"]
P --> N2["every near-neighbour"]
P --> N3["every 'similar enough' query"]
end
style P fill:#fbe9e8,stroke:#d62221,stroke-width:2.5px
style K fill:#f3eee5In plain words. In plain words: a smart cache that matches by meaning will also spread a wrong answer by meaning. If one bad response gets in, everyone who asks anything similar gets it back. That's why write-time defense matters more than read-time speed.
And LLM systems produce bad writes constantly. Hallucinations. Prompt injections that smuggle instructions into what looks like an answer. A response computed for tenant A that would be a data leak if tenant B ever saw it. If your cache trusts every write, your cache is an amplifier for your worst outputs.
Five stages, every write
flowchart TD
W["candidate write"] --> S1["stage 1 · coherence, does the answer fit the question? · weight 0.30"]
S1 --> S2["stage 2 · content heuristics · weight 0.10"]
S2 --> S3["stage 3 · source trust, ledger history · weight 0.30"]
S3 --> S4["stage 4 · tenant isolation · weight 0.15"]
S4 --> S5["stage 5 · neighbourhood agreement · weight 0.15"]
S5 --> G{"composite ≥ 0.75?"}
G -- yes --> OK["accepted into the cache"]
G -- no --> NO["refused + trust-ledger entry"]
style OK fill:#fbe9e8,stroke:#d62221,stroke-width:2.5px
style NO fill:#f3eee5The two heavyweights are coherence and source trust, and that's deliberate. Coherence catches injected content that doesn't actually answer the question. Source trust means a writer that produced garbage before has to earn its way back, every accept and refuse lands in an append-only ledger, so trust has memory.
Trust has memory
2026-04-29T14:02:11 writer=svc-support ACCEPT composite=0.91
2026-04-29T14:02:38 writer=svc-support ACCEPT composite=0.88
2026-04-29T14:03:02 writer=ext-webhook REFUSE stage=1 coherence=0.31
2026-04-29T14:03:05 writer=ext-webhook REFUSE stage=3 trust=0.22
# ext-webhook now faces a higher bar on every future writeA cache that can't say no isn't infrastructure. It's a liability with a hit rate.
Refusal is a feature you can see
Blocked writes show up in the dashboard's live feed with the stage that vetoed them, and the rejection counters break down by stage so you can see what kind of poison your system actually attracts. That visibility matters more than it sounds: the first question every operator asks about a safety system is 'what is it actually doing?', and the answer should never be 'trust us.'
Tenant isolation deserves the last word. It isn't only an access-control rule at read time, it's a scored stage at write time. An entry that smells like it crossed a tenant boundary doesn't get a chance to be mis-served later, because it never enters the cache at all.