@deepseek-ai/dsh-session-projection-cache 0.1.6-alpha.2 → 0.1.7-alpha.1

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: 3125f50b07f222feb0ef2af5e28abcfae19631a7
6
- README.zh.md: c98cda187dd898a0b30a9721c0bc7412a91964c5
5
+ README.md: 0ba9c5a47fbf2006be1593cb1805adb5bf9fbc59
6
+ README.zh.md: d697b0520c0edf89572e9994b62e987111481a77
package/README.md CHANGED
@@ -58,11 +58,11 @@ Three mandatory points always write: session creation persists the seed-derived
58
58
 
59
59
  ### Reading cached values
60
60
 
61
- `cachedSnapshot(meta, inheritedEventCount)` 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. `cachedPredecessorTitle(meta, inheritedEventCount)` is the narrower listing-only exception: a structurally admitted predecessor record whose lifecycle matches may expose only a current-version-compatible `title` row. The title is a possibly stale fact from a durable prefix, not a fold seed; it carries the sentinel `asOfSeq: -1` because a cardinality-changing Session migration can invalidate the predecessor row's numeric sequence. All other predecessor rows remain unavailable. An unseeded listing knows that its cut is zero; a seeded header-only listing does not know the numeric cut and skips both fast paths until an authoritative body read supplies it. `coldSnapshot(meta, inheritedEventCount, events)` accepts the exact cut with a complete ordered log, skips the checkpointed prefix while folding, and refreshes the record without reading persistence itself.
61
+ `cachedSnapshot(meta, keys?)` is the read-only face: it synchronously serves client values from the storage domain's in-memory tables with zero I/O. It accepts a record whose lifecycle identity (`formatVersion`, `createdAt`, `cwd`, `isSeeded`) matches the header and serves its version- and schema-matching keys as one block whose `asOfSeq` is the lowest watermark among the served rows. That watermark is the stored record's own: a header witnesses no inherited cut and cannot vouch that the row sequence is comparable with the log a consumer later opens, so the Session list labels the block `cached` and the client lets every value the connected Session later produces supersede it. Within one format generation the cut is fixed at fork time and distinguishes no lifecycle the other fields do not, and a viewed value never seeds a fold, so seeded (forked) sessions are served exactly like unseeded ones. `cachedPredecessorTitle(meta)` is the narrower listing-only exception across a Session-format edge: a structurally admitted predecessor record whose lifecycle matches may expose only a current-version-compatible `title` row, because title text is invariant across the adjacent edges. All other predecessor rows remain unavailable. `coldSnapshot(meta, inheritedEventCount, events)` is a fold face: it accepts the exact cut with a complete ordered log, skips the checkpointed prefix while folding, and refreshes the record without reading persistence itself.
62
62
 
63
63
  ### What the cache guarantees
64
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 complete lifecycle identity (`formatVersion`, `createdAt`, `cwd`, `isSeeded`, and `inheritedEventCount`), so a row folded from another Session format generation or fork cut cannot seed the caller. The JSON backend stores each record at `<root>/session_projcache/sessions/<id>.json` in an owner-only directory tree.
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 the lifecycle identity (`formatVersion`, `createdAt`, `cwd`, `isSeeded`); the fold faces (`hydratePrepared`, `coldSnapshot`, and checkpoint writes) also require the exact `inheritedEventCount`, so a row folded from another Session format generation or fork cut cannot seed the caller. The JSON backend stores each record at `<root>/session_projcache/sessions/<id>.json` in an owner-only directory tree. Domain read validation preserves every own JSON key in checkpoint values, including `__proto__` and `constructor` inside opaque metadata. It rejects values that cannot survive a lossless JSON round trip, using the same rules as checkpoint writes. Each projection then validates its hydration state with its own `stateSchema`; fields carrying opaque JSON need a validator that preserves their keys.
66
66
 
67
67
  Upgrades never block startup or expose an unproven fold. Records stamped with a version in the spec's `compatibleVersions` remain structurally readable for a current checkpoint rewrite, but a missing or older `formatVersion` never matches a current Session and therefore cannot seed hydration. A lifecycle-matching predecessor title remains available only through the listing hint above because title text is invariant across the adjacent Session-format edges and its row still passes the current projection `stateVersion` and schema. Once the format matches, absent lineage fields decode as the unseeded lineage — exact for unseeded sessions, while a seeded caller fails the identity match and refolds cold. A stored record that still fails schema validation is moved aside as `<id>.json.bak.<stamp>` under the domain's `invalidRecords: 'backup-and-skip'` policy, logged with its cause, and rebuilt by the next checkpoint.
