@deepseek-ai/dsh-client-runtime
English | 中文
Client cordis boot and React-free object services: SlotsService wraps SlotCore and supplies renderer data sources; SessionsService owns Session objects, list/scope/history state; WorkspacesService depends on SessionsService and owns Workspace objects, list/actions, default-target derivation, and the New Session blank-reuse entry (connectWorkspace). The runtime fans the shared Host stream into both managers. Client sessions are always Host-born (Session+Agent+cwd in one session.create); the client holds no pre-entity session state — a session's Agent scope (the client mirror of host dsh-scope, keyed by the shared agent/session id) is born when its row enters the list mirror and dies with the prune. Contract: api-contracts v3 §4. Generic full-log domain values such as todos and title live in the per-session projection store: history-tail projections seed it, session/projection frames update it under higher-seq-wins, and consumers read keys through useProjection. Durable ConversationSnapshot.metrics instead comes from the separate history-tail value and live session/metrics frames because point-in-time token-meter pressure can advance at the same durable log revision; only nondecreasing log and projection revisions are accepted. ConversationSnapshot.modelRequestContextWindow separately retains capacity from the latest session/model-request observed on the current mux connection. A later request replaces or clears that value, while session/subscribed clears both metrics ordering and capacity; reconnect, restore, and a new subscription therefore show no percentage until another request is observed. Missing metrics remain null rather than being inferred from the visible node window.
Workspace and Session lists
Workspace and Session lists have independent monotone pending → ready baseline phases and separate refresh activity/error state. Incremental upsert/removal frames and unary mutation echoes arriving during a list request replay over its response. The first successful baseline establishes Host order; later refreshes update rows and membership without changing the relative order of identities already shown. Removed Workspace ids retain process-local tombstones so late changed frames cannot resurrect them; reconnect still takes workspace.list as the baseline. Workspace recency is derived only after both baselines are ready and never changes Workspace list order.
WorkspacesService.delete(workspaceId) removes the registration from the client projection after the successful unary response; the matching host/workspace-removed frame is idempotent and synchronizes other tabs. Session state and the current Session selection are independent, so accounted Sessions immediately project under Ungrouped after their Workspace disappears.
SlotsService gives the renderer separate bare observables for useSessions and useWorkspaces; web-react creates the hooks. Workspace business state does not enter SessionListState or an entry store.
New Session and the blank mirror
WorkspacesService.connectWorkspace(workspaceId) resolves the session a New Session flow lands in: it reuses the workspace's existing blank session from the list mirror (blank && cwd == workspace.path) or calls session.create({workspaceId}), returning the session id for the caller to open. SessionSummary.blank mirrors the host's derived empty-log bit and only ever lowers on the client: seeded by session.list / the host/session-added frame, flipped false by the first ACCEPTED local prompt() (on the RPC success response — acceptance proves the user message is in the host log; a rejected first prompt keeps the session blank and reusable) and by any running: true status frame, re-aligned by every list re-pull. List surfaces hide blank rows; the store carries every row. SessionsService.create accepts an optional caller-preallocated SessionId and throws SessionCreateError (carrying requestedSessionId) on failure.
Code Mode sub-dispatch index
ConversationSnapshot.codeDispatches groups a run_code call's sub-dispatches under their parent callId, in start order, using the native call-block shapes: a tool/code-dispatch-start event lands the RunningToolCall form (rows derive the running ring from the shape) and its tool/code-dispatch settlement replaces it in place with the ToolResultNode form, callTime carrying the paired start's time. A settle whose start fell outside the replay window appends directly with callTime: null (duration unknown — never a fabricated zero). Live mux frames and history replay build the identical index; sub-calls never join the surface nodes flow; per-parent array and map references are memo-stable across unrelated snapshot swaps.
Session title projection
SessionManager retains the generic per-session projection store independently of Session-instance arrival, so live title frames can update list rows before a conversation opens. A subscription baseline truncates projection rows beyond lastSeq; the next history-tail baseline re-seeds durable values, and explicit Session removal clears the store. The client-facing SessionSummary.title is therefore only the actual durable title; displayTitle always falls back through the cwd basename and session id while the title key is absent.
Session model selection
Each resident Session owns a modelSelection snapshot containing the current provider/model target, provider-grouped directory, provider-local failures, and the idle/loading/ready/selecting/error state. History establishes or refreshes the current target, opening a selector refreshes the directory, and selection failures preserve the last target and usable groups. Directory and selection operations share a monotonically increasing generation so an older response cannot overwrite a newer selection. A reconnect rebuild restores the target reported by the Host without replacing unchanged selection substructure.
Model Experience
None, as the session object layer selects the provider/model route used by a later Host request but adds no model-visible content.
KV Cache effect
Changing the target can change or invalidate provider-side cache reuse; this package does not alter the prompt prefix itself.
Known Limitations and Deferred Work
loader.unloadis a stub (throws not-implemented) — the full chain (fiber dispose → registration cascade → style removal) lands with the HMR project.- Scope teardown is stage-driven, single-occupant today — the staged session follows
list.currentexactly (staging is the open signal: the event window opens ⟺ the session is on stage); a removed-while-staged session's scope survives frozen until the stage moves on, not until true observer count reaches zero. Resolution (binding()/scope()) is pure addressing, render-safe; the render layer reads the current bundle through thecurrentProvideInfoobservable. The staged state can widen to a multi-pane list when concurrent panes land. - Value imports of this package from plugin bundles must use the
/clientsubpath — the bare package name is not in the loader externals table and inlines a second module instance, whose private scope-tag Symbol never matches (the empty-state P0 postmortem).