@deepseek-ai/dsh-session-projection
English | 中文
Session-projection Service Definition and drive registry. It owns ctx.sessionProjections, the registry that folds committed session events through every registered projection 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. Registrants with the same key andstateVersionshare its cells; an incompatible version or invalidstateVersionthrows. The registration is an effect on the calling fiber, so the last unload removes the key and its cached cells from subsequent drives and snapshots.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(), apply(state, event), wire?, stateVersion }: a synchronous state-driven computation unit.wiresuppliesviewSchemaandview; omitting it makes the unit host-only.
Contract
- The framework drives, the domain computes. The registry subscribes to
session/eventonce and runs every unit'sapplyover each committed event. 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, foldsinitover 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. - Whole-value event rule (load-bearing). A state-carrying log event MUST carry the complete post-change state, never a bare delta.
- 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. - Dependency follows use. A domain that requires projection state declares
sessionProjectionsas a Cordis service dependency; optional contributors may register underctx.inject(['sessionProjections'], …). Carriers usectx.get('sessionProjections')and omit their block or frames 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 — bounded by each unit's transition 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.