68
68
 
package/README.zh.md CHANGED
@@ -58,11 +58,11 @@ kind: "package-reference"
58
58
 
59
59
  ### 读取缓存值
60
60
 
61
- `cachedSnapshot(meta, inheritedEventCount)` 以零 I/O 从存储域的内存表同步提供客户端值。它只接受身份匹配的记录以及版本和 schema 均匹配的 key,再按所服务行的最低水位返回 `{ asOfSeq, values }` 切面。`cachedPredecessorTitle(meta, inheritedEventCount)` 是更窄的列表专用例外:生命周期匹配且已通过结构准入的 predecessor record 只能公开与当前版本兼容的 `title` row。该 title 是 durable prefix 中可能过时的事实,而不是 fold seed;它携带 sentinel `asOfSeq: -1`,因为改变事件数量的 Session 迁移会使 predecessor row 的数字序号失效。其他 predecessor row 仍不可用。未 seeded 的列表知道切点为零;仅 header 的 seeded 列表不知道数字切点,因此两条快速路径都要跳过,直到权威正文读取提供它。`coldSnapshot(meta, inheritedEventCount, events)` 接受精确切点与完整有序日志,在折叠时跳过已检查点化的前缀,并在自身不读取持久化层的情况下刷新记录。
61
+ `cachedSnapshot(meta, keys?)` 是只读面:以零 I/O 从存储域的内存表同步提供客户端值。它接受生命周期身份(`formatVersion`、`createdAt`、`cwd`、`isSeeded`)与 header 匹配的记录,把其中版本和 schema 均匹配的 key 作为一个 block 提供,其 `asOfSeq` 是所服务各行中最低的水位。这个水位是存储记录自己的:header 作证不了 inherited cut,也作证不了行序号与消费者稍后打开的日志可比,因此 Session list 把该 block 标为 `cached`,客户端让建连后的 Session 产出的任何值覆盖它。在同一格式代内,cut 在 fork 时写死,不能区分其他字段区分不了的生命周期,而只读视图也从不播种 fold,所以 seeded(fork 出来的)会话与 unseeded 会话被同样地提供。`cachedPredecessorTitle(meta)` 是跨 Session 格式 edge 的更窄列表专用例外:生命周期匹配且已通过结构准入的 predecessor record 只能公开与当前版本兼容的 `title` row,因为 title 文本在相邻 edge 之间保持不变。其他 predecessor row 仍不可用。`coldSnapshot(meta, inheritedEventCount, events)` 是 fold 面:接受精确切点与完整有序日志,在折叠时跳过已检查点化的前缀,并在自身不读取持久化层的情况下刷新记录。
62
62
 
63
63
  ### 缓存保证什么
64
64
 
65
- 日志领先,缓存跟随:活会话检查点先把会话的缓冲事件持久化,然后才保存缓存记录。因此崩溃可能让缓存落后于日志,但绝不会让缓存领先。读取和写入共享存储域内一致的内存状态;逐单元写入链只在持久化成功后修改内存。每个带版本戳的记录必须匹配当前运行单元的 schema 与完整生命周期身份(`formatVersion`、`createdAt`、`cwd`、`isSeeded` 和 `inheritedEventCount`),因此从另一会话格式代或 fork 切点折叠出的行不能播种调用方。JSON 后端把每条记录存于仅所有者可访问的 `<root>/session_projcache/sessions/<id>.json` 目录树中。
65
+ 日志领先,缓存跟随:活会话检查点先把会话的缓冲事件持久化,然后才保存缓存记录。因此崩溃可能让缓存落后于日志,但绝不会让缓存领先。读取和写入共享存储域内一致的内存状态;逐单元写入链只在持久化成功后修改内存。每个带版本戳的记录必须匹配当前运行单元的 schema 与生命周期身份(`formatVersion`、`createdAt`、`cwd`、`isSeeded`);fold 面(`hydratePrepared`、`coldSnapshot` 与检查点写入)还要求精确的 `inheritedEventCount`,因此从另一会话格式代或 fork 切点折叠出的行不能播种调用方。JSON 后端把每条记录存于仅所有者可访问的 `<root>/session_projcache/sessions/<id>.json` 目录树中。Domain 读取校验保留检查点值中的所有自有 JSON 键,包括不透明元数据中的 `__proto__` 与 `constructor`。它拒绝无法无损完成 JSON 往返的值,并与检查点写入使用相同规则。随后,每个 projection 用自己的 `stateSchema` 校验 hydration 状态;承载不透明 JSON 的字段需要使用保留其键的校验器。
66
66
 
