# Conflicts: # .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml # docs/architecture.i18n.yaml # docs/architecture.md # docs/architecture.zh.md # docs/cookbook/extension-cookbook.i18n.yaml # docs/cookbook/extension-cookbook.md # docs/cookbook/extension-cookbook.zh.md # docs/core-data-structures/llm-streaming.i18n.yaml # docs/core-data-structures/session.i18n.yaml # docs/defensive-patterns.i18n.yaml # packages/acp/acp/README.i18n.yaml # packages/client/runtime/README.i18n.yaml # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-goal/README.i18n.yaml # packages/compact/compact-basic/README.i18n.yaml # packages/context/README.i18n.yaml # packages/context/README.md # packages/context/README.zh.md # packages/context/session-reference/README.i18n.yaml # packages/context/session-reference/README.md # packages/context/session-reference/README.zh.md # packages/context/tmux-context/README.i18n.yaml # packages/core/session/README.i18n.yaml # packages/core/session/README.md # packages/core/session/README.zh.md # packages/goal/command-goal/README.i18n.yaml # packages/goal/goal-session/README.i18n.yaml # packages/goal/goal-session/README.zh.md # packages/guard/README.i18n.yaml # packages/guard/README.md # packages/guard/README.zh.md # packages/guard/repeat-tool-guard/README.i18n.yaml # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.md # packages/host/apiproxy/README.zh.md # packages/plan/plan-mode/README.i18n.yaml # packages/sdk/sdk-client/README.i18n.yaml # packages/sdk/sdk-client/README.md # packages/sdk/sdk-client/README.zh.md # packages/session-persistence/session-persistence/README.i18n.yaml # packages/subagent/subagent-dsh-sdk/README.i18n.yaml # python/sdk/README.i18n.yaml
@deepseek-ai/dsh-session-projection
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.
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 unit whose state reference changed, per committed event, carrying the schema-validated view and the causing seq. Effect-tied likeregister.ctx.sessionProjections.snapshot(session): ProjectionSnapshotOne consistent synchronous cut over every registered unit —{ asOfSeq, values }withasOfSeq= the seq of the last event every value reflects (-1for an empty log).
Key Types
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.
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, 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 — it keeps every transition trivially cheap and every served value self-describing (last-wins for consumers).
- Synchronous unit discipline.
init/apply/viewMUST be synchronous; carriers readsnapshot()in the same tick as their page slice, which is what makesasOfSeqone consistent cut. An accidentally-asyncviewreturns a Promise, which fails the boundaryschema.parseloudly. - State is plain JSON,
stateVersionis its invalidation anchor. The persisted projection cache stores(sessionId, key, ver, seq, val)rows; bumpstateVersionwhenever the state shape or the fold semantics change so stale rows are discarded instead of forward-applied into garbage. - 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 seam. 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 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.
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 registered 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.
- Eager drive touches every unit per event — cheap by construction (whole-value rule, 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 — the boundary
schema.parserejects a Promise-returningview, but anapplythat blocks or reads torn non-session state is a review concern; the invariant companion documents why no runtime check exists.