skills/cache-expert/references/cache-storage.md
This document explains how cache storage works in the current dagql cache stack.
There are two layers to reason about:
dagql/cache.go)dagql/session_cache.go)The base cache is specialized for dagql results.
dagql/cache.go)sharedResult holds cached call entry state:
storageKey, resultCallKey, contentDigestKeysafeToPersistCache, persistToDBPer-call Result[T] wraps shared payload and carries per-call metadata:
idOverride) used for content-digest-hit identity preservation
(when a lookup hits by content digest instead of recipe digest, the payload is reused
but the caller still sees the ID it asked for)GetOrInitArbitrary caches opaque any values by plain string key using sharedArbitraryResult.
This path is:
CacheKey fields:
ID (required)ConcurrencyKeyTTLDoNotCacheGetOrInitCall builds and uses:
callKey = ID.Digest().String()storageKey (may differ from callKey for TTL/session handling)contentKey = ID.ContentDigest().String() (optional)In-memory indexes:
completedCalls[storageKey]completedCalls[resultCallKey] when result key differscompletedCallsByContent[contentDigestKey] when presentongoingCalls[(callKey, concurrencyKey)] for in-flight dedupeHigh-level GetOrInitCall behavior:
DoNotCache path:DoNotCache path:ConcurrencyKey is setwait):If storage-key lookup misses but content digest hits:
Why: caller should keep recipe identity they requested, while still reusing equivalent content.
SQLite lives under dagql/db/* and stores TTL metadata only:
Important details:
safeToPersistCache is true.synchronous=OFF), so persistence is best-effort cache metadata, not durability-critical state.When CacheKey.TTL > 0 and DB is enabled:
callKey in SQLite.safeToPersistCache is true, cache writes (callKey -> storageKey, expiration) to SQLite.If safeToPersistCache is false, the result still works in-memory for the current session but no TTL metadata is persisted.
For TTL calls, storage key creation currently mixes in SessionID when creating a fresh key.
Reason:
This is a deliberate transitional tradeoff; code comments in dagql/cache.go call out future model cleanup.
Both call and arbitrary results are refcounted.
Entry removal happens when:
refCount == 0waiters == 0On removal, optional OnRelease callbacks run.
This is why callers must release results when done (session cache automates this per session).
dagql/session_cache.go)SessionCache wraps base cache to provide:
ReleaseAndClose)seenKeys)noCacheNext)Error-retry behavior:
DoNotCacheCurrentStorageKey(ctx) exposes storage key via context so downstream code can mix cache identity into buildkit-facing execution metadata.
This is transitional integration, but still important when debugging function-caching behavior end to end.
Filesync does not use dagql base cache internals for its change-tracking cache.
Current filesync change cache is a dedicated typed implementation in:
engine/filesync/change_cache.goSee filesync.md for details.
dagql/cache.godagql/db/dagql/session_cache.godagql/objects.go