67
67
  升级绝不拖垮启动,也不会暴露未经证明的折叠结果。版本戳落在 spec `compatibleVersions` 集合内的记录仍可被结构化读取并等待当前检查点重写,但缺失或更旧的 `formatVersion` 绝不匹配当前 Session,因此不能作为 hydrate seed。生命周期匹配的 predecessor title 只能通过上述列表 hint 读取,因为 title 文本在相邻 Session format edge 之间保持不变,并且该 row 仍须通过当前 projection `stateVersion` 与 schema。格式匹配后,缺失的 lineage 字段解码为 unseeded lineage——对非 fork 会话精确无误,seeded 调用方则通不过身份比对、回落冷折叠。仍然通不过 schema 校验的存量记录会按域的 `invalidRecords: 'backup-and-skip'` 策略移出为 `<id>.json.bak.<时间戳>`、连同原因写入日志,并由下一次检查点重建。
68
68
 
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Service } from "@deepseek-ai/cordis";
2
2
  import z from "@deepseek-ai/schemastery";
3
- import { snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
3
+ import { isJsonValue, snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
4
4
  import { SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
5
5
  import { z as z$1 } from "zod";
6
6
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
@@ -19,15 +19,16 @@ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
19
19
  /**
20
20
  * One persisted checkpoint row (the RFC's `(sessionId, key, ver, seq, val)`
21
21
  * minus the two record keys). `val` is the unit's internal state — plain
22
- * JSON by the unit contract; `z.json()` enforces that at the durable
23
- * boundary. A row is never wrong, only possibly stale: `seq` says exactly
24
- * how stale, and a `ver` mismatch against the live unit's `stateVersion`
22
+ * JSON by the unit contract. Validation uses the same lossless JSON rules as
23
+ * writes and preserves every state key without cloning. A row is never wrong,
24
+ * only possibly stale: `seq` says exactly how stale, and a `ver` mismatch
25
+ * against the live unit's `stateVersion`
25
26
  * discards it at read time (never a migration).
26
27
  */
27
28
  const checkpointRow = z$1.object({
28
29
  ver: z$1.number().int().nonnegative(),
29
30
  seq: z$1.number().int().gte(-1).transform((value) => value === -1 ? -1 : SessionSeq(value)),
30
- val: z$1.json()
31
+ val: z$1.custom(isJsonValue, { message: "checkpoint state must be losslessly JSON-serializable" })
31
32
  });
32
33
  /**
33
34
  * The stored-log identity a record is bound to: the immutable header fields
@@ -172,22 +173,27 @@ var SessionProjectionCache = class extends Service {
172
173
  }
173
174
  /**
174
175
  * The zero-I/O listing read: whole values viewed straight from the stored
175
- * rows (version-matching keys only), each cut carried with its watermark so
176
- * a client value store can seed under its higher-seq-wins rule — as stale
177
- * as the last durable checkpoint but never wrong, and never from an
178
- * unrelated log (the caller's header is the identity witness). Fresher
179
- * paths (the history tail baseline) supersede these values whenever a
180
- * session is actually opened.
176
+ * rows (version-matching keys only) of the record bound to the caller's
177
+ * lifecycle. The header is the only identity witness a listing holds, so
178
+ * this face matches the lifecycle identity (`formatVersion`, `createdAt`,
179
+ * `cwd`, `isSeeded`) and not the inherited cut: within one format
180
+ * generation the cut is fixed at fork time, so it distinguishes no
181
+ * lifecycle the other fields do not, and a viewed value never seeds a fold.
182
+ * The view is as stale as the last durable checkpoint but never wrong and
183
+ * never from an unrelated log. Its `asOfSeq` is the lowest watermark among
184
+ * the served rows: the stored record's own position, which the header
185
+ * cannot relate to the log the caller later opens. The Session list
186
+ * therefore labels the block as cached, and the client lets every value the
187
+ * connected Session produces supersede it whatever this number says.
181
188
  * @param meta - the listed session's header (identity witness; no log read).
182
- * @param inheritedEventCount - exact inherited prefix length that completes
183
- * the checkpoint identity.
184
189
  * @param keys - optional projection keys required by the caller's audience.
185
- * @returns the cut (`asOfSeq` = lowest served-row watermark), or
186
- * `undefined` when no usable row exists for this lifecycle.
190
+ * @returns the viewed block, or `undefined` when no usable row exists for
191
+ * this lifecycle at the current Session format.
187
192
  */
188
- cachedSnapshot(meta, inheritedEventCount, keys) {
189
- const record = this.recordFor(meta.id, identityOf(meta, inheritedEventCount));
190
- if (record === void 0) return void 0;
193
+ cachedSnapshot(meta, keys) {
194
+ const expected = lifecycleIdentityOf(meta);
195
+ const record = this.requireTable().get(meta.id);
196
+ if (record === void 0 || !currentLifecycleMatches(record.identity, expected)) return void 0;
191
197
  return this.viewRecord(record, keys);
192
198
  }
193
199
  /**
@@ -199,36 +205,33 @@ var SessionProjectionCache = class extends Service {
199
205
  * fact from this Session. The registry still requires the current title
200
206
  * projection's row version and schema. No other predecessor projection is
201
207
  * exposed: format normalization can change their current meaning, and the
202
- * strict {@link cachedSnapshot} / hydration paths continue to reject them.
208
+ * {@link cachedSnapshot} / hydration paths continue to reject them.
203
209
  * @param meta - authoritative listed Session header.
204
- * @param inheritedEventCount - exact inherited cut completing the lifecycle identity.
205
- * @returns a title-only checkpoint view with `asOfSeq: -1`, or `undefined`
206
- * when the record is current, newer, unrelated, missing, or incompatible
207
- * with the title unit. The sentinel avoids reusing a sequence that a
208
- * cardinality-changing Session migration may have remapped.
210
+ * @returns a title-only block at the stored title row's watermark, or
211
+ * `undefined` when the record is current, newer, unrelated, missing, or
212
+ * incompatible with the title unit.
209
213
  */
210
- cachedPredecessorTitle(meta, inheritedEventCount) {
211
- const expected = identityOf(meta, inheritedEventCount);
214
+ cachedPredecessorTitle(meta) {
215
+ const expected = lifecycleIdentityOf(meta);
212
216
  const record = this.requireTable().get(meta.id);
213
217
  if (record === void 0 || !predecessorIdentityMatches(record.identity, expected)) return void 0;
214
- const title = this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
215
- return title === void 0 ? void 0 : {
216
- ...title,
217
- asOfSeq: -1
218
- };
218
+ return this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
219
219
  }
220
- /** View selected wire rows and bind them to their lowest served watermark. */
220
+ /**
221
+ * View selected wire rows as one block bound to the lowest served
222
+ * watermark: the seq every served value has folded through at least. The
223
+ * number is the record's own; whether a consumer may compare it with a
224
+ * live Session's seqs is decided by the face that serves the block, not
225
+ * here.
226
+ */
221
227
  viewRecord(record, keys) {
222
228
  const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys);
223
- const servedKeys = Object.keys(values);
224
- if (servedKeys.length === 0) return void 0;
225
- const firstKey = servedKeys[0];
226
- let asOfSeq = record.rows[firstKey].seq;
227
- for (const key of servedKeys.slice(1)) {
228
- const row = record.rows[key];
229
- if (row.seq < asOfSeq) asOfSeq = row.seq;
229
+ let asOfSeq;
230
+ for (const [key, row] of Object.entries(record.rows)) {
231
+ if (!Object.hasOwn(values, key)) continue;
232
+ if (asOfSeq === void 0 || row.seq < asOfSeq) asOfSeq = row.seq;
230
233
  }
231
- return {
234
+ return asOfSeq === void 0 ? void 0 : {
232
235
  asOfSeq,
233
236
  values
234
237
  };
@@ -357,26 +360,43 @@ var SessionProjectionCache = class extends Service {
357
360
  return this.table;
358
361
  }
359
362
  };
360
- /** Project a header onto the identity fields a record is bound to. */
361
- function identityOf(header, inheritedEventCount) {
362
- const cut = SessionLogOffset(inheritedEventCount);
363
- if (!header.isSeeded && cut !== 0) throw new Error("unseeded projection-cache identity inherited event count must be 0");
363
+ /** Project a header onto the identity fields a header alone can witness. */
364
+ function lifecycleIdentityOf(header) {
364
365
  return {
365
366
  formatVersion: header.version,
366
367
  createdAt: header.createdAt,
367
368
  ...header.cwd === void 0 ? {} : { cwd: header.cwd },
368
- isSeeded: header.isSeeded,
369
+ isSeeded: header.isSeeded
370
+ };
371
+ }
372
+ /** Project a header and its exact inherited cut onto the complete fold identity. */
373
+ function identityOf(header, inheritedEventCount) {
374
+ const cut = SessionLogOffset(inheritedEventCount);
375
+ if (!header.isSeeded && cut !== 0) throw new Error("unseeded projection-cache identity inherited event count must be 0");
376
+ return {
377
+ ...lifecycleIdentityOf(header),
369
378
  inheritedEventCount: cut
370
379
  };
371
380
  }
372
381
  /**
373
- * Whether a stored record's bound identity names the caller's lifecycle.
374
- * An absent format generation cannot prove the fold semantics and never
375
- * matches. Once the format matches, absent lineage fields (records admitted
376
- * via `compatibleVersions` predate them) read as the unseeded lineage: exact
377
- * for an unseeded caller, while a seeded caller fails the match.
382
+ * Whether a stored record may seed the caller's fold: the current format
383
+ * generation, the same lifecycle, and the same inherited cut. A record folded
384
+ * under another cut encodes that cut in unit states (`schedule`,
385
+ * `subagentCatalog`, `permissions`) and would carry it into the continued
386
+ * fold and the next checkpoint. Absent lineage fields (records admitted via
387
+ * `compatibleVersions` predate them) read as the unseeded lineage: exact for
388
+ * an unseeded caller, while a seeded caller fails the match.
378
389
  */
379
390
  function identityMatches(stored, expected) {
391
+ return currentLifecycleMatches(stored, expected) && (stored.inheritedEventCount ?? 0) === expected.inheritedEventCount;
392
+ }
393
+ /**
394
+ * Whether a stored record was folded from the caller's lifecycle at the
395
+ * current Session format. An absent format generation cannot prove the fold
396
+ * semantics and never matches. This is the whole identity a header-only
397
+ * reader can check, and the whole identity a view needs.
398
+ */
399
+ function currentLifecycleMatches(stored, expected) {
380
400
  return stored.formatVersion === expected.formatVersion && lifecycleIdentityMatches(stored, expected);
381
401
  }
382
402
  /** Match one predecessor cache record to the authoritative listed lifecycle. */
@@ -385,7 +405,7 @@ function predecessorIdentityMatches(stored, expected) {
385
405
  }
386
406
  /** Match the format-independent fields that distinguish one Session lifecycle. */
387
407
  function lifecycleIdentityMatches(stored, expected) {
388
- return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded && (stored.inheritedEventCount ?? 0) === expected.inheritedEventCount;
408
+ return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded;
389
409
  }
390
410
  //#endregion
391
411
  export { Config, SessionProjectionCache, SessionProjectionCache as default, checkpointIdentity, checkpointRecord, checkpointRow, projectionCacheDomainSpec };
@@ -74,20 +74,24 @@ export declare class SessionProjectionCache extends Service {
74
74
  private recordFor;
75
75
  /**
76
76
  * The zero-I/O listing read: whole values viewed straight from the stored
77
- * rows (version-matching keys only), each cut carried with its watermark so
78
- * a client value store can seed under its higher-seq-wins rule — as stale
79
- * as the last durable checkpoint but never wrong, and never from an
80
- * unrelated log (the caller's header is the identity witness). Fresher
81
- * paths (the history tail baseline) supersede these values whenever a
82
- * session is actually opened.
77
+ * rows (version-matching keys only) of the record bound to the caller's
78
+ * lifecycle. The header is the only identity witness a listing holds, so
79
+ * this face matches the lifecycle identity (`formatVersion`, `createdAt`,
80
+ * `cwd`, `isSeeded`) and not the inherited cut: within one format
81
+ * generation the cut is fixed at fork time, so it distinguishes no
82
+ * lifecycle the other fields do not, and a viewed value never seeds a fold.
83
+ * The view is as stale as the last durable checkpoint but never wrong and
84
+ * never from an unrelated log. Its `asOfSeq` is the lowest watermark among
85
+ * the served rows: the stored record's own position, which the header
86
+ * cannot relate to the log the caller later opens. The Session list
87
+ * therefore labels the block as cached, and the client lets every value the
88
+ * connected Session produces supersede it whatever this number says.
83
89
  * @param meta - the listed session's header (identity witness; no log read).
84
- * @param inheritedEventCount - exact inherited prefix length that completes
85
- * the checkpoint identity.
86
90
  * @param keys - optional projection keys required by the caller's audience.
87
- * @returns the cut (`asOfSeq` = lowest served-row watermark), or
88
- * `undefined` when no usable row exists for this lifecycle.
91
+ * @returns the viewed block, or `undefined` when no usable row exists for
92
+ * this lifecycle at the current Session format.
89
93
  */
90
- cachedSnapshot(meta: SessionHeader, inheritedEventCount: SessionLogOffset, keys?: readonly Extract<keyof SessionProjectionMap, string>[]): ProjectionSnapshot | undefined;
94
+ cachedSnapshot(meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[]): ProjectionSnapshot | undefined;
91
95
  /**
92
96
  * Read only a predecessor checkpoint's title as a zero-I/O listing hint.
93
97
  *
@@ -97,16 +101,20 @@ export declare class SessionProjectionCache extends Service {
97
101
  * fact from this Session. The registry still requires the current title
98
102
  * projection's row version and schema. No other predecessor projection is
99
103
  * exposed: format normalization can change their current meaning, and the
100
- * strict {@link cachedSnapshot} / hydration paths continue to reject them.
104
+ * {@link cachedSnapshot} / hydration paths continue to reject them.
101
105
  * @param meta - authoritative listed Session header.
102
- * @param inheritedEventCount - exact inherited cut completing the lifecycle identity.
103
- * @returns a title-only checkpoint view with `asOfSeq: -1`, or `undefined`
104
- * when the record is current, newer, unrelated, missing, or incompatible
105
- * with the title unit. The sentinel avoids reusing a sequence that a
106
- * cardinality-changing Session migration may have remapped.
106
+ * @returns a title-only block at the stored title row's watermark, or
107
+ * `undefined` when the record is current, newer, unrelated, missing, or
108
+ * incompatible with the title unit.
109
+ */
110
+ cachedPredecessorTitle(meta: SessionHeader): ProjectionSnapshot | undefined;
111
+ /**
112
+ * View selected wire rows as one block bound to the lowest served
113
+ * watermark: the seq every served value has folded through at least. The
114
+ * number is the record's own; whether a consumer may compare it with a
115
+ * live Session's seqs is decided by the face that serves the block, not
116
+ * here.
107
117
  */
108
- cachedPredecessorTitle(meta: SessionHeader, inheritedEventCount: SessionLogOffset): ProjectionSnapshot | undefined;
109
- /** View selected wire rows and bind them to their lowest served watermark. */
110
118
  private viewRecord;
111
119
  /**
112
120
  * Hydrate projection cells for an already-prepared Session without another
@@ -10,20 +10,22 @@
10
10
  * @module @deepseek-ai/dsh-session-projection-cache/src/spec
11
11
  */
12
12
  import { z } from 'zod';
13
+ import type { JsonValue } from '@deepseek-ai/dsh-util-values';
13
14
  import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session';
14
15
  import type { SessionId } from '@deepseek-ai/dsh-session';
15
16
  /**
16
17
  * One persisted checkpoint row (the RFC's `(sessionId, key, ver, seq, val)`
17
18
  * minus the two record keys). `val` is the unit's internal state — plain
18
- * JSON by the unit contract; `z.json()` enforces that at the durable
19
- * boundary. A row is never wrong, only possibly stale: `seq` says exactly
20
- * how stale, and a `ver` mismatch against the live unit's `stateVersion`
19
+ * JSON by the unit contract. Validation uses the same lossless JSON rules as
20
+ * writes and preserves every state key without cloning. A row is never wrong,
21
+ * only possibly stale: `seq` says exactly how stale, and a `ver` mismatch
22
+ * against the live unit's `stateVersion`
21
23
  * discards it at read time (never a migration).
22
24
  */
23
25
  export declare const checkpointRow: z.ZodObject<{
24
26
  ver: z.ZodNumber;
25
27
  seq: z.ZodPipe<z.ZodNumber, z.ZodTransform<-1 | SessionSeq, number>>;
26
- val: z.ZodJSONSchema;
28
+ val: z.ZodCustom<JsonValue, JsonValue>;
27
29
  }, z.core.$strip>;
28
30
  /**
29
31
  * The stored-log identity a record is bound to: the immutable header fields
@@ -67,7 +69,7 @@ export declare const checkpointRecord: z.ZodObject<{
67
69
  rows: z.ZodRecord<z.ZodString, z.ZodObject<{
68
70
  ver: z.ZodNumber;
69
71
  seq: z.ZodPipe<z.ZodNumber, z.ZodTransform<-1 | SessionSeq, number>>;
70
- val: z.ZodJSONSchema;
72
+ val: z.ZodCustom<JsonValue, JsonValue>;
71
73
  }, z.core.$strip>>;
72
74
  }, z.core.$strip>;
73
75
  /** One stored per-session checkpoint record, inferred from {@link checkpointRecord}. */
@@ -114,7 +116,7 @@ export declare const projectionCacheDomainSpec: {
114
116
  rows: Record<string, {
115
117
  ver: number;
116
118
  seq: -1 | SessionSeq;
117
- val: z.core.util.JSONType;
119
+ val: JsonValue;
118
120
  }>;
119
121
  }>;
120
122
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-session-projection-cache",
3
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.6-alpha.2",
4
+ "version": "0.1.7-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -28,21 +28,21 @@
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
30
  "zod": "^4.4.3",
31
- "@deepseek-ai/dsh-util-values": "^0.1.6-alpha.2",
32
- "@deepseek-ai/schemastery": "^3.18.2"
31
+ "@deepseek-ai/dsh-util-values": "^0.1.7-alpha.1",
32
+ "@deepseek-ai/schemastery": "^3.18.3"
33
33
  },
34
34
  "peerDependencies": {
35
- "@deepseek-ai/cordis": "^4.0.2",
36
- "@deepseek-ai/dsh-session": "^0.1.6-alpha.2",
37
- "@deepseek-ai/dsh-session-projection": "^0.1.6-alpha.2",
38
- "@deepseek-ai/dsh-storage-domain": "^0.1.6-alpha.2"
35
+ "@deepseek-ai/cordis": "^4.0.3",
36
+ "@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
37
+ "@deepseek-ai/dsh-session-projection": "^0.1.7-alpha.1",
38
+ "@deepseek-ai/dsh-storage-domain": "^0.1.7-alpha.1"
39
39
  },
40
40
  "devDependencies": {
41
- "@deepseek-ai/cordis": "^4.0.2",
42
- "@deepseek-ai/dsh-session": "^0.1.6-alpha.2",
43
- "@deepseek-ai/dsh-session-projection": "^0.1.6-alpha.2",
44
- "@deepseek-ai/dsh-storage": "^0.1.6-alpha.2",
45
- "@deepseek-ai/dsh-storage-domain": "^0.1.6-alpha.2",
46
- "@deepseek-ai/dsh-storage-json": "^0.1.6-alpha.2"
41
+ "@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
42
+ "@deepseek-ai/dsh-session-projection": "^0.1.7-alpha.1",
43
+ "@deepseek-ai/dsh-storage": "^0.1.7-alpha.1",
44
+ "@deepseek-ai/cordis": "^4.0.3",
45
+ "@deepseek-ai/dsh-storage-json": "^0.1.7-alpha.1",
46
+ "@deepseek-ai/dsh-storage-domain": "^0.1.7-alpha.1"
47
47
  }
48
48
  }