
Security News
Deno Goes the Way of the Dinosaur
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.
dsh-session-toolkit
Advanced tools
Unified local plugin: session identity, global prompt, auto-resume, session log reposition, peer message (5 plugins consolidated).
A consolidated plugin toolkit for the DeepSeek Harness. Five previously separate local plugins — session identity, global prompt, session auto-resume, Session-log button relocation, and peer-session messaging — merged into a single installable package that ships in the official bundle form (dsh.bundle.patch) and installs with dsh plugin add; it also includes a Prompt Dedup feature.
Current version: 1.0.0, verified against DeepSeek Harness dsh-v0.2.0-rc.1 — which is also its minimum supported version (contract surfaces + the full gate suite — not a complete functional regression): 0.1.6 and earlier fail loudly by design rather than degrading silently (see Compatibility).
Per-session persona prompt injected into that session's system prompt (independent section session-identity, order 40, resolved per agent at every assembly), with a default identity plus per-session overrides. UI provides an identity dialog (enable switch, 4000-char soft limit, save/reset, edit default, inherit default) and status buttons in both conversation.session.header.actions (id session-identity, order 40) and conversation.input.left (id session-identity-input, order 40). The dialog card can be dragged by its title row: the offset is clamped to the viewport on every move and re-clamped on window resize, and when the card is larger than the viewport each axis stays movable, so every edge remains reachable. The position is not persisted — it resets when the dialog is closed.
A settings page (settings.section, id global-prompt, order 30) rendered as Tabs (Global / Per workspace / Groups). The Global tab injects one prompt into every conversation's system prompt (section global-prompt, order 50); the Per workspace tab injects per-workspace prompts (section workspace-prompt, order 60); the Groups tab injects per-group prompts (section group-prompt, order 55 — see Group Prompt). All five prompt sections — session-identity (order 40), peer-inbox-discipline (order 45, see Peer Messaging), global-prompt (order 50), group-prompt (order 55) and workspace-prompt (order 60) — are registered with interpolate: false, so {{...}} inside prompt text and referenced files stays literal — user content is never rewritten, and an unregistered {{name}} can never fail assembly. On kernels older than 0.1.6 (no per-section interpolate flag) lib/prompt-literal.js falls back to escaping { runs in the assembled text.
The plugin also exposes a read-only state route GET /api/session-toolkit/state (live workspaces + referenced-file read status + the live session list the group picker needs; any other method answers 405), which is how the settings page reads those runtime projections — they no longer occupy a settings namespace and are never persisted.
Per-workspace prompt injected for sessions whose cwd prefix-matches a configured workspace directory (that directory plus its subdirectories). The workspace list is derived from active sessions' cwd (ctx.agents.roots()), deduplicated and counted. When several enabled workspaces prefix-match a session's cwd, the most-specific (longest matching path) one wins. removed records paths the user removed so the active-workspace sync never re-adds them. The workspace row's enable switch is live-save (persisted immediately); the Save button only persists the prompt content + referenced files.
A tab of its own on the same settings page: Groups inject a prompt into arbitrary sessions across workspaces. Each group is a named entry holding an enable switch, a prompt body, a referenced-file list and an explicit member list of session ids; a session is matched by its own session id, not by its directory — which is exactly what lets one group span several workspaces. Membership is an additional injection layer: it never replaces the global or the per-workspace prompt. Every enabled group whose sessions contains the current session id injects into the section group-prompt (order 55, between global 50 and workspace 60), and several matched groups stack in group-key order with one blank line between their blocks. The picker lists live sessions only and never auto-adds one: a brand-new session joins no group until you tick it, and a session still listed in a group but currently offline is shown as not online (removable). When the assembly context carries no session id, the section injects nothing.
Global, workspace and group prompts can each reference a list of files. Every assembly re-reads each referenced file (UTF-8, content cached by mtimeMs + size, so an unchanged file is not re-read) and injects it after the prompt text. Byte budgets are enforced (globalPrompt.maxFileBytes / maxTotalBytes, defaults 256 KiB / 1 MiB): an oversized file is skipped rather than blocking assembly. A read failure is skipped too, and both cases are reported in the UI with the reason. Supports plain text and markdown. Per-file read status is a host runtime projection delivered to the UI by the read-only route GET /api/session-toolkit/state (ok: N chars / fail: reason / pending); the browser half polls it only while the settings page is open. The status never touches a configuration file, and its projection object is only replaced when the content actually changed.
Sessions with the per-session switch on are resumed automatically after a GUI restart, through the official resume path (ctx.sessionController.resolveAgent) when it exists — that is what restores the session's own model selection (via installSelection) in addition to the preset mount, and it validates subagent ownership and de-duplicates concurrent resumes. Older kernels without sessionController fall back to ctx.agents.resume plus a preset mount, carrying the default model from agentDefaultModel. Switching a session on resumes it immediately (false→true edge). Filters: switch on, top-level only (no subagent origin, no delegationDepth > 0, no parentSession), non-blank (eventCount !== 0, snapshot shape). Concurrency-bounded (default 3, configurable via autoResume.concurrency) with per-item failure isolation and an in-flight set that prevents duplicate resume.
Shadows the official entry in conversation.session.header.utilities (same id session-log-download, priority −1, cell-shadowing) and registers a copy in conversation.session.header.actions (id session-log-download-moved, order 41), reusing the official sessionLogDownload controller (ctx.get('sessionLogDownload')) so download behavior stays identical to stock. The copy mirrors the 0.1.6 official surface — a "⋯ More actions" menu whose single item triggers the shared download dialog (localized through this plugin's own locale namespace) — and is a frozen replica: a stock upgrade that changes that UI must be synced by hand, and the shadowing registration must be re-checked whenever the official entry gains new menu items.
The host plane registers two more tools on the same plane as send_to_session / list_sessions:
create_session — create a new top-level session (a chat window in the GUI's left navigation). Both cwd and prompt are required: cwd must be an absolute path (a session without cwd never enters the host list) and prompt is the new session's first message. A successful create produces a real user message (it genuinely runs one model turn and consumes one call); by kernel design any session with events is persisted, so this tool deliberately offers no "register-only, never speaks" ephemeral session. An optional title sets the title immediately and pins it. The result carries sessionId, cwd, status, title and notes.rename_session — retitle a live session. A rename pins the title so automatic title generation no longer overwrites it. The target must be a top-level session and currently live: a subagent target (origin=subagent or delegationDepth>0) is refused explicitly rather than silently rewritten.Both return structured results and never throw an uncaught exception from the tool body: success is { ok: true, … }, failure is { ok: false, error: '<code>', errorText: '<original reason>' }. Codes: MODEL_UNAVAILABLE / MODEL_SELECTION_FAILED / MODEL_SELECTION_INVALID / EMPTY_CWD / CWD_NOT_ABSOLUTE / EMPTY_PROMPT / PROMPT_TOO_LONG / PRESET_RESOLVE_FAILED / CREATE_FAILED / CREATE_UNAVAILABLE / CREATE_NO_AGENT / EMPTY_TARGET / EMPTY_TITLE / SESSION_UNAVAILABLE / TARGET_IS_SUBAGENT / TITLE_SERVICE_UNAVAILABLE / UNEXPECTED.
(A missing prompt is rejected by the kernel's tool-argument validation at the framework layer — the kernel turns that into a tool-error result, and the exception never passes through this plugin's code; only a prompt that is present but blank is answered with this plugin's EMPTY_PROMPT. Neither creates a session.)
Two honest disclosures:
prompt actually appears in the left navigation depends on the kernel genuinely starting a turn (navigation filters on the "blank session" bit, which only flips on turn/start; followup merely enqueues and wakes the driver). The result therefore draws no "it is in the navigation" conclusion, and notes says so.agentPresets service exists but default-preset resolution fails, the tool returns PRESET_RESOLVE_FAILED instead of handing back a crippled session with no preset mounted; when the service is absent entirely that is a legitimate degradation, and the session is still created with an explanatory notes entry.send_to_session / list_sessions tools on the host plane (session addressing by id or workspace path, wakeup delivery) plus a "copy session ID" button in both conversation.session.header.actions (id copy-session-id, order 30) and conversation.input.left (id copy-session-id-input, order 30). Outgoing message content is converted to plain text (toPlainText) before delivery so recipients see tidy text rather than raw markdown. wakeup has two distinct delivery paths: with wakeup: true (steer) a message delivered while the target is executing enters the next step boundary of the running turn — it neither starts a new turn nor interrupts the current step — and a message delivered while the target is idle starts a turn immediately; with wakeup: false (inject) the message is only deposited and nobody is woken. Two windows still queue rather than splice: the target is idle at delivery (no step boundary exists to splice into, so the message is taken at the next turn boundary) and the kernel's wakingAfterAbort re-classification, which silently degrades steer to next-turn while the previous activity is being cancelled. steer is therefore not a promise that a message never waits — whether a message was actually spliced is decided by three things together: target was next-step, a turn was running at delivery time, and no new turn/start appeared between delivery and consumption.
Performs cross-section line-level deduplication across the identity / global / group / workspace system-prompt sections (session-identity, global-prompt, group-prompt, workspace-prompt, orders 40/50/55/60). promptDedup.enabled is on by default (disable only by setting it to false). Splits each section on \n and keeps only the first occurrence of each identical original line (a single seen set spans all four sections, so intra-section self-duplicates also collapse); duplicate lines in later sections are dropped. Blank lines (including whitespace-only lines) never take part in deduplication — they are markdown's paragraph/list separators, and treating them as duplicates silently collapsed every section after the first one. Every section's unique content is preserved. It does not parse {{name}} placeholders (single-line complete groups, never split by line), does not break markdown, never sets complete, and never touches harness-owned sections (harness:identity / deployment:persona / tool sections). Mechanism: subscribe to the system-prompt/assemble waterfall on the plugin's root ctx, await next(), then deduplicate sections on the returned result before returning it.
The plugin is verified against DeepSeek Harness dsh-v0.2.0-rc.1 — its minimum supported version (contract surfaces + the full gate suite, not a complete functional regression; the declared dependency ranges, ^0.2.0-rc.1 for the harness packages and ^3.18.4 for schemastery, apply): 0.1.7 replaced the settings model (a plugin registering a namespace, read through ctx.settingsScope.bind) with entry Config volatile fields read through ctx.configForms, so the whole user-data surface moved (see Settings fields). On 0.1.6 and earlier the client entry stays at pending (waiting for service: configForms) and web boot reports Failed to load plugins — a loud failure rather than a silent degradation; upgrade the harness or uninstall this plugin. The existing fallbacks for interpolate: false and ctx.sessionController.resolveAgent are unchanged.
@deepseek-ai/cordis 4.0.4 and @deepseek-ai/schemastery 3.18.4 (the versions dsh-v0.2.0-rc.1 vendors). The @deepseek-ai/schemastery floor is ^3.18.4: volatile() exists only from 3.18.3, and earlier builds make the Config constructor throw. The plugin loads through the cordis harness and registers as a bundle via dsh.bundle.patch.Config's volatile() fields read live through .get(), with ctx.on('loader/volatile-update', …) notifying after a commit (no remount); ctx.get('settings').update('session-toolkit', patch) as the host write path back into the entry config (used by the workspace auto-add); ctx.systemPrompt.section({ name, order, text, interpolate: false }); ctx.agents.{ get, resume({ resumeSessionId, agentOptions, setup }), roots, requireInitiator }; ctx.sessionController.resolveAgent(sessionId); session.header fields (cwd, origin, delegationDepth, parentSession, agentPreset; there is no seedLength); ctx.inject(names, cb) for late-arriving optional services; ctx.get('webServer').register({ kind: 'exact', path, handler }); @deepseek-ai/dsh-tools defineTool + tools.register(); and ctx.get('agentDefaultModel'), sessionPersistence, sessionTitle, workspaceRegistry, sessionLogDownload, timer, on, effect.window.__ModuleLoader__.load({ id, factory }); ctx.get('slots') → slots.register(meta, render) / slots.inject(name, fn) with lower-priority shadows; ctx.get('configForms').get('session-toolkit') → form { getSnapshot()/.value/.status, subscribe, set(field, value), unset(field), mutate(ops, expectedRevision) }, whose host-validated writes land in the active profile's cordis.patch.yml; ctx.get('locale') → register(ns, { zh, en }) / bind(ns); and the timer client service (ctx.timeout). The bundle's runtime requires resolve against the platform seed words (react, react/jsx-runtime, @deepseek-ai/dsh-client-store, @deepseek-ai/dsh-client-ui-primitives, …).The plugin's host-side Config validates the entire configuration tree with schemastery as soon as the plugin loads; resolution order is schema default → profile patch (user layer), both resolved on the host before the plugin sees them.
From DSH 0.1.7 on, settings projects only fields declared volatile(), and a settings surface is identified by exactly two facts: the id of the edited entry (session-toolkit) and that entry's Config. Consequently:
volatile fields: the browser half reads and writes the same entry Config through ctx.get('configForms').get('session-toolkit'), and host-validated writes land in the active profile's cordis.patch.yml (there is no settings.yaml namespace and no settingsScope service any more)..get() wherever it needs a value: a committed volatile update does not remount the plugin, so the identity and global-prompt sections pick the new text up on the next assembly, and auto-resume resumes a newly enabled session immediately through loader/volatile-update.volatile: they are still visible in the settings form, but editing them goes through the normal cordis configuration lifecycle.GET /api/session-toolkit/state and never land in a file or in a form.lib/index.js composes eight feature modules (identity.js, global-prompt.js, auto-resume.js, peer-message.js, session-admin.js, log-reposition.js, prompt-dedup.js, prompt-literal.js). inject is the deduplicated union of module dependencies; each module's apply runs inside a safe() guard so one failing module never takes the whole package down. Every contribution is lifecycle-bound (ctx.effect for prompt sections and HTTP routes, plugin-fiber registrations for tools; timers go through the timer service). global-prompt.js owns the globalPrompt, workspacePrompt and groupPrompt volatile fields, the three prompt sections (global-prompt order 50 / workspace-prompt order 60 / group-prompt order 55), the readPromptFiles helper (mtime/size-cached live read; the cache is namespaced per section, so one section's eviction never drops another section's entries), the live runtime projections (active workspaces plus the live session list behind GET /api/session-toolkit/state), and the config write that adds newly discovered workspace paths back into the entry (ctx.get('settings').update('session-toolkit', …)); prompt-literal.js is the pre-0.1.6 fallback for literal prompt rendering.client/client.js is a single window.__ModuleLoader__.load bundle; the four UI modules live in IIFEs and are collected into one apply that registers all slots in order (guarded per module). All UI uses React.createElement; styles are injected as data-plugin style tags with theme CSS variables and dark-mode coverage; no global DOM manipulation. The global-prompt module renders the Tabs (Global / Per workspace / Groups) page plus a reusable FileRefsPanel (add/remove referenced files, per-file status from the polled GET /api/session-toolkit/state projection) and the group editor (add/rename/remove groups, enable switches, prompt body, referenced files, and a session picker that lists live sessions as title ?? short id and marks saved-but-offline members not online).| Slot | Id | Order / priority | Feature |
|---|---|---|---|
settings.section | global-prompt | order 30 | Global + workspace + group prompt page (Tabs) |
conversation.session.header.actions | copy-session-id | order 30 | Copy session ID |
conversation.session.header.actions | session-identity | order 40 | Identity button |
conversation.session.header.actions | session-log-download-moved | order 41 | Session log download |
conversation.input.left | copy-session-id-input | order 30 | Copy session ID (tool row) |
conversation.input.left | session-identity-input | order 40 | Identity button (tool row) |
conversation.session.header.utilities | session-log-download | priority −1 (shadow) | Hide stock button |
sidebar.workspaces.session.menu.item | dsh-session-toolkit.copy-session-id | order 500 | Copy session ID (session row ⋯ menu) |
The entry id is fixed at session-toolkit; the volatile() fields below are exactly the data the settings page reads and writes. They are schema-validated and land in the active profile's cordis.patch.yml. The field name is the config path (identity.sessions etc.), and the browser half addresses the same paths through configForms.get('session-toolkit').
| Field | Schema | Notes |
|---|---|---|
identity.default | {enabled: boolean, text: string} | Default identity. Resolution: session record → default → empty. Disabled or empty entries inject nothing. |
identity.sessions | Record<sessionId, {enabled, text}> | Per-session identity, clipped to identity.maxText (8000 chars, token guard). |
autoResume.sessions | Record<sessionId, boolean> | Per-session "resume after restart" switch; absent keys mean off. |
globalPrompt.{enabled,content,files} | {enabled: boolean, content: string, files: string[]} | Injected into every conversation when enabled. files is the list of referenced files appended at assembly (live read with mtime/size cache; failed or oversized files skipped). |
workspacePrompt.workspaces | Record<path,{enabled, content, files: string[]}> | Per-workspace prompt. A session gets the most-specific (longest matching path) enabled workspace whose directory prefix-matches its cwd. |
workspacePrompt.removed | string[] | Paths the user removed, so the active-workspace sync never re-adds them. |
groupPrompt.groups | Record<groupKey,{enabled, content, files: string[], sessions: string[]}> | Named groups (the dictionary key is the group name). A group injects its prompt (section group-prompt, order 55) when enabled and its sessions contains the current session id — membership is by session id, so a group can span workspaces, and it is additive (the global and per-workspace prompts still apply). v1 lists live sessions only and never auto-adds one. |
client.* | 2 UI knobs (see the table below) | Browser-half runtime parameters (char limit, copy feedback). |
Runtime projections (never persisted, not configuration): live workspaces [{path, sessionCount}] aggregated from ctx.agents.roots() (each agent's session.header.cwd, deduped and counted; never sourced from workspaceRegistry, which is not visible in this plugin's scope), the live session list [{id, cwd, title}] the group picker consumes (the title comes from the optional sessionTitle service and is null when that service is missing or throws — reading it must never fail the route), and referenced-file read status Record<global\|path, [{filePath, status: 'ok'\|'fail', charCount?, reason?}]>; both reach the settings page through GET /api/session-toolkit/state.
The plugin exposes a single Config (schemastery schema) with per-feature keys. Defaults equal current behavior; override them via the plugin row's config in cordis.yml / cordis.patch.yml without touching code. The .volatile() keys (user data plus client.*) are exactly what the settings page reads and writes, and the browser half sees the same resolved values through configForms.get('session-toolkit') (see Configuration and the settings data plane).
- id: session-toolkit
name: 'dsh-session-toolkit'
config:
identity:
maxText: 8000
sectionOrder: 40
default: # volatile: default identity
enabled: false
text: ''
sessions: {} # volatile: Record<sessionId, {enabled, text}>
globalPrompt:
sectionOrder: 50
workspaceSectionOrder: 60
maxFileBytes: 262144 # per referenced file; oversized files are skipped and reported as fail
maxTotalBytes: 1048576 # across all referenced files of one section
enabled: false # volatile: global prompt switch
content: '' # volatile: global prompt body
files: [] # volatile: referenced file list
workspacePrompt: # volatile: per-workspace prompts
workspaces: {} # Record<path, {enabled, content, files}>
removed: [] # paths the user removed
groupPrompt:
sectionOrder: 55
groups: {} # volatile: Record<groupKey, {enabled, content, files, sessions}>
autoResume:
concurrency: 3
sessions: {} # volatile: Record<sessionId, boolean>
promptDedup:
enabled: true # cross-section line-level dedup across identity/global/group/workspace; default true (disable only by setting false)
client:
identityCharLimit: 4000 # volatile: the 2 keys below are read by the browser half
copyFeedbackMs: 1600
| Key | Default | Meaning |
|---|---|---|
identity.maxText | 8000 | Identity text clip limit (chars, token guard). UI soft limit is client.identityCharLimit (4000, editor); host hard-clip is this value (8000). |
identity.sectionOrder | 40 | System-prompt order of the identity section. Migration: users who explicitly pinned identity.sectionOrder: 55 must set it to 40 to keep the identity-before-global ordering. |
globalPrompt.sectionOrder | 50 | System-prompt order of the global prompt section. |
globalPrompt.workspaceSectionOrder | 60 | System-prompt order of the workspace prompt section (placed last). |
groupPrompt.sectionOrder | 55 | System-prompt order of the group prompt section (between global 50 and workspace 60; several matched groups are concatenated in group-key order with one blank line between blocks). |
globalPrompt.maxFileBytes | 262144 | Per referenced file byte limit; an oversized file is skipped (status fail) instead of blocking assembly. |
globalPrompt.maxTotalBytes | 1048576 | Combined byte budget for all referenced files of one section. |
autoResume.concurrency | 3 | Max in-flight resumes during startup restore. |
promptDedup.enabled | true | Cross-section line-level deduplication across the identity/global/group/workspace system-prompt sections (on by default; disable only by setting it to false). When on, each identical non-blank original line across the four sections keeps only its "first occurrence" (a single seen set spans all four); duplicate lines in later sections are dropped, while blank lines are always preserved (they are markdown's paragraph/list separators). Every section's unique content is preserved. It does not parse {{name}} placeholders, does not break markdown, never sets complete, and never touches harness-owned sections. |
identity.default / identity.sessions | empty | User data (volatile): default identity and per-session identities. Written by the "Session identity" settings page; also editable directly in the profile patch. |
globalPrompt.enabled / .content / .files | off / empty | User data (volatile): global prompt switch, body, and referenced file list. |
workspacePrompt.workspaces / .removed | empty | User data (volatile): per-workspace prompts plus the removed-path list. The active-workspace sync adds newly discovered paths to workspaces (through ctx.get('settings').update); paths listed in removed are never re-added. |
autoResume.sessions | empty | User data (volatile): per-session "resume after restart". A false→true edge resumes that session immediately. |
The client.* keys below are validated by the host; they are the very fields the browser half reads through configForms.get('session-toolkit') (when the form is unavailable the client falls back to UI_FALLBACK in client/client.js, whose values equal the historical defaults). You can edit them in the plugin's settings page. |
| Key | Default | Meaning |
|---|---|---|
client.identityCharLimit | 4000 | Identity editor character limit (UI soft limit; the global-prompt editor uses it too). |
client.copyFeedbackMs | 1600 | Copy-feedback checkmark duration. |
Install into any profile (bundle layer; single source, no copies):
# from npm
dsh plugin --profile web add dsh-session-toolkit
# from GitHub
dsh plugin --profile web add github:Han-Yao94/dsh-session-toolkit
# from a local checkout / tarball
dsh plugin --profile web add ./dsh-session-toolkit-<version>.tgz
The package's dsh.bundle.patch (cordis.patch.yml) registers the single entry (id: session-toolkit, name: 'dsh-session-toolkit') as a bundle layer — applied after dsh-base / dsh-web-app and before the profile patch layer (layer order: bundles in sequence → profile patch → home patch → --patch overlay).
Uninstall: dsh plugin --profile web remove dsh-session-toolkit.
To iterate on the source without publishing, install the checkout directly (dsh plugin --profile web add <path-to-checkout>, which uses a pnpm link: dependency), or use a manual junction into the profile's node_modules plus an explicit - insert: entry in the profile's cordis.patch.yml. Prefer dsh plugin add.
The maintainers' verification gate — syntax over every shipped JS file, plus a packaging contract (entry reachability, undeclared imports, EN/ZH README version parity) — runs against the source checkout. It is not part of this repository and not part of the published package: only lib/, client/, cordis.patch.yml, the READMEs and package.json are tracked here. The gate asserts that the working-tree content equals the tarball content, so it fails on purpose if a prepare/prepack/prepublishOnly script is ever added. The two aggregate commands (pnpm check, pnpm verify) cover only syntax and this packaging contract — neither runs any assertion gate: each assertion gate is invoked on its own and carries its own exit-code contract.
Published on npm as dsh-session-toolkit (latest published version: v0.1.16, MIT) and mirrored on GitHub at github.com/Han-Yao94/dsh-session-toolkit. Pure-JS package — no build step, no prepare script. files whitelists lib/, client/, cordis.patch.yml and the READMEs.
npm carries the current version.
0.1.16is published, sodsh plugin --profile web add dsh-session-toolkitgives you the session-management tools (create_session/rename_session) and everything else described above.0.1.9and0.1.10were tagged but never reached npm;0.1.11was the first published version since0.1.8. Installing from this checkout or from GitHub (dsh plugin --profile web add github:Han-Yao94/dsh-session-toolkit) is equivalent.
dsh plugin --profile web add dsh-session-toolkit; new versions are released with npm publish (or pnpm publish).dsh plugin --profile web add github:Han-Yao94/dsh-session-toolkit.pnpm pack → dsh plugin --profile web add ./dsh-session-toolkit-<version>.tgz.Runtime dependencies (@deepseek-ai/schemastery — floor ^3.18.4 —, @deepseek-ai/dsh-tools, @deepseek-ai/dsh-home-paths) are declared in dependencies and install automatically; platform modules (react, @deepseek-ai/cordis, @deepseek-ai/dsh-client-locale, @deepseek-ai/dsh-client-store, @deepseek-ai/dsh-client-ui-primitives) are peerDependencies provided by the DSH host. Harness-provided ranges name one prerelease generation each — ^0.2.0-rc.1 — because a caret range never crosses a minor line, and semver additionally requires a comparator carrying a prerelease of the candidate's own major.minor.patch; that is why ^0.1.2-alpha.5 || ^0.1.6-alpha.2 used to be written as a union, and it is exactly how the resolution skew happened (the plugin kept its own older copy while the host ran a newer one). With dsh-v0.2.0-rc.1 as the minimum supported version, the older union members are gone: a harness upgrade now requires bumping these ranges, and a dependency skew measurement against a live profile (expect SKEW_COUNT=0) is the check that tells you when. Ranges take effect at install time only — reinstall and restart the GUI before re-measuring. @deepseek-ai/dsh-client-ui-slots is intentionally absent: the slots service is seeded by the web shell, so a npm peer declaration was dead weight. Verified: a clean install of the packed tarball resolves all imports without any local junction. Two install-side checks back this up (they need an external checkout/profile, so they are not CI jobs): a dependency skew measurement against a live profile, and a drift audit of the frozen session-log replica against the harness checkout. Both live in the maintainers' working copy.
Five sections are contributed per assembly, in order: session-identity (order 40) → peer-inbox-discipline (order 45) → global-prompt (order 50) → group-prompt (order 55) → workspace-prompt (order 60), placed after the deployment persona and before tool guidance (100–199). The identity section is resolved per agent (AssembleContext.agent) at assembly time from session-identity settings and is skipped for subagents (origin/delegationDepth). The peer-inbox discipline section emits a fixed reminder of the inbound-peer-message rule; it is likewise skipped for subagents and has no settings of its own. The workspace section injects the most-specific (longest matching path) enabled workspace prompt for a session whose cwd prefix-matches a configured workspace; otherwise nothing. The group section injects every enabled group whose sessions list contains the session's own id — groups match by session id, not directory, so one group may span workspaces; multiple matching groups concatenate in group-key order with a blank line between blocks, and no match injects nothing.
Each of the global and workspace sections appends its referenced files' content after the prompt text: every assembly re-reads files with fs.readFileSync (UTF-8, live), sanitizes each file's content (spaces out { runs), and concatenates them. A file that cannot be read is skipped (its content is not injected) but its read status is recorded for the UI. Empty sections are dropped at render.
All five sections repeat their text on every request — the four prompt sections when enabled, the peer-inbox discipline section for every non-subagent session. The global prompt applies to every conversation; the identity text applies only to sessions that resolve it (their own record or the default); the workspace text applies only to sessions whose cwd prefix-matches an enabled configured workspace (most-specific match wins); the group text applies only to sessions whose own id is listed in an enabled group (all matching groups concatenate in key order); the peer-inbox discipline text applies to every non-subagent session. Referenced files add their full content to the effective prompt and therefore consume additional tokens — a large referenced file meaningfully increases the per-request token cost. Identity text is clipped to 8000 chars as a token guard.
Each section's rendered text is a fixed part of the request prefix while its settings are unchanged. Editing a session identity or a global/workspace prompt (or editing / adding a referenced file) may invalidate provider cache reuse from the first changed token (same semantics as stock persona sections).
send_to_session, list_sessions, inbox_check, create_session and rename_session are registered on the host plane and visible to every session (subagents inherit them through the standing preset composition). Their arguments and results are JSON-compatible. All five are exposed to the model, so create_session's semantic consequence — creating one produces a real user message and consumes one model call — is model-visible. (inbox_check is read-only, has no side effects, and always targets the calling session itself.)
settings.yaml (0.1.6 → 0.1.7)Up to 0.1.6 this plugin kept its user data in namespaces inside <DSH_HOME>/settings.yaml. When 0.1.7 boots, it renames that file to settings.yaml.imported and imports only sections whose name equals a live entry id; this plugin's old namespaces (session-identity, …) match no entry id, so they are refused and left untouched in settings.yaml.imported.
Migration map (old section → new config path); a single pass moves everything:
Old settings.yaml section | New config path |
|---|---|
session-identity.default / .sessions | identity.default / identity.sessions |
global-prompt.{enabled,content,files} | globalPrompt.{enabled,content,files} |
workspace-prompt.{workspaces,removed} | workspacePrompt.{workspaces,removed} |
session-auto-resume.sessions | autoResume.sessions |
session-toolkit-ui.* | client.* (same defaults; usually nothing to move) |
workspace-registry-active, prompt-file-status | Discard: runtime projections, now served by GET /api/session-toolkit/state |
Two ways to land it, pick one:
Settings page: open the dsh-session-toolkit entry in Plugins and paste the old values into the matching fields (the form writes them into the profile patch).
Edit the profile patch directly (handy for bulk moves): append a - id: session-toolkit item to <DSH_HOME>/profiles/<profile>/cordis.patch.yml and put the right-hand paths above under config:. Validate the shape first with the plugin's own Config:
import plugin from 'dsh-session-toolkit' // or import the checkout's lib/index.js
plugin.Config(migratedConfig) // throws when the shape is wrong
volatile field's ref.get() returns a deepFreeze snapshot (immutable). Anything that edits it must first { ... } copy (and .slice() arrays): the host half hands the new value to ctx.get('settings').update(...), the browser half hands it to the form's set / mutate (which submit by config path instead of replacing the whole section). Writing to the frozen object directly throws object is not extensible (this was the root cause of the "workspace list empty" bug fixed here). The same { ... } copy rule applies on the client for workspacePrompt.workspaces writes (onWsFilesChange / save / saveWsEnabled / removeWorkspace).readPromptFiles runs inside the prompt text() on every assembly; a failing file never breaks assembly and its status is recorded into the in-process projection served by GET /api/session-toolkit/state and shown by the settings page.dispose() — AgentHandle.dispose() removes the session from storage; turning a switch off only affects the next restart, it never takes a live session down.session-log-download cell at a lower priority; the stock entry abdicates gracefully if the shadow crashes.toPlainText (10 rules, code-fence state machine, loose matching) runs at send time only; the message structure and source: { kind: 'user' } are unchanged.lib/ and client/client.js.react.useState must also call require('react'): react/jsx-runtime does not provide it, and a missing binding throws while the component renders. The slot renderer catches that throw and drops the entry, so the symptom is a button that is silently not there -- not an error anyone sees. This shipped from 2026-09-18 (3e44642, which added the hooks calls without adding the require) until 2026-09-22, through every gate.send_to_session records the delivery with producer attribution — source: { kind: 'agent-message', form: 'relay', senderSessionId } — and the client renders every non-human source through its turn-trigger row, which is collapsed until you click it. Writing kind: 'user' instead would render inline like a human message, but would record another agent's words as the user's in the one field the V4 format made producer-owned. The attribution wins; click the row to read the body (it still names its sender in the first line).@deepseek-ai/dsh-client-ui-primitives icons from IconXxxOutline<size> to IconXxxOutlineRegular / IconXxxOutlineMedium (1 px vs 1.3 px stroke; the artwork keeps the old default size), so the client half must use the target harness's names. A name that no longer exists evaluates to undefined and React.createElement(undefined, …) throws, which blanks that component's subtree while its navigation entry still appears (registration and rendering are separate). Every other gate stays green on this — syntax, packaging and the anchor gate all passed. The maintainers' gate for this compiles the list of members the plugin references and fails on any that the installed harness does not export.sessionLogDownload controller interface and mirrors the 0.1.6 official surface (a "⋯ More actions" menu). It is a frozen replica: after a DSH upgrade the maintainers re-run a drift audit against the harness checkout, which inspects both sides for the same anchors and reports drift rather than passing silently. Deliberate divergence: the official header menu has since gained a second item (feedback); this replica carries download only. That is a decided state, not an open question — the drift gate reports it as a note rather than a failure because following a new upstream capability is itself a decision, and that decision was taken on 2026-09-22: do not follow. Re-open it only if the feedback entry is wanted here too.toPlainText can drop * pairs in non-format positions (e.g. a * b * c); acceptable for agent-generated messages, boundary tightening is optional.inject union waits for every listed service; a profile missing one service delays the whole package (web profile provides all of them today).pnpm install must be re-run after changing them, and the resulting install should be checked with a dependency skew measurement against a live profile (expect SKEW_COUNT=0; DE-INSTANCE = same version, different instance, which is treated as acceptable). A measurement is only meaningful when the host side is given explicitly: a profile root is the root of an installed dependency tree, not the running host instance — a --profile run is a self-consistency measurement, labelled HOST_BASELINE=PROFILE-SELF, and its own output states that it does not constitute a conclusion about the running host.ctx.get('agentDefaultModel'), sessionTitle and workspaceRegistry are resolved lazily at call time and degrade to cwd/path addressing; tools and webServer are awaited through ctx.inject so a late-arriving service cannot silently disable a feature (the loader creates entries concurrently, so apply-time ctx.get had no ordering guarantee).readPromptFiles runs a statSync per referenced file on every assembly and reads only when mtimeMs/size changed; the per-file and total byte budgets prevent a huge file from blocking assembly or inflating the prompt, and the status projection is written only on change. On the client, files are saved immediately (onWsFilesChange / save).client.* — the browser half reads those fields through configForms.get('session-toolkit') and falls back to the frozen UI_FALLBACK when the form is unavailable. A client entry still receives no cordis row config, but reading settings no longer needs a host mirror: one entry Config is visible to both sides.dsh-v0.2.0-rc.1 — the settings data plane became entry-Config volatile fields plus configForms in 0.1.7. On 0.1.6 and earlier there is no configForms, so the client entry stays pending and the web client reports Failed to load plugins; that is a deliberate loud failure (hard inject), not a silent degradation.Uninstall the bundle: dsh plugin --profile web remove dsh-session-toolkit, then restart the GUI. To roll back to the pre-consolidation layout, re-enable the original plugins instead of installing this package.
FAQs
Unified local plugin: session identity, global prompt, auto-resume, session log reposition, peer message (5 plugins consolidated).
We found that dsh-session-toolkit demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.

Security News
Anthropic’s free OSS Scanner uses Mythos and other Claude models to find open source vulnerabilities and send maintainers reports with proposed fixes.

Security News
On Front Lines’ Early Adopters: CISO podcast, Andrew Becherer shares lessons from Datadog, weighs the long-term costs of AI-built tools, and explains how founders can actually gain a CISO’s trust.