From bfc89ed4a952c0f22a953786994c282d7907e9b2 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Wed, 4 Nov 2026 11:56:50 +0800 Subject: [PATCH] fix(team): reuse compatible owner-name checkpoints --- .../skills/dsh-session-projection/SKILL.md | 2 +- .../conversation-and-session.i18n.yaml | 4 +- .../conversation-and-session.md | 2 +- .../conversation-and-session.zh.md | 2 +- ...2026-11-02-team-task-owner-names.i18n.yaml | 4 +- .../2026-11-02-team-task-owner-names.md | 2 +- .../2026-11-02-team-task-owner-names.zh.md | 2 +- docs/subsystems/session-projection.md | 8 ++-- docs/subsystems/session-projection.zh.md | 8 ++-- .../session-projection/README.i18n.yaml | 4 +- packages/session/session-projection/README.md | 2 +- .../session/session-projection/README.zh.md | 2 +- .../session/session-projection/src/index.ts | 8 ++-- packages/team/agent-team/README.i18n.yaml | 4 +- packages/team/agent-team/README.md | 2 +- packages/team/agent-team/README.zh.md | 2 +- packages/team/agent-team/src/projection.ts | 3 +- .../tests/projection-events.spec.ts | 42 +++++++++++++++++-- 18 files changed, 70 insertions(+), 33 deletions(-) diff --git a/.agents/skills/dsh-session-projection/SKILL.md b/.agents/skills/dsh-session-projection/SKILL.md index f2820d7454..1384fdb128 100644 --- a/.agents/skills/dsh-session-projection/SKILL.md +++ b/.agents/skills/dsh-session-projection/SKILL.md @@ -17,7 +17,7 @@ Keep recoverable session facts in Projection and expose them through their ownin 3. State recovery through `init` and `apply` is synchronous and deterministic. Do not perform I/O or trigger Cordis hooks, models, or tools. Runtime hooks own those effects. Replay must not repeat them. Return the same state reference for unrelated events. Keep views synchronous and preserve their references when visible values do not change. -4. Persist JSON state that its schema accepts. Follow the README's state-version and checkpoint-watermark rules. Restoring a checkpoint and replaying its suffix must match full replay. Change the version when state fields or recovery semantics change. +4. Persist JSON state that its schema accepts. Follow the README's state-version and checkpoint-watermark rules. Restoring a checkpoint and replaying its suffix must match full replay. Change the version when state fields or recovery semantics change, except for optional additions that preserve historical meaning when absent and have tested old-checkpoint suffix equivalence with full replay. Schema acceptance alone is insufficient. 5. Prefer fixed-size or sublinear state. Growth is acceptable only when slow enough to remain negligible. Necessary linear state must have a current use and explicit memory, replay, and per-event costs. Do not retain history-sized indexes, including event sequence collections, by habit. Do not discard required data or change behavior to reduce size. Start with a complete implementation; optimize for a concrete need. diff --git a/docs/defensive-patterns/conversation-and-session.i18n.yaml b/docs/defensive-patterns/conversation-and-session.i18n.yaml index 210cfe8b06..d8d0fcc6ae 100644 --- a/docs/defensive-patterns/conversation-and-session.i18n.yaml +++ b/docs/defensive-patterns/conversation-and-session.i18n.yaml @@ -6,8 +6,8 @@ en: 499609ba1c6822d8 zh: 7429eabcce7d7c14 /conversation-and-session-rules/projection-is-the-only-read-and-compute-path: - en: 9fa9a332c93ba386 - zh: ce01ccb47dfa5df7 + en: 99cd4d8469857945 + zh: 57dd7d1137ddde80 /conversation-and-session-rules/cold-reads-are-the-last-resort: en: 32991da738e47f7f zh: b138910f8c16d103 diff --git a/docs/defensive-patterns/conversation-and-session.md b/docs/defensive-patterns/conversation-and-session.md index f2f610ebe4..c8fe7af265 100644 --- a/docs/defensive-patterns/conversation-and-session.md +++ b/docs/defensive-patterns/conversation-and-session.md @@ -10,7 +10,7 @@ Session data is read and computed through Projection. A client reads a projectio - Judged condition: every Session-derived value a client renders arrives through a projection read at the render position, and every new in-Session computation is registered as a unit whose result reaches clients as a published value. A component that fetches Session data itself, or a computation a reader triggers on demand, is the violation. - State and View: a unit folds host-side State and declares the client-visible View over it, and the registration states that division. The `ProjectionDefinition` fields and the registry that drives them belong to [session-projection.md](../subsystems/session-projection.md#ctxsessionprojections--sessionprojectionregistry). -- Version: `stateVersion` starts at 1 and is bumped whenever the serialized state fields or the fold semantics change, compatible or not. Prefer a compatible change where one exists, but still bump; registering one key twice with different `stateVersion` values throws instead of merging. +- Version: `stateVersion` starts at 1. Follow the [checkpoint version rules](../../packages/session/session-projection/README.md): state fields or fold changes require a bump, except for tested optional additions that preserve historical meaning and old-checkpoint suffix equivalence with full replay. Registering one key twice with different versions throws instead of merging. - Compute strategy: units compute in full by default. Narrowing what a unit computes needs the owning domain expert's confirmation, never the implementer's own decision. - Boundary: a partial projection view may serve interface availability for individual keys, such as a cached block drawn before the connected Session answers. It never decides an externally visible result, which waits for the standard projection value. diff --git a/docs/defensive-patterns/conversation-and-session.zh.md b/docs/defensive-patterns/conversation-and-session.zh.md index 448ce061ec..b1281c6b5d 100644 --- a/docs/defensive-patterns/conversation-and-session.zh.md +++ b/docs/defensive-patterns/conversation-and-session.zh.md @@ -10,7 +10,7 @@ Session 数据一律经 Projection 读取与计算。客户端读投影值,而 - 判定条件:客户端渲染的每个 Session 派生值都在渲染位置经投影读取取得,每个新增的会话内计算都注册为单元、其结果以已发布值的形式到达客户端。组件自己拉取 Session 数据、或读者按需触发一次计算,都属违规。 - State 与 View:单元折叠 host 侧 State,并在其上声明客户端可见的 View,二者分工写在注册里。`ProjectionDefinition` 的字段与驱动它们的注册表归 [session-projection.zh.md](../subsystems/session-projection.zh.md#ctxsessionprojections--sessionprojectionregistry)。 -- 版本:`stateVersion` 从 1 起,只要序列化 state 字段或折叠语义变化就自增,兼容与否都一样。存在兼容改法时优先兼容,但版本仍要自增;同一 key 以不同 `stateVersion` 注册两次会抛错,而不是合并。 +- 版本:`stateVersion` 从 1 起。遵循[检查点版本规则](../../packages/session/session-projection/README.zh.md):状态字段或折叠变化需要递增版本,但经过测试、保留历史含义且旧检查点加后缀恢复与全量回放一致的可选字段扩展除外。同一 key 以不同版本注册两次会抛错,而不是合并。 - 计算策略:单元默认全量计算。收窄某个单元的计算范围需要对应领域专家确认,绝不能由实现者自行决定。 - 边界:非完全 projection 渲染可用于某些 key 的界面可用性展示,例如在连接中的 Session 应答之前先画缓存块。它绝不参与对外可见结果的判定,那些结果等标准 projection 值。 diff --git a/docs/persistence-changes/2026-11-02-team-task-owner-names.i18n.yaml b/docs/persistence-changes/2026-11-02-team-task-owner-names.i18n.yaml index fdcd2a8617..95205ac30f 100644 --- a/docs/persistence-changes/2026-11-02-team-task-owner-names.i18n.yaml +++ b/docs/persistence-changes/2026-11-02-team-task-owner-names.i18n.yaml @@ -18,8 +18,8 @@ en: 6907136f28e48c90 zh: 02f8298485f3d8e7 /2026-11-02-team-task-owner-names/compatibility: - en: ecc6e4f87618a643 - zh: f7f4f2b4f77eba52 + en: ffa0669cd780f696 + zh: cdc5a302b3fc4ee1 /2026-11-02-team-task-owner-names/verification: en: 90d1e1c177ff1fc0 zh: 6438d6dcb53596b8 diff --git a/docs/persistence-changes/2026-11-02-team-task-owner-names.md b/docs/persistence-changes/2026-11-02-team-task-owner-names.md index f8b9d9ebb3..5b65f23bed 100644 --- a/docs/persistence-changes/2026-11-02-team-task-owner-names.md +++ b/docs/persistence-changes/2026-11-02-team-task-owner-names.md @@ -35,7 +35,7 @@ changes: ## Compatibility -Current readers accept existing task payloads without ownerName and display ownerId without rewriting or backfilling historical logs. New task records retain the same ownerId authorization and version 2 task semantics. Earlier Team projections use a strict task schema and reject records containing ownerName, retaining their last valid tasks and reporting a projection failure rather than silently misinterpreting ownership. Session format version remains unchanged. The agentTeam projection state version advances to 8 so version 6 and 7 checkpoints rebuild from events. +Current readers accept existing task payloads without ownerName and display ownerId without rewriting or backfilling historical logs. New task records retain the same ownerId authorization and version 2 task semantics. Earlier Team projections use a strict task schema and reject records containing ownerName, retaining their last valid tasks and reporting a projection failure rather than silently misinterpreting ownership. Session format version remains unchanged. The agentTeam projection state version remains 7: version-7 checkpoints without ownerName remain usable, and suffix replay preserves names from later task events. Version-6 checkpoints still rebuild from events. ## Verification diff --git a/docs/persistence-changes/2026-11-02-team-task-owner-names.zh.md b/docs/persistence-changes/2026-11-02-team-task-owner-names.zh.md index 50efc9d840..c4c4f8200d 100644 --- a/docs/persistence-changes/2026-11-02-team-task-owner-names.zh.md +++ b/docs/persistence-changes/2026-11-02-team-task-owner-names.zh.md @@ -35,7 +35,7 @@ changes: ## 兼容性 -当前读取方接受没有 ownerName 的已有任务载荷,直接显示 ownerId,不改写或回填历史日志。新任务记录保留相同的 ownerId 授权和第 2 版任务语义。旧 Team 投影使用严格任务 schema,会拒绝包含 ownerName 的记录,保留最后的有效任务并报告投影失败,不会静默误读归属。Session 格式版本保持不变。agentTeam 投影状态版本升为 8,使第 6、7 版检查点从事件重建。 +当前读取方接受没有 ownerName 的已有任务载荷,直接显示 ownerId,不改写或回填历史日志。新任务记录保留相同的 ownerId 授权和第 2 版任务语义。旧 Team 投影使用严格任务 schema,会拒绝包含 ownerName 的记录,保留最后的有效任务并报告投影失败,不会静默误读归属。Session 格式版本保持不变。agentTeam 投影状态版本保持为 7:缺少 ownerName 的第 7 版检查点仍可复用,后缀回放保留后续任务事件中的名称。第 6 版检查点仍从事件重建。 ## 验证 diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md index 512542d505..01dbdd99e2 100644 --- a/docs/subsystems/session-projection.md +++ b/docs/subsystems/session-projection.md @@ -96,10 +96,10 @@ interface ProjectionDefinition { summary?: boolean } : never /** - * Persisted-cache invalidation version: bump whenever the serialized state fields or the - * fold semantics change, so persisted `(sessionId, key, ver, seq, val)` - * rows from an older unit are discarded instead of being forward-applied - * into garbage. Non-negative integer. + * Non-negative persisted-cache invalidation version. Bump when state fields or fold + * semantics change, except for optional additions whose absence preserves historical + * meaning and whose old-checkpoint suffix replay matches full replay. Test that + * equivalence before retaining a version; schema acceptance alone is insufficient. */ stateVersion: number } diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md index d6b38f28d3..675d49b575 100644 --- a/docs/subsystems/session-projection.zh.md +++ b/docs/subsystems/session-projection.zh.md @@ -96,10 +96,10 @@ interface ProjectionDefinition { summary?: boolean } : never /** - * Persisted-cache invalidation version: bump whenever the serialized state fields or the - * fold semantics change, so persisted `(sessionId, key, ver, seq, val)` - * rows from an older unit are discarded instead of being forward-applied - * into garbage. Non-negative integer. + * Non-negative persisted-cache invalidation version. Bump when state fields or fold + * semantics change, except for optional additions whose absence preserves historical + * meaning and whose old-checkpoint suffix replay matches full replay. Test that + * equivalence before retaining a version; schema acceptance alone is insufficient. */ stateVersion: number } diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml index 92bdd362b1..d7c80bc585 100644 --- a/packages/session/session-projection/README.i18n.yaml +++ b/packages/session/session-projection/README.i18n.yaml @@ -21,8 +21,8 @@ en: e16fd6f4fcace77c zh: 4488f7f0b4751ea4 /deepseek-ai-dsh-session-projection/use-this-package/define-a-projection-unit: - en: 09758d1475fad86e - zh: 473637cb34aebe72 + en: afeb96d90f91de21 + zh: ceb09b13235bb9f3 /deepseek-ai-dsh-session-projection/use-this-package/register-and-read: en: 4ef214a05a5090b3 zh: 2119a4566d9e33eb diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index cc39a7ecc7..346de00541 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -33,7 +33,7 @@ Choose it when a domain keeps state that clients should see without re-deriving ### Define a projection unit -A domain contributes one `ProjectionDefinition` per state key: a key, a state schema, an initial state, a synchronous fold `apply(state, event)`, an optional `wire` block that projects state to a client view, and a `stateVersion` that bumps whenever the state fields or fold semantics change: +A domain contributes one `ProjectionDefinition` per state key: a key, a state schema, an initial state, a synchronous fold `apply(state, event)`, an optional `wire` block that projects state to a client view, and a `stateVersion` for checkpoint invalidation. Bump it when state fields or fold semantics change. An optional addition may retain the version only when absence preserves historical meaning and a regression test verifies that restoring an old checkpoint plus its suffix matches full replay; schema acceptance alone is insufficient: ```text const definition = { diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index 82edeb2891..30b280b056 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -33,7 +33,7 @@ kind: "package-reference" ### 定义投影单元 -领域为每个状态键贡献一个 `ProjectionDefinition`:一个 key、状态 schema、初始状态、同步折叠 `apply(state, event)`、可选的 `wire` 块(把状态投影为客户端视图),以及状态字段或折叠语义变化时递增的 `stateVersion`: +领域为每个状态键贡献一个 `ProjectionDefinition`:一个 key、状态 schema、初始状态、同步折叠 `apply(state, event)`、可选的 `wire` 块(把状态投影为客户端视图),以及用于使检查点失效的 `stateVersion`。状态字段或折叠语义变化时递增版本。仅当缺失字段保留历史含义,且回归测试验证旧检查点加后缀恢复与全量回放一致时,新增可选字段才可保持版本;仅通过 schema 校验不足以保留版本: ```text const definition = { diff --git a/packages/session/session-projection/src/index.ts b/packages/session/session-projection/src/index.ts index a3e9bc0f3b..b4aa95d5df 100644 --- a/packages/session/session-projection/src/index.ts +++ b/packages/session/session-projection/src/index.ts @@ -124,10 +124,10 @@ export interface ProjectionDefinition { summary?: boolean } : never /** - * Persisted-cache invalidation version: bump whenever the serialized state fields or the - * fold semantics change, so persisted `(sessionId, key, ver, seq, val)` - * rows from an older unit are discarded instead of being forward-applied - * into garbage. Non-negative integer. + * Non-negative persisted-cache invalidation version. Bump when state fields or fold + * semantics change, except for optional additions whose absence preserves historical + * meaning and whose old-checkpoint suffix replay matches full replay. Test that + * equivalence before retaining a version; schema acceptance alone is insufficient. */ stateVersion: number } diff --git a/packages/team/agent-team/README.i18n.yaml b/packages/team/agent-team/README.i18n.yaml index 71dbca6c13..2f1733cd95 100644 --- a/packages/team/agent-team/README.i18n.yaml +++ b/packages/team/agent-team/README.i18n.yaml @@ -60,8 +60,8 @@ en: a3eaccd15b02cc8f zh: c2829d437a1dfe57 /deepseek-ai-dsh-agent-team/understand-the-implementation/durability-model: - en: be87ba201535f004 - zh: 170538e9b3a87597 + en: 702ab0504cdb8fab + zh: 93cb260d2d108239 /deepseek-ai-dsh-agent-team/understand-the-implementation/disposal: en: a8baf51f2fcd6d49 zh: 357c8db36823fe0e diff --git a/packages/team/agent-team/README.md b/packages/team/agent-team/README.md index 9582ab7760..a58392c1c3 100644 --- a/packages/team/agent-team/README.md +++ b/packages/team/agent-team/README.md @@ -139,7 +139,7 @@ Team requires `sessionPersistence` before activation. `Service.init` awaits regi Task mutations honor cancellation before appending `team/task` to the exact live root Session, then await the flush. Cancellation cannot retract an appended record. A failed flush or one with no participating listener raises `TEAM_TASK_PERSISTENCE_UNCONFIRMED` with the task id and revision: the task is already visible, but durability is unknown. Read that task before retrying. Task records remain log-only. No committed task or historical payload generation is rewritten. -The projection ignores historical roster and mailbox records without validating or retaining their payloads. It stores only the task board, saved mode, and recovery metadata. Incompatible projection checkpoints rebuild from the unchanged Session log; see the [upgrade guide](../../../docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.md). +The projection ignores historical roster and mailbox records without validating or retaining their payloads. It stores only the task board, saved mode, and recovery metadata. Version-7 checkpoints without `ownerName` remain usable; suffix replay records names from later task events without backfilling historical tasks. Incompatible projection checkpoints rebuild from the unchanged Session log; see the [upgrade guide](../../../docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.md). ### Disposal diff --git a/packages/team/agent-team/README.zh.md b/packages/team/agent-team/README.zh.md index 1ac17f0a06..9e57943fa1 100644 --- a/packages/team/agent-team/README.zh.md +++ b/packages/team/agent-team/README.zh.md @@ -139,7 +139,7 @@ Team 在激活前要求 `sessionPersistence` 可用。`Service.init` 等待 `age 任务修改在向当前在线根 Session 追加 `team/task` 前响应取消,随后等待刷盘完成。取消不能撤回已追加的记录。刷盘失败或没有监听器参与时,以 `TEAM_TASK_PERSISTENCE_UNCONFIRMED` 报告任务 id 和修订号:任务已可见,但持久性未知。重试前先读取该任务。任务记录仅保留在日志中。不重写已提交任务或历史载荷代次。 -投影忽略历史名册与邮箱记录,不校验或保留其载荷。投影仅保存任务板、模式选择和恢复元数据。不兼容的投影检查点从未改写的 Session 日志重建;参见[升级指南](../../../docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.zh.md)。 +投影忽略历史名册与邮箱记录,不校验或保留其载荷。投影仅保存任务板、模式选择和恢复元数据。缺少 `ownerName` 的第 7 版检查点仍可复用;后缀回放从后续任务事件记录名称,不回填历史任务。不兼容的投影检查点从未改写的 Session 日志重建;参见[升级指南](../../../docs/upgrade-guide/v0.2.1-rc.3/team-projection/guide.zh.md)。 ### Dispose diff --git a/packages/team/agent-team/src/projection.ts b/packages/team/agent-team/src/projection.ts index 128df25fa7..dfad8265cc 100644 --- a/packages/team/agent-team/src/projection.ts +++ b/packages/team/agent-team/src/projection.ts @@ -205,10 +205,11 @@ export function teamProjectionView(state: TeamProjectionState): TeamProjection { * Team projection selected by the projected Session identity; the wire view * carries the selection and task board. Current subagents are * supplied separately by the existing `subagentCatalog` projection. + * Version-7 checkpoints without owner names remain usable without backfilling. */ export const teamProjectionDefinition = { key: 'agentTeam', - stateVersion: 8, + stateVersion: 7, stateSchema: teamProjectionEntrySchema, init: (header, inheritedEventCount: SessionLogOffset) => emptyTeamState(header.id, inheritedEventCount), apply: applyProjectionEvent, diff --git a/packages/team/agent-team/tests/projection-events.spec.ts b/packages/team/agent-team/tests/projection-events.spec.ts index 9690a5c631..42dbbc530d 100644 --- a/packages/team/agent-team/tests/projection-events.spec.ts +++ b/packages/team/agent-team/tests/projection-events.spec.ts @@ -300,7 +300,7 @@ describe('Agent Teams projection events', () => { ]) }) - it.each([6, 7])('rebuilds version-%s state and restores saved owner names through current checkpoint suffixes', async (version) => { + it('rebuilds version-6 state and restores saved owner names through current checkpoint suffixes', async () => { const ctx = new Context() onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(SessionProjectionRegistry) @@ -320,7 +320,7 @@ describe('Agent Teams projection events', () => { } const full = ctx.sessionProjections.restore({}, records, SessionLogOffset(0), header, SessionLogOffset(0)) const rebuilt = ctx.sessionProjections.restore( - { agentTeam: { ver: version, seq: SessionSeq(3), val: oldState } }, + { agentTeam: { ver: 6, seq: SessionSeq(3), val: oldState } }, records, SessionLogOffset(0), header, SessionLogOffset(0), ) expect(rebuilt).toEqual(full) @@ -332,10 +332,46 @@ describe('Agent Teams projection events', () => { id: TEAM, inheritedEventCount: 0, enabled: false, nextTaskNumber: 2, tasks: [task({ revision: 2, status: 'completed', ownerId: CHILD, ownerName: 'reviewer/checker' })], }) - expect(full.checkpoint['agentTeam']!.ver).toBe(8) + expect(full.checkpoint['agentTeam']!.ver).toBe(7) expect(teamProjectionView(state)).toMatchObject({ enabled: false, tasks: [{ status: 'completed' }] }) }) + it('reuses version-7 checkpoints without owner names and applies named task suffixes', async () => { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await ctx.plugin(SessionProjectionRegistry) + await ctx.sessionProjections.register(teamProjectionDefinition) + const header: SessionHeader = { version: SESSION_FORMAT_VERSION, id: ROOT, createdAt: 0, isSeeded: false } + const historicalTask = task({ ownerId: CHILD, status: 'in_progress' }) + const records = [ + event('team/mode', { enabled: true }, SessionSeq(0)), + event('team/task', { version: 2, teamId: TEAM, task: historicalTask }, SessionSeq(1)), + event('team/task', { + version: 2, teamId: TEAM, + task: task({ revision: 2, status: 'completed', ownerId: CHILD, ownerName: 'reviewer/checker' }), + }, SessionSeq(2)), + ] + const checkpoint = { agentTeam: { ver: 7, seq: SessionSeq(1), val: { + id: TEAM, inheritedEventCount: 0, enabled: true, nextTaskNumber: 2, tasks: [historicalTask], + } } } + expect(ctx.sessionProjections.restoreFloor(checkpoint)).toBe(1) + const cached = ctx.sessionProjections.viewCheckpoint(checkpoint) + expect(cached.agentTeam).toMatchObject({ tasks: [{ ownerId: CHILD }] }) + expect(cached.agentTeam?.tasks[0]).not.toHaveProperty('ownerName') + const restored = ctx.sessionProjections.restore(checkpoint, records.slice(1), SessionLogOffset(1), header, SessionLogOffset(0)) + const full = ctx.sessionProjections.restore({}, records, SessionLogOffset(0), header, SessionLogOffset(0)) + expect(restored).toEqual(full) + expect(restored.checkpoint['agentTeam']!.ver).toBe(7) + expect(restored.snapshot.values.agentTeam).toMatchObject({ tasks: [{ ownerName: 'reviewer/checker' }] }) + const invalid = { agentTeam: { ...checkpoint.agentTeam, val: { + ...checkpoint.agentTeam.val, tasks: [task({ ownerName: 'reviewer' })], + } } } + expect(ctx.sessionProjections.viewCheckpoint(invalid)).not.toHaveProperty('agentTeam') + expect(() => ctx.sessionProjections.restore( + invalid, records.slice(1), SessionLogOffset(1), header, SessionLogOffset(0), + )).toThrow(/ownerName requires ownerId/) + }) + it('records unsupported event versions without applying them', () => { const invalid = event('team/task', { version: 1 as 2,