git mv the 12 packages from session-persistence/, session-projection/, session-title/, and telemetry/ into one session/ group per the regrouping RFC; merge the four group READMEs into one bilingual triplet; rewrite the group segment in tsconfig references (intra-group references shorten to ../<pkg>), tsconfig.base.json paths/globs, knip.json keys, vitest include, gate scripts, and authored doc/note citations; regenerate module graph, doc graphs, catalogs, and the lockfile importer keys. No npm names change. Full unit suite: 8779 passed; the 18 reported failures reproduce as env flakes (ambient-proxy IPv6 tunneling, watched-dir inotify timeouts under parallel load) — each passes in isolation with NO_PROXY set, matching their known pre-existing behavior on master.
@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.