Plugin-owned message projections
插件拥有消息投影
The dedicated [image-offload event](2026-09-10-image-offload-events.md) changes derived message content without replacing nodes. Implementing its image traversal and target validation inside Session makes each feature-specific message transformation a core change. The ordinary session-projection registry derives domain state but does not participate in canonical model-history derivation.
English
Problem
The dedicated image-offload event changes derived message content without replacing nodes. Implementing its image traversal and target validation inside Session makes each feature-specific message transformation a core change. The ordinary session-projection registry derives domain state but does not participate in canonical model-history derivation.
Decision
The event-owning plugin supplies a pure SessionMessageProjection: one event type and a synchronous interpreter of the preceding history. It validates the complete durable payload and returns immutable updates for existing messages, preserving their identities. Session owns atomic acceptance, shared live and detached folding, and content-generation cache invalidation. It contains no image selection or image projection implementation.
The @messageProjection tag on the owning SessionEventMap member generates the required-interpreter inventory. Missing definitions reject append, seeded creation, restore, and pure folding. This inventory does not make unknown third-party event names readable or change ignorable compatibility. A projection event cannot also declare a surface operation.
The compaction-image-offload plugin registers its definition through a fiber-owned ctx.sessions.registerMessageProjection() effect. Each event has one registered owner. Removing a definition invalidates pending committed decisions and cached reads that used it; a replacement definition requires restoring the session. Rejected, uncommitted candidates retain no interpreter dependency.
Detached readers pass definitions explicitly. The current Session format catalog assembles the same browser-safe plugin exports for persistence validation and offline queries. This static assembly does not mount recovery listeners. Live sessions use the registered composition. No process-global registry or import-time registration is involved.
The image event payload and required-on-read semantics remain owned by the image-offload note. Its indexes, selection policy, retry behavior, and SDK recordings are unchanged; this decision partially supersedes that note's core-owned interpretation.
Alternatives considered
Keep image interpretation in Session. That gives detached callers an implicit built-in, but turns a plugin-owned transformation into core event-specific code. Explicit assembly preserves deterministic replay without assigning the algorithm to core.
Use the ordinary session-projection registry. Its folds expose domain state after commit. They cannot veto an invalid append or supply canonical deriveMessages() content without another integration point.
Transform only the outgoing request. Compaction, fork, and offline replay also derive messages. A send-time transformation does not cover these consumers.
Consequences
Adding a content-changing event requires its declaration, pure interpreter, live registration, and detached assembly entry. Ordinary log-only events need none of this. Consumers of the pure fold use the generic projectedMessages result. The event envelope and stored image decisions remain unchanged, so no structural Session format version is added.
Tests cover missing interpreters, generic atomic projection, loaded windows, restore and fork, duplicate registration, fiber disposal, cached and pending reads, and rejected candidates. Image-specific tests remain with their plugin. The catalog checks that every generated required type has a detached interpreter and rejects invalid image references. Existing TypeScript and Python image SDK recordings exercise the same durable event through shipped profiles.
中文
问题
独立的图片省略事件修改派生消息内容,不替换节点。如果图片遍历和目标校验在 Session 内实现,每种功能专用的消息转换都需要修改核心代码。普通的 session-projection 注册表派生领域状态,不参与模型历史的标准派生过程。
决策
事件所属的插件提供纯 SessionMessageProjection,包含一个事件类型和同步解释此前历史的处理器。它校验完整的持久载荷,返回现有消息的不可变更新,并保留消息身份。Session 负责原子接受、实时与独立回放共用的折叠,以及内容版本驱动的缓存失效。它不包含选图或图片投影实现。
所属 SessionEventMap 成员上的 @messageProjection 标记生成必需处理器清单。缺少处理器时,追加、带历史的创建、恢复和纯折叠都会被拒绝。这份清单不会让未知的第三方事件名变得可读,也不改变 ignorable 的兼容规则。投影事件不能同时声明 surface 操作。
compaction-image-offload 插件通过 fiber 拥有的 ctx.sessions.registerMessageProjection() effect 注册处理器。每种事件只有一个注册者。移除处理器后,尚未应用的已提交决策和使用过它的缓存读取都会失效。替换处理器需要重新恢复会话。被拒绝且未提交的候选事件不会保留对处理器的依赖。
独立读取器显式传入处理器。当前 Session 格式目录装配相同的浏览器安全插件导出,供持久数据校验和离线查询使用。这项静态装配不挂载恢复监听器。实时会话使用已注册的组合。不使用进程全局注册表,也不在导入模块时注册。
图片事件载荷和读取时必须识别的语义仍由图片省略记录负责。索引、选图策略、重试行为和 SDK 录制保持不变。本决策部分取代该记录中由核心负责解释事件的职责划分。
考虑过的替代方案
保留 Session 内的图片解释逻辑。 独立调用者可以隐式获得内置处理器,但插件拥有的转换会变成核心中特定事件的代码。显式装配能够保留确定性回放,无需让核心拥有算法。
使用普通的 session-projection 注册表。 它的折叠在提交后公开领域状态。没有额外集成点,它无法否决无效追加,也无法提供标准 deriveMessages() 内容。
只转换发出的请求。 压缩、分叉和离线回放也会派生消息。发送时转换无法覆盖这些消费方。
影响
增加修改内容的事件,需要事件声明、纯处理器、实时注册和独立装配项。普通的仅记录日志事件不需要这些机制。纯折叠的消费方使用通用的 projectedMessages 结果。事件信封和已经存储的图片决策保持不变,因此不增加结构性的 Session 格式版本。
测试覆盖缺少处理器、通用原子投影、已加载窗口、恢复与分叉、重复注册、fiber 释放、缓存与待应用读取,以及被拒绝的候选事件。图片专用测试保留在插件包。目录检查每种生成的必需类型都有独立处理器,并拒绝无效图片引用。现有 TypeScript 和 Python 图片 SDK 录制通过发布的 profile 覆盖相同的持久事件。