DSH / Atlas
2026-08-19implementedarchitecture

Session projections as a required reader seam

会话投影作为必需的读取 seam

An optional projection registry lets a plugin whose host behavior reads projected state activate without that state. Unless the reader rejects the missing registry or key, the host behavior or subagent catalog fields can silently disappear. Batch-only reads also materialize every client view when a consumer needs one host value. Some contributor sites intentionally retain optional `ctx.inject` registration, so their

English

Problem

An optional projection registry lets a plugin whose host behavior reads projected state activate without that state. Unless the reader rejects the missing registry or key, the host behavior or subagent catalog fields can silently disappear. Batch-only reads also materialize every client view when a consumer needs one host value. Some contributor sites intentionally retain optional ctx.inject registration, so their readers need an explicit missing-state rule.

Decision

This decision builds on the split between host projection state and client views in Session projection state and client views.

Every host reader treats the projection registry and its required key as mandatory state. A plugin either declares sessionProjections as a required injection or resolves the registry and key explicitly and throws on the first dependent access. Official compositions mount the registry before those plugins. ApiProxyService follows the required-injection form; the lower-level createApiProxy factory remains tolerant for isolated tests and diagnostics.

A domain contributor may register its unit through ctx.inject(['sessionProjections'], ...). Optional registration controls the child lifecycle only; it does not authorize a reader to substitute a default when the registry or key is absent.

The registry provides stateOf(session, key) for one typed host state and keeps snapshot() for batch carriers. Client views contain only consumed fields; host readers use stateOf for richer state.

onChanged publishes client-visible value changes only. Unit registration and removal remain effect-scoped registry lifecycle; register() returns the exact Cordis disposer so a composite domain owner can finish cleanup against projected state before removing its unit. Registration changes do not create a second Host event stream or client tombstone protocol. A later authoritative history or list baseline reflects the active key set.

Alternatives considered

  • Default missing projected state. This preserves more partial compositions but makes missing host state indistinguishable from a valid empty value. Rejected because official profiles mount the registry and configuration errors must fail explicitly.
  • Require every contributor at activation. This makes the key set uniform but unnecessarily couples contribution lifecycle to service activation. Explicit first-access failure preserves the optional registration form without allowing silent degradation.
  • Use snapshot() for every read. This keeps one method but computes unrelated wire views and encourages consumers to depend on batch transport data for host logic. Rejected in favor of typed single-key state reads.
  • Send full host values to clients. This avoids separate view types but exposes producer identities and policy knobs that no client consumes. Rejected in favor of explicit cropped views.
  • Broadcast registry additions and removals across Host and mux streams. The streams have no shared ordering, so clients need tombstones, buffered frames, and baseline retries to reconcile them. Rejected because plugin-key churn does not justify a second synchronization protocol.

Consequences

  • Missing projection composition fails either during activation or at the first dependent host access; it never degrades to a default value.
  • Host consumers avoid repeated whole-registry snapshots and log scans.
  • Wire payloads exclude host-only fields and per-key watermark wrappers; ordinary baselines communicate the active key set.

中文

问题

可选的投影注册表会让 host 行为读取投影状态的插件在缺少该状态时仍然激活。除非读取方拒绝缺失的注册表或 key,否则 host 行为或 subagent 目录字段可能静默消失。只有批量读取也会在消费方只需要一个 host 值时物化每个客户端 view。部分贡献位置有意保留可选的 ctx.inject 注册形式,因此其读取方需要明确的缺失状态规则。

决策

本决策建立在会话投影的 host 状态与客户端视图所定义的拆分之上。

每个 host 读取方都把投影注册表及其所需 key 视为必需状态。插件要么把 sessionProjections 声明为必需注入,要么显式解析注册表与 key,并在第一次依赖它们的访问时抛错。正式组合在这些插件之前挂载注册表。ApiProxyService 采用必需注入形式;较低层的 createApiProxy factory 对隔离测试和诊断保持容错。

领域贡献方可以通过 ctx.inject(['sessionProjections'], ...) 注册单元。可选注册只控制子功能生命周期;它并不允许读取方在注册表或 key 缺失时替换为默认值。

注册表提供 stateOf(session, key) 来读取一个类型化 host 状态,并为批量 carrier 保留 snapshot()。客户端 view 只包含消费方使用的字段;host 读取方通过 stateOf 取得更丰富的状态。

onChanged 只发布客户端可见值的变化。单元注册和移除仍是绑定 effect 的注册表生命周期;register() 返回 Cordis 的原始 disposer,使组合式领域 owner 可以先依据投影状态完成清理,再移除自身单元。注册变化不会创建第二条 Host 事件流或客户端 tombstone 协议。后续权威 history 或 list 基线会反映活跃 key 集。

考虑过的替代方案

  • 为缺失的投影状态提供默认值。 这会保留更多不完整组合,但缺失 host 状态将无法与合法空值区分。正式 profile 已挂载注册表,配置错误必须显式失败,因此否决。
  • 要求每个贡献方都在激活时强依赖。 这会统一 key 集,却会让贡献生命周期与服务激活产生不必要的耦合。首次访问时显式失败既能保留可选注册形式,也不会允许静默降级。
  • 每次读取都使用 snapshot()。 这只保留一个方法,但会计算无关 wire view,并鼓励消费方让 host 逻辑依赖批量传输数据。改用类型化单 key 状态读取。
  • 向客户端发送完整 host 值。 这避免单独的 view 类型,但会暴露客户端不消费的来源信息和策略旋钮。改用显式裁剪的 view。
  • 跨 Host 和 mux stream 广播注册表新增和移除。 两条 stream 没有共享顺序,客户端因此需要 tombstone、缓冲帧和基线重试来协调。插件 key 变化不值得引入第二套同步协议。

后果

  • 缺少投影组合会在激活期间或第一次依赖它的 host 访问时失败,绝不会降级为默认值。
  • host 消费方避免重复的全注册表快照和日志扫描。
  • 协议负载排除 host 内部字段和逐 key 水位包装;普通基线会传达活跃 key 集。