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

refactor(session): limit hydration views to all or none

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

+ 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: 9c7742cb537050d0edbf7985c1ed624ec2bcacfc
-2026-09-01-parent-owned-subagent-catalog.zh.md: 3836e01df49396fef65dd7bb8fb469582078f060
+2026-09-01-parent-owned-subagent-catalog.md: fced05dab48d281a2a89a60da03163f15e2e7b74
+2026-09-01-parent-owned-subagent-catalog.zh.md: 527583813ca0fa8cf06fd721f68ce5d2c8fa81eb

+ 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 an empty view selection through checkpoint hydration, including cache reuse and malformed-row recovery; unrelated wire views are neither computed nor validated. Direct-child and descendant listing still use the Session corpus and child identity projection.
+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.
 
 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 时,空视图选择会传递到检查点 hydration,包括缓存复用和损坏 row 的恢复;无关 wire view 不会被计算或校验。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。
+调用方可以向 `observeSession` 请求 `projectionStateKeys: ['subagentCatalog']`。实时观察克隆 registry 维护中的 state;冷观察 hydrate 已准备的 Session,并在观察 cursor 处分离出同一 state。仅读取 host state 时,`projectionMode: 'none'` 会传递到检查点 hydration,包括缓存复用和损坏 row 的恢复;无关 wire view 不会被计算或校验。Hydration 只接受 `all` 或 `none`,因为观察不会请求单独的客户端视图 key。直接子级和后代列表仍使用 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: 52e99245609872c61853792250a4e0fcf631f279
-session-projection.zh.md: 372c530ba36e83240c431c2c07edc2d39fb7d21b
+session-projection.md: 7552885c947b268831a9d3109244fea738e9ef2b
+session-projection.zh.md: eeb3bef9b52b5b8ab88e48a3bff61ccaacebfaaf

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

@@ -163,10 +163,10 @@ 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 keys - optional client-visible outputs; an empty list hydrates states without computing or validating views.
- * @returns selected projection values at the event cut, defaulting to all views.
+ * @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.
  */
-hydratePrepared( session: Session, events: readonly SessionEvent[], keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot
+hydratePrepared( session: Session, events: readonly SessionEvent[], projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot
 
 /**
  * Durably checkpoint one live session NOW (all mandatory points call
@@ -330,12 +330,12 @@ 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 keys - optional client-visible outputs; an empty list restores every state without computing or validating views.
+ * @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, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
+restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
 
 /**
  * Restore an exact cut and install its states on the supplied prepared Session.
@@ -345,10 +345,10 @@ 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 keys - optional client-visible outputs; an empty list installs every state without computing or validating views.
- * @returns selected projection values at the supplied cut, defaulting to all views.
+ * @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.
  */
-hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot
+hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot
 ```
 
 Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md) · [SessionLogOffset](session.md)

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

@@ -163,10 +163,10 @@ 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 keys - optional client-visible outputs; an empty list hydrates states without computing or validating views.
- * @returns selected projection values at the event cut, defaulting to all views.
+ * @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.
  */
-hydratePrepared( session: Session, events: readonly SessionEvent[], keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot
+hydratePrepared( session: Session, events: readonly SessionEvent[], projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot
 
 /**
  * Durably checkpoint one live session NOW (all mandatory points call
@@ -330,12 +330,12 @@ 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 keys - optional client-visible outputs; an empty list restores every state without computing or validating views.
+ * @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, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
+restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
 
 /**
  * Restore an exact cut and install its states on the supplied prepared Session.
@@ -345,10 +345,10 @@ 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 keys - optional client-visible outputs; an empty list installs every state without computing or validating views.
- * @returns selected projection values at the supplied cut, defaulting to all views.
+ * @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.
  */
-hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot
+hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, projectionMode: 'all' | 'none' = 'all', ): ProjectionSnapshot
 ```
 
 Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md) · [SessionLogOffset](session.zh.md)

+ 8 - 8
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[], keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot',
+        signature: 'hydratePrepared( session: Session, events: readonly SessionEvent[], projectionMode: \'all\' | \'none\' = \'all\', ): 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: 'keys', description: 'optional client-visible outputs; an empty list hydrates states without computing or validating views.' }],
-        returns: 'selected projection values at the event cut, defaulting to all views.',
+        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.',
       },
       {
         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, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }',
