@deepseek-ai/dsh-session-projection-cache 0.1.1-rc.2 → 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/session/session-projection-cache/README.md
5
- README.md: 33908578a5127f2b6bb78ed7467833aaaa2cf085
6
- README.zh.md: 0ca410f91562360d85faadf4cf64cb61ac467482
5
+ README.md: c5e438a2ededef00a1faa456e9913492a603b51d
6
+ README.zh.md: eab0b81af618558e6f1e5e6bc0c40da3fcc38884
package/README.md CHANGED
@@ -1,42 +1,41 @@
1
+ ---
2
+ description: "The persisted session-projection cache for deployments and maintainers choosing, configuring, or debugging durable checkpoints, zero-I/O list reads, and accelerated cold projection folds."
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-session-projection-cache
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- The persisted projection cache (`ctx.sessionProjectionCache`): durable checkpoints of every projection unit's state, one record per session on the domain data form (`session_projcache` domain — the shipped json backend lands it beside `workspace.json` under the configured storage root). Design authority: the [session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) (persisted projection cache section).
6
-
7
- 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:
10
+ ## Summary
8
11
 
9
- - **Every background write is fail-soft.** A failed durable write logs a warning and keeps the cache stale; the next write or cold read self-heals. A crash between writes costs a longer tail replay, never a wrong value.
10
- - **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.
11
- - **A row must pass the live unit's `stateSchema`.** A malformed row is omitted from the zero-I/O view and rejected by restore so the cold-read ladder refolds it from the log.
12
- - **Whole-record writes.** Each write replaces the session's full checkpoint (the registry cut is always complete), snapshotted through the lossless-JSON boundary — a unit state violating the plain-JSON contract fails loud.
13
- - **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 or stored header is the witness) before accepting a row, so a deleted-then-recreated id or a persistence store swapped under a surviving cache discards the unrelated record instead of seeding phantom values.
14
- - **The log leads, the cache follows.** A live checkpoint flushes the session's buffered events durably BEFORE the cache row lands, so a crash can leave the cache behind the log (a longer tail replay) but never ahead of it.
12
+ `dsh-session-projection-cache` persists the state checkpoints of every registered projection unit (`ctx.sessionProjectionCache`) as one versioned document per session in the `session_projcache` storage domain's `per-record` layout. The shipped JSON backend stores each record at `<root>/session_projcache/sessions/<id>.json`, and the cache never reads the session-persistence layer. A stored row is a fold shortcut, never an authority: it may be stale — its `seq` says exactly how stale — but never wrong. Three mandatory checkpoints (session creation, `turn/end`, and session disposal) plus configurable count and interval throttles keep the cache fresh. Choose it when list views need synchronous cached values or cold projection folds should skip an already-checkpointed prefix.
15
13
 
16
- ## Write policy
14
+ ## Table of Contents
17
15
 
18
- Two mandatory points, throttled in between:
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
19
22
 
20
- | Trigger | Nature |
21
- |---|---|
22
- | `turn/end` | Mandatory — the turn-final value is what cold reads want. |
23
- | Session disposal (detach) | Mandatory — the live-to-cold moment; after it the cold ladder serves this session. |
24
- | `writeEveryEvents` committed events | Config throttle (count). |
25
- | `writeIntervalMs` since the first dirty event | Config throttle (interval). |
23
+ -----
26
24
 
27
- Both `Config` fields are required (no defaults): flush cadence is a deployment choice with no universally correct value, stated in cordis.yml.
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
28
27
 
29
- ## Listing read (`cachedSnapshot(meta)`)
28
+ Mount this package beside the projection registry and the storage stack when clients should list projection values for cold sessions without loading their logs. Without it, consumers must obtain the log before they can reconstruct cold projection values.
30
29
 
31
- The zero-I/O rung: 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, or no usable rows); the api-proxy list carrier turns that into an absent column.
30
+ ### When to choose it
32
31
 
33
- ## Cold read (`coldSnapshot(id, signal?)`)
32
+ Choose it when a deployment restarts sessions and needs durable projection values for history lists, statistics, or goal snapshots. Skip it when projections serve only live sessions, or when the extra storage writes cost more than the saved projection work.
34
33
 
35
- The read ladder, zero full-log load on the happy path: cached rows → `sessionProjections.restoreFloor` (anchored one event below the lowest usable watermark) → persistence `readFrom(id, floor)` → `sessionProjections.restore` → fail-soft write-back of the refreshed rows. The anchor makes a shrunk log (crash-repair truncation) provable: an overreaching row triggers exactly one full re-read from seq 0 instead of serving a ghost value. No registered units serve `{asOfSeq: -1, values: {}}` without touching persistence; a session with no persisted log rejects with the seam's `not found`.
34
+ ### Minimal configuration
36
35
 
37
- `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).
36
+ Both throttle fields are required flush cadence is a deployment choice with no universally correct value:
38
37
 
39
- ## Composition
38
+ The cache opens its domain through the storage stack, so base mounts `storage`, `storage-json` (root `dshHomePath('storages')`), and `storage-domain` (`backend: json`) before it:
40
39
 
41
40
  ```yaml
42
41
  - id: session-projection-cache
@@ -46,11 +45,72 @@ The read ladder, zero full-log load on the happy path: cached rows → `sessionP
46
45
  writeIntervalMs: 5000
47
46
  ```
48
47
 
