@deepseek-ai/dsh-session-projection-cache 0.1.2-alpha.3 → 0.1.2-alpha.5

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: c5e438a2ededef00a1faa456e9913492a603b51d
6
- README.zh.md: eab0b81af618558e6f1e5e6bc0c40da3fcc38884
5
+ README.md: 9fd9766d75ab3f9a460b2802ac59810839f21ae4
6
+ README.zh.md: 98725d9ce76ab44821adf3d43e3807932cf6b675
package/README.md CHANGED
@@ -58,11 +58,13 @@ Three mandatory points always write: session creation persists the seed-derived
58
58
 
59
59
  ### Reading cached values
60
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.
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. An unseeded listing knows that its cut is zero; a seeded header-only listing does not know the numeric cut and must skip this fast path 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.
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 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.
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 (`createdAt`, `cwd`, `isSeeded`, and `inheritedEventCount`), so a row initialized under one fork cut cannot seed another. The JSON backend stores each record at `<root>/session_projcache/sessions/<id>.json` in an owner-only directory tree.
66
+
67
+ Upgrades never cost the boot or the listing: records stamped with a version in the spec's `compatibleVersions` stay readable (their absent lineage fields decode as the unseeded lineage — exact for unseeded sessions, while a seeded caller fails the identity match and refolds cold), and 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.
66
68
 
67
69
  -----
68
70
 
@@ -88,7 +90,7 @@ The cache stores one version-stamped document per session in the `session_projca
88
90
  |---|---|
89
91
  | [`src/index.ts`](src/index.ts) | Plugin entry: `SessionProjectionCache` service, write-behind listeners, cache reads |
90
92
  | [`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) |
93
+ | — | No runtime invariant companion is published; the cache's correctness relation (a stored row equals the registry fold at its `seq` watermark) is only checkable by re-running the fold over the persisted log — duplicating the implementation rather than detecting drift — and its staleness is by design (fail-soft writes). The durable boundary is schema-validated by the cache's own zod parse on every read, and the read ladder's version/watermark guards are proven by the package spec. |
92
94
 
93
95
  </details>
94
96
 
@@ -126,6 +128,7 @@ These limits define where the cache needs operational care. They are current pac
126
128
  - **No eviction or retention surface** — records accumulate per session; pruning stored checkpoints is out-of-band maintenance, same stance as session persistence itself.
127
129
  - **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.
128
130
  - **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.
131
+ - **Every schema or domain-version change must prove its upgrade story** — a change to the stored record schema or the domain version lands in the same PR with an archived fixture of the previously shipped on-disk format under `tests/fixtures/` and test cases in `tests/fixtures.spec.ts` proving the chosen disposition: read-compat recovery (`compatibleVersions`), current-version rewrite, or backup-and-skip salvage. A bump whose old records are simply discarded still proves that the discard neither fails the boot nor poisons the tree.
129
132
 
130
133
  <a id="dev-note"></a>
131
134
  ### Dev Note
package/README.zh.md CHANGED
@@ -58,11 +58,13 @@ kind: "package-reference"
58
58
 
59
59
  ### 读取缓存值
60
60
 
61
- `cachedSnapshot(meta)` 以零 I/O 从存储域的内存表同步提供客户端值。它只接受身份匹配的记录以及版本和 schema 均匹配的 key,再按所服务行的最低水位返回 `{ asOfSeq, values }` 切面。对于未知 id、无关生命周期、缺失或外来的记录文档,或没有可用行的情况,它返回 `undefined`。`coldSnapshot(meta, events)` 接受完整有序日志,在折叠时跳过已检查点化的前缀,并在自身不读取持久化层的情况下刷新记录。
61
+ `cachedSnapshot(meta, inheritedEventCount)` 以零 I/O 从存储域的内存表同步提供客户端值。它只接受身份匹配的记录以及版本和 schema 均匹配的 key,再按所服务行的最低水位返回 `{ asOfSeq, values }` 切面。未 seeded 的列表知道切点为零;仅 header 的 seeded 列表不知道数字切点,必须跳过该快速路径,直到权威正文读取提供它。`coldSnapshot(meta, inheritedEventCount, events)` 接受精确切点与完整有序日志,在折叠时跳过已检查点化的前缀,并在自身不读取持久化层的情况下刷新记录。
62
62
 
