Просмотр исходного кода

refactor(session-query): return all projection states by default

Dudu-0223 3 недель назад
Родитель
Сommit
3dcabbba44

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md
-2026-09-01-parent-owned-subagent-catalog.md: fced05dab48d281a2a89a60da03163f15e2e7b74
-2026-09-01-parent-owned-subagent-catalog.zh.md: 527583813ca0fa8cf06fd721f68ce5d2c8fa81eb
+2026-09-01-parent-owned-subagent-catalog.md: f424943c523c1143884116fd5aeccdf6e20423f2
+2026-09-01-parent-owned-subagent-catalog.zh.md: b4c524ab81e4c2df9a666d0119e1de9c8b771e8b

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md

@@ -38,6 +38,6 @@ Snapshot normalizers zero `childCreatedAt` because it originates from the proces
 
 ## Consequences
 
-A caller can request `projectionStateKeys: ['subagentCatalog']` from `observeSession`. Live observations clone the maintained registry state; cold observations hydrate their prepared Session and detach the same state at the observation cursor. A host-state-only read passes `projectionMode: 'none'` through checkpoint hydration, including cache reuse and malformed-row recovery; unrelated wire views are neither computed nor validated. Hydration accepts only `all` or `none` because observations do not request individual client view keys. Direct-child and descendant listing still use the Session corpus and child identity projection.
+Default Session observations expose every registered host state in `projectionStates` alongside client views. The registry’s existing `checkpoint` operation supplies detached state values; no key-selection parameter or additional hydration mode is required. `projectionMode: 'none'` retains its projection-free behavior. Direct-child and descendant listing still use the Session corpus and child identity projection.
 
 Backends that do not know the required event refuse the log under the existing Session event mechanism. Pre-release format policy requires no fallback scan for old logs.

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md

@@ -38,6 +38,6 @@ snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clo
 
 ## 后果
 
-调用方可以向 `observeSession` 请求 `projectionStateKeys: ['subagentCatalog']`。实时观察克隆 registry 维护中的 state;冷观察 hydrate 已准备的 Session,并在观察 cursor 处分离出同一 state。仅读取 host state 时,`projectionMode: 'none'` 会传递到检查点 hydration,包括缓存复用和损坏 row 的恢复;无关 wire view 不会被计算或校验。Hydration 只接受 `all` 或 `none`,因为观察不会请求单独的客户端视图 key。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。
+默认 Session 观察在返回客户端视图的同时,通过 `projectionStates` 暴露全部已注册 host state。注册表现有的 `checkpoint` 操作提供独立状态值,无需 key 选择参数或额外 hydration 模式。`projectionMode: 'none'` 保留不执行投影的行为。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。
 
 不认识该 required event 的 backend 会按既有 Session event 机制拒绝日志。pre-release format policy 不要求为旧日志保留 fallback scan。

+ 2 - 2
docs/subsystems/session-projection.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md
-session-projection.md: 7552885c947b268831a9d3109244fea738e9ef2b
-session-projection.zh.md: eeb3bef9b52b5b8ab88e48a3bff61ccaacebfaaf
+session-projection.md: 8568b6df94b1427a341568777c64c82d74176a17
+session-projection.zh.md: 5e50341a6443efde2a3b49b7f5185bc44896efc6

+ 5 - 8
docs/subsystems/session-projection.md

@@ -163,10 +163,9 @@ cachedPredecessorTitle( meta: SessionHeader, inheritedEventCount: SessionLogOffs
  * because the logical observation may contain recovery events not yet durable.
  * @param session - exact unpublished Session retained by persistence.
  * @param events - exact logical event prefix represented by the observation.
- * @param projectionMode - `none` hydrates every state without computing or validating client views; defaults to `all`.
- * @returns the event cut with all client views, or empty values in `none` mode.
+ * @returns all projection values at the event cut.
  */
-hydratePrepared( session: Session, events: readonly SessionEvent[], projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot
+hydratePrepared( session: Session, events: readonly SessionEvent[], ): ProjectionSnapshot
 
 /**
  * Durably checkpoint one live session NOW (all mandatory points call
@@ -330,12 +329,11 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: readonly Extract<keyof
  * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty).
  * @param header - immutable metadata for the Session being restored.
  * @param inheritedEventCount - exact fork-inherited prefix length supplied to unit initialization.
- * @param projectionMode - `none` restores every state without computing or validating client views; defaults to `all`.
  * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
  *   supplied event's seq, `baseSeq - 1` for an empty tail) plus the
  *   refreshed checkpoint rows at that cut, ready for a durable write-back.
  */
-restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
+restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
 
 /**
  * Restore an exact cut and install its states on the supplied prepared Session.
@@ -345,10 +343,9 @@ restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], base
  * @param checkpoint - persisted rows for this Session lifecycle.
  * @param events - exact events at the observation cut.
  * @param baseSeq - first supplied event sequence.
- * @param projectionMode - `none` installs every state without computing or validating client views; defaults to `all`.
- * @returns the supplied cut with all client views, or empty values in `none` mode.
+ * @returns all projection values at the supplied cut.
  */
-hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot
+hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, ): ProjectionSnapshot
 ```
 
 Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md) · [SessionLogOffset](session.md)

+ 5 - 8
docs/subsystems/session-projection.zh.md

@@ -163,10 +163,9 @@ cachedPredecessorTitle( meta: SessionHeader, inheritedEventCount: SessionLogOffs
  * because the logical observation may contain recovery events not yet durable.
  * @param session - exact unpublished Session retained by persistence.
  * @param events - exact logical event prefix represented by the observation.
- * @param projectionMode - `none` hydrates every state without computing or validating client views; defaults to `all`.
- * @returns the event cut with all client views, or empty values in `none` mode.
+ * @returns all projection values at the event cut.
  */
-hydratePrepared( session: Session, events: readonly SessionEvent[], projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot
+hydratePrepared( session: Session, events: readonly SessionEvent[], ): ProjectionSnapshot
 
 /**
  * Durably checkpoint one live session NOW (all mandatory points call
@@ -330,12 +329,11 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: readonly Extract<keyof
  * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty).
  * @param header - immutable metadata for the Session being restored.
  * @param inheritedEventCount - exact fork-inherited prefix length supplied to unit initialization.
- * @param projectionMode - `none` restores every state without computing or validating client views; defaults to `all`.
  * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
  *   supplied event's seq, `baseSeq - 1` for an empty tail) plus the
  *   refreshed checkpoint rows at that cut, ready for a durable write-back.
  */
-restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
+restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
 
 /**
  * Restore an exact cut and install its states on the supplied prepared Session.
@@ -345,10 +343,9 @@ restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], base
  * @param checkpoint - persisted rows for this Session lifecycle.
  * @param events - exact events at the observation cut.
  * @param baseSeq - first supplied event sequence.
- * @param projectionMode - `none` installs every state without computing or validating client views; defaults to `all`.
- * @returns the supplied cut with all client views, or empty values in `none` mode.
+ * @returns all projection values at the supplied cut.
  */
-hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot
+hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, ): ProjectionSnapshot
 ```
 
 Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md) · [SessionLogOffset](session.zh.md)

