diff --git a/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.i18n.yaml index 223ad912f1..7b6ad0fc3e 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.md b/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.md index 1b5b0f3d3d..62ba71bf1f 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.md +++ b/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.md @@ -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 — `//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_.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. diff --git a/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.zh.md b/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.zh.md index adf4ed1261..e3af483711 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.zh.md @@ -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`,存放在缓存自己的存储根下——`//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_.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 后端再导出一套并行实现被否决。 diff --git a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.i18n.yaml index 3b39d84067..db2f7170e0 100644 --- a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.i18n.yaml +++ b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.i18n.yaml @@ -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 diff --git a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md index 9650b43a24..c06226c345 100644 --- a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md +++ b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md @@ -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 `/.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 diff --git a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md index 0d944f017c..6082bc7872 100644 --- a/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md +++ b/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md @@ -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` 却是*正确*的——工作区记录是权威数据,不可派生——所以缺的概念是按域声明权威性,而不是全局改行为。 ## 提案 diff --git a/apps/cli/composition.md b/apps/cli/composition.md index 4d37b9186a..d82beeb5ed 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -44,6 +44,8 @@ flowchart LR cfg --> plugin_dsh_base_llm_pi_ai plugin_dsh_base_session_persistence_jsonl["session-persistence-jsonl
@deepseek-ai/dsh-session-persistence-jsonl"] cfg --> plugin_dsh_base_session_persistence_jsonl + plugin_dsh_base_session_projection_cache["session-projection-cache
@deepseek-ai/dsh-session-projection-cache"] + cfg --> plugin_dsh_base_session_projection_cache plugin_dsh_base_attachment_local["attachment-local
@deepseek-ai/dsh-attachment-local"] cfg --> plugin_dsh_base_attachment_local plugin_dsh_base_session_query_sqlite["session-query-sqlite
@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` | diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 12824b94de..c995d27168 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -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 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index bd15d4c0b5..b5c487120f 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -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 `/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) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 190500df89..435ba990e6 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -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 `/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) diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml index 6ec80ed2a9..477dfdeca3 100644 --- a/docs/subsystems/session-projection.i18n.yaml +++ b/docs/subsystems/session-projection.i18n.yaml @@ -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 diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md index 66543b6bff..f17e70d3dd 100644 --- a/docs/subsystems/session-projection.md +++ b/docs/subsystems/session-projection.md @@ -27,8 +27,6 @@ interface ProjectionDefinition< key: K /** Validates persisted state before it seeds a fold. */ stateSchema: ZodType - /** 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 - -/** - * 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 ``` 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) diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md index 4d99c208a8..cc6a4edf1b 100644 --- a/docs/subsystems/session-projection.zh.md +++ b/docs/subsystems/session-projection.zh.md @@ -27,8 +27,6 @@ interface ProjectionDefinition< key: K /** Validates persisted state before it seeds a fold. */ stateSchema: ZodType - /** 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 - -/** - * 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 ``` 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)