63
63
  ### 缓存保证什么
64
64
 
65
- 日志领先,缓存跟随:实时检查点先把会话的缓冲事件持久化,然后才保存缓存记录。因此崩溃可能让缓存落后于日志,但绝不会让缓存领先。读取和写入共享存储域内一致的内存状态;逐单元写入链只在持久化成功后修改内存。每个带版本戳的记录必须匹配实时单元 schema 与会话 header 身份(`createdAt`、`cwd`),因此畸形、陈旧或无关的记录都会读作不存在。JSON 后端把每条记录存于仅所有者可访问的 `<root>/session_projcache/sessions/<id>.json` 目录树中。
65
+ 日志领先,缓存跟随:实时检查点先把会话的缓冲事件持久化,然后才保存缓存记录。因此崩溃可能让缓存落后于日志,但绝不会让缓存领先。读取和写入共享存储域内一致的内存状态;逐单元写入链只在持久化成功后修改内存。每个带版本戳的记录必须匹配实时单元 schema 与完整生命周期身份(`createdAt`、`cwd`、`isSeeded` 和 `inheritedEventCount`),因此在一个 fork 切点下初始化的行不能播种另一个切点。JSON 后端把每条记录存于仅所有者可访问的 `<root>/session_projcache/sessions/<id>.json` 目录树中。
66
+
67
+ 升级绝不拖垮启动或列表:版本戳落在 spec `compatibleVersions` 集合内的记录保持可读(缺失的 lineage 字段解码为 unseeded lineage——对非 fork 会话精确无误,seeded 调用方则通不过身份比对、回落冷折叠),而仍然通不过 schema 校验的存量记录会按域的 `invalidRecords: 'backup-and-skip'` 策略移出为 `<id>.json.bak.<时间戳>`、连同原因写入日志,并由下一次检查点重建。
66
68
 
67
69
  -----
68
70
 
@@ -88,7 +90,7 @@ kind: "package-reference"
88
90
  |---|---|
89
91
  | [`src/index.ts`](src/index.ts) | 插件入口:`SessionProjectionCache` 服务、写后监听器、缓存读取 |
90
92
  | [`src/spec.ts`](src/spec.ts) | `session_projcache` 域 spec 与记录身份类型 |
91
- | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;正确性在写入与读取路径强制) |
93
+ | | 不发布运行时不变式伴生入口;正确性在写入与读取路径强制。 |
92
94
 
93
95
  </details>
94
96
 
@@ -126,6 +128,7 @@ kind: "package-reference"
126
128
  - **无淘汰或保留接口**——记录按会话持续累积;清理已存储检查点属于带外维护,与会话持久化采用相同策略。
127
129
  - **间隔节流采用按会话的粗粒度控制**——一次无脏数据的写入完成后,计时器在首个脏事件到达时启动;持续但低于条数阈值的事件流每间隔写入一次,而非滑动窗口。
128
130
  - **缓存侧不做冷重折叠**——缓存只服务并刷新自己的记录,从不读取会话日志,因为它不依赖持久化层;需要保证冷快照的消费方自行从日志重新折叠。
131
+ - **每次 schema 或域版本变更都必须论证升级路径**——改动存储记录 schema 或域版本时,同一 PR 必须在 `tests/fixtures/` 下归档此前已发布的磁盘格式样本,并在 `tests/fixtures.spec.ts` 中用测试论证所选的处置方式:读兼容恢复(`compatibleVersions`)、当前版本重写,或 backup-and-skip 抢救。即便选择直接丢弃旧记录的 bump,也要证明丢弃既不炸启动、也不污染缓存树。
129
132
 
130
133
  <a id="dev-note"></a>