+ 9 - 9
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -1627,10 +1627,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         returns: 'a title-only checkpoint view with `asOfSeq: -1`, or `undefined` when the record is current, newer, unrelated, missing, or incompatible with the title unit. The sentinel avoids reusing a sequence that a cardinality-changing Session migration may have remapped.',
       },
       {
-        signature: 'hydratePrepared( session: Session, events: readonly SessionEvent[], projectionMode: \'all\' | \'none\' = \'all\', ): ProjectionSnapshot',
+        signature: 'hydratePrepared( session: Session, events: readonly SessionEvent[], ): ProjectionSnapshot',
         description: 'Hydrate projection cells for an already-prepared Session without another persistence read. The cache seeds matching rows; the supplied exact log advances every unit to the observation cut. No checkpoint is written because the logical observation may contain recovery events not yet durable.',
-        parameters: [{ name: 'session', description: 'exact unpublished Session retained by persistence.' }, { name: 'events', description: 'exact logical event prefix represented by the observation.' }, { name: 'projectionMode', description: '`none` hydrates every state without computing or validating client views; defaults to `all`.' }],
-        returns: 'the event cut with all client views, or empty values in `none` mode.',
+        parameters: [{ name: 'session', description: 'exact unpublished Session retained by persistence.' }, { name: 'events', description: 'exact logical event prefix represented by the observation.' }],
+        returns: 'all projection values at the event cut.',
       },
       {
         signature: 'async write(session: Session): Promise<void>',
@@ -1706,16 +1706,16 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         returns: 'whole values per key with a usable row; empty when none.',
       },
       {
-        signature: 'restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, projectionMode: \'all\' | \'none\' = \'all\', ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }',
+        signature: 'restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }',
         description: 'Cold read: fold every persisted unit over a stored log suffix, seeding each from its checkpoint row when usable — the one read recipe (cached state + forward tail replay + `view`) applied without a live `Session`. Call with the stored events at or past `restoreFloor(checkpoint)` (a `SessionHandle.read` slice) and that same floor as `baseSeq`; the floor\'s one-below anchor makes the supplied end honest, so a shrunk log is detected here. A row is usable iff its `ver` matches the live unit\'s `stateVersion`, it does not predate `baseSeq` (`seq >= baseSeq - 1`), and it does not claim events past the supplied end (`seq <= endSeq`); an unusable row is discarded and its key refolds from `init` — which is only sound over the full log, so a discarded row with `baseSeq > 0` throws (the caller re-reads from seq 0, e.g. after a crash-repair truncation shrank the log below a row\'s watermark).',
-        parameters: [{ name: 'checkpoint', description: 'persisted rows for one session (possibly stale or empty).' }, { name: 'events', description: 'the stored events with `seq >= baseSeq`, in seq order.' }, { name: 'baseSeq', description: 'the seq `events` starts at (its first event\'s seq when non-empty).' }, { name: 'header', description: 'immutable metadata for the Session being restored.' }, { name: 'inheritedEventCount', description: 'exact fork-inherited prefix length supplied to unit initialization.' }, { name: 'projectionMode', description: '`none` restores every state without computing or validating client views; defaults to `all`.' }],
+        parameters: [{ name: 'checkpoint', description: 'persisted rows for one session (possibly stale or empty).' }, { name: 'events', description: 'the stored events with `seq >= baseSeq`, in seq order.' }, { name: 'baseSeq', description: 'the seq `events` starts at (its first event\'s seq when non-empty).' }, { name: 'header', description: 'immutable metadata for the Session being restored.' }, { name: 'inheritedEventCount', description: 'exact fork-inherited prefix length supplied to unit initialization.' }],
         returns: 'the snapshot cut at the supplied log end (`asOfSeq` is the last supplied event\'s seq, `baseSeq - 1` for an empty tail) plus the refreshed checkpoint rows at that cut, ready for a durable write-back.',
       },
       {
-        signature: 'hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, projectionMode: \'all\' | \'none\' = \'all\', ): ProjectionSnapshot',
+        signature: 'hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, ): ProjectionSnapshot',
         description: 'Restore an exact cut and install its states on the supplied prepared Session. A later publication reuses these cells; ordinary live reads and event drive advance any constructor-owned suffix exactly once.',
-        parameters: [{ name: 'session', description: 'exact prepared Session that owns the restored log prefix.' }, { name: 'checkpoint', description: 'persisted rows for this Session lifecycle.' }, { name: 'events', description: 'exact events at the observation cut.' }, { name: 'baseSeq', description: 'first supplied event sequence.' }, { name: 'projectionMode', description: '`none` installs every state without computing or validating client views; defaults to `all`.' }],
-        returns: 'the supplied cut with all client views, or empty values in `none` mode.',
+        parameters: [{ name: 'session', description: 'exact prepared Session that owns the restored log prefix.' }, { name: 'checkpoint', description: 'persisted rows for this Session lifecycle.' }, { name: 'events', description: 'exact events at the observation cut.' }, { name: 'baseSeq', description: 'first supplied event sequence.' }],
+        returns: 'all projection values at the supplied cut.',
       },
     ],
   },
