Files
deepseek-harness/packages/session/session-projection-cache
_Kerman 89321489db refactor(session-projection-cache): own the cache tree under a config root
Store each session's projection_cache.json under the cache's own root tree
(<root>/<session-id>/projection_cache.json, wired to dshHomePath('projections')
in the base bundle) instead of beside the session log via
sessionPersistence.locate(). The cache owns its directory layout, keys
directories by the code-generated session id, and never consults the
persistence layer; the service now injects only sessionProjections and
sessions.

Drop the coldSnapshot method and its readFrom-tail fold ladder: every cold
consumer refolds from the log itself, so the cache only serves the listing
read (cachedSnapshot, one async file read per session) and the write side.
Fail-soft durability, per-path write serialization, in-flight drain, and
atomic 0600 writes are unchanged; the chain cleanup now observes its own
rejection so a failed write cannot surface as an unhandled error.

dsh-session-persistence leaves peer/dev dependencies and the tsconfig
reference; dsh-atomic-write moves to peerDependencies. Config gains a
required root.
2026-08-19 22:25:54 +08:00
..

@deepseek-ai/dsh-session-projection-cache

English | 中文

The persisted projection cache (ctx.sessionProjectionCache): durable checkpoints of every projection unit's state, one projection_cache.json per session under the cache's own storage root (<root>/<session-id>/projection_cache.json). The cache owns its directory tree and never consults the persistence layer. Design authority: the session-projection RFC (persisted projection cache section).

A stored row (key → {ver, seq, val}) is a fold shortcut, never an authority: possibly stale (seq says exactly how stale) but never wrong. Consequences the implementation commits to:

  • Every background write is fail-soft. A failed durable write logs a warning and keeps the cache stale; the next write self-heals. A crash between writes costs a longer tail replay, never a wrong value.
  • A ver mismatch against the live unit's stateVersion discards, never migrates. A unit bump invalidates its rows at read time; the key refolds from the log.
  • A row must pass the live unit's stateSchema. A malformed file reads as "no cache row", so the cold path refolds from the log.
  • Whole-record writes. Each write atomically replaces the session's cache file (the registry cut is always complete), snapshotted through the lossless-JSON boundary — a unit state violating the plain-JSON contract fails loud. Writes to one cache file serialize, so a newer cut never lands before an older one.
  • Records are bound to a log lifecycle, not just an id. Each record stores the header identity (createdAt, cwd) it was folded from; every read validates it (the live header is the witness) before accepting a record, so a deleted-then-recreated id cannot let an old record seed state folded from an unrelated log.
  • The log leads, the cache follows. A live checkpoint flushes the session's buffered events durably BEFORE the cache file lands, so a crash can leave the cache behind the log (a longer tail replay) but never ahead of it.
  • The cache owns its tree, private by default. Session directories and cache files are created owner-only (0o700/0o600). The cache does not depend on which persistence backend is mounted — no locate, no per-session-dir probing.

Write policy

Two mandatory points, throttled in between:

Trigger Nature
turn/end Mandatory — the turn-final value is what listing reads want.
Session disposal (detach) Mandatory — the live-to-cold moment; after it the cache serves this session's final cut.
writeEveryEvents committed events Config throttle (count).
writeIntervalMs since the first dirty event Config throttle (interval).

root and both throttle triggers are required Config fields (no defaults): the cache root and flush cadence are deployment choices stated in cordis.yml.

Listing read (cachedSnapshot(meta))

One file read per session: client values viewed straight from the identity-matching stored record (version- and state-schema-matching keys only), returned as a {asOfSeq, values} cut — asOfSeq is the lowest served-row watermark, so a client seeding its per-session value store under higher-seq-wins can never let a stale list block overwrite a newer push frame. Host-only rows are never returned. undefined when no usable client row exists (unknown id, unrelated lifecycle, missing or malformed file, or no usable rows); the api-proxy list carrier turns that into an absent column.

write(session) is the synchronous-cut checkpoint both mandatory points use; carriers may call it directly (not fail-soft — the fail-soft wrappers own containment).

Composition

- id: session-projection-cache
  name: '@deepseek-ai/dsh-session-projection-cache'
  config:
    root: !!js dshHomePath('projections')
    writeEveryEvents: 200
    writeIntervalMs: 5000

Injects sessionProjections, sessions. Without this row the projection system runs live-only (watermark cache; cold reads fall back to full log loads wherever a carrier implements them).

Model Experience

None, as the cache only persists host-side read models of already-logged session state and touches no prompt, message, schema, stream, or tool result.

KV Cache effect

None; the cache never assembles or sends provider requests.

Known Limitations and Deferred Work

  • No eviction or retention surface — records accumulate per session; pruning stored checkpoints is out-of-band maintenance, same stance as session persistence itself.
  • Interval throttle is per-session coarse — the timer arms at the first dirty event after a clean write; a steady sub-threshold trickle writes once per interval, not a sliding window.
  • No cache-side cold refold — the cache serves and refreshes its files but never reads the session log (it does not depend on the persistence layer); a consumer that needs a guaranteed cold snapshot refolds from the log itself.