dsh-session-toolkit
August 30, 2026 · View on GitHub
A consolidated plugin toolkit for the DeepSeek Harness. Six previously separate local plugins — session identity, global prompt, session auto-resume, web restart service, 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.
Current version: 0.1.4, adapted to DeepSeek Harness dsh-v0.1.2-alpha.1.
Features
Session Identity
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).
Global Prompt
A settings page (settings.section, id global-prompt, order 30) rendered as Tabs (Global / Per workspace). 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). { runs are spaced out (/\{+/g) to avoid prompt-variable conflicts.
Workspace Prompt
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.
Referenced Files
Both global and workspace prompts can reference a list of files. Every assembly re-reads each referenced file with fs.readFileSync (UTF-8, live), sanitizes its content (spaces out { runs), and injects it after the prompt text. A read failure is skipped (that file's content is not injected) and reported in the UI with the reason. Supports plain text and markdown; no size cap. Per-file read status is projected to the UI via the prompt-file-status namespace (ok: N chars / fail: reason / pending).
Session Auto-Resume
Sessions with the per-session switch on are resumed automatically after a GUI restart (ctx.agents.resume, 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 (seedLength !== 0). Concurrency-bounded (CONCURRENCY = 3) with per-item failure isolation and an in-flight set that prevents duplicate resume.
Web Restart
A "Restart service" entry in the General settings (settings.general.item, id web-restart, order 90) that restarts the GUI server with no UAC prompt (the spawn inherits the server process token, so the restart script's elevated branch is never reached) and shows a full-screen progress overlay (probe-driven progress, fill-up animation before reload, 90 s timeout fallback with manual refresh). Routes: GET /api/restart (health probe, constant 200) and POST /api/restart (trigger; 409 while a restart is in flight; 202 + 500 ms buffer before spawn). Recovery is detected by interruption-then-restore: the overlay only reloads after it observes the probe fail for client.restartFailThreshold consecutive checks and then return 200 again; if the probe is reachable the whole time it reports "no restart detected" (noRestart) until the timeout, offering a manual refresh.
Session-Log Button Relocation
Shadows the official download button 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.
Peer Messaging
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.
Compatibility
The plugin targets DeepSeek Harness dsh-v0.1.2-alpha.1. The host- and client-side integration points it uses are present in that native version:
- Framework:
@deepseek-ai/cordis4.0.1 and@deepseek-ai/schemastery3.18.1 (the harness vendors exactly these versions). The plugin loads through the cordis harness and registers as a bundle viadsh.bundle.patch. - Host services (verified against the native source):
ctx.settings.register(ns, schema, { applies: 'live' })→ scope{ get / watch(next, prev) / update / replace }whereget()returns a deep-frozen value;ctx.systemPrompt.section({ name, order, text: (ctx) => ctx.agent?.session.header … });ctx.agents.{ get, resume({ resumeSessionId, agentOptions }), roots, requireInitiator };session.headerfields (cwd,origin,delegationDepth,parentSession,seedLength,agentPreset);ctx.get('webServer').register({ kind: 'exact', path, handler });@deepseek-ai/dsh-toolsdefineTool+tools.register(); andctx.get('agentDefaultModel'),sessionPersistence,sessionTitle,workspaceRegistry,sessionLogDownload,timer,on,effect. - Client services (verified):
window.__ModuleLoader__.load({ id, factory });ctx.get('slots')→slots.register(meta, render)/slots.inject(name, fn)with lower-priority shadows;ctx.get('settingsScope').bind({ namespace })→{ getSnapshot()/.value/.status, set(field, value), subscribe };ctx.get('locale')→register(ns, { zh, en })/bind(ns); and thetimerclient service (ctx.timeout). The bundle's runtimerequires resolve against the platform seed words (react,react/jsx-runtime,@deepseek-ai/dsh-client-store,@deepseek-ai/dsh-client-ui-primitives, …).
Configuration validation
The plugin's host-side Config validates the entire configuration tree — including everything under config.client — through schemastery as soon as the plugin loads. The client half reads that already-validated config.client and falls back to its own defaults per key. Because the native harness does not expose @deepseek-ai/schemastery to the client-side runtime module table (it is vendored inlined at build time with no shared identity), the client bundle does its own schemastery import inside a guarded fallback: if schemastery is unavailable, the client half degrades gracefully and does not export its own Config — configuration is still schema-validated on the host, so behavior and defaults are unaffected.
Architecture
- Host half —
lib/index.jscomposes six feature modules (identity.js,global-prompt.js,auto-resume.js,web-restart.js,peer-message.js,log-reposition.js).injectis the deduplicated union of module dependencies; each module'sapplyruns inside asafe()guard so one failing module never takes the whole package down. Every contribution is lifecycle-bound (ctx.effectfor prompt sections and HTTP routes, plugin-fiber registrations for tools; timers go through thetimerservice).global-prompt.jsowns theglobal-prompt,workspace-prompt,workspace-registry-active, andprompt-file-statusnamespaces, thereadPromptFileshelper (livefs.readFileSyncre-read), and the workspace/live-workflow projection (agents.roots()→ active workspaces). - Client half —
client/client.jsis a singlewindow.__ModuleLoader__.loadbundle; the five UI modules live in IIFEs and are collected into oneapplythat registers all slots in order (guarded per module). All UI usesReact.createElement; styles are injected asdata-pluginstyle tags with theme CSS variables and dark-mode coverage; no global DOM manipulation. The global-prompt module renders the Tabs (Global / Per workspace) page plus a reusableFileRefsPanel(add/remove referenced files, per-file status via the boundprompt-file-statusscope).
Registered slots
| Slot | Id | Order / priority | Feature |
|---|---|---|---|
settings.section | global-prompt | order 30 | Global + workspace prompt page (Tabs) |
settings.general.item | web-restart | order 90 | Restart entry |
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 |
Configuration
Settings namespaces
Schema-validated namespaces, applies: live, persisted in settings.yaml:
| Namespace | Schema | Notes |
|---|---|---|
session-identity | { default: {enabled: boolean, text: string}, sessions: Record<sessionId, {enabled, text}> } | Resolution: session record → default → empty. Disabled or empty entries inject nothing. Identity text is clipped to 8000 chars (token guard). |
session-auto-resume | { sessions: Record<sessionId, boolean> } | Switch per session; absent keys mean off. |
global-prompt | { enabled: boolean, content: string, files: string[] } | Injected into every conversation when enabled. files is the list of referenced files appended at assembly (live read, failed files skipped). |
workspace-prompt | { workspaces: Record<path,{enabled, content, files: string[]}>, removed: string[] } | Per-workspace prompt. A session gets the most-specific (longest matching path) enabled workspace whose directory prefix-matches its cwd. removed lists paths the user removed so the active-workspace sync never re-adds them. |
workspace-registry-active | { active: [{path, sessionCount}] } | Read-only projection of live workspaces 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). |
prompt-file-status | { byScope: Record<global|path, [{filePath, status: 'ok'|'fail', charCount?, reason?}]> } | Read-only projection of the most recent read result of each scoped referenced file; the UI's FileRefsPanel reads it to show ok: N chars / fail: reason. |
Plugin Config (cordis)
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 client half follows the same cordis mechanism (it receives config.client); if schemastery is unavailable inside the client bundle, the client half degrades to defaults and does not export its own Config — configuration is still validated on the host.
- id: session-toolkit
name: 'dsh-session-toolkit'
config:
identity:
maxText: 8000
sectionOrder: 40
globalPrompt:
sectionOrder: 50
workspaceSectionOrder: 60
autoResume:
concurrency: 3
webRestart:
scriptPath: '' # optional; default derived as <DSH_HOME>/autostart/dsh-web-restart.cmd
spawnDelayMs: 500
client:
identityCharLimit: 4000
restartTimeoutMs: 90000
restartPollMs: 1000
restartFillMs: 600
restartFailThreshold: 2
restartSettleMs: 8000
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). |
autoResume.concurrency | 3 | Max in-flight resumes during startup restore. |
webRestart.scriptPath | derived | Restart script path; default <DSH_HOME>/autostart/dsh-web-restart.cmd via dsh-home-paths. Setting it to an empty string derives it at runtime via dsh-home-paths (no explicit config needed). |
webRestart.spawnDelayMs | 500 | Delay before spawning the restart script (202 buffer). |
client.identityCharLimit | 4000 | Identity editor character limit (UI soft limit). |
client.restartTimeoutMs | 90000 | Restart overlay timeout before the manual-refresh hint. |
client.restartPollMs | 1000 | Restart health-poll interval. |
client.restartFillMs | 600 | Progress fill animation after recovery detected. |
client.restartFailThreshold | 2 | Consecutive failed health polls before an interruption is considered observed. |
client.restartSettleMs | 8000 | Settle window (ms) after recovery before the auto-reload. DSH session titles are generated asynchronously by the LLM with no "ready" signal, so this is the wait window for the first post-restart reload to reduce the title fallback (showing the workspace name). If a session title still shows the workspace name, refresh manually or raise this value. A full fix requires DSH to expose a "titles ready" signal. |
client.copyFeedbackMs | 1600 | Copy-feedback checkmark duration. |
Deployment
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.
Local development
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.
Share & Install
Published on npm as dsh-session-toolkit (v0.1.4, 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: consumers run
dsh plugin --profile web add dsh-session-toolkit; new versions are released withnpm publish(orpnpm publish). - GitHub:
dsh plugin --profile web add github:Han-Yao94/dsh-session-toolkit. - Tarball:
pnpm pack→dsh plugin --profile web add ./dsh-session-toolkit-<version>.tgz.
Runtime dependencies (@deepseek-ai/schemastery, @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-slots, @deepseek-ai/dsh-client-ui-primitives) are peerDependencies provided by the DSH host. Peers and dependencies are pinned to the 0.1.2-alpha.1 line to match dsh-v0.1.2-alpha.1. Verified: a clean install of the packed tarball resolves all imports without any local junction.
Model Experience
System prompt contributions
What the model sees
Three sections are contributed per assembly, in order: session-identity (order 40) → global-prompt (order 50) → 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 workspace section injects the most-specific (longest matching path) enabled workspace prompt for a session whose cwd prefix-matches a configured workspace; otherwise 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.
Token effect
All three sections repeat their text on every request when enabled. 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). 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.
KV Cache effect
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).
Tool surface
send_to_session and list_sessions 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.
Mechanisms and Red Lines
- Identity injection uses a single global section whose text provider resolves per agent — no per-agent registration, no lifecycle churn, real-time on settings change.
- Frozen-settings rule (red line) — DSH's
ctx.settings.register(...).scope.get()returns a value frozen bydeepFreeze(immutable). Any host write to a scope must first{ ... }copy the object (and.slice()arrays) into a mutable object, then useupdate()(the register scope hasget/watch/update/replace, noset); writing to the frozen object directly throwsobject is not extensible(this was the root cause of the "workspace list empty" bug fixed here). On the client side,settingsScope.bind().set(field, value)is used (the client scope supportsset). The same{ ... }copy rule applies on the client forworkspace-promptwrites (onWsFilesChange/save/saveWsEnabled/removeWorkspace). - Referenced files are read live and failures are skipped —
readPromptFilesruns inside the prompttext()on every assembly; a failing file never breaks assembly and its status is recorded inprompt-file-statusfor the UI. - Auto-resume never calls
dispose()—AgentHandle.dispose()removes the session from storage; turning a switch off only affects the next restart, it never takes a live session down. - Restart is UAC-free by construction — the spawn inherits the server process token (SYSTEM or user), so
taskkilltargets a same-privilege process and the script's elevated branch (the only UAC source) is unreachable. If port 3080 is held by another program, an elevated retry may still appear (documented in the restart script). - Shadowing is cell-based — the utilities entry re-registers the stock
session-log-downloadcell at a lower priority; the stock entry abdicates gracefully if the shadow crashes. - Plain-text conversion —
toPlainText(10 rules, code-fence state machine, loose matching) runs at send time only; the message structure andsource: { kind: 'user' }are unchanged.
Known Limitations and Deferred Work
- Client half is a hand-maintained single-file IIFE bundle; adding a feature touches both
lib/andclient/client.js. - The relocated Session-log button depends on the official
sessionLogDownloadcontroller interface; a stock upgrade that changes it requires a sync (seelib/log-reposition.js). - Loose emphasis matching in
toPlainTextcan drop*pairs in non-format positions (e.g.a * b * c); acceptable for agent-generated messages, boundary tightening is optional. - The aggregate
injectunion waits for every listed service; a profile missing one service delays the whole package (web profile provides all of them today). ctx.get('agentDefaultModel')is resolved atapplytime (non-lazy); the gateway mounts the service before this package, so the web profile always has a value.- Restart probe window — a restart is only detected when the health probe fails for
restartFailThreshold × restartPollMs(default 2 × 1000 ms = 2 s) and then recovers. If a relaunch completes in under that window, the overlay can misreport "no restart detected" (noRestart); loweringrestartFailThresholdto 1 makes detection more sensitive but also lets a single transient failure masquerade as a restart interruption. - Referenced files are read synchronously inside assembly —
readPromptFilesusesfs.readFileSyncfor every referenced file on every assembly; many or very large files block assembly (the "no size cap" requirement is a deliberate trade-off).prompt-file-statusis updated on the assembly path each time (and the UI'sFileRefsPanelre-renders per status), which is a minor performance noisiness. On the client,filesare saved immediately (onWsFilesChange/save). - Client-side config schema — because the native harness does not expose
@deepseek-ai/schemasteryto the client runtime module table, the client bundle validates its own config only when schemastery is available; otherwise validation happens on the host (see Compatibility).
Recovery
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.