49
- Injects `storageDomain`, `sessionProjections`, `sessionPersistence`, `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).
48
+ | Field | Default | Meaning |
49
+ |---|---|---|
50
+ | `writeEveryEvents` | required | Committed events per session that force a durable checkpoint write between mandatory points |
51
+ | `writeIntervalMs` | required | Longest time a dirty checkpoint may stay unwritten between mandatory points |
52
+
53
+ The plugin injects `storageDomain`, `sessionProjections`, and `sessions`. The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-projection-cache) is the exhaustive source for every accepted field and its JSDoc.
54
+
55
+ ### How checkpoints are written
56
+
57
+ Three mandatory points always write: session creation persists the seed-derived cut, `turn/end` persists the value that listing reads want, and session disposal persists the final live cut. Between them, the configured count and interval throttles write as events accumulate. Every write atomically replaces the session's complete record through the domain write chain; a failure logs a warning and keeps the cache stale, and the next write self-heals.
58
+
59
+ ### Reading cached values
60
+
61
+ `cachedSnapshot(meta)` synchronously serves client values from the storage domain's in-memory tables with zero I/O. It accepts only an identity-matching record and version- and schema-matching keys, then returns a `{ asOfSeq, values }` cut at the lowest served-row watermark. It returns `undefined` for an unknown id, unrelated lifecycle, absent or foreign record document, or no usable rows. `coldSnapshot(meta, events)` accepts a complete ordered log, skips the checkpointed prefix while folding, and refreshes the record without reading the persistence layer itself.
62
+
63
+ ### What the cache guarantees
64
+
65
+ The log leads and the cache follows: a live checkpoint flushes the session's buffered events durably before the cache row lands, so a crash can leave the cache behind the log but never ahead of it. Reads and writes share the storage domain's coherent in-memory state; the per-unit write chain mutates memory only after durability. Each version-stamped record must match the live unit schema and session header identity (`createdAt`, `cwd`), so malformed, stale, or unrelated records read as absent. The JSON backend stores each record at `<root>/session_projcache/sessions/<id>.json` in an owner-only directory tree.
66
+
67
+ -----
68
+
69
+ <a id="understand-the-implementation"></a>
70
+ ## Understand the implementation
71
+
72
+ <details>
73
+ <summary>Implementation internals — click to expand</summary>
74
+
75
+ This section explains the cache's durability and storage ownership; the observable behavior is covered in [Use this package](#use-this-package).
76
+
77
+ ### Design concept
78
+
79
+ The cache is a fold shortcut over the projection registry's checkpoint face, stored in a `per-record` domain data table. It commits to six consequences: reads never bypass the domain write chain; every background write is fail-soft; a `ver` mismatch discards rather than migrates a row; a record must pass the live unit's `stateSchema`; writes replace one complete session record through the lossless-JSON boundary; and the log leads, the cache follows.
80
+
81
+ ### Read and write ownership
50
82
 
83
+ The cache stores one version-stamped document per session in the `session_projcache` domain. It does not depend on a session-persistence backend, call `locate`, or inspect per-session directories. A malformed or stale record reads as absent, and consumers that require a cold value own any log refold.
84
+
85
+ ### Source map
86
+
87
+ | File | Role |
88
+ |---|---|
89
+ | [`src/index.ts`](src/index.ts) | Plugin entry: `SessionProjectionCache` service, write-behind listeners, cache reads |
90
+ | [`src/spec.ts`](src/spec.ts) | The `session_projcache` domain spec and record identity types |
91
+ | [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; correctness is enforced at the write and read paths) |
92
+
93
+ </details>
94
+
95
+ -----
96
+
97
+ <a id="further-exploration"></a>
98
+ ## Further Exploration
99
+
100
+ Read these pages when the package-level contract is not enough. They move from the cache to the registry it checkpoints and the storage domain that holds its records.
101
+
102
+ - [Session projections subsystem](../../../docs/subsystems/session-projection.md) — the projection unit contract and drive semantics this cache checkpoints.
103
+ - [Session projection registry](../session-projection/README.md) — the `ctx.sessionProjections` service whose checkpoints this cache persists.
104
+ - [Storage subsystem](../../../docs/subsystems/storage.md) — the domain routing and backend behavior that store cache records.
105
+ - [Session package map](../README.md) — adjacent persistence, title, and telemetry packages.
106
+ - [Session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) — the persisted projection cache design rationale.
107
+
108
+ -----
109
+
110
+ <a id="model-experience"></a>
51
111
  ## Model Experience
52
112
 
53
- None, as the cache only persists and restores host-side read models of already-logged session state and touches no prompt, message, schema, stream, or tool result.
113
+ None, as the persisted cache accelerates host-side reads of projection state and registers nothing model-facing.
54
114
 
55
115
  #### KV Cache effect
56
116
 
@@ -58,6 +118,21 @@ None; the cache never assembles or sends provider requests.
58
118
 
59
119
  ## Known Limitations and Deferred Work
60
120
 
121
+ <a id="known-limitations-and-deferred-work"></a>
122
+
123
+
124
+ These limits define where the cache needs operational care. They are current package constraints, not a task backlog.
125
+
61
126
  - **No eviction or retention surface** — records accumulate per session; pruning stored checkpoints is out-of-band maintenance, same stance as session persistence itself.
62
127
  - **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.
63
- - **`coldSnapshot` reads are not deduplicated** — two concurrent cold reads of one session each run the ladder; last write-back wins (rows are equivalent), acceptable for listing-scale call rates.
128
+ - **No cache-side cold refold** — the cache serves and refreshes its rows 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.
129
+
130
+ <a id="dev-note"></a>
131
+ ### Dev Note
132
+
133
+ <details>
134
+ <summary>Working context for maintainers — click to expand</summary>
135
+
136
+ None.
137
+
138
+ </details>
package/README.zh.md CHANGED
@@ -1,42 +1,41 @@
1
+ ---
2
+ description: "面向部署方与维护者的持久会话投影缓存说明,用于选择、配置或排查持久检查点、零 I/O 列表读取与加速的冷投影折叠。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-session-projection-cache
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- 持久投影缓存(`ctx.sessionProjectionCache`):把每个投影单元的状态保存为检查点,基于域数据形态(domain data form)每会话一条记录(`session_projcache` 域——出厂 JSON 后端将其落在配置的存储根目录下、`workspace.json` 旁边)。设计权威:[session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md)(persisted projection cache 一节)。
6
-
7
- 一条存储行 `(key → {ver, seq, val})` 是折叠捷径,绝不是权威:可能陈旧(`seq` 精确说明陈旧到哪),但绝不会错。实现据此承诺:
10
+ ## 概述
8
11
 
9
- - **每次后台写入都 fail-soft。** 持久写失败只记一条警告并保持缓存陈旧;下一次写入或冷读自愈。两次写之间崩溃的代价是更长的尾部回放,绝不是错误的值。
10
- - **`ver` 与当前运行单元的 `stateVersion` 不匹配即丢弃,绝不迁移。** 单元递增版本会在读取时使其行失效;该 key 从日志重新折叠。
11
- - **存储行必须通过当前单元的 `stateSchema`。** 畸形行从零 I/O view 中省略,并被 restore 拒绝,使冷读阶梯从日志重新折叠。
12
- - **整记录写入。** 每次写入替换该会话的完整检查点(注册表切面始终是完整的),并经无损 JSON 边界快照——违反纯 JSON 约定的单元状态会显式失败并报错。
13
- - **记录绑定到日志生命周期,而不只是 id。** 每条记录存储其折叠来源的 header 身份(`createdAt`、`cwd`);每次读取先以活 header 或存储 header 为证验证它,再接受任何行——被删后重建的 id、或缓存幸存而持久化存储被换掉时,无关记录被整体丢弃,绝不播种幻影值。
14
- - **日志领先,缓存跟随。** 活会话检查点先把缓冲事件持久 flush,缓存行才落地,因此崩溃只会让缓存落后于日志(更长的尾部回放),绝不领先于它。
12
+ `dsh-session-projection-cache` 将每个已注册投影单元的状态检查点(`ctx.sessionProjectionCache`)存为 `session_projcache` 存储域 `per-record` 布局下的逐会话版本化文档。随附 JSON 后端将每条记录存于 `<root>/session_projcache/sessions/<id>.json`,缓存绝不读取会话持久化层。存储行是折叠捷径,绝不是权威:它可能陈旧——`seq` 精确说明陈旧到哪——但绝不会错。三个必写点(会话创建、`turn/end` 与会话释放)加上可配置的条数与间隔节流让缓存保持新鲜。当列表视图需要同步缓存值,或冷投影折叠应跳过已检查点化的前缀时,选择本包。
15
13
 
16
- ## 写策略
14
+ ## 目录
17
15
 
18
- 两个必写点,其间节流:
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
19
22
 
20
- | 触发 | 性质 |
21
- |---|---|
22
- | `turn/end` | 必写——冷读要的正是轮次终值。 |
23
- | 会话释放(detach) | 必写——live 转 cold 的时刻;此后冷读阶梯接管该会话。 |
24
- | 累计 `writeEveryEvents` 个已提交事件 | 配置节流(条数)。 |
25
- | 距首个脏事件 `writeIntervalMs` 毫秒 | 配置节流(间隔)。 |
23
+ -----
26
24
 
27
- 两个 `Config` 字段均必填(无默认值):写入节奏是部署选择,没有普适正确值,由 cordis.yml 明示。
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
28
27
 
29
- ## 列表读(`cachedSnapshot(meta)`)
28
+ 当客户端应在不加载日志的情况下列出冷会话投影值时,把本包与投影注册表及存储栈一起挂载。没有它时,消费方必须先取得日志,才能重建冷投影值。
30
29
 
31
- I/O 一档:从身份匹配的存储记录直接 view 客户端值(仅版本与 state schema 均匹配的 key),以 `{asOfSeq, values}` 切面返回——`asOfSeq` 取所服务行的最低水位,客户端在 higher-seq-wins 规则下播种值存储时,陈旧列表块永远压不过更新的推送帧。host-only 行永不返回。无可用客户端行(未知 id、无关生命周期、无可用行)时返回 `undefined`;api-proxy 列表载体将其转为列缺席。
30
+ ### 何时选择
32
31
 
33
- ## 冷读(`coldSnapshot(id, signal?)`)
32
+ 当部署会重启会话,并需要为历史列表、统计信息或 goal 快照提供持久投影值时,选择本包。当投影只服务实时会话,或额外存储写入的成本高于所节省的投影工作时,跳过本包。
34
33
 
35
- 读取阶梯,正常路径无需加载全量日志:缓存行 → `sessionProjections.restoreFloor`(锚定在最低可用水位之前一个事件的位置)→ 持久化 `readFrom(id, floor)` → `sessionProjections.restore` → 刷新行的 fail-soft 写回。这个锚使缩短的日志(崩溃修复截断)可被证明:越界的行恰好触发一次从 seq 0 的全量重读,而不是把幽灵值当现值服务。无已注册单元时直接服务 `{asOfSeq: -1, values: {}}`,不触碰持久化;无持久日志的会话以 seam 的 `not found` 拒绝。
34
+ ### 最小配置
36
35
 
37
- `write(session)` 是两个必写点共用的同步切面检查点;载体可以直接调用(非 fail-soft——由 fail-soft 包装层负责遏制)。
36
+ 两个节流字段均必填——写入节奏是部署选择,没有普适正确值:
38
37
 
39
- ## 组合
38
+ 缓存通过存储栈打开自己的域,因此 base 先挂 `storage`、`storage-json`(根 `dshHomePath('storages')`)与 `storage-domain`(`backend: json`):
40
39
 
41
40
  ```yaml
