English | 中文
Session-projection seam. It owns ctx.sessionProjections, the registry that drives every registered projection unit over committed session events and serves finished whole values to carriers, currently the api-proxy history tail page and session/projection push frame. A domain registers pure mathematics; the framework owns the drive. The session-projection RFC records the design rationale.
SessionProjectionRegistry (ctx key: sessionProjections)ctx.sessionProjections.register(definition): () => void Register one domain's unit. Duplicate keys and invalid stateVersion throw; the registration is an effect on the calling fiber, so an unloaded domain plugin's key (with its cached cells) disappears from subsequent drives and snapshots — clients read that as capability absence.ctx.sessionProjections.onChanged(listener): () => void Subscribe to the change feed: one call per unit whose state reference changed, per committed event, carrying the schema-validated view and the causing seq. Effect-tied like register.ctx.sessionProjections.snapshot(session): ProjectionSnapshot One consistent synchronous cut over every registered unit — { asOfSeq, values } with asOfSeq = the seq of the last event every value reflects (-1 for an empty log).SessionProjectionMap — the single merge-extensible type table for the whole chain (host unit, wire block, React hook). Values are wire-JSON whole values; rendering belongs to the slot system, never this layer.ProjectionDefinition<K, S> — { key, schema, init(), apply(state, event), view(state), stateVersion }: a state-driven computation unit of three pure synchronous functions plus declarations, never an opaque getter.session/event once; every committed event passes every unit's apply eagerly. Domains hold no subscriptions. Cells ({state, observedSeq} per unit per session, WeakMap-keyed) build lazily — a unit registered after events flowed, or a read of a session predating the registration, folds init over the in-memory log on first touch.apply MUST return the same state reference for events that do not concern the unit; the drive gates the change feed on Object.is, so non-matching events cost one call and nothing downstream.init/apply/view MUST be synchronous; carriers read snapshot() in the same tick as their page slice, which is what makes asOfSeq one consistent cut. An accidentally-async view returns a Promise, which fails the boundary schema.parse loudly.stateVersion is its invalidation anchor. The persisted projection cache stores (sessionId, key, ver, seq, val) rows; bump stateVersion whenever the state shape or the fold semantics change so stale rows are discarded instead of forward-applied into garbage.session/projection) and blocks from them.ctx.inject(['sessionProjections'], …) so headless assemblies without the registry stay unaffected; carriers use ctx.get('sessionProjections') and omit their block/frames entirely when the registry is absent.This is the interface-plus-drive package of the capability-seam split: domain host plugins (e.g. dsh-tool-todo) contribute units, carriers (dsh-host-apiproxy) consume the snapshot and change feed, and neither knows the other.
None, as the registry only computes client-facing read models of already-logged session state and touches no prompt, message, schema, stream, or tool result.
None; projections never assemble or send provider requests.
dsh-session-projection-cache seed that fold from persisted rows instead.schema.parse rejects a Promise-returning view, but an apply that blocks or reads torn non-session state is a review concern; the invariant companion documents why no runtime check exists.