@deepseek-ai/dsh-session-projection
English | 中文
Session-projection Service Definition and drive registry. 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.
Service: SessionProjectionRegistry (ctx key: sessionProjections)
Public API
ctx.sessionProjections.register(definition): () => voidRegister one domain's unit. Duplicate keys and invalidstateVersionthrow; 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): () => voidSubscribe to the change feed: one call per client-visible unit whose state reference changed, per committed event, carrying the schema-validated view and the causing seq. Effect-tied likeregister.ctx.sessionProjections.stateOf(session, key)Read one registered unit's current host state without computing unrelated views. The returned value is a live read-only reference; callers must not mutate it.ctx.sessionProjections.snapshot(session): ProjectionSnapshotOne consistent synchronous cut over every registered client-visible unit —{ asOfSeq, values }withasOfSeq= the seq of the last event every value reflects (-1for an empty log). Host-only state is available only throughstateOf.
Key Types
SessionProjectionMap— the merge-extensible client-view table shared by wire blocks and client hooks. Values are wire-JSON whole values; rendering belongs to the slot system, never this layer.SessionProjectionStateMap— the merge-extensible host fold-state table. Every client-visible key appears in both tables; host-only keys appear only here.ProjectionDefinition<K, S>—{ key, stateSchema, init(header), apply(state, event), wire?, stateVersion }: a synchronous state-driven computation unit.initreceives the immutableSessionHeader;wiresuppliesviewSchemaandview, and omitting it makes the unit host-only.
Contract
- The framework drives, the domain computes. The registry subscribes to
session/eventonce; every committed event passes every unit'sapplyeagerly. 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, callsinit(session.header)and folds the in-memory log on first touch. - Same-reference means no work.
applyMUST return the same state reference for events that do not concern the unit; the drive gates the change feed onObject.is, so non-matching events cost one call and nothing downstream. - Deterministic fold, complete wire value. A unit synchronously validates and folds the Session events its domain owns; those durable events may be complete values or domain transitions. When a
wireview exists, it always publishes the complete current value rather than a client-side delta. - Initialization follows the event source. Live lazy and event-driven builds pass
session.header; detached restore callers pass the immutable header returned with the same persisted event read. The registry rejects aseedLengthbeyond the observed log before folding. A unit may derive its boundary withheader.seedLength ?? 0, but must not fetch Session or process state behind the registry. - Synchronous unit discipline.
init/apply/wire.viewMUST be synchronous; carriers readsnapshot()in the same tick as their page slice, which is what makesasOfSeqone consistent cut. An accidentally async view returns a Promise, which failswire.viewSchema.parse. - State is validated plain JSON,
stateVersionis its invalidation anchor. The persisted projection cache stores(sessionId, key, ver, seq, val)rows and validatesvalwithstateSchemabefore use; bumpstateVersionwhenever the state fields or fold semantics change. Every unit's state is checkpointed — client-visible and host-only alike. - No wire vocabulary here. The registry exposes only the change feed and the snapshot read face; carriers (api-proxy) mint their own frames (
session/projection) and blocks from them. - Optional capability. Domain plugins register under
ctx.inject(['sessionProjections'], …)so headless assemblies without the registry stay unaffected; carriers usectx.get('sessionProjections')and omit their block/frames entirely when the registry is absent.
Role
This package owns the Service Definition and drive roles of the capability seam: 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.
Model Experience
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.
KV Cache effect
None; projections never assemble or send provider requests.
Known Limitations and Deferred Work
- Every tail page carries every client-visible key — there is no per-key opt-out or lazy-key request shape yet; acceptable while values are UI-scale whole states (a todo list, a goal snapshot), revisit if a domain's value grows large.
- The unit table is process-wide, so key presence is not a per-session capability signal — a key registered by ANY agent preset appears in every session's snapshot, including sessions whose own composition mounts nothing that produces it. A client must read the VALUE (
plan.active, an empty todo list) rather than treat an absent key as absence of the feature; a unit whose empty value is indistinguishable from a real one belongs on the host plane instead, which is whydsh-token-metersits there. - Eager drive touches every unit per event — cheap by construction (deterministic synchronous folds and the same-reference gate), but a hot path would justify per-unit event-type prefilters, addable without contract change.
- Registry cells live in memory only — a restart rebuilds by folding the log on first touch; compositions that mount
dsh-session-projection-cacheseed that fold from persisted rows instead. - Synchronous unit discipline is only partially mechanical —
wire.viewSchema.parserejects a Promise-returning view, but anapplythat blocks or reads torn non-session state is a review concern; the invariant companion documents why no runtime check exists.