@@ -5217,7 +5217,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'SessionObservationOptions',
-    declaration: 'export interface SessionObservationOptions {\n    readonly signal?: AbortSignal;\n    readonly projectionMode?: \'all\' | \'none\';\n    readonly projectionStateKeys?: readonly Extract<keyof SessionProjectionStateMap, string>[];\n}',
+    declaration: 'export interface SessionObservationOptions {\n    readonly signal?: AbortSignal;\n    readonly projectionMode?: \'all\' | \'none\';\n}',
   },
   {
     name: 'SessionOpenWorkspacePathRequest',

+ 2 - 2
packages/session-query/session-query/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/session-query/session-query/README.md
-README.md: b754f342e4614137b92aa749c3b6822a23e2e4bf
-README.zh.md: 9c41cc0ce97a99d8b64c206af26799527e97d102
+README.md: 9b7f7b09e91dea8dc6c853eb342c5ca434614f99
+README.zh.md: 7f098a082393cd78fb477b2cf9c07eb74d831d1b

+ 1 - 1
packages/session-query/session-query/README.md

@@ -108,7 +108,7 @@ The decision history lives in the [unified service decision](../../../.agents/no
 
 ### Observation cache
 
-`observeSession` builds point observations without a listing preflight. A live observation fixes its cut as the current log length and materializes `events` on first read, so header-, cursor-, or projection-only consumers never copy the log; the log only appends, so a late first read still yields exactly that prefix. The cold path stats the stored session first and consults an own bounded cache keyed by the persistence instance and the `stat` revision: an unchanged revision reuses the restored unpublished Session without re-reading the log; a changed revision, or a replaced persistence instance, reloads through the handle seam and replaces the entry. The cache holds `preparedSessionCacheSize` entries with least-recently-used eviction, entries pinned by active observation leases are never evicted, and a session that goes live mid-read retries the live path. `projectionStateKeys` detaches named host projection states at the same cut independently of `projectionMode`: a live observation clones the registry's maintained cells, and a cold observation hydrates its prepared Session's cells once so states and any views share one cut; unregistered keys are omitted and an absent registry leaves `projectionStates` undefined. With `projectionMode: 'none'`, both live and cold observations omit client view computation and validation even when host states are requested.
+`observeSession` builds point observations without a listing preflight. A live observation fixes its cut as the current log length and materializes `events` on first read, so header-, cursor-, or projection-only consumers never copy the log; the log only appends, so a late first read still yields exactly that prefix. The cold path stats the stored session first and consults an own bounded cache keyed by the persistence instance and the `stat` revision: an unchanged revision reuses the restored unpublished Session without re-reading the log; a changed revision, or a replaced persistence instance, reloads through the handle seam and replaces the entry. The cache holds `preparedSessionCacheSize` entries with least-recently-used eviction, entries pinned by active observation leases are never evicted, and a session that goes live mid-read retries the live path. Default observations return every registered host state in `projectionStates` alongside client views. State values come from the registry’s detached checkpoint rows at the observation cursor. `projectionMode: 'none'` omits both views and states and performs no projection hydration or checkpointing.
 
 ### Reads and traces
 

+ 1 - 1
packages/session-query/session-query/README.zh.md

@@ -108,7 +108,7 @@ kind: "package-reference"
 
 ### 观察缓存
 
-`observeSession` 不经过列表预检直接构建定点观察。实时观察以当前日志长度固定 cut,并在首次读取时才物化 `events`,因此只需要 header、cursor 或 projection 的消费者永远不会复制日志;日志只会追加,所以延后的首次读取得到的仍然正好是该前缀。冷路径先对存储会话执行 `stat`,再查询自有的有界缓存,缓存键为持久化实例加 `stat` 修订:修订未变则复用已恢复的未发布 Session,不再重读日志;修订变化或持久化实例被替换则经 handle 缝重新加载并替换条目。缓存保留 `preparedSessionCacheSize` 个条目并按最久未用淘汰,被活跃观察租约钉住的条目从不被淘汰;读取中途转为实时的会话会重试实时路径。 `projectionStateKeys` 在同一 cut 上分离出指定的 host projection state,与 `projectionMode` 无关:实时观察克隆 registry 维护中的 cell,冷观察则一次性 hydrate 其已准备 Session 的 cell,使 state 与任何 view 共享同一 cut;未注册的 key 被省略,registry 缺失时 `projectionStates` 为 undefined。使用 `projectionMode: 'none'` 时,即使请求了 host state,实时和冷观察也都不会计算或校验客户端视图。
+`observeSession` 不经过列表预检直接构建定点观察。实时观察以当前日志长度固定 cut,并在首次读取时才物化 `events`,因此只需要 header、cursor 或 projection 的消费者永远不会复制日志;日志只会追加,所以延后的首次读取得到的仍然正好是该前缀。冷路径先对存储会话执行 `stat`,再查询自有的有界缓存,缓存键为持久化实例加 `stat` 修订:修订未变则复用已恢复的未发布 Session,不再重读日志;修订变化或持久化实例被替换则经 handle 缝重新加载并替换条目。缓存保留 `preparedSessionCacheSize` 个条目并按最久未用淘汰,被活跃观察租约钉住的条目从不被淘汰;读取中途转为实时的会话会重试实时路径。 默认观察在返回客户端视图的同时,通过 `projectionStates` 返回全部已注册 host state。状态值来自注册表在观察 cursor 处生成的独立检查点行。`projectionMode: 'none'` 同时省略视图和状态,不执行投影 hydration 或检查点生成。
 
 ### 读取与追踪
 

+ 18 - 44
packages/session-query/session-query/src/observation.ts

@@ -35,8 +35,8 @@ export interface SessionObservation extends Disposable {
   /** Exact projection baseline at {@link cursor}, when the registry is mounted. */
   readonly projections?: ProjectionSnapshot
   /**
-   * Requested host projection states detached at {@link cursor}; present only
-   * when the registry is mounted, and contains only the registered keys.
+   * All registered host projection states detached at {@link cursor}; omitted
+   * when projections are disabled or the registry is absent.
    */
   readonly projectionStates?: Partial<SessionProjectionStateMap>
   /**
@@ -50,14 +50,8 @@ export interface SessionObservation extends Disposable {
 export interface SessionObservationOptions {
   /** Optional cancellation while resolving a cold source. */
   readonly signal?: AbortSignal
-  /** Whether to compute client views; requested host states are still materialized. */
+  /** Whether to return projection views and states or leave projections untouched. */
   readonly projectionMode?: 'all' | 'none'
-  /**
-   * Host projection states to detach from the same exact cut, independent of
-   * {@link projectionMode}. A cold observation hydrates its registry cells to
-   * serve them.
-   */
-  readonly projectionStateKeys?: readonly Extract<keyof SessionProjectionStateMap, string>[]
 }
 
 /**
@@ -103,32 +97,32 @@ export class SessionObservationReader {
   /**
    * Observe one live-preferred Session and retain a cold preparation until disposal.
    * @param sessionId - logical Session identity.
-   * @param options - cancellation, client views, and requested host states for this read.
+   * @param options - cancellation and projection views and states for this read.
    * @returns one exact immutable observation.
    */
   async read(
     sessionId: SessionId,
     options: SessionObservationOptions = {},
   ): Promise<SessionObservation> {
-    const { signal, projectionMode = 'all', projectionStateKeys } = options
+    const { signal, projectionMode = 'all' } = options
     for (;;) {
       throwIfObservationAborted(signal)
       const live = this.ctx.sessions.get(sessionId)
-      if (live !== undefined) return this.live(live, projectionMode, projectionStateKeys)
+      if (live !== undefined) return this.live(live, projectionMode)
       const persistence = this.ctx.get('sessionPersistence')
       if (persistence === undefined) throw notFound(sessionId)
 
       const snapshot = await this.statSource(persistence, sessionId, signal)
       const attachedDuringStat = this.ctx.sessions.get(sessionId)
       if (attachedDuringStat !== undefined) {
-        return this.live(attachedDuringStat, projectionMode, projectionStateKeys)
+        return this.live(attachedDuringStat, projectionMode)
       }
       let entry = this.cachedEntry(persistence, sessionId, snapshot.revision)
       if (entry === undefined) {
         const loaded = await this.loadSource(persistence, sessionId, signal)
         throwIfObservationAborted(signal)
         const attached = this.ctx.sessions.get(sessionId)
-        if (attached !== undefined) return this.live(attached, projectionMode, projectionStateKeys)
+        if (attached !== undefined) return this.live(attached, projectionMode)
         // The handle marks persisted events as adoptable; synthetic closers
         // are owned by this read, so the combined seed needs no copy.
         const seed = loaded.events
@@ -164,12 +158,9 @@ export class SessionObservationReader {
       let projections: ProjectionSnapshot | undefined
       let projectionStates: Partial<SessionProjectionStateMap> | undefined
       try {
-        if (projectionMode === 'all' || projectionStateKeys !== undefined) {
-          // Hydration installs every unit's cell on the prepared Session, so a
-          // requested host state reads the same cut the views were taken from.
-          const snapshot = this.preparedProjections(entry, projectionMode)
-          projections = projectionMode === 'all' ? snapshot : undefined
-          projectionStates = this.projectionStates(entry.session, projectionStateKeys)
+        if (projectionMode === 'all') {
+          projections = this.preparedProjections(entry)
+          projectionStates = this.projectionStates(entry.session)
         }
       } catch (error: unknown) {
         throw new SessionQueryError(
@@ -297,7 +288,6 @@ export class SessionObservationReader {
   private live(
     session: Session,
     projectionMode: NonNullable<SessionObservationOptions['projectionMode']>,
-    projectionStateKeys: SessionObservationOptions['projectionStateKeys'],
   ): SessionObservation {
     // The cut is the log length now. The log only appends, so the prefix
     // below `seq` is the same array whenever a consumer first reads `events`.
@@ -306,7 +296,7 @@ export class SessionObservationReader {
     const projections = projectionMode === 'none'
       ? undefined
       : this.ctx.get('sessionProjections')?.snapshot(session)
-    const projectionStates = this.projectionStates(session, projectionStateKeys)
+    const projectionStates = projectionMode === 'none' ? undefined : this.projectionStates(session)
     const lease = (): SessionObservation => {
       let disposed = false
       return {
@@ -330,36 +320,20 @@ export class SessionObservationReader {
     return lease()
   }
 
-  private preparedProjections(
-    entry: PreparedEntry,
-    projectionMode: NonNullable<SessionObservationOptions['projectionMode']>,
-  ): ProjectionSnapshot | undefined {
+  private preparedProjections(entry: PreparedEntry): ProjectionSnapshot | undefined {
     const registry = this.ctx.get('sessionProjections')
     if (registry === undefined) return undefined
     const cache = this.ctx.get('sessionProjectionCache')
     return cache === undefined
-      ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0), projectionMode)
-      : cache.hydratePrepared(entry.session, entry.events, projectionMode)
+      ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0))
+      : cache.hydratePrepared(entry.session, entry.events)
   }
 
-  /**
-   * Detach the requested host states from the registry cells already at this
-   * Session's cut. Registry cells are live references that later drives
-   * advance, so each state is cloned into the immutable observation.
-   */
-  private projectionStates(
-    session: Session,
-    keys: SessionObservationOptions['projectionStateKeys'],
-  ): Partial<SessionProjectionStateMap> | undefined {
-    if (keys === undefined) return undefined
+  /** Checkpoint rows already own detached state values at the Session cut. */
+  private projectionStates(session: Session): Partial<SessionProjectionStateMap> | undefined {
     const registry = this.ctx.get('sessionProjections')
     if (registry === undefined) return undefined
-    const states: Record<string, unknown> = {}
-    for (const key of keys) {
-      const state = registry.stateOf(session, key)
-      if (state !== undefined) states[key] = structuredClone(state)
-    }
-    return states
+    return Object.fromEntries(Object.entries(registry.checkpoint(session)).map(([key, row]) => [key, row.val]))
   }
 }
 

+ 48 - 42
packages/session-query/session-query/tests/observation.spec.ts

@@ -21,6 +21,12 @@ import { titleProjectionDefinition } from '@deepseek-ai/dsh-session-title'
 import { describe, expect, it, vi } from 'vitest'
 import { SessionObservationReader } from '../src/observation.ts'
 
+declare module '@deepseek-ai/dsh-session-projection/types' {
+  interface SessionProjectionStateMap {
+    'observation-test/titles': (string | null)[]
+  }
+}
+
 function header(id: string): SessionHeader {
   return { version: SESSION_FORMAT_VERSION, id: SessionId(id), createdAt: 1, isSeeded: false, cwd: '/workspace' }
 }
@@ -762,7 +768,7 @@ describe('SessionObservationReader cold projections', () => {
     await ctx.fiber.dispose()
   })
 
-  it.each(['all', 'none'] as const)('hydrates prepared projections through a mounted projection cache in %s mode', async (projectionMode) => {
+  it('hydrates prepared projections through a mounted projection cache', async () => {
     const ctx = await readerContext()
     await ctx.plugin(SessionProjectionRegistry)
     const meta = header('cold-cache')
@@ -772,14 +778,10 @@ describe('SessionObservationReader cold projections', () => {
     const hydratePrepared = vi.fn().mockReturnValue(snapshot)
     ctx.provide('sessionProjectionCache', { hydratePrepared } as never)
 
-    using observed = await new SessionObservationReader(ctx).read(meta.id, {
-      projectionMode,
-      projectionStateKeys: ['title'],
-    })
+    using observed = await new SessionObservationReader(ctx).read(meta.id)
 
-    expect(observed.projections).toBe(projectionMode === 'all' ? snapshot : undefined)
+    expect(observed.projections).toBe(snapshot)
     expect(hydratePrepared).toHaveBeenCalledOnce()
-    expect(hydratePrepared.mock.calls[0]?.[2]).toBe(projectionMode)
     await ctx.fiber.dispose()
   })
 
@@ -802,7 +804,7 @@ describe('SessionObservationReader cold projections', () => {
 })
 
 describe('SessionObservationReader host projection states', () => {
-  it('detaches a live registry state at the observed cut without computing views', async () => {
+  it('returns detached live states and views at the observed cut', async () => {
     const ctx = await readerContext()
     await ctx.plugin(SessionProjectionRegistry)
     ctx.sessionProjections.register(titleProjectionDefinition)
@@ -810,17 +812,17 @@ describe('SessionObservationReader host projection states', () => {
     session.append('session/title', { title: 'Live title', messageSeqs: [], source: { kind: 'user' } })
     const reader = new SessionObservationReader(ctx)
 
-    using observed = await reader.read(session.id, { projectionMode: 'none', projectionStateKeys: ['title'] })
+    using observed = await reader.read(session.id)
     session.append('session/title', { title: 'Later title', messageSeqs: [], source: { kind: 'user' } })
-    using later = await reader.read(session.id, { projectionMode: 'none', projectionStateKeys: ['title'] })
+    using later = await reader.read(session.id)
 
-    expect(observed.projections).toBeUndefined()
+    expect(observed.projections?.values.title).toBeDefined()
     expect(observed.projectionStates).toEqual({ title: 'Live title' })
     expect(later.projectionStates).toEqual({ title: 'Later title' })
     await ctx.fiber.dispose()
   })
 
-  it('hydrates a prepared Session once and serves the requested state from its cells', async () => {
+  it('returns cold states and views from the prepared Session', async () => {
     const ctx = await readerContext()
     await ctx.plugin(SessionProjectionRegistry)
     ctx.sessionProjections.register(titleProjectionDefinition)
@@ -829,53 +831,60 @@ describe('SessionObservationReader host projection states', () => {
     ctx.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 }))
     const hydrate = vi.spyOn(ctx.sessionProjections, 'hydrate')
 
-    using observed = await new SessionObservationReader(ctx).read(meta.id, {
-      projectionMode: 'none',
-      projectionStateKeys: ['title'],
-    })
+    using observed = await new SessionObservationReader(ctx).read(meta.id)
 
     expect(observed.source).toBe('prepared')
-    expect(observed.projections).toBeUndefined()
+    expect(observed.projections?.values.title).toBeDefined()
     expect(observed.projectionStates).toEqual({ title: 'Cold title' })
     expect(hydrate).toHaveBeenCalledOnce()
     await ctx.fiber.dispose()
   })
 
-  it('omits wire views on fresh and reused cold host-state reads', async () => {
+  it.each(['live', 'cold'] as const)('returns all detached states on repeated %s reads and omits them in none mode', async (source) => {
     const ctx = await readerContext()
     try {
       await ctx.plugin(SessionProjectionRegistry)
-      const view = vi.fn(titleProjectionDefinition.wire.view)
+      ctx.sessionProjections.register(titleProjectionDefinition)
       ctx.sessionProjections.register({
-        ...titleProjectionDefinition,
-        wire: { ...titleProjectionDefinition.wire, view },
+        key: 'observation-test/titles',
+        stateVersion: 1,
+        stateSchema: titleProjectionDefinition.stateSchema.array(),
+        init: () => [] as (string | null)[],
+        apply: (state, event) => event.type === 'session/title'
+          ? [...state, event.data.title] : state,
       })
-      const meta = header('cold-host-only')
-      const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'Cold title')], revision: 'r1' }]])
-      ctx.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 }))
-      const reader = new SessionObservationReader(ctx)
-      for (let read = 0; read < 2; read++) {
-        using observed = await reader.read(meta.id, { projectionMode: 'none', projectionStateKeys: ['title'] })
-        expect(observed.projections).toBeUndefined()
-        expect(observed.projectionStates).toEqual({ title: 'Cold title' })
-        expect(view).not.toHaveBeenCalled()
+      const meta = header(`all-states-${source}`)
+      if (source === 'live') {
+        const session = ctx.sessions.create(meta.id)
+        session.append('session/title', { title: 'First', messageSeqs: [], source: { kind: 'user' } })
+      } else {
+        const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'First')], revision: 'r1' }]])
+        ctx.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 }))
       }
-      using full = await reader.read(meta.id)
-      expect(full.projections?.values).toEqual({ title: 'Cold title' })
-      expect(view).toHaveBeenCalledOnce()
+      const reader = new SessionObservationReader(ctx)
+      using first = await reader.read(meta.id)
+      expect(first.projectionStates).toEqual({ title: 'First', 'observation-test/titles': ['First'] })
+      first.projectionStates?.['observation-test/titles']?.push('caller edit')
+      using second = await reader.read(meta.id)
+      expect(second.projectionStates).toEqual({ title: 'First', 'observation-test/titles': ['First'] })
+      expect(second.projections?.values).toEqual({ title: 'First' })
+      const checkpoint = vi.spyOn(ctx.sessionProjections, 'checkpoint')
+      const hydrate = vi.spyOn(ctx.sessionProjections, 'hydrate')
+      using none = await reader.read(meta.id, { projectionMode: 'none' })
+      expect(none.projections).toBeUndefined()
+      expect(none.projectionStates).toBeUndefined()
+      expect(checkpoint).not.toHaveBeenCalled()
+      expect(hydrate).not.toHaveBeenCalled()
     } finally {
       await ctx.fiber.dispose()
     }
   })
 
-  it('omits unregistered keys and the whole map when no registry is mounted', async () => {
+  it('returns an empty state map for an empty registry and omits it without a registry', async () => {
     const withRegistry = await readerContext()
     await withRegistry.plugin(SessionProjectionRegistry)
     const registered = withRegistry.sessions.create(SessionId('unregistered-key'))
-    using partial = await new SessionObservationReader(withRegistry).read(registered.id, {
-      projectionMode: 'none',
-      projectionStateKeys: ['title'],
-    })
+    using partial = await new SessionObservationReader(withRegistry).read(registered.id)
     expect(partial.projectionStates).toEqual({})
     await withRegistry.fiber.dispose()
 
@@ -883,10 +892,7 @@ describe('SessionObservationReader host projection states', () => {
     const meta = header('no-registry')
     const store = new Map([[meta.id, { header: meta, events: [titleEvent(0, 'Cold title')], revision: 'r1' }]])
     withoutRegistry.provide('sessionPersistence', stubPersistence(store, { stat: 0, open: 0, read: 0 }))
-    using absent = await new SessionObservationReader(withoutRegistry).read(meta.id, {
-      projectionMode: 'none',
-      projectionStateKeys: ['title'],
-    })
+    using absent = await new SessionObservationReader(withoutRegistry).read(meta.id)
     expect(absent.projectionStates).toBeUndefined()
     await withoutRegistry.fiber.dispose()
   })

+ 2 - 2
packages/session/session-projection-cache/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/session/session-projection-cache/README.md
-README.md: 9ef25b737252d051b16b375442b8edefbd92a939
-README.zh.md: 650d6e4fe7fcd3365c99975c72e140d26f9592bf
+README.md: 4ac2ddabe878202a3b324d5b2aaecbea8da99e5f
+README.zh.md: ffed860eb59621d82672c25989d603b79634a498

+ 0 - 3
packages/session/session-projection-cache/README.md

@@ -52,8 +52,6 @@ The cache opens its domain through the storage stack, so base mounts `storage`,
 
 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.
 
-`hydratePrepared` restores every state from the checkpoint and event tail. Its `projectionMode` defaults to `all`; `none` suppresses client view computation and validation, including when a malformed checkpoint requires a full refold.
-
 ### How checkpoints are written
 
 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.
@@ -124,7 +122,6 @@ None; the cache never assembles or sends provider requests.
 
 <a id="known-limitations-and-deferred-work"></a>
 
-
 These limits define where the cache needs operational care. They are current package constraints, not a task backlog.
 
 - **No eviction or retention surface** — records accumulate per session; pruning stored checkpoints is out-of-band maintenance, same stance as session persistence itself.

+ 0 - 3
packages/session/session-projection-cache/README.zh.md

@@ -52,8 +52,6 @@ kind: "package-reference"
 
 本插件注入 `storageDomain`、`sessionProjections` 与 `sessions`。生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-projection-cache)是每个受支持字段及其 JSDoc 的穷尽式真源。
 
-`hydratePrepared` 从检查点与事件尾部恢复所有状态。其 `projectionMode` 默认为 `all`;`none` 会跳过客户端视图的计算与校验,即使检查点损坏、需要重新折叠完整日志也如此。
-
 ### 检查点如何写入
 
 三个必写点总是写入:会话创建保存由种子派生的切面,`turn/end` 保存列表读取所需的轮次终值,会话释放保存最终实时切面。其间,配置的条数与间隔节流随事件累积写入。每次写入通过领域写入链以原子方式替换该会话的完整记录;失败会记录警告并让缓存保持陈旧,后续写入会自行修复。
@@ -124,7 +122,6 @@ kind: "package-reference"
 
 <a id="known-limitations-and-deferred-work"></a>
 
-
 这些限制说明缓存何时需要运维注意。它们是当前包约束,不是任务积压。
 
 - **无淘汰或保留接口**——记录按会话持续累积;清理已存储检查点属于带外维护,与会话持久化采用相同策略。

+ 3 - 6
packages/session/session-projection-cache/src/index.ts

@@ -207,20 +207,18 @@ export class SessionProjectionCache extends Service {
    * because the logical observation may contain recovery events not yet durable.
    * @param session - exact unpublished Session retained by persistence.
    * @param events - exact logical event prefix represented by the observation.
-   * @param projectionMode - `none` hydrates every state without computing or validating client views; defaults to `all`.
-   * @returns the event cut with all client views, or empty values in `none` mode.
+   * @returns all projection values at the event cut.
    */
   hydratePrepared(
     session: Session,
     events: readonly SessionEvent[],
-    projectionMode: 'all' | 'none' = 'all',
   ): ProjectionSnapshot {
     const record = this.recordFor(
       session.id,
       identityOf(session.header, session.inheritedEventCount),
     )
     if (record === undefined) {
-      return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), projectionMode)
+      return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0))
     }
     try {
       return this.ctx.sessionProjections.hydrate(
@@ -228,12 +226,11 @@ export class SessionProjectionCache extends Service {
         record.rows,
         events,
         SessionLogOffset(0),
-        projectionMode,
       )
     } catch {
       // Cached rows are disposable derived data. Retry from the exact log so a
       // stale schema cannot make a valid Session unreadable.
-      return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), projectionMode)
+      return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0))
     }
   }
 

+ 0 - 35
packages/session/session-projection-cache/tests/cache.spec.ts

@@ -651,41 +651,6 @@ describe('SessionProjectionCache cold-read seeding', () => {
     })
   })
 
-  it.each(['matching', 'malformed', 'absent'])('hydrates host states without client views from a %s checkpoint', async (record) => {
-    const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-'))
-    roots.push(root)
-    const id = SessionId('selected-views')
-    if (record !== 'absent') {
-      await seedRecord(root, id, {
-        'cache-test/marks': { ver: 1, seq: SessionSeq(1), val: { marks: record === 'matching' ? ['cached'] : 'malformed' } },
-      })
-    }
-    const { cache, ctx } = await harness({ root })
-    const view = vi.fn(secondaryMarksUnit.wire.view)
-    const schema = secondaryMarksUnit.wire.viewSchema.clone()
-    const parseView = vi.spyOn(schema, 'parse')
-    ctx.sessionProjections.register({
-      ...secondaryMarksUnit,
-      wire: { viewSchema: schema, view },
-    })
-    const events = storedLog([['fresh']])
-    const session = Session.create(id, events, headerOf(id))
-    const marks = { marks: [record === 'matching' ? 'cached' : 'fresh'] }
-
-    for (let read = 0; read < 2; read++) {
-      expect(cache.hydratePrepared(session, events, 'none')).toEqual({ asOfSeq: 2, values: {} })
-      expect(ctx.sessionProjections.stateOf(session, 'cache-test/marks')).toEqual(marks)
-      expect(ctx.sessionProjections.stateOf(session, 'cache-test/secondary-marks')).toEqual({ marks: ['fresh'] })
-      expect(view).not.toHaveBeenCalled()
-      expect(parseView).not.toHaveBeenCalled()
-    }
-    expect(cache.hydratePrepared(session, events).values).toEqual({
-      'cache-test/marks': marks,
-      'cache-test/secondary-marks': { marks: ['fresh'] },
-    })
-    expect(view).toHaveBeenCalledOnce()
-  })
-
   it('coldSnapshot traverses the full log but applies only the events after each cached watermark', async () => {
     const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-'))
     roots.push(root)

+ 2 - 2
packages/session/session-projection/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/session/session-projection/README.md
-README.md: 9bd08d119cf1961a47595bec29efabdc8d7fb276
-README.zh.md: e7982626f6e0c734ef426ec1a2851562ca01525a
+README.md: ced9837dbf69a0fc168da7f574342e803d83bfea
+README.zh.md: 2af7d33d617414baac24575ad425db0db8300cd8

+ 0 - 1
packages/session/session-projection/README.md

@@ -126,7 +126,6 @@ None; projections never assemble or send provider requests.
 
 <a id="known-limitations-and-deferred-work"></a>
 
-
 These limits define where the projection registry needs care at scale. They are current package constraints, not a task backlog.
 
 - **Every tail page carries every client-visible key** — there is no per-key opt-out or lazy-key request shape yet; acceptable while values are UI-scale whole states, revisit if a domain's value grows large.

+ 0 - 1
packages/session/session-projection/README.zh.md

@@ -126,7 +126,6 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session)
 
 <a id="known-limitations-and-deferred-work"></a>
 
-
 这些限制说明投影注册表在大规模下何时需要特别处理。它们是当前包约束,不是任务积压。
 
 - **每个尾页携带每个 client-visible key**——尚无逐 key 的 opt-out 或惰性 key 请求形状;在值都是 UI 量级的全量状态时可以接受,若某领域的值变大再重议。

+ 6 - 15
packages/session/session-projection/src/index.ts

@@ -488,7 +488,6 @@ export class SessionProjectionRegistry extends Service {
    * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty).
    * @param header - immutable metadata for the Session being restored.
    * @param inheritedEventCount - exact fork-inherited prefix length supplied to unit initialization.
-   * @param projectionMode - `none` restores every state without computing or validating client views; defaults to `all`.
    * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
    *   supplied event's seq, `baseSeq - 1` for an empty tail) plus the
    *   refreshed checkpoint rows at that cut, ready for a durable write-back.
@@ -499,7 +498,6 @@ export class SessionProjectionRegistry extends Service {
     baseSeq: SessionLogOffset,
     header: SessionHeader,
     inheritedEventCount: SessionLogOffset,
-    projectionMode: 'all' | 'none' = 'all',
   ):
   { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } {
     const endSeq: SessionSeqCursor = events.at(-1)?.seq ?? cursorBefore(baseSeq)
@@ -532,9 +530,7 @@ export class SessionProjectionRegistry extends Service {
         }
         state = def.apply(state, event)
       }
-      if (projectionMode === 'all' && def.wire !== undefined) {
-        values[def.key] = def.wire.viewSchema.parse(def.wire.view(state))
-      }
+      if (def.wire !== undefined) values[def.key] = def.wire.viewSchema.parse(def.wire.view(state))
       refreshed[def.key] = { ver: def.stateVersion, seq: endSeq, val: state }
     }
     return {
@@ -551,15 +547,13 @@ export class SessionProjectionRegistry extends Service {
    * @param checkpoint - persisted rows for this Session lifecycle.
    * @param events - exact events at the observation cut.
    * @param baseSeq - first supplied event sequence.
-   * @param projectionMode - `none` installs every state without computing or validating client views; defaults to `all`.
-   * @returns the supplied cut with all client views, or empty values in `none` mode.
+   * @returns all projection values at the supplied cut.
    */
   hydrate(
     session: Session,
     checkpoint: ProjectionCheckpoint,
     events: readonly SessionEvent[],
     baseSeq: SessionLogOffset,
-    projectionMode: 'all' | 'none' = 'all',
   ): ProjectionSnapshot {
     const endSeq: SessionSeqCursor = events.at(-1)?.seq ?? cursorBefore(baseSeq)
     let complete = true
@@ -572,12 +566,10 @@ export class SessionProjectionRegistry extends Service {
     }
     if (complete) {
       const values: Record<string, unknown> = {}
-      if (projectionMode === 'all') {
-        for (const registration of this.registrations.values()) {
-          if (registration.def.wire === undefined) continue
-          const current = registration.cells.get(session) as UnitCell
-          values[registration.def.key] = this.viewCell(registration, current)
-        }
+      for (const registration of this.registrations.values()) {
+        if (registration.def.wire === undefined) continue
+        const current = registration.cells.get(session) as UnitCell
+        values[registration.def.key] = this.viewCell(registration, current)
       }
       return { asOfSeq: endSeq, values }
     }
@@ -587,7 +579,6 @@ export class SessionProjectionRegistry extends Service {
       baseSeq,
       session.header,
       session.inheritedEventCount,
-      projectionMode,
     )
     for (const registration of this.registrations.values()) {
       const row = restored.checkpoint[registration.def.key]