Agent docs

Caching

> Evidence: 2 file:line witnesses as of 2026-08-18.

Evidence: 2 file:line witnesses as of 2026-08-18. Confidence: derived, written from the seams and the estate's failures rather than from citations; treat each rule as a design call and argue with it. What changes it: the first module built against this doc (../PLAN.md forge-1) and its instance breakdown.

Not asking the same expensive question twice: an LLM call, a scrape, a research pass, a vendor lookup. Three layers exist in the estate for different reasons, and the one the owner asked about (cross-service caching across apps, with the Redis already up on Render) does not exist yet.

Audience: anyone whose payload spends money on a call whose answer could be reused.

What the instances do#

versable-runnerspeedwaywalmart-mvp
result cacheGCS-backed KV keyed by pipeline input (app/gcs_cache.py, LazyKVBackend("enhancement"), plus services_cache for part-type and attribute agents)scrape cache, cross-workspace on purpose: SERP results in Firestore with 7-day TTL, page text in the object store with a 1-month lifespan enforced at read and by a bucket lifecycle rule (app/lib/scrapecache.server.ts:1-11)none for results
hot-path cachenonein-memory TTL microcache with single-flight for the session, user, workspace, membership chain before every render (app/lib/microcache.server.ts:1-10)none
Redisnonenonequeue (arq) and the fleet-wide vendor rate-limit slots (ratelimit.py), not results
scopeper module deploymentper appper app

The rule#

Cache by content, scope by what the content is, state the TTL, and never let a cache change an answer's shape or hide its provenance.

Three layers, three reasons#

LayerKeyed byLivesPurpose
result cachea hash of the capability, its version, the variant, the reference-data versions, and the item's relevant input fieldsobject storage or Redis, per module or sharedskip a paid call whose inputs have not changed
vendor cachethe outbound request (a URL, a SERP query, a model prompt)shared across modules that call the same vendorskip a scrape or a lookup any module already did
hot-path cachewhatever an app reads before every requestin-process, seconds of TTL, single-flightspeed, not money; app-side

The result cache is the runner's (01-runner-and-payload.md: the payload gets a cached(key, compute) verb, and never opens a cache client itself). The vendor cache is a shared service or a shared Redis, and it is what "cross-service caching" means. The hot-path cache is an app concern and speedway's microcache is the model.

Scope follows the content#

speedway's rule is adopted verbatim: "a page's content is not workspace data". A scraped page, a SERP result, a model's answer to a tenant-free prompt are cross-tenant by nature and cached without a tenant in the key. An answer that embeds tenant data (copy generated from a customer's template) is tenant-scoped in the key or not cached across tenants at all. The rule of thumb: if the cached value could reveal one tenant's data to another, the tenant is in the key.

The key includes everything that would change the answer#

Capability id and version, variant, judge, reference-data versions, model name, and the input fields the capability declares as cache-relevant in its manifest. A cache hit after a taxonomy snapshot bump is a wrong answer with a fast response time; the version in the key is what prevents it.

Provenance survives a hit#

An outcome served from cache says so (cached: true, cache_key, cached_at) and still records a usage event (meter cache.hit, cost zero) so stats and bills see it. A caller can pass settings.cache: bypass on a job to force fresh computation, and the manifest says whether the capability supports it.

TTLs are stated and enforced by the store#

Every cache has a TTL in the manifest and a matching expiry on the store (Redis TTL, a bucket lifecycle rule). Enforcing at read time alone, as speedway does for page text in addition to the lifecycle rule, is fine as a belt; the store's rule is the braces.

Cross-service caching, on Redis or not#

The owner named the Redis on Render already used for App V5 caching. A shared vendor cache is the natural first tenant of a shared Redis, with two cautions:

  • a Render Redis reached from GCP crosses clouds on every hit; for a scrape cache whose alternative is a paid Oxylabs call that is fine, for a hot-path cache it is not
  • Redis is a cache, not a store; anything that must survive an eviction (results, usage) lives in object storage or a database, and the Redis layer sits in front

Whether the shared cache is one Redis, a small cache module with its own surface, or per-cloud Redis instances with the same key scheme is a deployment decision left open until a second module needs it. The key scheme and the scope rule above are what make any of the three work.

What a cached answer tells its consumer#

Marking an outcome cached is the floor, not the contract. An outcome served from cache carries a cache block, and a consumer can render it without knowing which module produced it:

fieldmeaning
hitwhether this outcome came from cache
key_versionthe capability and reference-data versions the key was built from, so a consumer can see WHY something did or did not hit
stored_atwhen the value was written
expires_atwhen the store will drop it, so a consumer can show remaining life rather than a raw TTL
age_show old the value is at the moment it was served
scopetenant, capability, or shared, matching the key scheme above

A miss carries the same block with hit: false, because the absence of a hit is itself information and a consumer that only sees the block on hits cannot tell a miss from a module that never reports.

Owner ruling, 2026-08-22: preview runs reuse their outputs, and the cache metadata is "useful later" beyond that. The immediate case is a preview of one to five rows whose result the eventual job should not pay for twice; the durable case is a consumer that can explain to a person why a run was instant or why it cost money.

Do-nots#

  • Do not put a tenant's data in a cross-tenant cache key, and do not leave the tenant out of a key when the value carries tenant data.
  • Do not cache without the capability version and reference-data versions in the key. A stale hit is a wrong answer.
  • Do not serve a cached outcome without marking it cached.
  • Do not let a payload open a cache client. The runner provides the verb.
  • Do not treat Redis as durable storage.
  • Do not ship a cache without a TTL the store enforces.
  • Do not report a cache hit as a bare boolean. Carry the block, including on a miss.
  • Do not omit the key's version inputs from what the consumer sees. A hit nobody can explain is a hit nobody can trust.
@versable-git/ui · reference, canon, and method, read in place