42
41
  - id: session-projection-cache
@@ -46,18 +45,94 @@
46
45
  writeIntervalMs: 5000
47
46
  ```
48
47
 
49
- 注入 `storageDomain`、`sessionProjections`、`sessionPersistence`、`sessions`。没有这一行时,投影系统只跑 live(水位缓存;冷读在实现了它的载体处退回全量日志加载)。
48
+ | 字段 | 默认值 | 含义 |
49
+ |---|---|---|
50
+ | `writeEveryEvents` | 必填 | 在各必写点之间强制一次持久检查点写入的每会话已提交事件数 |
51
+ | `writeIntervalMs` | 必填 | 各必写点之间脏检查点最长可保持未写入的时间 |
52
+
53
+ 本插件注入 `storageDomain`、`sessionProjections` 与 `sessions`。生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-projection-cache)是每个受支持字段及其 JSDoc 的穷尽式真源。
54
+
55
+ ### 检查点如何写入
56
+
57
+ 三个必写点总是写入:会话创建保存由种子派生的切面,`turn/end` 保存列表读取所需的轮次终值,会话释放保存最终实时切面。其间,配置的条数与间隔节流随事件累积写入。每次写入通过领域写入链以原子方式替换该会话的完整记录;失败会记录警告并让缓存保持陈旧,后续写入会自行修复。
58
+
59
+ ### 读取缓存值
60
+
61
+ `cachedSnapshot(meta)` 以零 I/O 从存储域的内存表同步提供客户端值。它只接受身份匹配的记录以及版本和 schema 均匹配的 key,再按所服务行的最低水位返回 `{ asOfSeq, values }` 切面。对于未知 id、无关生命周期、缺失或外来的记录文档,或没有可用行的情况,它返回 `undefined`。`coldSnapshot(meta, events)` 接受完整有序日志,在折叠时跳过已检查点化的前缀,并在自身不读取持久化层的情况下刷新记录。
62
+
63
+ ### 缓存保证什么
64
+
65
+ 日志领先,缓存跟随:实时检查点先把会话的缓冲事件持久化,然后才保存缓存记录。因此崩溃可能让缓存落后于日志,但绝不会让缓存领先。读取和写入共享存储域内一致的内存状态;逐单元写入链只在持久化成功后修改内存。每个带版本戳的记录必须匹配实时单元 schema 与会话 header 身份(`createdAt`、`cwd`),因此畸形、陈旧或无关的记录都会读作不存在。JSON 后端把每条记录存于仅所有者可访问的 `<root>/session_projcache/sessions/<id>.json` 目录树中。
66
+
67
+ -----
68
+
69
+ <a id="understand-the-implementation"></a>
70
+ ## 理解实现
71
+
72
+ <details>
73
+ <summary>实现细节——点击展开</summary>
74
+
75
+ 本节说明缓存的持久性与存储所有权;可观察行为已在[使用本包](#use-this-package)中说明。
76
+
77
+ ### 设计理念
78
+
79
+ 缓存是投影注册表检查点接口上的折叠捷径,存于 `per-record` 领域数据表中。它带来六项后果:读取绝不绕过领域写入链;每次后台写入都 fail-soft;`ver` 不匹配时丢弃而不迁移记录;记录必须通过实时单元的 `stateSchema`;写入通过无损 JSON 边界替换一份完整会话记录;日志领先,缓存跟随。
80
+
81
+ ### 读写所有权
50
82
 
83
+ 缓存在 `session_projcache` 领域中为每个会话保存一份带版本戳的文档。它不依赖会话持久化后端,不调用 `locate`,也不检查逐会话目录。畸形或陈旧的记录读作不存在;需要冷值的消费方负责提供日志以重新折叠。
84
+
85
+ ### 源码地图
86
+
87
+ | 文件 | 职责 |
88
+ |---|---|
89
+ | [`src/index.ts`](src/index.ts) | 插件入口:`SessionProjectionCache` 服务、写后监听器、缓存读取 |
90
+ | [`src/spec.ts`](src/spec.ts) | `session_projcache` 域 spec 与记录身份类型 |
91
+ | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;正确性在写入与读取路径强制) |
92
+
93
+ </details>
94
+
95
+ -----
96
+
97
+ <a id="further-exploration"></a>
98
+ ## 进一步探索
99
+
100
+ 当包级约定不够用时阅读以下页面。它们从缓存逐步进入它检查点化的注册表与保存其记录的存储域。
101
+
102
+ - [会话投影子系统](../../../docs/subsystems/session-projection.zh.md)——本缓存检查点化的投影单元约定与驱动语义。
103
+ - [会话投影注册表](../session-projection/README.zh.md)——本缓存持久化其检查点的 `ctx.sessionProjections` 服务。
104
+ - [存储子系统](../../../docs/subsystems/storage.zh.md)——保存缓存记录的领域路由与后端行为。
105
+ - [会话包映射](../README.zh.md)——相邻的持久化、标题与遥测包。
106
+ - [会话投影 RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md)——持久投影缓存的设计理由。
107
+
108
+ -----
109
+
110
+ <a id="model-experience"></a>
51
111
  ## 模型体验
52
112
 
53
- 无,因为缓存只持久化并恢复 host 侧的、由已写入日志的会话状态派生的读模型,不触碰任何提示词、消息、schema、流或工具结果。
113
+ 无,因为持久缓存只加速主机侧的投影状态读取,不注册任何模型可见内容。
54
114
 
55
115
  #### KV Cache 影响
56
116
 
57
117
  无;缓存从不组装或发送提供方请求。
58
118
 
59
- ## 已知局限与延后工作
119
+ ## 已知限制与延期工作
120
+
121
+ <a id="known-limitations-and-deferred-work"></a>
122
+
123
+
124
+ 这些限制说明缓存何时需要运维注意。它们是当前包约束,不是任务积压。
125
+
126
+ - **无淘汰或保留接口**——记录按会话持续累积;清理已存储检查点属于带外维护,与会话持久化采用相同策略。
127
+ - **间隔节流采用按会话的粗粒度控制**——一次无脏数据的写入完成后,计时器在首个脏事件到达时启动;持续但低于条数阈值的事件流每间隔写入一次,而非滑动窗口。
128
+ - **缓存侧不做冷重折叠**——缓存只服务并刷新自己的记录,从不读取会话日志,因为它不依赖持久化层;需要保证冷快照的消费方自行从日志重新折叠。
129
+
130
+ <a id="dev-note"></a>
131
+ ### 开发备注
132
+
133
+ <details>
134
+ <summary>维护者的工作上下文——点击展开</summary>
135
+
136
+ 无。
60
137
 
61
- - **不提供淘汰或保留接口**:记录会按会话持续累积;清理已存储的检查点属于带外维护,与会话持久化采用相同策略。
62
- - **间隔节流采用按会话的粗粒度控制**:一次无脏数据的写入完成后,计时器会在首个脏事件到达时启动;对于持续但未达到条数阈值的事件流,系统每个间隔写入一次,而不采用滑动窗口。
63
- - **`coldSnapshot` 读取不去重**——同一会话的两个并发冷读各跑一遍阶梯;写回最后者胜(行等价),对列表级调用频率可接受。
138
+ </details>
package/lib/index.js CHANGED
@@ -1,17 +1,18 @@
1
1
  import { Service } from "@deepseek-ai/cordis";
2
2
  import z from "@deepseek-ai/schemastery";
3
- import { snapshotJsonValue } from "@deepseek-ai/dsh-session";
3
+ import { snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
4
4
  import { z as z$1 } from "zod";
5
5
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
6
6
  //#region lib/types/spec.js
7
7
  /**
8
- * The session-projcache domain declaration: one `sessions` table keyed by
8
+ * The projection-cache domain declaration: one `sessions` table keyed by
9
9
  * {@link SessionId}, each record the full projection checkpoint for one
10
- * session (`key → {ver, seq, val}` rows). The spec object
11
- * is the single source of the domain's identity, version, and record schema;
12
- * the storage-domain routing decides the medium (the shipped composition's
13
- * json backend lands it at `<root>/session_projcache.json`, beside
14
- * `workspace.json`).
10
+ * session (`key → {ver, seq, val}` rows). The spec object is the single
11
+ * source of the domain's identity, version, layout, and record schema; the
12
+ * storage-domain routing decides the medium (the shipped composition's json
13
+ * backend stores the domain `per-record`: one document per session under
14
+ * `<root>/session_projcache/sessions/`, so a checkpoint write rewrites one
15
+ * session's document instead of the whole unit).
15
16
  * @module @deepseek-ai/dsh-session-projection-cache/src/spec
16
17
  */
17
18
  /**
@@ -32,9 +33,9 @@ const checkpointRow = z$1.object({
32
33
  * that distinguish one session lifecycle from another under the same id. A
33
34
  * session id names a slot, not a lifecycle — a deleted-then-recreated id, or
34
35
  * a persistence root swapped under a surviving cache, would otherwise let an
35
- * old row pass every watermark check and seed state folded from an unrelated
36
- * log. Reads validate this against the live header (listing) or the stored
37
- * header (cold read) before accepting any row.
36
+ * old record pass every watermark check and seed state folded from an
37
+ * unrelated log. Reads validate this against the live header (listing) or
38
+ * the stored header (cold read) before accepting any record.
38
39
  */
39
40
  const checkpointIdentity = z$1.object({
40
41
  createdAt: z$1.number().int().nonnegative(),
@@ -51,27 +52,34 @@ const checkpointRecord = z$1.object({
51
52
  rows: z$1.record(z$1.string(), checkpointRow)
52
53
  });
53
54
  /**
54
- * The session-projcache domain spec. Version bumps discard the whole medium
55
- * (cache semantics: a stale or unreadable cache costs a longer tail replay,
56
- * never a wrong value).
55
+ * The session-projcache domain spec. The `per-record` layout scopes version
56
+ * bumps per session: after a bump, a stale session document is discarded on
57
+ * open (cache semantics — a stale or unreadable cache costs a longer tail
58
+ * replay, never a wrong value) while the rest of the domain stays usable,
59
+ * instead of rejecting the whole medium.
57
60
  */
58
61
  const projectionCacheDomainSpec = defineDomain({
59
62
  name: "session_projcache",
60
- version: 3,
63
+ version: 4,
64
+ layout: "per-record",
61
65
  tables: { sessions: domainTable(checkpointRecord) }
62
66
  });
63
67
  //#endregion
64
68
  //#region lib/types/index.js
65
69
  /**
66
70
  * Persisted projection cache (`ctx.sessionProjectionCache`): durable
67
- * checkpoints of every client-visible or explicitly persisted projection unit's state, one record per
68
- * session on the domain data form (`session_projcache` domain — the shipped
69
- * json backend lands it beside `workspace.json`). The cache is a fold
70
- * shortcut, never an authority: a row is possibly stale (its `seq`
71
- * says how stale) but never wrong, so every write path is fail-soft (a lost
72
- * write costs a longer tail replay on the next cold read) and a
73
- * `ver` mismatch discards the row instead of migrating it. Design
74
- * authority: the session-projection RFC
71
+ * checkpoints of every projection unit's state, one record per session on
72
+ * the `session_projcache` domain (`per-record` layout — the shipped json
73
+ * backend stores one document per session under its root). Reads and writes
74
+ * share ONE coherent state: the domain's in-memory tables serve every read
75
+ * synchronously, and each write lands on the domain's write chain (durability
76
+ * first, then memory), so a read can never observe a disk write the memory
77
+ * has not applied, or a memory value the disk does not hold. The cache is a
78
+ * fold shortcut, never an authority: a row
79
+ * is possibly stale (its `seq` says how stale) but never wrong, so every
80
+ * write path is fail-soft (a lost write costs a longer tail replay on the
81
+ * next cold read) and a `ver` mismatch discards the row instead of migrating
82
+ * it. Design authority: the session-projection RFC
75
83
  * (.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md).
76
84
  * @module @deepseek-ai/dsh-session-projection-cache
77
85
  */
@@ -82,18 +90,17 @@ const Config = z.object({
82
90
  /**
83
91
  * The persisted projection cache service. Opens the `session_projcache`
84
92
  * domain at init, checkpoints live sessions on a throttled write-behind
85
- * (count/interval triggers from {@link Config}) plus two mandatory points —
86
- * `turn/end` and session disposal (the live-to-cold moment) — and serves the
87
- * cold-read ladder: cached row, persistence `readFrom` tail, registry
88
- * `restore`, durable write-back. Every durable write is fail-soft: failures
89
- * log a warning and the cache self-heals on the next write or cold read.
93
+ * (count/interval triggers from {@link Config}) plus three mandatory points —
94
+ * session creation, `turn/end`, and session disposal (the live-to-cold
95
+ * moment) and serves the
96
+ * cached rows for a session header. Every durable write is fail-soft:
97
+ * failures log a warning and the cache self-heals on the next write.
90
98
  */
91
99
  var SessionProjectionCache = class extends Service {
92
100
  config;
93
101
  static inject = [
94
102
  "storageDomain",
95
103
  "sessionProjections",
96
- "sessionPersistence",
97
104
  "sessions"
98
105
  ];
99
106
  static Config = Config;
@@ -115,7 +122,9 @@ var SessionProjectionCache = class extends Service {
115
122
  * identity matches `expected`. A session id names a slot, not a lifecycle:
116
123
  * a recreated id or a persistence store swapped under a surviving cache
117
124
  * must not let an old record seed state folded from an unrelated log.
118
- * Synchronous from the domain's in-memory state.
125
+ * Synchronous from the domain's in-memory state — the same state every
126
+ * write mutated, so a read can never go around the write chain to the
127
+ * medium.
119
128
  * @param id - the session whose record is read.
120
129
  * @param expected - the log identity the caller holds (live or stored header).
121
130
  * @returns the identity-matching record, or `undefined` (absent or unrelated).
@@ -127,32 +136,53 @@ var SessionProjectionCache = class extends Service {
127
136
  }
128
137
  /**
129
138
  * The zero-I/O listing read: whole values viewed straight from the stored
130
- * rows (version-matching keys only), each cut carried with its watermark
131
- * so a client value store can seed under its higher-seq-wins rule — as
132
- * stale as the last durable checkpoint but never wrong, and never from an
139
+ * rows (version-matching keys only), each cut carried with its watermark so
140
+ * a client value store can seed under its higher-seq-wins rule — as stale
141
+ * as the last durable checkpoint but never wrong, and never from an
133
142
  * unrelated log (the caller's header is the identity witness). Fresher
134
- * paths (the history tail baseline, {@link coldSnapshot}) supersede these
135
- * values whenever a session is actually opened.
143
+ * paths (the history tail baseline) supersede these values whenever a
144
+ * session is actually opened.
136
145
  * @param meta - the listed session's header (identity witness; no log read).
146
+ * @param keys - optional projection keys required by the caller's audience.
137
147
  * @returns the cut (`asOfSeq` = lowest served-row watermark), or
138
148
  * `undefined` when no usable row exists for this lifecycle.
139
149
  */
140
- cachedSnapshot(meta) {
150
+ cachedSnapshot(meta, keys) {
141
151
  const record = this.recordFor(meta.id, identityOf(meta));
142
152
  if (record === void 0) return void 0;
143
- const values = this.ctx.sessionProjections.viewCheckpoint(record.rows);
144
- const keys = Object.keys(values);
145
- if (keys.length === 0) return void 0;
153
+ const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys);
154
+ const servedKeys = Object.keys(values);
155
+ if (servedKeys.length === 0) return void 0;
146
156
  return {
147
- asOfSeq: Math.min(...keys.map((key) => record.rows[key].seq)),
157
+ asOfSeq: Math.min(...servedKeys.map((key) => record.rows[key].seq)),
148
158
  values
149
159
  };
150
160
  }
151
161
  /**
152
- * Durably checkpoint one live session NOW (both mandatory points call
162
+ * Hydrate projection cells for an already-prepared Session without another
163
+ * persistence read. The cache seeds matching rows; the supplied exact log
164
+ * advances every unit to the observation cut. No checkpoint is written
165
+ * because the logical observation may contain recovery events not yet durable.
166
+ * @param session - exact unpublished Session retained by persistence.
167
+ * @param meta - observed lifecycle header.
168
+ * @param events - exact logical event prefix represented by the observation.
169
+ * @returns all projection values at the event cut.
170
+ */
171
+ hydratePrepared(session, meta, events) {
172
+ const record = this.recordFor(meta.id, identityOf(meta));
173
+ if (record === void 0) return this.ctx.sessionProjections.hydrate(session, {}, events, 0);
174
+ try {
175
+ return this.ctx.sessionProjections.hydrate(session, record.rows, events, 0);
176
+ } catch {
177
+ return this.ctx.sessionProjections.hydrate(session, {}, events, 0);
178
+ }
179
+ }
180
+ /**
181
+ * Durably checkpoint one live session NOW (all mandatory points call
153
182
  * this; tests and carriers may too). The registry cut is snapshotted at
154
- * this boundary (states are live references), then the whole record is
155
- * replaced. NOT fail-soft — callers on the fail-soft paths contain it.
183
+ * this boundary (states are live references), then the session's record is
184
+ * replaced on the domain's write chain. NOT fail-soft — callers on the
185
+ * fail-soft paths contain it.
156
186
  * @param session - the live session to checkpoint.
157
187
  * @returns resolution after durability and event emission.
158
188
  */
@@ -163,37 +193,22 @@ var SessionProjectionCache = class extends Service {
163
193
  await this.put(session.id, identityOf(session.header), rows);
164
194
  }
165
195
  /**
166
- * Cold-read one persisted session's projections with zero full-log load:
167
- * cached rows + a persistence `readFrom` tail from the registry's restore
168
- * floor, refolded by the registry and written back (fail-soft) so the next
169
- * cold read starts closer. A cache row invalidated by a shrunk log
170
- * (crash-repair truncation) triggers one full re-read from seq 0 the
171
- * ladder's slow rung, still no crash. Rejects when the session has no
172
- * persisted log (`not found` from the persistence seam).
173
- * @param id - the persisted session to read.
174
- * @param signal - optional cancellation for the persistence reads.
175
- * @returns the snapshot cut at the stored log end.
196
+ * Cold-read one session's projections from its complete log. Each unit is
197
+ * seeded from the identity-checked cached rows the registry skips `apply`
198
+ * for the already-folded prefix (events at or below the row's `seq`) and
199
+ * the refreshed checkpoint is written back (fail-soft, fire-and-forget), so
200
+ * the first cold read creates the cache row and later ones seed from it.
201
+ * The caller supplies the complete log in seq order: this service never
202
+ * consults the persistence layer.
203
+ * @param meta - the stored session header (identity witness).
204
+ * @param events - the session's complete log, in seq order.
205
+ * @returns the projection cut at the log end.
176
206
  */
177
- async coldSnapshot(id, signal) {
178
- const record = this.requireTable().get(id);
179
- const cached = record?.rows ?? {};
180
- const floor = this.ctx.sessionProjections.restoreFloor(cached);
181
- const persistence = this.ctx.sessionPersistence;
182
- if (floor === void 0) return {
183
- asOfSeq: (await persistence.readFrom(id, 0, signal)).events.at(-1)?.seq ?? -1,
184
- values: {}
185
- };
186
- let restored;
187
- const tail = await persistence.readFrom(id, floor, signal);
188
- const related = record === void 0 || identityMatches(record.identity, identityOf(tail.meta));
189
- try {
190
- if (!related) throw new Error("unrelated log identity");
191
- restored = this.ctx.sessionProjections.restore(cached, tail.events, floor);
192
- } catch {
193
- const whole = await persistence.readFrom(id, 0, signal);
194
- restored = this.ctx.sessionProjections.restore({}, whole.events, 0);
195
- }
196
- await this.putSoft(id, identityOf(tail.meta), restored.checkpoint, "cold-read write-back");
207
+ coldSnapshot(meta, events) {
208
+ const restored = this.ctx.sessionProjections.restore(this.recordFor(meta.id, identityOf(meta))?.rows ?? {}, events, 0, meta);
209
+ this.put(meta.id, identityOf(meta), restored.checkpoint).catch((error) => {
210
+ this.ctx.logger.warn(`session projection cache: cold-read write-back for "${meta.id}" failed (cache stays stale): ${String(error)}`);
211
+ });
197
212
  return restored.snapshot;
198
213
  }
199
214
  installWritePath() {
@@ -216,6 +231,9 @@ var SessionProjectionCache = class extends Service {
216
231
  this.flushSoft(session, "interval");
217
232
  }, this.config.writeIntervalMs);
218
233
  });
234
+ this.ctx.on("session/created", (session) => {
235
+ this.flushSoft(session, "create");
236
+ });
219
237
  this.ctx.on("session/disposed", (session) => {
220
238
  this.flushSoft(session, "detach");
221
239
  this.markClean(session);
@@ -229,7 +247,7 @@ var SessionProjectionCache = class extends Service {
229
247
  /**
230
248
  * One fail-soft durable checkpoint. Every caller has work by construction:
231
249
  * the throttle triggers only fire dirty (markClean clears the timer with
232
- * the counter) and the two mandatory points write unconditionally.
250
+ * the counter) and the mandatory points write unconditionally.
233
251
  */
234
252
  async flushSoft(session, trigger) {
235
253
  try {
@@ -257,14 +275,6 @@ var SessionProjectionCache = class extends Service {
257
275
  rows: detached
258
276
  });
259
277
  }
260
- /** Fail-soft {@link put}: cache writes must never fail their caller's read or event path. */
261
- async putSoft(id, identity, rows, what) {
262
- try {
263
- await this.put(id, identity, rows);
264
- } catch (error) {
265
- this.ctx.logger.warn(`session projection cache: ${what} for "${id}" failed (cache stays stale): ${String(error)}`);
266
- }
267
- }
268
278
  requireTable() {
269
279
  /* v8 ignore next -- Service.init assigns the table before the service becomes injectable */
270
280
  if (this.table === void 0) throw new Error("session projection cache is not initialized");
package/lib/invariant.js CHANGED
@@ -13,8 +13,8 @@ const inject = ["invariants"];
13
13
  * the registry fold at its `seq` watermark) is only checkable by re-running the
14
14
  * fold over the persisted log — duplicating the implementation rather than
15
15
  * detecting drift — and its staleness is by design (fail-soft writes). The
16
- * durable boundary is already schema-validated by the storage-domain layer
17
- * on every reopen, and the read ladder's version/watermark guards are proven
16
+ * durable boundary is schema-validated by the cache's own zod parse on every
17
+ * read, and the read ladder's version/watermark guards are proven
18
18
  * by the package spec.
19
19
  */
20
20
  const install = () => {};
@@ -1,20 +1,24 @@
1
1
  /**
2
2
  * Persisted projection cache (`ctx.sessionProjectionCache`): durable
3
- * checkpoints of every client-visible or explicitly persisted projection unit's state, one record per
4
- * session on the domain data form (`session_projcache` domain — the shipped
5
- * json backend lands it beside `workspace.json`). The cache is a fold
6
- * shortcut, never an authority: a row is possibly stale (its `seq`
7
- * says how stale) but never wrong, so every write path is fail-soft (a lost
8
- * write costs a longer tail replay on the next cold read) and a
9
- * `ver` mismatch discards the row instead of migrating it. Design
10
- * authority: the session-projection RFC
3
+ * checkpoints of every projection unit's state, one record per session on
4
+ * the `session_projcache` domain (`per-record` layout — the shipped json
5
+ * backend stores one document per session under its root). Reads and writes
6
+ * share ONE coherent state: the domain's in-memory tables serve every read
7
+ * synchronously, and each write lands on the domain's write chain (durability
8
+ * first, then memory), so a read can never observe a disk write the memory
9
+ * has not applied, or a memory value the disk does not hold. The cache is a
10
+ * fold shortcut, never an authority: a row
11
+ * is possibly stale (its `seq` says how stale) but never wrong, so every
12
+ * write path is fail-soft (a lost write costs a longer tail replay on the
13
+ * next cold read) and a `ver` mismatch discards the row instead of migrating
14
+ * it. Design authority: the session-projection RFC
11
15
  * (.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md).
12
16
  * @module @deepseek-ai/dsh-session-projection-cache
13
17
  */
14
18
  import { Context, Service } from '@deepseek-ai/cordis';
15
19
  import z from '@deepseek-ai/schemastery';
16
- import type { Session, SessionHeader, SessionId } from '@deepseek-ai/dsh-session';
17
- import type { ProjectionSnapshot } from '@deepseek-ai/dsh-session-projection';
20
+ import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session';
21
+ import type { ProjectionSnapshot, SessionProjectionMap } from '@deepseek-ai/dsh-session-projection';
18
22
  export { checkpointIdentity, checkpointRecord, checkpointRow, projectionCacheDomainSpec } from './spec.ts';
19
23
  export type { CheckpointIdentity, CheckpointRecord } from './spec.ts';
20
24
  declare module '@deepseek-ai/cordis' {
@@ -25,8 +29,9 @@ declare module '@deepseek-ai/cordis' {
25
29
  /**
26
30
  * Plugin config. Both throttle triggers are deployment choices with no
27
31
  * universally correct value, so the composition states them explicitly
28
- * (cordis.yml); the two mandatory write points (`turn/end` and session
29
- * disposal) are policy, not tunables, and always fire.
32
+ * (cordis.yml); the three mandatory write points (session creation,
33
+ * `turn/end`, and session disposal) are policy, not tunables, and always
34
+ * fire.
30
35
  */
31
36
  export interface Config {
32
37
  /** Committed events per session that force a durable checkpoint write between mandatory points. */
@@ -38,11 +43,11 @@ export declare const Config: z<Config>;
38
43
  /**
39
44
  * The persisted projection cache service. Opens the `session_projcache`
40
45
  * domain at init, checkpoints live sessions on a throttled write-behind
41
- * (count/interval triggers from {@link Config}) plus two mandatory points —
42
- * `turn/end` and session disposal (the live-to-cold moment) — and serves the
43
- * cold-read ladder: cached row, persistence `readFrom` tail, registry
44
- * `restore`, durable write-back. Every durable write is fail-soft: failures
45
- * log a warning and the cache self-heals on the next write or cold read.
46
+ * (count/interval triggers from {@link Config}) plus three mandatory points —
47
+ * session creation, `turn/end`, and session disposal (the live-to-cold
48
+ * moment) and serves the
49
+ * cached rows for a session header. Every durable write is fail-soft:
50
+ * failures log a warning and the cache self-heals on the next write.
46
51
  */
47
52
  export declare class SessionProjectionCache extends Service {
48
53
  config: Config;
@@ -58,7 +63,9 @@ export declare class SessionProjectionCache extends Service {
58
63
  * identity matches `expected`. A session id names a slot, not a lifecycle:
59
64
  * a recreated id or a persistence store swapped under a surviving cache
60
65
  * must not let an old record seed state folded from an unrelated log.
61
- * Synchronous from the domain's in-memory state.
66
+ * Synchronous from the domain's in-memory state — the same state every
67
+ * write mutated, so a read can never go around the write chain to the
68
+ * medium.
62
69
  * @param id - the session whose record is read.
63
70
  * @param expected - the log identity the caller holds (live or stored header).
64
71
  * @returns the identity-matching record, or `undefined` (absent or unrelated).
@@ -66,52 +73,63 @@ export declare class SessionProjectionCache extends Service {
66
73
  private recordFor;
67
74
  /**
68
75
  * The zero-I/O listing read: whole values viewed straight from the stored
69
- * rows (version-matching keys only), each cut carried with its watermark
70
- * so a client value store can seed under its higher-seq-wins rule — as
71
- * stale as the last durable checkpoint but never wrong, and never from an
76
+ * rows (version-matching keys only), each cut carried with its watermark so
77
+ * a client value store can seed under its higher-seq-wins rule — as stale
78
+ * as the last durable checkpoint but never wrong, and never from an
72
79
  * unrelated log (the caller's header is the identity witness). Fresher
73
- * paths (the history tail baseline, {@link coldSnapshot}) supersede these
74
- * values whenever a session is actually opened.
80
+ * paths (the history tail baseline) supersede these values whenever a
81
+ * session is actually opened.
75
82
  * @param meta - the listed session's header (identity witness; no log read).
83
+ * @param keys - optional projection keys required by the caller's audience.
76
84
  * @returns the cut (`asOfSeq` = lowest served-row watermark), or
77
85
  * `undefined` when no usable row exists for this lifecycle.
78
86
  */
79
- cachedSnapshot(meta: SessionHeader): ProjectionSnapshot | undefined;
87
+ cachedSnapshot(meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[]): ProjectionSnapshot | undefined;
80
88
  /**
81
- * Durably checkpoint one live session NOW (both mandatory points call
89
+ * Hydrate projection cells for an already-prepared Session without another
90
+ * persistence read. The cache seeds matching rows; the supplied exact log
91
+ * advances every unit to the observation cut. No checkpoint is written
92
+ * because the logical observation may contain recovery events not yet durable.
93
+ * @param session - exact unpublished Session retained by persistence.
94
+ * @param meta - observed lifecycle header.
95
+ * @param events - exact logical event prefix represented by the observation.
96
+ * @returns all projection values at the event cut.
97
+ */
98
+ hydratePrepared(session: Session, meta: SessionHeader, events: readonly SessionEvent[]): ProjectionSnapshot;
99
+ /**
100
+ * Durably checkpoint one live session NOW (all mandatory points call
82
101
  * this; tests and carriers may too). The registry cut is snapshotted at
83
- * this boundary (states are live references), then the whole record is
84
- * replaced. NOT fail-soft — callers on the fail-soft paths contain it.
102
+ * this boundary (states are live references), then the session's record is
103
+ * replaced on the domain's write chain. NOT fail-soft — callers on the
104
+ * fail-soft paths contain it.
85
105
  * @param session - the live session to checkpoint.
86
106
  * @returns resolution after durability and event emission.
87
107
  */
88
108
  write(session: Session): Promise<void>;
89
109
  /**
90
- * Cold-read one persisted session's projections with zero full-log load:
91
- * cached rows + a persistence `readFrom` tail from the registry's restore
92
- * floor, refolded by the registry and written back (fail-soft) so the next
93
- * cold read starts closer. A cache row invalidated by a shrunk log
94
- * (crash-repair truncation) triggers one full re-read from seq 0 the
95
- * ladder's slow rung, still no crash. Rejects when the session has no
96
- * persisted log (`not found` from the persistence seam).
97
- * @param id - the persisted session to read.
98
- * @param signal - optional cancellation for the persistence reads.
99
- * @returns the snapshot cut at the stored log end.
110
+ * Cold-read one session's projections from its complete log. Each unit is
111
+ * seeded from the identity-checked cached rows the registry skips `apply`
112
+ * for the already-folded prefix (events at or below the row's `seq`) and
113
+ * the refreshed checkpoint is written back (fail-soft, fire-and-forget), so
114
+ * the first cold read creates the cache row and later ones seed from it.
115
+ * The caller supplies the complete log in seq order: this service never
116
+ * consults the persistence layer.
117
+ * @param meta - the stored session header (identity witness).
118
+ * @param events - the session's complete log, in seq order.
119
+ * @returns the projection cut at the log end.
100
120
  */
101
- coldSnapshot(id: SessionId, signal?: AbortSignal): Promise<ProjectionSnapshot>;
121
+ coldSnapshot(meta: SessionHeader, events: readonly SessionEvent[]): ProjectionSnapshot;
102
122
  private installWritePath;
103
123
  /**
104
124
  * One fail-soft durable checkpoint. Every caller has work by construction:
105
125
  * the throttle triggers only fire dirty (markClean clears the timer with
106
- * the counter) and the two mandatory points write unconditionally.
126
+ * the counter) and the mandatory points write unconditionally.
107
127
  */
108
128
  private flushSoft;
109
129
  /** Reset one session's dirty bookkeeping (its checkpoint is being written). */
110
130
  private markClean;
111
131
  /** Replace one session's stored record with its log identity and a detached snapshot of `rows`. */
112
132
  private put;
113
- /** Fail-soft {@link put}: cache writes must never fail their caller's read or event path. */
114
- private putSoft;
115
133
  private requireTable;
116
134
  }
117
135
  export default SessionProjectionCache;
@@ -1,15 +1,16 @@
1
1
  /**
2
- * The session-projcache domain declaration: one `sessions` table keyed by
2
+ * The projection-cache domain declaration: one `sessions` table keyed by
3
3
  * {@link SessionId}, each record the full projection checkpoint for one
4
- * session (`key → {ver, seq, val}` rows). The spec object
5
- * is the single source of the domain's identity, version, and record schema;
6
- * the storage-domain routing decides the medium (the shipped composition's
7
- * json backend lands it at `<root>/session_projcache.json`, beside
8
- * `workspace.json`).
4
+ * session (`key → {ver, seq, val}` rows). The spec object is the single
5
+ * source of the domain's identity, version, layout, and record schema; the
6
+ * storage-domain routing decides the medium (the shipped composition's json
7
+ * backend stores the domain `per-record`: one document per session under
8
+ * `<root>/session_projcache/sessions/`, so a checkpoint write rewrites one
9
+ * session's document instead of the whole unit).
9
10
  * @module @deepseek-ai/dsh-session-projection-cache/src/spec
10
11
  */
11
12
  import { z } from 'zod';
12
- import { SessionId } from '@deepseek-ai/dsh-session';
13
+ import type { SessionId } from '@deepseek-ai/dsh-session';
13
14
  /**
14
15
  * One persisted checkpoint row (the RFC's `(sessionId, key, ver, seq, val)`
15
16
  * minus the two record keys). `val` is the unit's internal state — plain
@@ -28,9 +29,9 @@ export declare const checkpointRow: z.ZodObject<{
28
29
  * that distinguish one session lifecycle from another under the same id. A
29
30
  * session id names a slot, not a lifecycle — a deleted-then-recreated id, or
30
31
  * a persistence root swapped under a surviving cache, would otherwise let an
31
- * old row pass every watermark check and seed state folded from an unrelated
32
- * log. Reads validate this against the live header (listing) or the stored
33
- * header (cold read) before accepting any row.
32
+ * old record pass every watermark check and seed state folded from an
33
+ * unrelated log. Reads validate this against the live header (listing) or
34
+ * the stored header (cold read) before accepting any record.
34
35
  */
35
36
  export declare const checkpointIdentity: z.ZodObject<{
36
37
  createdAt: z.ZodNumber;
@@ -58,13 +59,16 @@ export declare const checkpointRecord: z.ZodObject<{
58
59
  /** One stored per-session checkpoint record, inferred from {@link checkpointRecord}. */
59
60
  export type CheckpointRecord = z.infer<typeof checkpointRecord>;
60
61
  /**
61
- * The session-projcache domain spec. Version bumps discard the whole medium
62
- * (cache semantics: a stale or unreadable cache costs a longer tail replay,
63
- * never a wrong value).
62
+ * The session-projcache domain spec. The `per-record` layout scopes version
63
+ * bumps per session: after a bump, a stale session document is discarded on
64
+ * open (cache semantics — a stale or unreadable cache costs a longer tail
65
+ * replay, never a wrong value) while the rest of the domain stays usable,
66
+ * instead of rejecting the whole medium.
64
67
  */
65
68
  export declare const projectionCacheDomainSpec: {
66
69
  name: string;
67
70
  version: number;
71
+ layout: "per-record";
68
72
  tables: {
69
73
  sessions: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<SessionId, {
70
74
  identity: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-session-projection-cache",
3
- "description": "Persisted projection cache (ctx.sessionProjectionCache): durable per-session projection checkpoints over the domain data form, throttled write-behind, and the cold-read ladder (cache row + persistence tail replay)",
4
- "version": "0.1.1-rc.2",
3
+ "description": "Persisted projection cache (ctx.sessionProjectionCache): durable per-session checkpoint records on the session_projcache storage domain (per-record layout), throttled write-behind, and the cached listing read",
4
+ "version": "0.1.2-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -33,23 +33,23 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
35
  "zod": "^4.4.3",
36
- "@deepseek-ai/schemastery": "^3.18.1"
36
+ "@deepseek-ai/dsh-util-values": "^0.1.2-alpha.2",
37
+ "@deepseek-ai/schemastery": "^3.18.2"
37
38
  },
38
39
  "peerDependencies": {
39
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
40
- "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
41
- "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2",
42
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
43
- "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.2",
44
- "@deepseek-ai/cordis": "^4.0.1"
40
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
41
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
42
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
43
+ "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.2",
44
+ "@deepseek-ai/cordis": "^4.0.2"
45
45
  },
46
46
  "devDependencies": {
47
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
48
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
49
- "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
50
- "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2",
51
- "@deepseek-ai/dsh-storage": "^0.1.1-rc.2",
52
- "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.2",
53
- "@deepseek-ai/cordis": "^4.0.1"
47
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
48
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
49
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
50
+ "@deepseek-ai/dsh-storage": "^0.1.2-alpha.2",
51
+ "@deepseek-ai/cordis": "^4.0.2",
52
+ "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.2",
53
+ "@deepseek-ai/dsh-storage-json": "^0.1.2-alpha.2"
54
54
  }
55
55
  }