TLDR: Don't cache cheap, user-mutable reads in a long-lived execution context. The staleness cost will eat you alive.
The Setup
Apollo (my AI assistant, built on the Claude agent SDK) has a "parallel suggestions" block — a prompt pattern that fires multiple MCP (Model Context Protocol, a way for AI agents to call external tools) calls at once to pull my task state from Things (my task manager) before offering suggestions.
Three calls, every time: get_today, get_inbox, and get_logbook.
Early on I thought: why refetch on every single turn? That's wasteful, right? Cache the result at session start, reuse it throughout.
Classic builder instinct. And completely wrong for this use case.
The Bug I Couldn't Figure Out at First
I kept noticing Apollo surfacing tasks I'd already checked off.
Not occasionally. Repeatedly. The same completed items, resurfacing like they were new.
My first instinct was to blame MCP — some staleness in the Things integration, maybe a sync lag. I poked at that for a bit.
It wasn't MCP.
What Was Actually Happening
Here's the sequence:
- Session starts → Apollo caches
get_today + get_inbox→ pool is built - I complete a task in Things mid-session
get_todayno longer returns that item (Things handles this correctly)- But Apollo's cached pool still holds it
- Next parallel block fires → it surfaces the dead task again
The cache was holding a snapshot of reality that had already changed.
And here's the thing I had to admit to myself: the cache was a premature optimization. The MCP call is cheap. I was solving a non-problem and creating a real one.
The Fix
Simple. Drop the cache entirely.
Refetch get_today, get_inbox, and get_logbook fresh every single time the parallel block fires. No exceptions.
Then add a filter step as belt-and-suspenders: build the candidate pool from Today ∪ Inbox, then remove any item whose UUID appears in today's Logbook.
get_today already excludes completed items — but the Logbook cross-check guards against mid-session completions Apollo wasn't aware of, MCP staleness, and cross-session drift. Two layers, cheap to run, zero regression.
The Discriminating Question
I now ask two things before I cache anything:
- Does this data change during the cache's lifetime? (Task state: obviously yes.)
- Is the fetch actually expensive enough to justify staleness risk? (An MCP call to a local app: no.)
If either answer goes the wrong way, you don't cache.
Contrast that with my session-state recency cache file — a rolling session-state recency cache, a recent addition to Apollo. That one's worth caching: writing it is a deliberate in-turn operation, reading it is a cross-session cold-start fix, and it only changes when I explicitly update it. Totally different tradeoff.
Same word, "cache." Completely different calculus.
Why This Matters to Me
I wasted maybe two sessions debugging what I assumed was a tool problem before I looked at my own architecture. The fix was five lines. The lesson is permanent.
If your agent is haunting you with stale state, don't look at the external tool first. Look at what you decided to remember.
P.S. — The fix commit on 2026-05-11 also quietly broke every terminal I had open, because updating the hook reminder prose added an apostrophe (
today's logbook) to an inlinedprintfcommand insettings.json. Single-quote closed early. Different bug, same day. Lesson: externalize prose-heavy hook commands to a separate.mdfile andcatit. Don't inline.