The last part ended with a workaround: after fixing a bug, I had to clear the cache by hand each time to confirm the fix actually worked, since a cached wrong answer looks exactly like a fresh wrong answer to a visitor asking the same question again. That workaround was the argument for building the one piece still missing: telling the cache when the underlying content has changed, instead of trusting it to catch up on its own.
The design stayed close to what I'd sketched out earlier. A single timestamp, updated by the blog's write API every time an article is created or edited. The chat assistant compares that timestamp against the age of each cached answer, and anything older gets treated as if it never existed, not patched, not partially trusted, just dropped and re-fetched on the next matching question. No per-article bookkeeping, no reverse index from content back to cache entries. One clock, checked on the way in.
Wiring that up meant crossing a boundary that didn't exist yet. The blog API had never called out to the chat assistant before, only the other way around. A new endpoint, guarded by a shared secret the two sides agree on ahead of time, is the whole interface. Reject anything that doesn't present it, including an unconfigured server that would otherwise accept an empty key as if that meant no check was needed.
A design only means something once it holds up in practice, so the real test was simple: use it exactly the way a visitor would, and see what breaks. Asked a specific question through the widget, checked the response, asked it again, and confirmed it came back from cache the second time. Then edited the source article underneath it on purpose, one throwaway character. Asked the exact same question a third time, and this time the response said it hadn't come from cache. That's the entire test. It's also the first time in this whole project that a fix got proven correct before any real visitor could have hit the bug it was fixing.
One more thing turned up in the same session, unrelated to any of this. The same question, asked four times in a row without changing a word, got answered correctly three times, then opened with "Hello! It's great to chat with you about Patrick" on the fourth, as if greeting someone it had already been talking to for a minute. Nothing about the input had changed. The model just occasionally reaches for a friendly opener that doesn't belong on a direct answer to a direct question. The instructions never said not to, so I added that too, though I would be lying if I said one fix is guaranteed to be the last time it happens.
I also added a new green "Ask" button, visible the moment the page loads. It's a plain button that shakes every twenty seconds, just enough to invite someone to actually try the assistant rather than scroll past it.
The interface had its own run of real bugs, all of them on mobile. The floating launcher sat hidden behind Safari's own toolbar until the layout accounted for that safe area explicitly. The panel's height was set in a unit that iOS Safari doesn't shrink when its toolbar is showing, so it needed the newer viewport unit built for exactly that case. Turnstile's own container had been hidden outright, which is enough to make the whole widget silently refuse to render. Three fixes, three separate pushes, each one looking done until the next device found the next gap.
That closes out the plan I started with: embeddings that work the same locally and at the edge, a knowledge store keyed on content instead of raw text, a cache that knows when to forget, and a widget that actually answers with them, on a phone as much as a desktop. What is left now is not a checklist, it is just watching what real questions turn up next.