+        signature: 'restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, projectionMode: \'all\' | \'none\' = \'all\', ): { 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: 'keys', description: 'optional client-visible outputs; an empty list restores every state without computing or validating views.' }],
+        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`.' }],
         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, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot',
+        signature: 'hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, projectionMode: \'all\' | \'none\' = \'all\', ): 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: 'keys', description: 'optional client-visible outputs; an empty list installs every state without computing or validating views.' }],
-        returns: 'selected projection values at the supplied cut, defaulting to all views.',
+        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.',
       },
     ],
   },

+ 2 - 3
packages/session-query/session-query/src/observation.ts

@@ -337,10 +337,9 @@ export class SessionObservationReader {
     const registry = this.ctx.get('sessionProjections')
     if (registry === undefined) return undefined
     const cache = this.ctx.get('sessionProjectionCache')
-    const keys = projectionMode === 'none' ? [] : undefined
     return cache === undefined
-      ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0), keys)
-      : cache.hydratePrepared(entry.session, entry.events, keys)
+      ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0), projectionMode)
+      : cache.hydratePrepared(entry.session, entry.events, projectionMode)
   }
 
   /**

+ 1 - 1
packages/session-query/session-query/tests/observation.spec.ts

@@ -779,7 +779,7 @@ describe('SessionObservationReader cold projections', () => {
 
     expect(observed.projections).toBe(projectionMode === 'all' ? snapshot : undefined)
     expect(hydratePrepared).toHaveBeenCalledOnce()
-    expect(hydratePrepared.mock.calls[0]?.[2]).toEqual(projectionMode === 'all' ? undefined : [])
+    expect(hydratePrepared.mock.calls[0]?.[2]).toBe(projectionMode)
     await ctx.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: ab9c4f0ebb8f68d53ebdb952997805f2b52f59af
-README.zh.md: c6ac28788ea4000327e0232d45fb0e1344e500da
+README.md: 9ef25b737252d051b16b375442b8edefbd92a939
+README.zh.md: 650d6e4fe7fcd3365c99975c72e140d26f9592bf

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

@@ -52,7 +52,7 @@ 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` accepts optional view keys and restores every state from the checkpoint and event tail. An empty key list suppresses client view computation and validation, including when a malformed checkpoint requires a full refold.
+`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
 

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

@@ -52,7 +52,7 @@ kind: "package-reference"
 
 本插件注入 `storageDomain`、`sessionProjections` 与 `sessions`。生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-projection-cache)是每个受支持字段及其 JSDoc 的穷尽式真源。
 
-`hydratePrepared` 接受可选的视图 key,并从检查点与事件尾部恢复所有状态。空 key 列表会跳过客户端视图的计算与校验,即使检查点损坏、需要重新折叠完整日志也如此。
+`hydratePrepared` 从检查点与事件尾部恢复所有状态。其 `projectionMode` 默认为 `all`;`none` 会跳过客户端视图的计算与校验,即使检查点损坏、需要重新折叠完整日志也如此。
 
 ### 检查点如何写入
 

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

@@ -207,20 +207,20 @@ 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 keys - optional client-visible outputs; an empty list hydrates states without computing or validating views.
-   * @returns selected projection values at the event cut, defaulting to all views.
+   * @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.
    */
   hydratePrepared(
     session: Session,
     events: readonly SessionEvent[],
-    keys?: readonly Extract<keyof SessionProjectionMap, string>[],
+    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), keys)
+      return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), projectionMode)
     }
     try {
       return this.ctx.sessionProjections.hydrate(
@@ -228,12 +228,12 @@ export class SessionProjectionCache extends Service {
         record.rows,
         events,
         SessionLogOffset(0),
-        keys,
+        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), keys)
+      return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0), projectionMode)
     }
   }
 

+ 2 - 7
packages/session/session-projection-cache/tests/cache.spec.ts

@@ -651,7 +651,7 @@ describe('SessionProjectionCache cold-read seeding', () => {
     })
   })
 
-  it.each(['matching', 'malformed', 'absent'])('hydrates every state with only selected views from a %s checkpoint', async (record) => {
+  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')
@@ -673,17 +673,12 @@ describe('SessionProjectionCache cold-read seeding', () => {
     const marks = { marks: [record === 'matching' ? 'cached' : 'fresh'] }
 
     for (let read = 0; read < 2; read++) {
-      expect(cache.hydratePrepared(session, events, [])).toEqual({ asOfSeq: 2, values: {} })
+      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, ['cache-test/marks'])).toEqual({
-      asOfSeq: 2,
-      values: { 'cache-test/marks': marks },
-    })
-    expect(view).not.toHaveBeenCalled()
     expect(cache.hydratePrepared(session, events).values).toEqual({
       'cache-test/marks': marks,
       'cache-test/secondary-marks': { marks: ['fresh'] },

+ 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: 9cbe4ea8f065787ee8ba568d4598a2a1cf69812a
-README.zh.md: 16fa8202c4fe13823062bc3396f63e19c26713cc
+README.md: 9bd08d119cf1961a47595bec29efabdc8d7fb276
+README.zh.md: e7982626f6e0c734ef426ec1a2851562ca01525a

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

@@ -64,7 +64,7 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session)
 
 A domain that requires projected state declares `sessionProjections` as a Cordis service dependency; optional contributors may register under `ctx.inject(['sessionProjections'], …)`. Carriers use `ctx.get('sessionProjections')` and omit their block or frames when the registry is absent.
 
-`restore` and `hydrate` accept optional view keys. Omitting them returns all client views; an empty list restores every host state without computing or validating any client view.
+`restore` and `hydrate` accept `projectionMode`, which defaults to `all`. Mode `none` restores every host state without computing or validating client views.
 
 ### Persisted checkpoints
 

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

@@ -64,7 +64,7 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session)
 
 必须使用投影状态的领域把 `sessionProjections` 声明为 Cordis 服务依赖;可选贡献方可以在 `ctx.inject(['sessionProjections'], …)` 下注册。载体使用 `ctx.get('sessionProjections')`,注册表缺席时省略自己的块或帧。
 
-`restore` 和 `hydrate` 接受可选的视图 key。省略时返回全部客户端视图;传入空列表时恢复所有 host state,但不计算或校验任何客户端视图。
+`restore` 和 `hydrate` 接受 `projectionMode`,默认值为 `all`。模式 `none` 恢复所有 host state,但不计算或校验客户端视图。
 
 ### 持久检查点
 

+ 13 - 14
packages/session/session-projection/src/index.ts

@@ -488,7 +488,7 @@ 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 keys - optional client-visible outputs; an empty list restores every state without computing or validating views.
+   * @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,14 +499,13 @@ export class SessionProjectionRegistry extends Service {
     baseSeq: SessionLogOffset,
     header: SessionHeader,
     inheritedEventCount: SessionLogOffset,
-    keys?: readonly Extract<keyof SessionProjectionMap, string>[],
+    projectionMode: 'all' | 'none' = 'all',
   ):
   { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } {
     const endSeq: SessionSeqCursor = events.at(-1)?.seq ?? cursorBefore(baseSeq)
     const beforeBase = cursorBefore(baseSeq)
     const values: Record<string, unknown> = {}
     const refreshed: ProjectionCheckpoint = {}
-    const selected = keys === undefined ? undefined : new Set<string>(keys)
     for (const registration of this.registrations.values()) {
       const def = registration.def
       const row = checkpoint[def.key]
@@ -533,7 +532,7 @@ export class SessionProjectionRegistry extends Service {
         }
         state = def.apply(state, event)
       }
-      if (def.wire !== undefined && (selected === undefined || selected.has(def.key))) {
+      if (projectionMode === 'all' && def.wire !== undefined) {
         values[def.key] = def.wire.viewSchema.parse(def.wire.view(state))
       }
       refreshed[def.key] = { ver: def.stateVersion, seq: endSeq, val: state }
@@ -552,15 +551,15 @@ 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 keys - optional client-visible outputs; an empty list installs every state without computing or validating views.
-   * @returns selected projection values at the supplied cut, defaulting to all views.
+   * @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.
    */
   hydrate(
     session: Session,
     checkpoint: ProjectionCheckpoint,
     events: readonly SessionEvent[],
     baseSeq: SessionLogOffset,
-    keys?: readonly Extract<keyof SessionProjectionMap, string>[],
+    projectionMode: 'all' | 'none' = 'all',
   ): ProjectionSnapshot {
     const endSeq: SessionSeqCursor = events.at(-1)?.seq ?? cursorBefore(baseSeq)
     let complete = true
@@ -573,12 +572,12 @@ export class SessionProjectionRegistry extends Service {
     }
     if (complete) {
       const values: Record<string, unknown> = {}
-      const selected = keys === undefined ? undefined : new Set<string>(keys)
-      for (const registration of this.registrations.values()) {
-        if (registration.def.wire === undefined) continue
-        if (selected !== undefined && !selected.has(registration.def.key)) continue
-        const current = registration.cells.get(session) as UnitCell
-        values[registration.def.key] = this.viewCell(registration, current)
+      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)
+        }
       }
       return { asOfSeq: endSeq, values }
     }
@@ -588,7 +587,7 @@ export class SessionProjectionRegistry extends Service {
       baseSeq,
       session.header,
       session.inheritedEventCount,
-      keys,
+      projectionMode,
     )
     for (const registration of this.registrations.values()) {
       const row = restored.checkpoint[registration.def.key]