What It Does
EveryWorkspace ships with a two-layer cache so repeated work against remote backends (S3, GDrive, Slack, …) hits local state instead of the network:
- Index cache. Listings and metadata. The first directory walk hits the API; subsequent ones serve from the index until the TTL expires.
- File cache. Object bytes. The first read streams from origin; later pipelines read from cache.
ConsistencyPolicy.ALWAYS, cached file bytes are checked against live backend metadata, bypassing the index. A changed fingerprint triggers a fresh read; a deleted file is evicted. If the backend cannot supply a fingerprint or its probe fails, the read must fetch current bytes or report an error. LAZY continues to trust cached bytes.
A fresh directory listing proves a child absent only when that child is not listed. If a listed child’s metadata has been evicted independently, object-store stat fetches it from the backend.
Stores
Each layer is a pluggable store with two built-ins:- RAM (default): in-process, zero setup, 512 MB file cache and 10-minute index TTL. Best for single-process apps and notebooks.
- Redis: shared across workers, processes, and machines. Best for serverless, multi-replica services, or for cache state that survives restarts.
Eviction & Limits
The two layers are bounded differently:
Raising the file limit keeps more bytes warm at the cost of memory; lengthening the index TTL serves listings longer between API walks at the cost of staleness.
Miss/Hit Lifecycle
Metadata Index Contract
The RAM and Redis metadata indexes share the same lookup states: a directory never listed isNOT_FOUND, a cached empty directory is a successful empty
listing, and a listing at or past its deadline is EXPIRED. Entry metadata
remains readable after a directory expires. invalidate() expires listings
without deleting their metadata; invalidate_dir / invalidateDir removes a
listing and its direct children’s entries, and prefix invalidation removes a
whole subtree. clear() removes all index data.
Direct backend lookups trust cached metadata only when a fresh parent listing
includes the path. Partial or filtered listings may store individual entries
without a complete parent; these entries require another parent refresh on
each direct lookup. The old target is removed before refreshing so an omitted
or renamed item cannot survive as an orphaned cache hit.
Concurrent lookups sharing an index serialize each parent’s refresh through
the final entry lookup. GitHub and Hugging Face snapshot readers hold the same
guard for the whole mount, so one reader cannot remove a snapshot another is
still reading. These guards coordinate tasks within one process; unrelated
parent directories and mounts can refresh concurrently.
seed() merges snapshots by path and copies their child lists. Redis queues
these synchronous calls and flushes them before the next index operation or
close(); clear() discards queued snapshots. Multiple seeds accumulate, and
failed flushes remain queued for retry. entries() returns all stored metadata,
including entries whose directory listing has expired.
Redis stores each entry as the IndexEntry JSON under mirage:idx:entry:
and each directory’s children and deadline as the IndexDirectory JSON under
mirage:idx:directory:. Both are the documents pydantic writes, snake_case
and every field, and TypeScript writes and reads the same bytes
(IndexEntry.toJSON / IndexEntry.fromJSON), so one Redis serves both
languages and a row that does not parse is an error rather than a miss.
Invalidation uses permanent keys beside the payload:
mirage:idx:generation for the whole store and
mirage:idx:generation:<absolute-directory> for each directory.
Each listing records both tokens, and one MGET reads the listing and its two
current tokens. A missing or changed token makes the listing EXPIRED.
New tokens are unique so eviction or a refill cannot revive an old listing.
Competing initializers retain their attempted tokens; a worker that loses
initialization incurs a cache refill instead of adopting a later generation.
Global invalidation rotates the global token atomically. Directory invalidation
removes that directory’s token; subtree invalidation removes tokens under that
path, preserving fresh listings outside a scoped invalidation.
Like RAM, these records retain expired listings until explicit removal; they do
not use Redis TTL deletion. Redis server eviction can still turn any cached
record into a miss. Configure Redis maxmemory for server-side size limits.
The layout is not versioned and earlier layouts are not read. Upgrade every
worker that shares an index together, and flush the key prefix first
(clear(), or delete the keys under it), so no worker opens a row another
release wrote.
The Redis file cache likewise uses server-side limits instead of RAM’s
cache_size, cache_entries, or LRU. File bytes and metadata occupy separate
keys, so server eviction can remove either independently: a freshness check
alone does not guarantee the bytes remain cached.
Relationship To Snapshots
The file cache is exactly what a snapshot serializes:ws.snapshot() writes the cached bytes for every touched path into the tar, and Workspace.load() restores them into the file cache so a replayed run reads from local state. The index cache is not snapshotted; it rebuilds lazily after load.