131
134
  ### 开发备注
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Service } from "@deepseek-ai/cordis";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
4
+ import { SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
4
5
  import { z as z$1 } from "zod";
5
6
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
6
7
  //#region lib/types/spec.js
@@ -25,7 +26,7 @@ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
25
26
  */
26
27
  const checkpointRow = z$1.object({
27
28
  ver: z$1.number().int().nonnegative(),
28
- seq: z$1.number().int().gte(-1),
29
+ seq: z$1.number().int().gte(-1).transform((value) => value === -1 ? -1 : SessionSeq(value)),
29
30
  val: z$1.json()
30
31
  });
31
32
  /**
@@ -36,10 +37,19 @@ const checkpointRow = z$1.object({
36
37
  * old record pass every watermark check and seed state folded from an
37
38
  * unrelated log. Reads validate this against the live header (listing) or
38
39
  * the stored header (cold read) before accepting any record.
40
+ *
41
+ * The lineage fields are optional because records admitted through
42
+ * `compatibleVersions` predate them. The reader (`identityMatches`)
43
+ * interprets their absence as the unseeded lineage — exact for an unseeded
44
+ * session, while a seeded expectation fails the match and the record is
45
+ * discarded to a cold rebuild. Current-version writes always store both
46
+ * fields.
39
47
  */
40
48
  const checkpointIdentity = z$1.object({
41
49
  createdAt: z$1.number().int().nonnegative(),
42
- cwd: z$1.string().optional()
50
+ cwd: z$1.string().optional(),
51
+ isSeeded: z$1.boolean().optional(),
52
+ inheritedEventCount: z$1.number().int().nonnegative().transform(SessionLogOffset).optional()
43
53
  });
44
54
  /**
45
55
  * One session's stored record: the log identity it was folded from plus its
@@ -56,11 +66,27 @@ const checkpointRecord = z$1.object({
56
66
  * bumps per session: after a bump, a stale session document is discarded on
57
67
  * open (cache semantics — a stale or unreadable cache costs a longer tail
58
68
  * replay, never a wrong value) while the rest of the domain stays usable,
59
- * instead of rejecting the whole medium.
69
+ * instead of rejecting the whole medium. The `compatibleVersions` entries
70
+ * are declared because those records differ from the current version only
71
+ * by the absent optional lineage fields, so upgraded homes keep serving
72
+ * their cached listing projections instead of dropping every title until
73
+ * each session is reopened; the per-record version map lives in the
74
+ * read-compat Agent Note
75
+ * (.agents/notes/implemented/architecture/2026-09-02-projcache-cross-version-read-compat.md).
76
+ * The per-row `ver` guard and the identity match still discard anything the
77
+ * current fold semantics cannot vouch for.
78
+ *
79
+ * `invalidRecords: 'backup-and-skip'`: a stored record that fails the schema
80
+ * anyway is disposable derived data, so it must never cost the boot — the
81
+ * domain layer moves the document aside as `<key>.json.bak.<stamp>`, logs
82
+ * the concrete validation failure, and serves the session as uncached (a
83
+ * cold read rebuilds and rewrites it).
60
84
  */
61
85
  const projectionCacheDomainSpec = defineDomain({
62
86
  name: "session_projcache",
63
- version: 4,
87
+ version: 5,
88
+ compatibleVersions: [3, 4],
89
+ invalidRecords: "backup-and-skip",
64
90
  layout: "per-record",
65
91
  tables: { sessions: domainTable(checkpointRecord) }
66
92
  });
@@ -143,18 +169,27 @@ var SessionProjectionCache = class extends Service {
143
169
  * paths (the history tail baseline) supersede these values whenever a
144
170
  * session is actually opened.
145
171
  * @param meta - the listed session's header (identity witness; no log read).
172
+ * @param inheritedEventCount - exact inherited prefix length that completes
173
+ * the checkpoint identity.
146
174
  * @param keys - optional projection keys required by the caller's audience.
147
175
  * @returns the cut (`asOfSeq` = lowest served-row watermark), or
148
176
  * `undefined` when no usable row exists for this lifecycle.
149
177
  */
150
- cachedSnapshot(meta, keys) {
151
- const record = this.recordFor(meta.id, identityOf(meta));
178
+ cachedSnapshot(meta, inheritedEventCount, keys) {
179
+ const record = this.recordFor(meta.id, identityOf(meta, inheritedEventCount));
152
180
  if (record === void 0) return void 0;
153
181
  const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys);
154
182
  const servedKeys = Object.keys(values);
155
183
  if (servedKeys.length === 0) return void 0;
184
+ let asOfSeq;
185
+ for (const key of servedKeys) {
186
+ const row = record.rows[key];
187
+ if (row !== void 0 && (asOfSeq === void 0 || row.seq < asOfSeq)) asOfSeq = row.seq;
188
+ }
189
+ /* v8 ignore next -- A nonempty checkpoint view contains a stored row for every returned key. */
190
+ if (asOfSeq === void 0) return void 0;
156
191
  return {
157
- asOfSeq: Math.min(...servedKeys.map((key) => record.rows[key].seq)),
192
+ asOfSeq,
158
193
  values
159
194
  };
160
195
  }
@@ -164,17 +199,16 @@ var SessionProjectionCache = class extends Service {
164
199
  * advances every unit to the observation cut. No checkpoint is written
165
200
  * because the logical observation may contain recovery events not yet durable.
166
201
  * @param session - exact unpublished Session retained by persistence.
167
- * @param meta - observed lifecycle header.
168
202
  * @param events - exact logical event prefix represented by the observation.
169
203
  * @returns all projection values at the event cut.
170
204
  */
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);
205
+ hydratePrepared(session, events) {
206
+ const record = this.recordFor(session.id, identityOf(session.header, session.inheritedEventCount));
207
+ if (record === void 0) return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
174
208
  try {
175
- return this.ctx.sessionProjections.hydrate(session, record.rows, events, 0);
209
+ return this.ctx.sessionProjections.hydrate(session, record.rows, events, SessionLogOffset(0));
176
210
  } catch {
177
- return this.ctx.sessionProjections.hydrate(session, {}, events, 0);
211
+ return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
178
212
  }
179
213
  }
180
214
  /**
@@ -190,7 +224,7 @@ var SessionProjectionCache = class extends Service {
190
224
  const rows = this.ctx.sessionProjections.checkpoint(session);
191
225
  this.markClean(session);
192
226
  if (this.ctx.sessions.get(session.id) === session) await this.ctx.sessions.flush(session);
193
- await this.put(session.id, identityOf(session.header), rows);
227
+ await this.put(session.id, identityOf(session.header, session.inheritedEventCount), rows);
194
228
  }
195
229
  /**
196
230
  * Cold-read one session's projections from its complete log. Each unit is
@@ -201,12 +235,14 @@ var SessionProjectionCache = class extends Service {
201
235
  * The caller supplies the complete log in seq order: this service never
202
236
  * consults the persistence layer.
203
237
  * @param meta - the stored session header (identity witness).
238
+ * @param inheritedEventCount - exact inherited prefix length for projection initialization and identity.
204
239
  * @param events - the session's complete log, in seq order.
205
240
  * @returns the projection cut at the log end.
206
241
  */
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) => {
242
+ coldSnapshot(meta, inheritedEventCount, events) {
243
+ const identity = identityOf(meta, inheritedEventCount);
244
+ const restored = this.ctx.sessionProjections.restore(this.recordFor(meta.id, identity)?.rows ?? {}, events, SessionLogOffset(0), meta, inheritedEventCount);
245
+ this.put(meta.id, identity, restored.checkpoint).catch((error) => {
210
246
  this.ctx.logger.warn(`session projection cache: cold-read write-back for "${meta.id}" failed (cache stays stale): ${String(error)}`);
211
247
  });
212
248
  return restored.snapshot;
@@ -282,15 +318,25 @@ var SessionProjectionCache = class extends Service {
282
318
  }
283
319
  };
284
320
  /** Project a header onto the identity fields a record is bound to. */
285
- function identityOf(header) {
321
+ function identityOf(header, inheritedEventCount) {
322
+ const cut = SessionLogOffset(inheritedEventCount);
323
+ if (!header.isSeeded && cut !== 0) throw new Error("unseeded projection-cache identity inherited event count must be 0");
286
324
  return {
287
325
  createdAt: header.createdAt,
288
- ...header.cwd === void 0 ? {} : { cwd: header.cwd }
326
+ ...header.cwd === void 0 ? {} : { cwd: header.cwd },
327
+ isSeeded: header.isSeeded,
328
+ inheritedEventCount: cut
289
329
  };
290
330
  }
291
- /** Whether a stored record's bound identity names the caller's lifecycle. */
331
+ /**
332
+ * Whether a stored record's bound identity names the caller's lifecycle.
333
+ * Absent lineage fields (records admitted via `compatibleVersions` predate
334
+ * them) read as the unseeded lineage: exact for an unseeded caller, and a
335
+ * seeded caller's expectation then fails the match, discarding the record to
336
+ * a cold rebuild.
337
+ */
292
338
  function identityMatches(stored, expected) {
293
- return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd;
339
+ return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded && (stored.inheritedEventCount ?? 0) === expected.inheritedEventCount;
294
340
  }
295
341
  //#endregion
296
342
  export { Config, SessionProjectionCache, SessionProjectionCache as default, checkpointIdentity, checkpointRecord, checkpointRow, projectionCacheDomainSpec };
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { Context, Service } from '@deepseek-ai/cordis';
19
19
  import z from '@deepseek-ai/schemastery';
20
+ import { SessionLogOffset } from '@deepseek-ai/dsh-session';
20
21
  import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session';
21
22
  import type { ProjectionSnapshot, SessionProjectionMap } from '@deepseek-ai/dsh-session-projection';
22
23
  export { checkpointIdentity, checkpointRecord, checkpointRow, projectionCacheDomainSpec } from './spec.ts';
@@ -80,22 +81,23 @@ export declare class SessionProjectionCache extends Service {
80
81
  * paths (the history tail baseline) supersede these values whenever a
81
82
  * session is actually opened.
82
83
  * @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.
83
86
  * @param keys - optional projection keys required by the caller's audience.
84
87
  * @returns the cut (`asOfSeq` = lowest served-row watermark), or
85
88
  * `undefined` when no usable row exists for this lifecycle.
86
89
  */
87
- cachedSnapshot(meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[]): ProjectionSnapshot | undefined;
90
+ cachedSnapshot(meta: SessionHeader, inheritedEventCount: SessionLogOffset, keys?: readonly Extract<keyof SessionProjectionMap, string>[]): ProjectionSnapshot | undefined;
88
91
  /**
89
92
  * Hydrate projection cells for an already-prepared Session without another
90
93
  * persistence read. The cache seeds matching rows; the supplied exact log
91
94
  * advances every unit to the observation cut. No checkpoint is written
92
95
  * because the logical observation may contain recovery events not yet durable.
93
96
  * @param session - exact unpublished Session retained by persistence.
94
- * @param meta - observed lifecycle header.
95
97
  * @param events - exact logical event prefix represented by the observation.
96
98
  * @returns all projection values at the event cut.
97
99
  */
98
- hydratePrepared(session: Session, meta: SessionHeader, events: readonly SessionEvent[]): ProjectionSnapshot;
100
+ hydratePrepared(session: Session, events: readonly SessionEvent[]): ProjectionSnapshot;
99
101
  /**
100
102
  * Durably checkpoint one live session NOW (all mandatory points call
101
103
  * this; tests and carriers may too). The registry cut is snapshotted at
@@ -115,10 +117,11 @@ export declare class SessionProjectionCache extends Service {
115
117
  * The caller supplies the complete log in seq order: this service never
116
118
  * consults the persistence layer.
117
119
  * @param meta - the stored session header (identity witness).
120
+ * @param inheritedEventCount - exact inherited prefix length for projection initialization and identity.
118
121
  * @param events - the session's complete log, in seq order.
119
122
  * @returns the projection cut at the log end.
120
123
  */
121
- coldSnapshot(meta: SessionHeader, events: readonly SessionEvent[]): ProjectionSnapshot;
124
+ coldSnapshot(meta: SessionHeader, inheritedEventCount: SessionLogOffset, events: readonly SessionEvent[]): ProjectionSnapshot;
122
125
  private installWritePath;
123
126
  /**
124
127
  * One fail-soft durable checkpoint. Every caller has work by construction:
@@ -10,6 +10,7 @@
10
10
  * @module @deepseek-ai/dsh-session-projection-cache/src/spec
11
11
  */
12
12
  import { z } from 'zod';
13
+ import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session';
13
14
  import type { SessionId } from '@deepseek-ai/dsh-session';
14
15
  /**
15
16
  * One persisted checkpoint row (the RFC's `(sessionId, key, ver, seq, val)`
@@ -21,7 +22,7 @@ import type { SessionId } from '@deepseek-ai/dsh-session';
21
22
  */
22
23
  export declare const checkpointRow: z.ZodObject<{
23
24
  ver: z.ZodNumber;
24
- seq: z.ZodNumber;
25
+ seq: z.ZodPipe<z.ZodNumber, z.ZodTransform<-1 | SessionSeq, number>>;
25
26
  val: z.ZodJSONSchema;
26
27
  }, z.core.$strip>;
27
28
  /**
@@ -32,10 +33,19 @@ export declare const checkpointRow: z.ZodObject<{
32
33
  * old record pass every watermark check and seed state folded from an
33
34
  * unrelated log. Reads validate this against the live header (listing) or
34
35
  * the stored header (cold read) before accepting any record.
36
+ *
37
+ * The lineage fields are optional because records admitted through
38
+ * `compatibleVersions` predate them. The reader (`identityMatches`)
39
+ * interprets their absence as the unseeded lineage — exact for an unseeded
40
+ * session, while a seeded expectation fails the match and the record is
41
+ * discarded to a cold rebuild. Current-version writes always store both
42
+ * fields.
35
43
  */
36
44
  export declare const checkpointIdentity: z.ZodObject<{
37
45
  createdAt: z.ZodNumber;
38
46
  cwd: z.ZodOptional<z.ZodString>;
47
+ isSeeded: z.ZodOptional<z.ZodBoolean>;
48
+ inheritedEventCount: z.ZodOptional<z.ZodPipe<z.ZodNumber, z.ZodTransform<SessionLogOffset, number>>>;
39
49
  }, z.core.$strip>;
40
50
  /** The identity fields a record is bound to, inferred from {@link checkpointIdentity}. */
41
51
  export type CheckpointIdentity = z.infer<typeof checkpointIdentity>;
@@ -49,10 +59,12 @@ export declare const checkpointRecord: z.ZodObject<{
49
59
  identity: z.ZodObject<{
50
60
  createdAt: z.ZodNumber;
51
61
  cwd: z.ZodOptional<z.ZodString>;
62
+ isSeeded: z.ZodOptional<z.ZodBoolean>;
63
+ inheritedEventCount: z.ZodOptional<z.ZodPipe<z.ZodNumber, z.ZodTransform<SessionLogOffset, number>>>;
52
64
  }, z.core.$strip>;
53
65
  rows: z.ZodRecord<z.ZodString, z.ZodObject<{
54
66
  ver: z.ZodNumber;
55
- seq: z.ZodNumber;
67
+ seq: z.ZodPipe<z.ZodNumber, z.ZodTransform<-1 | SessionSeq, number>>;
56
68
  val: z.ZodJSONSchema;
57
69
  }, z.core.$strip>>;
58
70
  }, z.core.$strip>;
@@ -63,21 +75,39 @@ export type CheckpointRecord = z.infer<typeof checkpointRecord>;
63
75
  * bumps per session: after a bump, a stale session document is discarded on
64
76
  * open (cache semantics — a stale or unreadable cache costs a longer tail
65
77
  * replay, never a wrong value) while the rest of the domain stays usable,
66
- * instead of rejecting the whole medium.
78
+ * instead of rejecting the whole medium. The `compatibleVersions` entries
79
+ * are declared because those records differ from the current version only
80
+ * by the absent optional lineage fields, so upgraded homes keep serving
81
+ * their cached listing projections instead of dropping every title until
82
+ * each session is reopened; the per-record version map lives in the
83
+ * read-compat Agent Note
84
+ * (.agents/notes/implemented/architecture/2026-09-02-projcache-cross-version-read-compat.md).
85
+ * The per-row `ver` guard and the identity match still discard anything the
86
+ * current fold semantics cannot vouch for.
87
+ *
88
+ * `invalidRecords: 'backup-and-skip'`: a stored record that fails the schema
89
+ * anyway is disposable derived data, so it must never cost the boot — the
90
+ * domain layer moves the document aside as `<key>.json.bak.<stamp>`, logs
91
+ * the concrete validation failure, and serves the session as uncached (a
92
+ * cold read rebuilds and rewrites it).
67
93
  */
68
94
  export declare const projectionCacheDomainSpec: {
69
95
  name: string;
70
96
  version: number;
97
+ compatibleVersions: number[];
98
+ invalidRecords: "backup-and-skip";
71
99
  layout: "per-record";
72
100
  tables: {
73
101
  sessions: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<SessionId, {
74
102
  identity: {
75
103
  createdAt: number;
76
104
  cwd?: string | undefined;
105
+ isSeeded?: boolean | undefined;
106
+ inheritedEventCount?: SessionLogOffset | undefined;
77
107
  };
78
108
  rows: Record<string, {
79
109
  ver: number;
80
- seq: number;
110
+ seq: -1 | SessionSeq;
81
111
  val: z.core.util.JSONType;
82
112
  }>;
83
113
  }>;
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.2-alpha.3",
4
+ "version": "0.1.2-alpha.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -18,38 +18,31 @@
18
18
  "types": "./lib/types/index.d.ts",
19
19
  "default": "./lib/index.js"
20
20
  },
21
- "./invariant": {
22
- "types": "./lib/types/invariant.d.ts",
23
- "default": "./lib/invariant.js"
24
- },
25
21
  "./src/*": "./src/*",
26
22
  "./package.json": "./package.json"
27
23
  },
28
24
  "files": [
29
25
  "lib/index.js",
30
- "lib/invariant.js",
31
26
  "lib/types/**/*.d.ts"
32
27
  ],
33
28
  "license": "MIT",
34
29
  "dependencies": {
35
30
  "zod": "^4.4.3",
36
- "@deepseek-ai/schemastery": "^3.18.2",
37
- "@deepseek-ai/dsh-util-values": "^0.1.2-alpha.3"
31
+ "@deepseek-ai/dsh-util-values": "^0.1.2-alpha.5",
32
+ "@deepseek-ai/schemastery": "^3.18.2"
38
33
  },
39
34
  "peerDependencies": {
40
35
  "@deepseek-ai/cordis": "^4.0.2",
41
- "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
42
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.3",
43
- "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.3",
44
- "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.3"
36
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
37
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.5",
38
+ "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.5"
45
39
  },
46
40
  "devDependencies": {
47
41
  "@deepseek-ai/cordis": "^4.0.2",
48
- "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
49
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.3",
50
- "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.3",
51
- "@deepseek-ai/dsh-storage": "^0.1.2-alpha.3",
52
- "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.3",
53
- "@deepseek-ai/dsh-storage-json": "^0.1.2-alpha.3"
42
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
43
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.5",
44
+ "@deepseek-ai/dsh-storage": "^0.1.2-alpha.5",
45
+ "@deepseek-ai/dsh-storage-json": "^0.1.2-alpha.5",
46
+ "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.5"
54
47
  }
55
48
  }
package/lib/invariant.js DELETED
@@ -1,28 +0,0 @@
1
- //#region lib/types/invariant.js
2
- /**
3
- * Package-owned invariant companion for `@deepseek-ai/dsh-session-projection-cache`.
4
- * @module @deepseek-ai/dsh-session-projection-cache/invariant
5
- */
6
- const PACKAGE_NAME = "@deepseek-ai/dsh-session-projection-cache";
7
- /** Cordis companion plugin name. */
8
- const name = "session-projection-cache-invariant";
9
- /** Service required before the companion can reserve package ownership. */
10
- const inject = ["invariants"];
11
- /**
12
- * No runtime invariant: the cache's correctness relation (a stored row equals
13
- * the registry fold at its `seq` watermark) is only checkable by re-running the
14
- * fold over the persisted log — duplicating the implementation rather than
15
- * detecting drift — and its staleness is by design (fail-soft writes). The
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
- * by the package spec.
19
- */
20
- const install = () => {};
21
- /**
22
- * Register this package's invariant companion.
23
- * @param ctx - Cordis context carrying the invariant service.
24
- * @returns the installed registration's disposer after setup succeeds.
25
- */
26
- const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
27
- //#endregion
28
- export { apply, inject, name };
@@ -1,16 +0,0 @@
1
- /**
2
- * Package-owned invariant companion for `@deepseek-ai/dsh-session-projection-cache`.
3
- * @module @deepseek-ai/dsh-session-projection-cache/invariant
4
- */
5
- import type { Context } from '@deepseek-ai/cordis';
6
- /** Cordis companion plugin name. */
7
- export declare const name = "session-projection-cache-invariant";
8
- /** Service required before the companion can reserve package ownership. */
9
- export declare const inject: string[];
10
- /**
11
- * Register this package's invariant companion.
12
- * @param ctx - Cordis context carrying the invariant service.
13
- * @returns the installed registration's disposer after setup succeeds.
14
- */
15
- export declare const apply: (ctx: Context) => Promise<() => void>;
16
- //# sourceMappingURL=invariant.d.ts.map