Han-Yao94/dsh-session-toolkit

dsh-session-toolkit

整合工具包:会话身份、全局/工作区提示词(含引用文件)、会话自动上线、免 UAC 网页重启、会话间消息。

Uncategorizedcordisdeepseekdeepseek-harness

dsh-session-toolkit

English | 中文

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).


Install

Into any profile — a 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.

What you are installing

Published on npm as dsh-session-toolkit (latest published version: v1.0.0, 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. 1.0.0 is published, so dsh plugin --profile web add dsh-session-toolkit gives you the session-management tools (create_session / rename_session) and everything else described above. 0.1.9 and 0.1.10 were tagged but never reached npm; 0.1.11 was the first published version since 0.1.8. Installing from this checkout or from GitHub (dsh plugin --profile web add github:Han-Yao94/dsh-session-toolkit) is equivalent.

  • npm: consumers run dsh plugin --profile web add dsh-session-toolkit; new versions are released with npm publish (or pnpm 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 — 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 DSH client 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.

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.

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: what is tracked here is the plugin source and its docs — lib/, client/, cordis.patch.yml, the two READMEs plus README.i18n.yaml, LICENSE, package.json and .gitignore; scripts/ and .github/ are deliberately untracked. 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.


Features

Two families: prompt layers (what the model sees) and session plumbing (the tools and UI buttons that drive sessions).

Prompt layers

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). 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.

Global Prompt

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; the Per workspace tab injects per-workspace prompts; the Groups tab injects per-group prompts (see Group Prompt). Those three, plus the session identity and the peer-inbox discipline section, make up the five prompt sections this plugin contributes — the map below lists them all with their orders.

The plugin also exposes three routes on the same webServer — two read-only and one write. The 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) is how the settings page reads those runtime projections — they no longer occupy a settings namespace and are never persisted. The search route GET /api/session-toolkit/search is the cross-session search panel's data source; it runs the same literal scan as the search_sessions model tool (q plus optional cwd / since / senders / surfaces / limit / perSession / maxSessions, with senders and surfaces passed as comma-separated lists); an invalid parameter answers 400 { ok: false, error, errorText }, and a scan that finds nothing is still 200 + ok: true with zero matches. It is not a full-text query: the harness full-text index is off by default (openAt: never), and enabling it would not change this path. The relay route POST /api/session-toolkit/relay is the write side: it is what the search view's hit rows use to hand one hit to the session you are currently in. Body = { target, source: { sessionId, seq, time }, text }, where target is the receiving session and source.sessionId is the session the hit came from — both must be sessions this deployment knows (listSessions()), and relaying text back to its own source session is refused with 409 SELF_TARGET. Anything but POST answers 405; a body over 8,192 bytes answers 413; text is capped at 6,000 UTF-8 bytes, and time is display-only and optional (omitted ⇒ shown as "unknown"; at most 200 single-line characters). The route is single-flight — a second request while one is in flight gets 409 RELAY_IN_FLIGHT instead of queueing. Success is 200 { ok: true, deliveredTo, peerMessageId }; a rejected request carries a verdict as { ok: false, error, errorText } (404 UNKNOWN_TARGET / UNKNOWN_SOURCE when an id is not a known session) — the only bodies without errorText are the guard's own 403 { ok: false, error: 'origin not allowed' } and the bare 405. The delivered message quotes the hit in a 4-space-indented block headed by source session, hit index and time, and states in plain text that it is data, not instructions. The relay route reuses the same originGuard as the two read routes: that guard binds Origin to Host and rejects Origin: null, so it fences off cross-site browser requests — it is not authentication, and a process that can reach this port can forge those headers.

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.

Group Prompt

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.

Referenced Files

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.

Prompt Dedup

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.

Prompt section map

Section idOrderInjected forNotes
session-identity40sessions that resolve an identity (their own record, else the default)resolved per agent at every assembly; skipped for subagents (origin / delegationDepth)
peer-inbox-discipline45every non-subagent sessionfixed reminder of the inbound-peer-message rule (see Peer Messaging); no settings of its own
global-prompt50every conversation
group-prompt55sessions whose own id is listed in an enabled groupseveral matched groups concatenate in group-key order, one blank line between blocks; no match injects nothing
workspace-prompt60sessions whose cwd prefix-matches an enabled workspacethe most-specific (longest matching path) match wins; otherwise nothing

All five prompt sections 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.

Session plumbing

Session Auto-Resume

Sessions with the per-session switch on are resumed automatically after a DSH client 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.

Session Admin

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 DSH client'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:

  • Visibility is not verified inside the tool — whether a session created with 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.
  • Preset degradation — when the 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.

Peer Messaging

send_to_session / list_sessions tools on the host plane (session addressing by id or workspace path, wakeup delivery). 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.

Session-Log Button Relocation

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.

search_sessions is the sixth model tool: a literal, case-insensitive, whitespace-flexible content search across sessions ("where did we discuss X?"), built on the harness sessionQuery service. It lists sessions, filters them by cwd / since, matches the query against each session's extracted semantic text, and returns the session title plus event coordinates (sessionId + seq — the two values the host's session-query reading tools take; this package exposes no reader tool of its own) and a text excerpt. Every failure comes back as a structured { ok: false, error, errorText } object — the tool never throws into the calling session.

senders narrows the hits to those a given set of sessions produced: each element is a source session id in either form the harness writes (session-<uuid> or a bare uuid) or one of the two producer kinds agent-message / subagent-settled (de-duplicated, at most 10; anything else — an unknown kind, an empty list, a non-string element — answers INVALID_SENDERS). It is an approximate filter, and the result says so: only hits on the current surface can have their producer determined, so hits on other surfaces are kept with sender: null and counted in sendersUnfiltered; hits on current with no producing session (user turns, injected context) are dropped and counted in senderlessExcluded; sendersSkippedSessions with sendersPartial report the sessions that could not be indexed within the scan's time budget. Those keys appear only when senders is passed — without it the payload is byte-for-byte what it was before the parameter existed.

Surfaces are the part worth knowing. By default the tool returns matches from current (text still in a session's effective model context) and shadowed (text a later snapshot replaced, or context compression dropped) — shadowed is usually where the thing you are trying to recover actually lives. log-only (raw-log-only records that never reached a surface) is excluded unless you pass surfaces explicitly. The scan is always literal — it goes through the harness filterEvents path and never touches the FTS index, so enabling the index changes nothing here. When the sessionQuery service is absent, the module registers nothing and the rest of the package keeps working.

Scan budget: 50 sessions / 5 matches per session / 20 matches total by default (maxSessions, perSession and limit raise these, with hard caps of 200 / 20 / 100); truncated.sessions / truncated.matches report when a session or match cap was hit, and an empty result is ok: true with zero matches, not a failure.


Configuration

Settings fields (volatile parts of the entry config)

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').

FieldSchemaNotes
identity.default{enabled: boolean, text: string}Default identity. Resolution: session record → default → empty. Disabled or emp