docs(session-projection): sync persist removal, cache root, and catalogs

- subsystem docs: drop the removed persist flag from the ProjectionDefinition
  type block (both languages).
- config-catalog regenerated (cache requires sessionProjections/sessions,
  config gains root) and the zh side synced by hand; doc graphs regenerated.
- Agent Notes: the per-session cache note records the owned root tree and
  no-persistence design; the storage-root proposal's link to it is corrected
  (two levels up).
This commit is contained in:
_Kerman
2026-08-19 22:26:16 +08:00
parent 32ed3e2bce
commit f62986c01a
13 changed files with 45 additions and 76 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.md
2026-08-19-projection-cache-per-session-files.md: 1b5b0f3d3df95fd8193e6674ca09f0806bd82db6
2026-08-19-projection-cache-per-session-files.zh.md: adf4ed1261680f11366b38225885a3bce7e71bc4
2026-08-19-projection-cache-per-session-files.md: 62ba71bf1f33461ab0e43fa87d3177bd6649ef28
2026-08-19-projection-cache-per-session-files.zh.md: e3af483711953fd5c1c471c105725eca8e0c300c
@@ -10,20 +10,20 @@ The persisted projection cache was one global `session_projcache.json` — a `se
## Decision
The cache becomes one `projection_cache.json` per session, stored inside the session's own persistence directory. The location comes from the persistence seam — `sessionPersistence.locate(meta)` — so the persistence backend owns the session-directory layout (the jsonl backend places the file beside the session log); the cache service never imports a backend's path helpers. The cache service keeps every other responsibility: checkpoint fold, write policy (turn/end + disposal mandatory, count/interval throttle), fail-soft durability, and the cold-read ladder.
The cache becomes one `projection_cache.json` per session, stored under the cache's own storage root — `<root>/<session-id>/projection_cache.json` (the session id is a code-generated string, used directly as the directory name). The cache owns its directory tree and never consults the persistence layer: no `locate`, no dependency on which backend is mounted. The cache service keeps every other responsibility: checkpoint fold, write policy (turn/end + disposal mandatory, count/interval throttle), fail-soft durability, and the listing read.
Reading a cache row is now one file read, so `cachedSnapshot(meta)` is async; `coldSnapshot` takes the session header (it needs the header to locate the file — the stored log's header remains the identity witness). A persistence backend without a per-session directory (e.g. sqlite) disables the durable cache: writes no-op and cold reads fall to the full-log rung.
Reading a cache row is one file read, so `cachedSnapshot(meta)` is async. The cache no longer runs a cold-refold ladder (that would require reading the session log, which belongs to the persistence layer); a consumer that needs a guaranteed cold snapshot refolds from the log itself. Session directories and cache files are created owner-only (`0o700`/`0o600`) via `@deepseek-ai/dsh-atomic-write`.
## Consequences
- Per-session write isolation: each throttled write replaces only that session's small file, removing the global write amplification.
- Per-session write isolation: each throttled write replaces only that session's small file, removing the global write amplification. Writes to one cache file serialize, so a newer cut never lands before an older one; plugin disposal drains in-flight writes.
- Listing pays N small file reads instead of one big load; a session without a cache file simply lacks the projection column.
- No migration: the cache is derived data, never an authority. An obsolete global cache (or any earlier format) is never read — the first cold read refolds from the log and writes the current format.
- The cache file is bound to the same log lifecycle as before: the stored `{createdAt, cwd}` identity guards against a recreated id or a swapped store.
- No migration: the cache is derived data, never an authority. An obsolete cache (any earlier format) is never read — the first cold read refolds from the log and writes the current format.
- The cache file is bound to the same log lifecycle as before: the stored `{createdAt, cwd}` identity guards against a recreated id.
## Alternatives considered
- **Keep the global sessions table.** Preserves one-load listing and a synchronous `cachedSnapshot`, but keeps the global write amplification and single-file blast radius that motivated the change.
- **One storage-domain unit per session in the storage root** (flat `session_projcache_<id>.json`). Rejected: unit names must match `[a-z0-9_]` (session ids cannot), and the files would sit outside the session's own directory, scattering the storage root instead of living beside the log.
- **Replicate the session-directory layout inside the cache.** Rejected: the persistence backend already owns that layout through `locate`; duplicating the path helpers couples the cache to a backend's internals.
- **Reuse `@deepseek-ai/dsh-atomic-write`** (the zero-dependency `writeFileAtomic` with `mode`/`dirMode`). Adopted: the cache writes through it with `{ mode: 0o600, dirMode: 0o700 }`, keeping the session tree owner-only like the jsonl backend's own 0o700 directories, instead of exporting a second atomic-write primitive from the storage-json backend plugin.
- **Keep the global sessions table.** Preserves one-load listing, but keeps the global write amplification and single-file blast radius that motivated the change.
- **Resolve the path through `sessionPersistence.locate(meta)`** (the file beside the session log). Rejected: the cache would have to guess "beside the log" from a log artifact path (`dirname` + fixed filename), coupling the cache to the persistence service and to a backend's layout. The cache keeps its own tree instead.
- **Replicate the session-directory layout inside the cache.** Rejected: the persistence backend already owns that layout; the cache does not need the log's project/session directory structure for its own derived files.
- **Use the existing `@deepseek-ai/dsh-atomic-write`.** Adopted: it is the repo's single atomic whole-file write primitive (owner-only modes included); exporting a second one from the storage-json backend was rejected.
@@ -10,20 +10,20 @@ Status: implemented
## Decision
缓存改为每会话一个 `projection_cache.json`,存放在该会话自己的持久化目录内。位置来自持久化 seam——`sessionPersistence.locate(meta)`——因此会话目录布局由持久化后端所有(jsonl 后端将其放在会话日志旁);缓存服务绝不 import 后端的路径 helper。缓存服务保留其余全部职责:检查点折叠、写策略(turn/end + dispose 强制点、count/interval 节流)、fail-soft 持久化与冷读阶梯
缓存改为每会话一个 `projection_cache.json`,存放在缓存自己的存储根下——`<root>/<session-id>/projection_cache.json`(session id 是代码生成的字符串,直接用作目录名)。缓存拥有自己的目录树,绝不咨询持久化层:没有 `locate`、不依赖挂载的是哪个后端。缓存服务保留其余全部职责:检查点折叠、写策略(turn/end + dispose 强制点、count/interval 节流)、fail-soft 持久化与列表读
读取缓存行现在是一次文件读取,因此 `cachedSnapshot(meta)` 变为异步`coldSnapshot` 改为接收会话 header(定位文件需要 header——存储日志的 header 仍是身份见证)。没有每会话目录的持久化后端(如 sqlite)会禁用持久缓存:写入变为 no-op,冷读落到全量日志那一级
读取缓存行现在是一次文件读取,因此 `cachedSnapshot(meta)` 变为异步。缓存不再运行冷重折叠阶梯(那需要读取会话日志,属于持久化层的职责);需要保证冷快照的消费方自行从日志重折叠。会话目录与缓存文件经 `@deepseek-ai/dsh-atomic-write` 以仅属主权限(`0o700`/`0o600`)创建
## Consequences
- 每会话写入隔离:每次节流写入只替换该会话的小文件,消除全局写放大。
- 每会话写入隔离:每次节流写入只替换该会话的小文件,消除全局写放大。同一缓存文件的写入被串行化,新切面绝不会先于旧切面落盘;插件释放时会排空在途写入。
- 列表读取从一次大加载变为 N 次小文件读取;没有缓存文件的会话只是缺少投影列。
- 无需迁移:缓存是派生数据,绝非权威。过时的全局缓存(任何更早格式)从不被读取——首次冷读从日志重折叠并写出当前格式。
- 缓存文件仍绑定同一日志生命周期:存储的 `{createdAt, cwd}` 身份防止被重建的 id 或替换的存储误导。
- 无需迁移:缓存是派生数据,绝非权威。过时的缓存(任何更早格式)从不被读取——首次冷读从日志重折叠并写出当前格式。
- 缓存文件仍绑定同一日志生命周期:存储的 `{createdAt, cwd}` 身份防止被重建的 id 误导。
## Alternatives considered
- **保留全局 sessions 表。** 保留一次加载式列表与同步 `cachedSnapshot`,但保留了促成此改动的全局写放大与单文件爆炸半径。
- **存储根目录下每会话一个 storage-domain unit**(扁平 `session_projcache_<id>.json`)。未采用:unit 名必须匹配 `[a-z0-9_]`(会话 id 做不到),且文件会落在会话目录之外,散落在存储根目录而不是日志旁
- **在缓存内复刻会话目录布局。** 未采用:持久化后端已通过 `locate` 拥有该布局;复刻路径 helper 会把缓存耦合到后端的内部实现
- **复用 `@deepseek-ai/dsh-atomic-write`**(零依赖的 `writeFileAtomic`,带 `mode`/`dirMode`)。已采纳:缓存以 `{ mode: 0o600, dirMode: 0o700 }` 经其写入,使会话目录树与 jsonl 后端自己的 0o700 目录保持一致、仅属主可读,而不是从 storage-json 后端插件再导出一套原子写原语
- **保留全局 sessions 表。** 保留一次加载式列表,但保留了促成此改动的全局写放大与单文件爆炸半径。
- **`sessionPersistence.locate(meta)` 解析路径**(文件放在会话日志旁)。未采用:缓存得从日志 artifact 路径"猜"日志旁边(`dirname` + 固定文件名),把缓存耦合到持久化服务与后端的布局。缓存改用自有目录树
- **在缓存内复刻会话目录布局。** 未采用:持久化后端已拥有该布局;缓存为自己派生的文件不需要日志的 project/session 目录结构
- **使用现有 `@deepseek-ai/dsh-atomic-write`** 采纳:它是仓库唯一的原子整文件写入原语(含仅属主权限);从 storage-json 后端再导出一套并行实现被否决
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md
2026-07-28-storage-root-and-derived-medium-recovery.md: 9650b43a2493a36658a410e93cc76ec0050974ae
2026-07-28-storage-root-and-derived-medium-recovery.zh.md: 0d944f017ce3187c1c1238893c8b83423f615d20
2026-07-28-storage-root-and-derived-medium-recovery.md: c06226c34518a6971a313cc5b7e78c9750a90a17
2026-07-28-storage-root-and-derived-medium-recovery.zh.md: 6082bc78729f2270840bb7e07860af227370f632
@@ -10,7 +10,7 @@ The persisted projection cache ([note](2026-07-27-session-projection-and-command
**Where the files actually live (root mismatch closed; resolve-once residual still open).** The shared base defaults the session store to the global harness home (`$DSH_HOME/sessions`, default `~/.dsh/sessions`), while the shipped Web overlay used to give the json backend the relative root `./.storages`: `workspace.json` and `session_projcache.json` landed under `<launch dir>/.storages/` — two launches from different directories shared their sessions yet saw different workspace registries and different projection caches, and the cache exists precisely to serve the cross-session cold listing, which missed for every session last cached under another launch directory. That mismatch is now closed: the overlay anchors `storage-json.root` to `$DSH_HOME/storages` with the same `!!js` expression the session root uses (`apps/cli/config/web.cordis.yml`). The residual hazard: `JsonStorageBackend` still never resolves its root — each unit open joins the path against whatever `process.cwd()` is at that moment (packages/storage/storage-json/src/index.ts); the shipped overlay root is already absolute and unaffected, but any relative root (bare Loader boots, tests) still splits on a later cwd change — the exact hazard the JSONL session backend resolves-once to prevent ("later process.cwd() changes cannot split one backend across roots", packages/session/session-persistence-jsonl/src/index.ts).
**How recovery works today.** Inside a healthy medium the cache is fully self-healing by design: a `stateVersion`-mismatched row is discarded and refolded, a log shrunk below a row's watermark is detected by the anchored restore floor and answered with one full re-read, and every background write is fail-soft. But at the *medium* level there is no recovery at all: a truncated, hand-edited, or version-bumped `session_projcache.json` fails `openJsonUnit` with `malformed-medium`/`version-mismatch` (packages/storage/storage-json/src/format.ts), a schema-drifted record fails domain open with `invalid-record` (packages/storage/storage-domain/src/index.ts), the rejection propagates through `SessionProjectionCache[Service.init]`, and under the CLI's fail-loud boot the assembly refuses to start. A file whose entire content is rebuildable from session logs can brick boot. This contradicts the cache package's own stated stance ("a stale or unreadable cache costs a longer tail replay, never a wrong value") and the cache domain spec's JSDoc ("version bumps discard the whole medium"), which today describes an aspiration, not the implementation. Partially superseded for the projection cache: [the per-session cache files note](../implemented/architecture/2026-08-19-projection-cache-per-session-files.md) removed the global `session_projcache` domain, so the cache half of this proposal (recovery on that domain) no longer applies; the `workspace.json` half remains current. The same fail-loud path is *correct* for `workspace.json` — workspace records are authoritative, not derivable — so the missing concept is a per-domain declaration of authority, not a global behavior change.
**How recovery works today.** Inside a healthy medium the cache is fully self-healing by design: a `stateVersion`-mismatched row is discarded and refolded, a log shrunk below a row's watermark is detected by the anchored restore floor and answered with one full re-read, and every background write is fail-soft. But at the *medium* level there is no recovery at all: a truncated, hand-edited, or version-bumped `session_projcache.json` fails `openJsonUnit` with `malformed-medium`/`version-mismatch` (packages/storage/storage-json/src/format.ts), a schema-drifted record fails domain open with `invalid-record` (packages/storage/storage-domain/src/index.ts), the rejection propagates through `SessionProjectionCache[Service.init]`, and under the CLI's fail-loud boot the assembly refuses to start. A file whose entire content is rebuildable from session logs can brick boot. This contradicts the cache package's own stated stance ("a stale or unreadable cache costs a longer tail replay, never a wrong value") and the cache domain spec's JSDoc ("version bumps discard the whole medium"), which today describes an aspiration, not the implementation. Partially superseded for the projection cache: [the per-session cache files note](../../implemented/architecture/2026-08-19-projection-cache-per-session-files.md) removed the global `session_projcache` domain, so the cache half of this proposal (recovery on that domain) no longer applies; the `workspace.json` half remains current. The same fail-loud path is *correct* for `workspace.json` — workspace records are authoritative, not derivable — so the missing concept is a per-domain declaration of authority, not a global behavior change.
## Proposal
@@ -10,7 +10,7 @@ Status: proposed
**文件到底存在哪(根错位已收口,resolve-once 残余仍开放)。** 共享 base 将会话存储默认为全局 harness home`$DSH_HOME/sessions`,默认 `~/.dsh/sessions`),而出厂 Web overlay 曾给 json 后端相对根 `./.storages``workspace.json``session_projcache.json` 落在 `<启动目录>/.storages/` 下——从两个不同目录启动,会话相同,工作区注册表和投影缓存却各是一份,而缓存存在的意义恰恰是跨会话冷列表,凡上次在别的启动目录下缓存过的会话全部 miss。这一错位已消除:overlay 现以与会话根同一段 `!!js` 表达式把 `storage-json.root` 锚定到 `$DSH_HOME/storages``apps/cli/config/web.cordis.yml`)。残余隐患:`JsonStorageBackend` 仍从不 resolve 根——每次打开 unit 都把路径 join 到当时的 `process.cwd()` 上(packages/storage/storage-json/src/index.ts);出厂 overlay 的根已是绝对路径不受影响,但任何相对根(裸 Loader 启动、测试)仍会被后续 cwd 变化劈开,JSONL 会话后端用「构造时 resolve 一次」防住的正是它("later process.cwd() changes cannot split one backend across roots"packages/session/session-persistence-jsonl/src/index.ts)。
**现在是怎么恢复的。** 在健康介质内部,缓存按设计完全自愈:`stateVersion` 不匹配的行被丢弃重折,日志缩短到行水位以下由带锚的 restore floor 检出并以一次全量重读回答,每次后台写都是 fail-soft。但在*介质*层面完全没有恢复:被截断、被手改或版本被 bump 的 `session_projcache.json` 会让 `openJsonUnit``malformed-medium`/`version-mismatch` 失败(packages/storage/storage-json/src/format.ts),schema 漂移的记录让域 open 以 `invalid-record` 失败(packages/storage/storage-domain/src/index.ts),拒绝一路穿过 `SessionProjectionCache[Service.init]`,在 CLI 的 fail-loud 启动下整个组装拒绝启动。一个内容完全可从会话日志重建的文件能把启动搞死。这与缓存包自己声明的立场("a stale or unreadable cache costs a longer tail replay, never a wrong value")和缓存域 spec 的 JSDoc"version bumps discard the whole medium")相矛盾——后者今天描述的是愿望而非实现。投影缓存半边已被[每会话缓存文件 note](../implemented/architecture/2026-08-19-projection-cache-per-session-files.md) 部分取代:全局 `session_projcache` domain 已移除,本提案的缓存恢复半边不再适用;`workspace.json` 半边仍然有效。同一条 fail-loud 路径对 `workspace.json` 却是*正确*的——工作区记录是权威数据,不可派生——所以缺的概念是按域声明权威性,而不是全局改行为。
**现在是怎么恢复的。** 在健康介质内部,缓存按设计完全自愈:`stateVersion` 不匹配的行被丢弃重折,日志缩短到行水位以下由带锚的 restore floor 检出并以一次全量重读回答,每次后台写都是 fail-soft。但在*介质*层面完全没有恢复:被截断、被手改或版本被 bump 的 `session_projcache.json` 会让 `openJsonUnit``malformed-medium`/`version-mismatch` 失败(packages/storage/storage-json/src/format.ts),schema 漂移的记录让域 open 以 `invalid-record` 失败(packages/storage/storage-domain/src/index.ts),拒绝一路穿过 `SessionProjectionCache[Service.init]`,在 CLI 的 fail-loud 启动下整个组装拒绝启动。一个内容完全可从会话日志重建的文件能把启动搞死。这与缓存包自己声明的立场("a stale or unreadable cache costs a longer tail replay, never a wrong value")和缓存域 spec 的 JSDoc"version bumps discard the whole medium")相矛盾——后者今天描述的是愿望而非实现。投影缓存半边已被[每会话缓存文件 note](../../implemented/architecture/2026-08-19-projection-cache-per-session-files.md) 部分取代:全局 `session_projcache` domain 已移除,本提案的缓存恢复半边不再适用;`workspace.json` 半边仍然有效。同一条 fail-loud 路径对 `workspace.json` 却是*正确*的——工作区记录是权威数据,不可派生——所以缺的概念是按域声明权威性,而不是全局改行为。
## 提案
+3
View File
@@ -44,6 +44,8 @@ flowchart LR
cfg --> plugin_dsh_base_llm_pi_ai
plugin_dsh_base_session_persistence_jsonl["session-persistence-jsonl<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
cfg --> plugin_dsh_base_session_persistence_jsonl
plugin_dsh_base_session_projection_cache["session-projection-cache<br/>@deepseek-ai/dsh-session-projection-cache"]
cfg --> plugin_dsh_base_session_projection_cache
plugin_dsh_base_attachment_local["attachment-local<br/>@deepseek-ai/dsh-attachment-local"]
cfg --> plugin_dsh_base_attachment_local
plugin_dsh_base_session_query_sqlite["session-query-sqlite<br/>@deepseek-ai/dsh-session-query-sqlite"]
@@ -186,6 +188,7 @@ flowchart LR
| `credentials` | `@deepseek-ai/dsh-credentials-local` |
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
| `session-projection-cache` | `@deepseek-ai/dsh-session-projection-cache` |
| `attachment-local` | `@deepseek-ai/dsh-attachment-local` |
| `session-query-sqlite` | `@deepseek-ai/dsh-session-query-sqlite` |
| `session-projection` | `@deepseek-ai/dsh-session-projection` |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: bd15d4c0b548526acd9adb578ba10e6a37f94410
config-catalog.zh.md: 190500df89c97b68e3fff201e90f51436250ab07
config-catalog.md: b5c487120f1272ddb1da29d8ed1230a5c1f3d0a9
config-catalog.zh.md: 435ba990e69f2efc157edc793568cd889e639d0a
+4 -2
View File
@@ -1698,7 +1698,7 @@ Source: [`packages/session/session-persistence-sqlite/src/index.ts:70`](../packa
## `@deepseek-ai/dsh-session-projection-cache`
Requires: `storageDomain` · `sessionProjections` · `sessionPersistence` · `sessions`
Requires: `sessionProjections` · `sessions`
```ts config-catalog
/**
@@ -1708,6 +1708,8 @@ Requires: `storageDomain` · `sessionProjections` · `sessionPersistence` · `se
* disposal) are policy, not tunables, and always fire.
*/
export interface Config {
/** Directory holding one `<session-id>/projection_cache.json` per session. */
root: string
/** Committed events per session that force a durable checkpoint write between mandatory points. */
writeEveryEvents: number
/** Longest time (milliseconds) a dirty checkpoint may stay unwritten between mandatory points. */
@@ -1715,7 +1717,7 @@ export interface Config {
}
```
Source: [`packages/session/session-projection-cache/src/index.ts:42`](../packages/session/session-projection-cache/src/index.ts)
Source: [`packages/session/session-projection-cache/src/index.ts:48`](../packages/session/session-projection-cache/src/index.ts)
<a id="deepseek-aidsh-session-query-sqlite"></a>
+4 -2
View File
@@ -1700,7 +1700,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
## `@deepseek-ai/dsh-session-projection-cache`
需要:`storageDomain` · `sessionProjections` · `sessionPersistence` · `sessions`
需要:`sessionProjections` · `sessions`
```ts config-catalog
/**
@@ -1710,6 +1710,8 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
* disposal) are policy, not tunables, and always fire.
*/
export interface Config {
/** Directory holding one `<session-id>/projection_cache.json` per session. */
root: string
/** Committed events per session that force a durable checkpoint write between mandatory points. */
writeEveryEvents: number
/** Longest time (milliseconds) a dirty checkpoint may stay unwritten between mandatory points. */
@@ -1717,7 +1719,7 @@ export interface Config {
}
```
来源:[`packages/session/session-projection-cache/src/index.ts:42`](../packages/session/session-projection-cache/src/index.ts)
来源:[`packages/session/session-projection-cache/src/index.ts:48`](../packages/session/session-projection-cache/src/index.ts)
<a id="deepseek-aidsh-session-query-sqlite"></a>
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md
session-projection.md: 66543b6bfff666435acb8c00dd2f87d5fb0bccb4
session-projection.zh.md: 4d99c208a8c54424655ec0b4238dbed37c75603b
session-projection.md: f17e70d3dd4a4d89381bbe2be1428647a612ff9a
session-projection.zh.md: cc6a4edf1ba394cf71abfa64ccff6667566f5d9d
+3 -22
View File
@@ -27,8 +27,6 @@ interface ProjectionDefinition<
key: K
/** Validates persisted state before it seeds a fold. */
stateSchema: ZodType<S>
/** Persist a host-only unit. Client-visible units are always persisted. */
persist?: boolean
/**
* State for the empty log.
* @returns the initial state.
@@ -114,7 +112,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
### `ctx.sessionProjectionCache` — `SessionProjectionCache`
The persisted projection cache service. Checkpoints live sessions on a throttled write-behind (count/interval triggers from Config) plus two mandatory points — `turn/end` and session disposal (the live-to-cold moment) — and serves the cold-read ladder: cached file, persistence `readFrom` tail, registry `restore`, durable write-back. Every durable write is fail-soft: failures log a warning and the cache self-heals on the next write or cold read. A persistence backend without a per-session directory (e.g. sqlite) disables the durable cache: writes no-op, cold reads fall to the full-log rung.
The persisted projection cache service. Checkpoints live sessions on a throttled write-behind (count/interval triggers from Config) plus two mandatory points — `turn/end` and session disposal (the live-to-cold moment) — and serves the cached rows for a session header. Every durable write is fail-soft: failures log a warning and the cache self-heals on the next write. The cache owns its directory tree and never consults the persistence layer.
```ts cordis-catalog
/**
@@ -122,9 +120,7 @@ The persisted projection cache service. Checkpoints live sessions on a throttled
* (version-matching keys only), each cut carried with its watermark so a
* client value store can seed under its higher-seq-wins rule — as stale as
* the last durable checkpoint but never wrong, and never from an unrelated
* log (the caller's header is the identity witness). Fresher paths (the
* history tail baseline, {@link coldSnapshot}) supersede these values
* whenever a session is actually opened.
* log (the caller's header is the identity witness).
* @param meta - the listed session's header (identity witness; no log read).
* @returns the cut (`asOfSeq` = lowest served-row watermark), or
* `undefined` when no usable row exists for this lifecycle.
@@ -141,26 +137,11 @@ async cachedSnapshot(meta: SessionHeader): Promise<ProjectionSnapshot | undefine
* @returns resolution after durability and event emission.
*/
async write(session: Session): Promise<void>
/**
* Cold-read one persisted session's projections with zero full-log load:
* cached rows + a persistence `readFrom` tail from the registry's restore
* floor, refolded by the registry and written back (fail-soft) so the next
* cold read starts closer. A cache row invalidated by a shrunk log
* (crash-repair truncation) triggers one full re-read from seq 0 — the
* ladder's slow rung, still no crash. Rejects when the session has no
* persisted log (`not found` from the persistence seam).
* @param meta - the persisted session whose projections are read (locates
* the cache file and witnesses the stored log identity).
* @param signal - optional cancellation for the persistence reads.
* @returns the snapshot cut at the stored log end.
*/
async coldSnapshot(meta: SessionHeader, signal?: AbortSignal): Promise<ProjectionSnapshot>
```
Types: [Session](session.md) · [SessionHeader](persistence.md)
Source: [`packages/session/session-projection-cache/src/index.ts:78`](../../packages/session/session-projection-cache/src/index.ts)
Source: [`packages/session/session-projection-cache/src/index.ts:80`](../../packages/session/session-projection-cache/src/index.ts)
<a id="ctxsessionprojections--sessionprojectionregistry"></a>
+3 -22
View File
@@ -27,8 +27,6 @@ interface ProjectionDefinition<
key: K
/** Validates persisted state before it seeds a fold. */
stateSchema: ZodType<S>
/** Persist a host-only unit. Client-visible units are always persisted. */
persist?: boolean
/**
* State for the empty log.
* @returns the initial state.
@@ -114,7 +112,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
### `ctx.sessionProjectionCache` — `SessionProjectionCache`
The persisted projection cache service. Checkpoints live sessions on a throttled write-behind (count/interval triggers from Config) plus two mandatory points — `turn/end` and session disposal (the live-to-cold moment) — and serves the cold-read ladder: cached file, persistence `readFrom` tail, registry `restore`, durable write-back. Every durable write is fail-soft: failures log a warning and the cache self-heals on the next write or cold read. A persistence backend without a per-session directory (e.g. sqlite) disables the durable cache: writes no-op, cold reads fall to the full-log rung.
The persisted projection cache service. Checkpoints live sessions on a throttled write-behind (count/interval triggers from Config) plus two mandatory points — `turn/end` and session disposal (the live-to-cold moment) — and serves the cached rows for a session header. Every durable write is fail-soft: failures log a warning and the cache self-heals on the next write. The cache owns its directory tree and never consults the persistence layer.
```ts cordis-catalog
/**
@@ -122,9 +120,7 @@ The persisted projection cache service. Checkpoints live sessions on a throttled
* (version-matching keys only), each cut carried with its watermark so a
* client value store can seed under its higher-seq-wins rule — as stale as
* the last durable checkpoint but never wrong, and never from an unrelated
* log (the caller's header is the identity witness). Fresher paths (the
* history tail baseline, {@link coldSnapshot}) supersede these values
* whenever a session is actually opened.
* log (the caller's header is the identity witness).
* @param meta - the listed session's header (identity witness; no log read).
* @returns the cut (`asOfSeq` = lowest served-row watermark), or
* `undefined` when no usable row exists for this lifecycle.
@@ -141,26 +137,11 @@ async cachedSnapshot(meta: SessionHeader): Promise<ProjectionSnapshot | undefine
* @returns resolution after durability and event emission.
*/
async write(session: Session): Promise<void>
/**
* Cold-read one persisted session's projections with zero full-log load:
* cached rows + a persistence `readFrom` tail from the registry's restore
* floor, refolded by the registry and written back (fail-soft) so the next
* cold read starts closer. A cache row invalidated by a shrunk log
* (crash-repair truncation) triggers one full re-read from seq 0 — the
* ladder's slow rung, still no crash. Rejects when the session has no
* persisted log (`not found` from the persistence seam).
* @param meta - the persisted session whose projections are read (locates
* the cache file and witnesses the stored log identity).
* @param signal - optional cancellation for the persistence reads.
* @returns the snapshot cut at the stored log end.
*/
async coldSnapshot(meta: SessionHeader, signal?: AbortSignal): Promise<ProjectionSnapshot>
```
Types: [Session](session.md) · [SessionHeader](persistence.md)
Source: [`packages/session/session-projection-cache/src/index.ts:78`](../../packages/session/session-projection-cache/src/index.ts)
Source: [`packages/session/session-projection-cache/src/index.ts:80`](../../packages/session/session-projection-cache/src/index.ts)
<a id="ctxsessionprojections--sessionprojectionregistry"></a>