* feat(workspace): stop a Session's running work before archiving it Archiving was a pure visibility write, so a Session archived mid-turn kept running out of sight. The Workspace registry now owns archive admission through two Host events: `workspace/session-activity` reports what still runs and refuses the plain archive as `workspace/session-active`; `workspace/session-stop`, requested with `stopActivity`, stops that work the way the user's own stop actions do before the archive write. The API Session Controller reports and stops turns, subagent descendants, and owned jobs and rejects any model step for an archived Session; the Schedule plugin reports and deletes active reminders. The sidebar turns the refusal into a stop-and-archive confirmation listing the Host's answer. * test(workspace): pin the widened archive signature and prove child liveness by heartbeat The Client inspect catalog spec pins archiveSession's new options parameter, and the archive e2e reads the Bash child's liveness from a heartbeat file instead of a pid: the Linux sandbox runs the tool in its own PID namespace, so the pid it reports is not comparable from the test. * test(workspace): cover the archive-admission providers and dialog to the per-file gate Drop the provider's unused disposer and the dialog's unreachable confirm guard, read reminders without a catch that no composition can reach, and add the cases the coverage lane found missing: a stop without a jobs registry, a looping lineage, a descendant whose projections reject, the dialog's Escape guard and non-Error reason, and a family merged by another provider. * fix(workspace): write the archive before stopping, gate subagent lineage, and delete reminders under the schedule barriers Review round on #4765. The registry now writes the archive set before it dispatches `workspace/session-stop`, so the durable set already gates every wake the stops induce; the controller's `agent/pre-step` gate also blocks subagent descendants of an archived Session and contains each cancel or kill on its own. The Schedule plugin's stop listener runs as a management delete in the agent's transaction queue between the same two persistence barriers the `schedule_delete` tool uses. The confirm dialog selects singular or plural English lines and keys list rows by index. Docs: archive-admission section with type-equiv blocks on the workspace subsystem page, write-first order and residual windows in the READMEs and the Agent Note, and the missing zh reminders sentence. * test(web): scope the question composer's New Session wait to the Sessions tree The wide sidebar's New Session button carries the same visible label as a fresh session row, so the unscoped `getByText` either settled on the button before the row existed or hit a strict-mode violation once the row rendered first, which is how the snapshots lane failed on ece68c2ab5. Waiting inside the Sessions tree names the row alone. * fix(schedule): answer archive admission from the owned runtime's fold, not the projection registry The `sessionProjections.snapshot` API is the Client projection view and is not a Host read path (review on #4765). `ScheduleRuntime.activeRecords()` folds the live log the way every drive does, and the archive-admission listeners ask the owned runtime for that fold: a session without a live agent or without an owned runtime has nothing armed to fire, so it reports nothing and has nothing to stop. Covers the runtime-less agent, a fold that turns unreadable at the persistence barrier, and the stopping/faulted runtime answering nothing. * refactor(subagent): report and stop subagent descendants from the Subagent runtime The `subagent` family of archive admission moves out of the API Session Controller into `dsh-subagent`, which records the lineage at delegation and owns the descriptor (review on #4765): `SubagentRuntime` answers `workspace/session-activity` with the running descendants, labelled from each child's descriptor through a live Session observation, and `workspace/session-stop` by cancelling each of them with the parent cause. The controller keeps turns, owned jobs, and the lineage step gate, and composes its provider through `ctx.plugin(SessionActivityProvider)` with declared injections instead of a bare installer call. `dsh-workspace` becomes an optional type-only peer of `dsh-subagent`. * refactor(jobs): answer archive admission for owned jobs from the job registry seam The `job` family moves out of the API Session Controller into `dsh-jobs` (review on #4765): the seam's constructor installs the `workspace/session-activity` and `workspace/session-stop` listeners for every implementation through the abstract `list` and `kill` alone, so the controller no longer knows the jobs vocabulary and keeps only the turn family and the archived-lineage step gate. `dsh-workspace` becomes an optional type-only peer of `dsh-jobs`. * refactor(workspace): leave SessionActivityKindMap empty and let each provider merge its family The workspace package no longer names any activity family (review on #4765): `SessionActivityKindMap` is empty, and each provider merges its own key from a module both its faces import — `turn` in the API Session Controller's types, `job` in the job registry seam's view, `subagent` in the Subagent runtime's control types, `schedule` in the Schedule plugin's types. The confirm dialog imports those modules for its cases and keeps the generic line for any other key; the registry and controller suites merge test-owned families instead of borrowing the providers'. * refactor(agent): report and stop the running turn from the Agent registry The `turn` family moves out of the API Session Controller into `dsh-agent` (review on #4765): the registry's constructor installs the `workspace/session-activity` and `workspace/session-stop` listeners for every Agent it publishes, answering with its own lookup and cancelling the turn with the user cause, and merges the `turn` key from its client-safe types. The controller keeps only the archived-lineage step gate, renamed `ArchivedSessionGate`. `dsh-workspace` becomes an optional type-only peer of `dsh-agent`; the workspace package depends on none of the providers, so the edge stays acyclic. Since #3127 a foreground Bash command is a registered job from its start, so the archive dialog lists it beside the turn; the e2e and its ARIA golden expect that row, and the capture keeps the running job by design.
20 KiB
description, kind
| description | kind |
|---|---|
| Host and Client session control: create, resume, prompt, follow history, and project live session state. | package-reference |
Session Controller
English | 中文
Summary
@deepseek-ai/dsh-api-session-controller owns the Host ctx.sessionController service and the generated Client session, skills, and fileReferences Remote namespaces. It serves Session lifecycle and history, the Host-generation model catalog, human background-job kill, workspace-path opening, user-invocable skill discovery, and Agent-scoped file references. Use it through API Gateway when a Client needs operations addressed by a Session.
Table of Contents
- Use this package
- Client references
- Session media references
- Configuration
- Model Experience
- Known Limitations and Deferred Work
- Dev Note
Use this package
History pages and follow opening snapshots carry one { type: 'event', event: SessionWireEvent } record per durable Session event. The Client retains each accepted record as one durable SessionEventLikeEntry; Assistant token boundaries remain inside the compact stream on assistant/message or assistant/attempt. Tool arguments, result content, failures, and tool/result.data.meta pass through unchanged; the controller does not resolve a Tool definition, run a presenter, or attach UI data.
The Client journal validates current Session event envelopes before publishing follow snapshots, live entries, or history pages. It reuses the browser-safe Session validators for required surface markers, exact replacement endpoints, earlier unique source seqs, embedded Assistant provider metadata, request-header omissions, and tool-error consistency. Invalid records fail without field stripping or normalization; range membership and source existence remain durable-log checks on the Host.
Each endpoint states its activation policy. List reads only stored headers and projection-cache rows: it never calls per-session stat or opens a cold Session body. A current-format cache identity may supply every list hint; a lifecycle-matching predecessor cache may supply only its version-compatible title as a stale display fact, never as an authoritative fold seed. Search, attachment, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; canOpenWorkspacePath() reports native-opening availability without addressing a Session. Cancellation requires live state; queue mutation, model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Prompt rejects content with neither non-whitespace text nor an attachment before resolving the Agent or appending Session events; queue edits accept only non-empty text content. Prompt admission consumes opaque receipts from the injected fileUploads Host service and resolves every same-Agent receipt before sending the complete ordered content list through ctx.attachments. Prompt retries whose requestId is already queued or logged return the original acceptance without inserting another message. Create and fork are the only operations that create a new Agent directly. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. Queue mutation has one narrow exception: a live child whose current projected identity is continuable and comes from its own non-seed suffix accepts the ordinary Edit, Remove, and QueueDock Steer actions across both inbox destinations. One-shot, missing, unknown, corrupt, seed-only, or cold children remain rejected without resume. The skill catalog uses a live Agent when present or the recorded preset's standing scope when cold, so listing never starts an Agent. The authenticated delivery routes use workspaceDesktop() for the serving Host name and file-manager behavior. openWorkspacePath({ path, action: "reveal" }) delegates file-manager navigation to the native adapter; omitting action opens the file-type association, including HTML and SVG. Both operations require the composed filesystem to map the requested Host path to the same canonical process path; unmapped remote paths are refused before a native command runs. session.projections reads one complete baseline through a live-preferred Session observation without activating an Agent. It returns null for a missing Session and serves any registered projection keys. The Client exposes shared values and explicit-read state through projectionsBySession; domains select their own keys. Session-list summaries carry agentAvailable, updated through existing summary and status events independently of durable projections. Initial reads and live projection frames use the same sequence ordering.
Client list rows and resident Sessions use the current sessionListMetadata projection to reject stale blank-session hints; recency is the later of the summary timestamp and the projected last user prompt. When SessionManager creates an instance before its list row arrives, it reconciles blankness with metadata already retained for that Session. An opened conversation is therefore not reused by New Session even when an older list response still marks it blank.
Explicit-id session.create adopts a live Session or resumes a persisted Session while retaining its writer lock. Writer contention returns session/writer-held; callers may try another blank without suppressing unrelated failures. session.list includes persisted blanks using cached metadata, without opening cold log bodies.
Client list refreshes retain unchanged row objects and reuse the items array when order and values match. Each row's retainedBy contains positive local reference-source counts; Host metadata refreshes cannot overwrite them. Cache membership checks use a per-refresh ID set, so reconciliation grows linearly with the current list and retained cache sizes. Host summary updates replace running and Agent availability; local create/fork responses only fill missing metadata on existing rows. Removed ordinary Sessions retain projection stores only when their catalogs contain children.
Background-job rows and observation streams belong to dsh-api-job-controller; the control stream carries projections only.
Projection reads retain loading and failure state independently of their values. Reconnect cancels reads from the previous connection and reloads previously requested projections; live membership updates arrive through the control stream. Parent availability comes from Host summaries and remains unknown until a summary or a successful list baseline establishes it. Address lookup resolves projected children without selecting them or creating scopes.
Accepted prompts and observed running keep their Client display conversion across list refresh, reconnect, and Session-object replacement. The Manager retains these observations by Session id; they do not establish durable history. Drafts, projection stores, and sidebar display rules retain their existing behavior. The blank rollback decision defines retention and late-response handling.
The Client adapter exposes SessionEventStream, a Gateway RemoteJournalStream bound to one ordinary or direct-subagent address. It opens follow before the initial page, publishes only contiguous replace, prepend, append, and settle-assistant changes, and repairs reconnect or sequence gaps through a tail page. Backwards paging has two verbs: loadOlder() pulls one 50-message page, and loadThrough(seq) — the turn-jump loader — loops 200-message pages until the window covers the target seq, lowering a shared target on repeated calls, stopping on a page that makes no progress, and reporting busy through the same loadingOlder snapshot bit. The Web adapter explicitly opts into cursorless Assistant frames: each opening carries the active attempt's startedAfterSeq, nextIndex, and compact stream, and every stream member becomes a Client-only assistant/live-chunk entry ordered between durable cursors. The Host captures a follower-local arrival ordinal with that baseline and suppresses buffered frames at or before the cut; a replacement Agent may restart frame revision at one. A durable assistant/message or assistant/attempt arriving after an active opening stays staged only when its seq follows startedAfterSeq and its Turn and Step match; the matching end type, seq, and index publishes one named settlement delta that retires the attempt's transient rows and adds the durable entry while earlier same-step retries remain visible. Revision, dense-index, or settlement gaps for a known attempt reopen follow, while a controller that missed the start ignores unknown-attempt frames and publishes their durable settlement normally. An abandoned end publishes a settlement delta without a durable entry so its transient rows retire immediately. A durable gap-repair page has no Assistant baseline, so its held notification reopens follow once for a paired page and baseline. Every history record covers exactly its event seq. A business, persistence, or unresolved continuity failure terminates the stream, while only physical carrier loss selects automatic resumption. SessionControlStream is a Gateway RemoteSnapshotStream; every generation opens with a complete process-local baseline, so reconnect replaces projection state instead of treating transient values as durable events. At each ready Host generation, a synchronous Client subscription clears retained projection values and watermarks before refreshing queries and restarting the control stream, including Sessions absent from the control baseline. The first control stream waits for generation readiness, so its opening values cannot precede invalidation. Outstanding list responses from the previous generation cannot republish those values. Within a generation, a delayed control baseline cannot overwrite or clear newer sequenced values, whether they arrived through a live Session's list block, a history page, or a frame; a cached list block, viewed from the projection cache for a cold Session, yields to the connected Session's baseline whatever watermark it carries. The durable inbox projection carries both pending lists through the same cold-read and reconnect path as other projections. Client Agent contexts provide the identity used by the independent fileUpload service; Session objects expose lifecycle, prompt, queue, and history operations rather than file transfer.
The Session object also carries local submission echoes: session.beginSubmission inserts one into SessionSnapshot.pendingSubmissions synchronously, before the caller serializes and prompts, so a conversation UI can show the message on the submit click's own frame. The echo stores ordered image previews and durable file references. Session derives its transcript, queued, or steering placement from the current running state and requested delivery mode, then retains that placement while serialization is in flight. The prompt's requestId is the correlation identity: the Host echoes it as the durable user source's rpcId, including pending messages in the inbox projection. An echo retires one animation frame after its durable event or queue occurrence is observed, immediately when its identified prompt fails or is abandoned, and as failed on disposal. Each retirement fires onRetire exactly once; an observed retirement includes the ordered durable attachment references so the composer can release successful cards while preserving failed drafts. Echoes are Client memory only; reload and reconnect rebuild the conversation from durable events alone.
The user-invocable skills/list metadata includes the winning provider’s optional instruction-file path. The composer can preview that file without loading every skill body or activating a cold Agent.
Fork copies the exact inclusive event prefix selected by atSeq, including a cut inside an open turn. The child records its inherited marker before synthetic fork results and closing events. Omitting atSeq selects the latest completed turn and its standalone tail, stopping before the next turn or queued input; a nonexistent event is rejected. The chat action selects a completed turn.
A resume blocked by an existing write handle returns session/writer-held with the Session id; other resume failures retain gateway/internal.
loadThrough(seq) retains older pages privately until its shared target is covered or loading ends, then publishes the successful pages as one ordered prepend. Live events remain visible while history loads. A later page failure retains the successful prefix; replacing the history window discards buffered pages from the replaced window. Ordinary loadOlder() still publishes its single page directly.
Queue edits replace pending content with non-empty text only.
Attachment authorization reads declared content fields of built-in Session events and completed assistant stream blocks, including flat V4 tool-role messages. Unknown event payloads and unrelated fields cannot authorize attachment reads.
The controller composes ArchivedSessionGate through ctx.plugin: it loads once the Agent registry, Session store, and Workspace registry exist and unwinds with the controller. Its agent/pre-step listener rejects a step proposed for an archived Session or for a subagent descendant of one — read from the Session header's lineage fields, never a fork — so a late waking delivery ends its turn as blocked without a model request; unarchiving lifts the gate for the whole lineage. The work an archived Session still runs is reported and stopped by its owners through the Workspace registry's archive admission (seam): the running turn by the Agent registry, owned jobs by the job registry seam, subagent descendants by the Subagent runtime, reminders by the Schedule plugin; this controller reports nothing itself.
Client references
sessions.retain(target, { source, signal? }) immediately acquires one exact Client generation and starts its shared initial history opening. The target is a known Session id or a durable direct-parent subagent address; the Host validates an explicit address when history opens. The returned reference supports idempotent release() and Symbol.dispose; its ready Promise follows the shared Session.open() result and resolves to the exact binding when that attempt settles, including when a Remote failure is represented by openState: 'error'. It rejects when Session.open() rejects, its waiter is cancelled, or the reference is released early. Cancelling one waiter does not cancel another owner's opening. sessions.using(target, options, operation) waits for that settlement, holds its reference until the callback settles, and propagates rejected readiness and callback failures.
References keep local Session data, scoped Contexts, and history streams alive, not Host Agents. Ordinary Session retention never adds a catalog row. Retained subagents with a known direct-parent address keep a fallback row and notify list readers even before the parent catalog arrives; these rows do not join Host-list membership. Fork-title assignment sends rename directly and applies the returned title projection without retaining the child or opening its history. Final release withdraws the generation before teardown; later acquisition can create a new generation with the same id. binding(id) and scope(id) only borrow an existing generation. retainInfo(id) observes stable read-only source counts independently of catalog membership and performs no history I/O. Consumer source keys are declaration-merge extensible; navigation and completion acknowledgement belong to UI consumers, not this Controller. See Client Session references for ownership and teardown rules. An unknown-mode subagent address permits history reads with direct-parent validation. Successful descriptor restoration determines the displayed mode; failures remain local to the child, and control requests still require a confirmed continuable identity.
Session media references
SessionMediaReferences mounts GET|HEAD /api/file?path=<absolute path> on the authenticated connection.fetch channel when connection, fs, and attachments are composed. It reads ordinary files through ctx.fs, including temporary paths outside registered workspaces and files in remote providers. Neither directory containment nor MIME categories restrict access; mime-types supplies the response type, with application/octet-stream for unknown extensions. GET reuses readBytes for preflight and ongoing byte limits; HEAD reads metadata only. All files use ctx.attachments.imageLimits.maxImageBytes (normally 20 MiB); exceeding this limit returns 413. Responses contain the complete file, ignore Range, and carry private, no-store, nosniff, and a sandbox CSP so directly opened HTML/SVG cannot execute with the API origin. The Client rewrite lives in ui-chat (AssistantMarkdown); audio/video responses are available, while Markdown audio/video player nodes remain separate work.
workspacePathApplications({ path }) returns the serving desktop’s file handlers after the same filesystem mapping check used by openWorkspacePath. An optional application in the open request selects a currently registered handler without changing the system default. Application names, default selection, icons, and platform coverage come from native-command. These operations do not activate an Agent or append Session events. Native failures return bounded messages; original command errors remain Host-side causes.
Configuration
| Field | Default | Meaning |
|---|---|---|
nativeOpen |
platform-detected | Whether Session workspace paths can be handed to a native desktop opener |
The generated configuration catalog is the exhaustive source for accepted fields and their JSDoc.
Model Experience
None, as invoked Agent commands own any model-visible effect.
KV Cache effect
No direct effect; model requests remain owned by the Agent and LLM packages.
Known Limitations and Deferred Work
- The image byte cap does not validate decoded dimensions or pixel count.
- A failed follow resumption remains visible to the caller instead of retrying indefinitely.
- The raw browser upload is one streaming HTTP request without resumable offsets; a retry sends the file again from byte zero.
- File-reference completion uses the shared Agent lookup and can resume a cold Session; the
skills/listcatalog is the non-activating alternative for skill metadata. - Accepted/running display memory lives only in Client memory and is lost on page reload.
- That memory is not shared between tabs: one tab can show a converted row while another still shows
New Sessionfor the same Session.
Dev Note
Working context for maintainers — click to expand
None.
Runtime invariant: No companion is published. Every page and frame is checked against the addressed durable Session.