dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
把 DSH 接到本机 MemVault 记忆库:核心记忆块注入系统提示词、结束的轮次经由 MemVault 自己的管线抽取,并提供浏览、追溯、复核与重抽的面板。
Push MemVault's core memory into the DeepSeek Harness system prompt — and let every finished turn flow back. With a memory panel in the GUI · zero runtime dependencies · no bundler · no second server
English | 中文
dsh-memvault is the missing half of a memory system that already exists. MemVault (a local long-term-memory service: a SQLite store, a FastAPI/CLI pipeline and a stdio MCP server) stores facts, embeds them and retrieves them — but it is a stdio MCP server, and MCP is a *pull* protocol: the server can only be *called*, it has no channel to put anything into the model's context. So "memory is already loaded" can never be achieved from the MCP side. It has to be done by the client, which is this plugin.
The two halves are deliberately separable: MemVault alone is a general MCP service any client can use (Claude Code, Cursor, Cline, a script), and this plugin is the DSH-specific layer — prompt injection, windowed turn extraction through MemVault's own pipeline, and the panel. Installing the plugin is what makes memory *already there* instead of something the model has to remember to look up.
The plugin does three things:
Half · What it does · Mechanism Inject (read) · Core memory blocks enter the system prompt, visible on every step without the model calling a tool · ctx.systemPrompt.context() — a dynamic runtime-context contribution (the same channel as skill-catalog) Extract (write) · Finished turns accumulate into a window, and the window is handed to MemVault's extraction/embedding pipeline as one conversation · ctx.on('session/event') → turn/end → window → python -m memvault.cli add --stdin (idle-triggered) Show (panel) · What is injected right now, what extraction has been doing, editing the core blocks, and browsing what is actually stored · five exact host routes (/memvault/api/*) + a browser half rendered as a conversation tab and a Plugins settings page
· 🧠 Injection without tool calls · Core blocks land in a dynamic runtime context, so they are re-evaluated on every assemble() and the model simply *has* them 🗄️ Direct read-only SQLite · Node's built-in node:sqlite opens memvault.db read-only — no HTTP hop, no subprocess, no third-party dependency, no server that must be up 💾 TTL-cached rendering · The system prompt *is* the KV-cache prefix; re-reading and re-rendering every step is wasted work and any byte change invalidates reuse — so the render is cached (refreshMs, default 30 s) 🧩 Turn-boundary slicing · The write half slices the turn from its own bounded event log by turn/end boundaries: idempotent, restart-proof, no watermark arithmetic to go stale ⏱️ Cost-aware extraction · The window is a knob: everyNTurns decides when extraction becomes worth considering, idleMs waits for a pause, windowTurns caps it, minTranscriptChars skips trivia, endReasons decides which turn endings count, timeoutMs kills a stuck child 🌙 Background, not on the critical path · Extraction runs when the session goes quiet — during a pause, not while you are waiting for an answer — and one call covers up to windowTurns turns instead of one call per turn 💾 Restart-safe window · The waiting window (its rendered transcript, bounded) is written to the state file on every turn, so a restart between turns does not silently drop memories; anything still pending is extracted on mount and marked as recovered 🎭 Role filtering · Assistant prose and tool traffic are excluded by default — shipping them stored the model's own words as "facts about the user" 🪶 Zero runtime dependencies · Plain ESM over the harness plugin protocol: node:sqlite, node:child_process, node:fs. Nothing to install, and the browser half is hand-written against the ModuleLoader envelope instead of being bundled 🧾 Every knob described once · One spec (lib/config.js) produces the code defaults, the Config schema DSH validates against, and the settings the Plugins page renders — so a default cannot drift between them, and a typo in a patch shows up in the panel instead of only in the log 🎛️ A real panel · A 记忆 tab in the conversation ring and a page under Settings → Plugins: the blocks currently injected (label, scope, characters against their stored limit, text), the read budget and cache age, the extraction knobs, sessions with a watermark, and the last five extraction outcomes — plus a 立即重读 button that ignores the 30 s render TTL ✏️ Editable core blocks · 编辑 / 删除 on any block, and a form to create one. A write is an upsert keyed by (scope_type, scope_id, label), it drops the render cache so the next step already sees it, and panel.writes: false turns the whole thing read-only 🧭 Know your paths at setup · The panel shows which store file, plugin state file, Python and project directory are in play, whether the store file exists (and its size), and which scopes are read versus written — plus the only two places to change any of it: the DSH plugin config or MemVault's .env. A missing store file is called out as "created on the first write", because a wrong path and an empty store look identical in a row count The paths in the shipped cordis.patch.yml are one machine's example rather than defaults: change them, or change nothing and let discovery find your checkout 🔭 See the structure · A third view answers what a list cannot: which scope dimensions hold what (and that one row counts under both user and agent — scopes are dimensions, not a partition), which core blocks are actually injected versus merely stored, and how the relation graph is shaped — nodes sized by degree, edges by weight, plus how many memories have no edge at all. Clicking a node opens its full audit chain 🔎 Browse what is stored · A second view searches the memories table by substring with type/scope filters and paging, showing type, scope, age and the id (one click to copy, so a tool call can act on it). Rows the store retyped on its own carry a badge, and "only retyped" lists exactly those — a heuristic verdict should not be silent, so a wrong one can be marked for review right there. It is a browse, not retrieval — semantic recall stays with the model's memory_search — and it never selects the embedding blob 🧾 Trace and review · Every extraction records which memories it produced, so a window links to its rows ("看这 2 条产出"). Each row can open its provenance — MemVault's own audit trail (history: ADD/UPDATE/DELETE with old and new text) and the contradictions it takes part in (relations) — and can be marked for review. Flags are plugin state: the store is never touched ♻️ A review loop that can act · The queue resolves each flagged memory with its window and retained input, and a mark can be cleared right there. Two actions: 复制修正请求 hands the model the ids, texts and provenance (it proposes; DSH's approval is the gate), and 重抽 sends that window's text through MemVault's own pipeline again — optionally edited, optionally with the other extractor. Replay is the panel's one store-writing action, and it writes through add(), never around it 🖥️ Host half needs no browser · The panel is optional: webServer is taken with ctx.inject, so a headless composition still injects memory and simply never registers the routes 🛟 Fail-soft by design · An unreadable store serves the last good text and warns once; a broken extraction never fails a turn and never poisons the next one 🔍 Observable from outside · Watermarks and the last five extraction diagnostics are written atomically to one JSON file, so "hook never fired" is distinguishable from "turn too short" from "ran and found nothing"
Question · Answer DSH Desktop · ✅ Verified live on 0.2.1-rc.3 with the reserved desktop profile: the bundle (dsh.bundle.patch) loads, the plugin row is active, /memvault/api/status answers 200, and the injected blocks appear in the runtime-context snapshot. DSH versions · Supports the 0.1.x and 0.2.x lines, including 0.2.1 prereleases. engines.dsh and both DSH peer declarations carry one range; its >=0.2.1-0 = 22.13 (flag-free node:sqlite). DSH ships its own Node, so this is only a statement about the plugin's API floor. Other harnesses (Claude Code, Codex, …) · ❌ Not this plugin. It is written against the cordis/DSH surface (ctx.systemPrompt.context, session/event). Those harnesses reach the same store through MemVault's own MCP tools, CLI, or API.
Prerequisite: a working MemVault checkout — its virtualenv and its memvault.db (the store is shared with the MemVault service and MCP clients; this plugin only ever SELECTs from it).
Install from npm (0.9.10 and newer):
dsh plugin --profile desktop add dsh-memvaultSelf-owned install (what this repo does on the author's machine):
// ~/.dsh/profiles/<profile>/package.json
{
"dependencies": { "dsh-memvault": "link:D:/_Projects/skill_mcp/dsh-memvault" },
"dsh": { "profile": { "bundles": ["…", "dsh-memvault", "…"] } }
}# make the module resolvable from the profile
New-Item -ItemType Junction `
-Path "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-memvault" `
-Target "D:\_Projects\skill_mcp\dsh-memvault"Restart DSH after adding a bundle — an already-loaded patch file hot-reloads, a newly added bundle does not.
⚠️ Two known traps.dsh plugin addrewrites the profile'spackage.json, and thedsh.profile.bundlesarray can silently lose *other* plugins' entries — always re-read the whole list afterwards. And pnpm can replace thelink:junction; recreate it if the bundle suddenly stops loading.
Installing from npm or a git spec is the same operation through dsh plugin or the Web sidebar's Plugins page (see @deepseek-ai/dsh-plugin-manager).
The browser half is committed as lib/client.js, so a fresh clone needs no build. After editing src/client/index.js:
npm run build # wraps the client source in the ModuleLoader envelope
npm test # fails when lib/client.js is staleA brand-new client half is picked up by a restart plus a page reload: the boot graph is rendered into the index response.
Every knob is described once, in lib/config.js: type, bounds, default and a description. From that one description come the code defaults (DEFAULT_EXTRACT and friends), the Config schema DSH validates the entry against and projects for the Plugins page, and the panel's view of what is configured. The tables below are that description in prose.
Setting values is unchanged — a config block in cordis.patch.yml. What changed is what happens when one is wrong: a value that does not type-check is reported (and the default is used), an undeclared key is reported, and both surface as a warning banner in the panel rather than only in the host log.
Field · Default · Meaning enabled · true · false injects the empty string forever dbPath · $MEMVAULT_DB_PATH else D:/Claude_code/memory/data/memvault.db · Path to memvault.db scopes · [{user,lenovo},{agent,claude-code-memory}] · Blocks are keyed by (scope_type, scope_id); both halves must be given explicitly — a foreign process has no "current scope" labels · [] · Only these labels; empty means every block in the scopes maxChars · 4000 · Budget for the whole rendered string, header included refreshMs · 30000 · TTL for the cached render order · 210 · Position among runtime-context contributions (e.g. relative to skill-catalog) name · memvault:core · The name shown in the trajectory's injected-context list
config.extract)Field · Default · Meaning enabled · true · false makes the plugin read-only everyNTurns · 3 · Turns that must accumulate before an extraction is considered; 1 = every turn (still windowed) idleMs · 20000 · Quiet time that hands the window over. A new finished turn re-arms it windowTurns · 8 · Hard cap: reaching this many turns extracts immediately, so a session that never pauses still gets extracted endReasons · ['completed','max-tokens'] · Which turn/end reasons count. aborted / error / interrupted / blocked are skipped — but max-tokens is not, because a truncated turn still holds the user's message includeAssistant / includeTools · false / false · The extractor does not separate roles; the assistant's own prose became stored "facts", so it is off by default maxInputChars · 6000 · Transcript budget; newest lines win, oldest are dropped minTranscriptChars · 40 · Below this a turn is not worth a process plus an LLM call — it is skipped, and the watermark still advances timeoutMs · 120000 · Timeout kills the child and records one warning pythonPath · $MEMVAULT_PYTHON else /.venv/{Scripts/python.exe,bin/python} · Interpreter that can import memvault projectDir · $MEMVAULT_DIR else D:/Claude_code/memory · Working directory of the child user / agent / run · lenovo / claude-code-memory / unset · The three axes are orthogonal; any combination is allowed env · {PYTHONUTF8:'1', PYTHONIOENCODING:'utf-8'} · Child environment. Removing these reintroduces the cp936/emoji bug below statePath · ~/.dsh/storages/dsh-memvault-state.json · Watermarks + the last five diagnostics; atomic write
The browser half has no knobs of its own; it reads the two routes the host half registers. They exist only when the composition provides webServer.
Route · Method · Answers /memvault/api/status · GET · Injected blocks (label, scope, characters, stored limit, value clamped to 2000), read config, cache age, extraction config, watermark session count, last five diagnostics, and whether writes are enabled /memvault/api/refresh · POST · The same payload after dropping the render TTL — the 立即重读 button /memvault/api/blocks · POST · One block action through MemVault's CLI: { action: 'set', type, id, label, value, limit? } (upsert) or { action: 'delete', type, id, label } /memvault/api/flush · POST · Extract every pending window now (the 立即抽取 button). The answer says how many sessions were handed over; the work stays queued. Answers 405 when extraction is disabled /memvault/api/memories · GET · Browse stored memories: q (substring), type, user, agent, run, ids (an explicit id list, how a diagnostic's output is looked up), flagged=1 (only marked rows), retyped=1 (only rows the store's own type refinement moved out of user), limit (default 20, capped at 200), offset. Answers { rows, total, limit, offset, order, applied, mode, flags, flaggedCount, retypedCount, produced } — applied echoes what was actually used, and mode: 'substring' says out loud that this is not ranked retrieval /memvault/api/memory · GET · One memory's provenance (?id=): the row, history (every audited decision with old/new text) and relations (the contradictions, with the other side's text and weight). 404 for an unknown id, missing: true in the body /memvault/api/flag · POST · Mark or clear one memory for review: { id, flagged: true \ · false, note? }. Writes the plugin's own state file — MemVault is untouched — and answers with the whole bounded flag map. 403 when panel.writes: false /memvault/api/review · GET · The review queue: every flagged memory with its provenance, the window that produced it, whether that window's input is still retained (replayable), the retained inputText when it is, and request — a ready-to-paste instruction listing ids, texts and provenance for a model to propose fixes /memvault/api/replay · POST · Replay one retained input: { key, text?, extractor?: 'inherit' \ · 'rule' \ · 'llm' }. Answers 202 because the work is queued; the outcome appears as a diagnostic with replayOf. Writes the store through MemVault's own add(); 403 when panel.writes: false /memvault/api/structure · GET · The store's shape: memories and type split per scope dimension, core blocks with their occupancy (chars / value_limit) and whether each is actually injected, the relation graph's nodes and edges (capped at 120 each, sampled reported when truncated), the ten heaviest relations, and how many memories have no edge at all. A store file that does not exist yet answers 200 with initialized: false — a fresh install is a state, not an error
The handlers refuse anything that is not a loopback Host with a matching Origin (when the browser sends one) and a same-site Sec-Fetch-Site, answering 403 otherwise. They are exact routes, so they match before the shell's index//api handlers. A bad action, an unknown scope type, an empty value or an over-long value is a 400 before anything is spawned; a CLI failure is a 502.
Field · Default · Meaning panel.writes · true · false makes the panel read-only: /memvault/api/blocks answers 403 instead of running the CLI
lib/schema.js builds a native Schemastery schema from the same spec and exports it as Config, which is what DSH reads off a plugin module. Two consequences:
(DSH's documented behaviour), so the schema is stricter than the resolver on purpose: bounds and types are enforced before the plugin runs.
dsh --dump-config-schema and the Plugins page project thesame schema into JSON Schema, which is what renders the fields.
@deepseek-ai/schemastery comes from the DSH runtime and is declared as a peer. The import is attempted once and tolerated when absent, because the bundled smoke tests run on plain Node: outside DSH the plugin exports no Config, still loads, and warns that it has no settings page. In DSH it always resolves.
flowchart LR
subgraph HOST["DSH host process"]
LOOP["Agent loop"]
ASM["systemPrompt.assemble()"]
CTX["ctx.systemPrompt.context('memvault:core')"]
HOOK["ctx.on('session/event')"]
BUFF["bounded event log<br/>400/session · 32 sessions"]
end
subgraph BROWSER["Web / Desktop client"]
TAB["记忆 tab<br/>Settings → Plugins page"]
end
DB[("memvault.db<br/>blocks · memories")]
PY["python -m memvault.cli add --stdin"]
STATE["dsh-memvault-state.json"]
DB -->|"read-only SELECT ~1 ms"| CTX
CTX -->|"dynamic runtime context, TTL 30 s"| ASM
ASM --> LOOP
LOOP --> HOOK
HOOK --> BUFF
BUFF -->|"turn/end boundary slice"| PY
PY -->|"extract · embed · upsert"| DB
HOOK --> STATE
STATE -->|"loadState (watermarks + last 5)"| API["exact routes<br/>/memvault/api/status · /refresh"]
DB -->|"same read as the prompt gets"| API
API --> TABRead path, per step:
assemble() asks every dynamic context for its text.memvault:core returns the cached render unless the TTL expired; on expiry it opens the store read-only and re-renders.- [scope_type/scope_id/label] value, headed by one attribution line, bounded by maxChars.'', the assembler drops the empty contribution, and nothing appears in the trajectory — that is the intended behaviour for an empty store, not a fault.Write path, per finished turn:
turn/end with an accepted reason, the turn is sliced by turn boundary from that buffer and pushed onto the session's window.everyNTurns it keeps waiting; at everyNTurns it arms an idle timer; at windowTurns — or when the timer fires — it hands the whole window over, as the newest lines of one bounded, user-only transcript.recovered.Panel path, on open and every 15 s:
/memvault/api/status from the same origin.Injection is the headline. Every step, the core blocks are assembled into the system prompt — no tool call, no round trip, no "did the model remember to look". The panel shows the rendered budget, the scopes it read, and the blocks themselves:
Memory structure — scope dimensions, which blocks actually reach the prompt, and the relation graph (only the 40 heaviest edges, or 120 nodes become an unreadable ring):
![The str