DSH Third-Party Long-Term Memory Plugin (Unofficial) ==================================================== An unofficial, third-party long-term memory plugin for DeepSeek Harness (DSH). Package id: dsh-thirdparty-memory. 中文名:DSH 第三方长期记忆插件(非官方)。 > Not affiliated. This is an independent open-source project. It is not associated with DeepSeek, Doubao, or their official teams, and carries no authorisation or endorsement from them. Product and company names are used only to describe compatibility. One master switch, automatic extraction, five kinds of memory, a review inbox, keyword-relevant injection, a local memory page, and a single-file Windows manager — with English / 简体中文 / 繁體中文 UI. > Docs: this file is the English root. Translations: 简体中文 (README.zh-CN.md) · 繁體中文 (README.zh-TW.md). > Every document also ships as plain text: README.txt, README.zh-CN.txt, README.zh-TW.txt. > Deep dive: docs/tutorial.md (also as .txt, with zh-CN / zh-TW versions in docs/). Version : 0.4.3 (2026-11-10) · MIT · unofficial Platforms : DSH Desktop (MSIX) · DeepSeek Harness portable (Electron) · CLI `dsh web` Runtime : Node.js 18+ (the exe ships Node 24; SQLite storage needs `node:sqlite`, Node 22.5+) Storage : local SQLite through the built-in `node:sqlite` (WAL) — no server, no cloud, no telemetry Vectors : local hashed n-gram index (256 dims, cosine similarity) — no dependency, no network License : MIT ------------------------------------------------------------------------ Why --- AI conversations fail at memory in two opposite ways: it remembers too little (you repeat your preferences, project paths and past mistakes in every new chat) and too much (everything gets stuffed into the system prompt, burning tokens and confusing the model with irrelevant facts). dsh-thirdparty-memory splits the difference: memories are classified, injection is relevance-filtered, and anything the AI infers about you has to pass a review inbox before it can ever reach the prompt. ------------------------------------------------------------------------ Features -------- * Four categories global (always injected) · project (activates with its project) · persona (always injected) · ephemeral (this session only) · plus general * Keyword-relevant injection Only entries matching the current message enter the prompt; the rest collapse into a numbered index. Typically 6.5 KB → 1.2 KB on unrelated questions. * Multi-topic coverage Mention several things in one sentence and each topic gets its own representative entries, so one hot topic cannot eat the whole budget. * Explicit recall Say "extract all memories about X" to drop the relevance threshold and get the whole batch (the budget widens too). * Review inbox Auto-extracted persona facts wait in the inbox; they are injected only after you accept them. * Self-directed writing The agent judges what is worth keeping (pitfalls, conclusions, personal facts) and writes it without asking — judgeMode: model (default). * Update, don't duplicate The agent searches first and updates the existing entry: one fact, one entry, always the latest text. * [全局] prefix Any memory whose text starts with [全局] / [global] is injected every turn. * Token dashboard See exactly how many bytes/tokens each section costs, with warnings when always-on memories crowd the budget. * Prompt preview One click shows the exact text that would be injected into a new chat. * Single-file manager dsh-memory-manager.exe (~34 MB) embeds Node + everything; double-click, tray icon, no URL to remember. * SQLite store node:sqlite (built into Node), WAL mode, one local database file — still nothing to install. * Local vector index Character/word n-grams hashed into 256 dimensions, cosine similarity blended with keyword scoring — literal closeness, not synonyms; vector: false turns it off. * Zero runtime deps The server is node:http; there is nothing to npm install. ------------------------------------------------------------------------ The five kinds of memory ------------------------ * 🌍 global Meaning: Language preference, hard prohibitions, workflow rules When injected: every turn * 📁 project Meaning: Project-bound paths, pitfalls, scripts, specs When injected: only while that project is active (named in the message, or currentProject) * 👤 persona Meaning: What you care about, what annoys you, taste, habits When injected: every turn * ⏳ ephemeral Meaning: Scratch context for this conversation When injected: same session only * 📝 general Meaning: Everything else When injected: keyword relevance Each entry also carries priority (high must follow / medium recommended / low reference) and an optional TTL (never / N days). > **Project memories are a gate and a filter**: a project entry becomes eligible when its project is active, and is still keyword-filtered before injection. Activating a project never dumps all of its entries into the prompt. What a persona entry has to look like A memory system that profiles its user in the third person turns creepy fast. What separates a usable persona entry from a creepy one is not tone, it is evidence — a well-formed entry has four properties: * dated What it means: it carries the date of the observation Why it matters: an undated observation cannot expire, and persona notes age * scene-bound What it means: it names what actually happened Why it matters: a scene can be confirmed; a character trait cannot * anchored What it means: it keeps their own words or a concrete action Why it matters: paraphrasing is where invention enters * one line of reading What it means: judgement comes last and stays short Why it matters: the evidence stays intact and the interpretation is clearly separate A badly formed entry has the opposite profile: adjective-first, undated, scene-free, nothing the reader could confirm or deny. A file full of those is what makes a person ask "why are you keeping notes on me?" Template (structure only, no content): YYYY-MM-DD — "" — . If no scene and no quote come to mind, write nothing — an empty persona section beats an invented one. ------------------------------------------------------------------------ Install ------- 1. Manager exe (easiest) Run dsh-memory-manager.exe. The first launch asks for the UI language (English / 简体中文 / 繁體中文), extracts the bundled runtime into %LOCALAPPDATA%\dsh-thirdparty-memory\, starts the memory page and opens it in your browser. A tray icon stays around: Open memory page · Copy address · Storage location… · Open storage folder · Change port… · Restart/Stop service · Attach to DSH… · Language · Quit dsh-memory-manager.exe open the page, stay in the tray dsh-memory-manager.exe --install attach the plugin to every detected DSH profile dsh-memory-manager.exe --start / --stop / --restart / --status dsh-memory-manager.exe --port 7900 custom port (refuses if the port is taken) dsh-memory-manager.exe --store D:\dsh-mem custom storage folder (rules below) dsh-memory-manager.exe --lang zh-CN switch UI language 2. Script powershell -ExecutionPolicy Bypass -File .\attach.ps1 # all detected profiles powershell -ExecutionPolicy Bypass -File .\attach.ps1 -CreateWeb # create a missing web profile too powershell -ExecutionPolicy Bypass -File .\attach.ps1 -Repair # strip a UTF-8 BOM from every profile manifest powershell -ExecutionPolicy Bypass -File .\attach.ps1 -ProfileDir "C:\Users\you\.dsh\profiles\web" powershell -ExecutionPolicy Bypass -File .\attach.ps1 -DataFile "D:/dsh-mem/memory.json" Without -DataFile, the script keeps whatever dataFile the patch already has — it no longer deletes that line, which used to move the store behind your back. -Repair only fixes manifests: it prints fixed=… clean=… invalid=… and exits. 3. Manual (four steps) For a profile directory such as %APPDATA%\com.yeagoo.dsh-desktop\harness\profiles\web: 1. mklink /J "\node_modules\dsh-thirdparty-memory" "" (or copy the folder) 2. add "dsh-thirdparty-memory" to dsh.profile.bundles in the profile's package.json 3. append to cordis.patch.yml: - id: thirdparty-memory name: dsh-thirdparty-memory config: enabled: true judgeMode: model inbox: true 4. give the plugin its peer: junction \node_modules\@deepseek-ai\dsh-tools → the host's @deepseek-ai\dsh-tools Restart DSH. Bundles are read at boot only. Afterwards Settings → Plugins lists dsh-thirdparty-memory, and the memory page is up at . ------------------------------------------------------------------------ Daily use --------- You: remember: → stored immediately (explicit request), no review needed You: [全局] → injected on every turn from now on; global entries are the priciest bytes You: , then → only the A and B entries are injected; everything else collapses into the index You: extract every memory about → explicit recall: relevance threshold off, budget widened, whole batch delivered You: forget entry 3 / update entry 9 to: → remember remove / remember update; numbering is shared with the page Nothing to type for persona facts: the agent applies the four properties above by itself (judgeMode: model) and only records an entry once it has a date, a scene and your own words. If a note is wrong, delete it on the memory page — every deletion asks for a second confirmation. ------------------------------------------------------------------------ The memory page --------------- (served on 127.0.0.1 as well) * 🧠 Memory Hub What it does: All entries with category filters, pin/unpin, delete (two-step confirm); click an entry → workbench drawer * 📥 Inbox What it does: Auto-extracted persona facts; accept / edit / reject, individually or in bulk * 📊 Token Dashboard What it does: Byte/token split per section, budget warnings, library KPIs * 🔍 Search What it does: Keyword search over content, tags and project * ⚙️ Settings What it does: Master switch, auto extraction, who decides what to remember, index on/off, injection parameters, language, export JSON * 👁 Prompt preview What it does: The exact prompt a new chat would receive ------------------------------------------------------------------------ Configuration ------------- Two layers, later wins: cordis.patch.yml (deployment) → memory page settings stored in the store (memory.sqlite). * enabled Default: true Meaning: Hard switch. Off = no injection, no extraction, add fails. * judgeMode Default: model Meaning: model = the agent decides what to remember · rules = built-in regex fallback · off = never auto-record * autoExtract Default: true Meaning: Enables rule extraction when judgeMode: rules * inbox Default: true Meaning: Rule-extracted persona facts require review * injectMode Default: relevant Meaning: relevant = keyword-selected · all = everything * maxBytes Default: 8192 Meaning: Injection byte budget * maxRelevant Default: 6 Meaning: Max matched entries per turn * perCluster Default: 2 Meaning: Entries per topic when several topics appear * minScore Default: 6 Meaning: Relevance threshold (higher = stricter) * recallMaxBytes Default: 16384 Meaning: Widened budget for explicit recall * maxRecall Default: 40 Meaning: Max entries returned by explicit recall * showIndex Default: true Meaning: Include the numbered index of unselected entries * indexChars Default: 18 Meaning: Title length in the index * currentProject Default: "" Meaning: Active project name (project entries become eligible) * dataFile Default: "" Meaning: Memory path. Empty = %APPDATA%\dsh-thirdparty-memory\memory.sqlite. A .json path is accepted and maps to the same-name .sqlite beside it. * vector Default: true Meaning: Local vector index on/off. false = keyword-only retrieval. * vectorWeight Default: 1 Meaning: Weight of the cosine bonus. 0 disables the bonus entirely. * vectorMinCos Default: 0.3 Meaning: Cosine floor; below it a match adds nothing (higher = stricter). * pageServer / pageHost / pagePort Default: true / 127.5.0.1 / 7891 Meaning: Memory page process Storage rules The manager enforces them whenever you change the folder (Storage location… or --store): 1. the drive must exist; 2. every folder name must be ASCII (A-Z a-z 0-9 space _ - .); 3. the target folder must be empty (or already your current store); 4. a bare drive root is refused. If the old folder still holds memories the manager asks copy them over? — Yes copies, No switches only, Cancel aborts. After a switch, dataFile is written into every attached profile's cordis.patch.yml, so plugin and page keep reading the same store. ------------------------------------------------------------------------ Storage, vectors and updates ---------------------------- Where the data lives (0.4.0) The store is a local SQLite database opened through Node's built-in node:sqlite — no native module, no npm install, no server. WAL mode is on, so the page server and the plugin do not block each other. * dataFile accepts a .json path (kept for old configs) or a .sqlite path * real database same folder, same base name: memory.json → memory.sqlite * default (Windows) %APPDATA%\dsh-thirdparty-memory\memory.sqlite * override the folder set DSH_THIRDPARTY_MEMORY_HOME * no node:sqlite (Node < 22.5) automatic fallback to JSON storage at the same path; same features except vector retrieval The default folder sits outside the plugin folder on purpose: updating the plugin replaces code, and a code update must never touch memories. Older versions kept the file at \data\memory.json — exactly the place an update overwrites — so that file is now only a read-only migration source. Existing users keep what they had: if the config already sets dataFile, 0.4.0 uses that path unchanged. Only a fresh install with no dataFile lands in the new default folder. One-time JSON → SQLite migration: copy only On the first read, when the .sqlite database does not exist yet but a JSON store does: 1. the JSON is copied to memory.json.bak_pre_sqlite_ in the same folder; 2. that copy is imported into the new database; 3. the original JSON is not modified, not deleted and not renamed. It happens once; afterwards the database is the source of truth. The backup is the escape hatch: if the runtime ever changes, the JSON is still sitting there. Local vector index 0.4.0 adds a small local vector layer, built offline and with zero dependencies: - input: characters (1-, 2- and 3-grams) plus Latin words; - every token is hashed with FNV-1a into 256 dimensions, TF-weighted, then L2-normalised; - the vector is stored as a Float32 BLOB in the same database; - retrieval score = keyword score + cosine bonus round(cos × 12 × vectorWeight); a pair below vectorMinCos adds nothing. Be precise about what that buys you: it catches literal closeness — the same characters or words, a reordered sentence, a query that repeats part of an entry. It is not synonym-level semantics: shared characters and words, not shared meaning. Key Default Meaning ------------------------------ vector true false = keyword-only retrieval vectorWeight 1 multiplier for the cosine bonus; 0 disables it vectorMinCos 0.3 cosine floor; lower = more (and looser) matches Without node:sqlite there is no vector table, so retrieval is keyword-only — that is the single feature the fallback does not carry over. Update safety - Replacing the plugin folder only swaps code. An update does not overwrite, migrate, rename or delete any memory file. - attach.ps1 keeps an existing dataFile. Without -DataFile it leaves the configured path alone; previously it dropped that line, which quietly moved the store. - attach.ps1 -Repair scans every DSH profile's package.json, backs up any manifest that carries a UTF-8 BOM and rewrites it without one. The BOM crash (fixed in 0.4.0) Store/MSIX builds of DSH Desktop crashed on every start after install. Root cause: the profile's package.json had been written as UTF-8 with BOM; the host's readProfileManifest calls JSON.parse directly without stripping a BOM, so it threw SyntaxError: Unexpected token '\uFEFF' and exited with code 1 — and after three consecutive crashes the host stopped auto-restarting. The fix: both installers (attach.ps1 and the manager exe) now write UTF-8 without a BOM, reading tolerates a BOM, and -Repair cleans up manifests that are already broken. ------------------------------------------------------------------------ 0.4.3 fixes ----------- 0.4.3 is a correctness release on top of 0.4.2. No storage or configuration change: replacing the plugin files is enough, and the SQLite store and the JSON store are read as before. The concurrency defect: one snapshot per read readStore read the entry rows and the revision counter in two separate statements. The DSH host and the memory page are always two writers, so when one committed between those statements the other read old entries together with a new revision; the optimistic lock then saw a matching revision and let the write through, deleting the entry that had just been added — with no error anywhere. Reproduced with four processes writing 25 entries each: revision reached 101 while the table held 100. readStore now wraps rows, settings and revision in one read transaction (BEGIN … COMMIT). storageInfo() no longer creates an empty database The status call used by the memory page opened the SQLite file to read its counters. Opening a missing path creates an empty database, which silently disabled the "database lost + migration sentinel" degraded state after one page refresh: the next read saw an ordinary empty store and accepted writes. storageInfo() now leaves a missing file alone and reports zeros. Injection follows the right conversation The latest user message was cached in one process-wide variable. Any session — a second window or a subagent — overwrote it, so one conversation's injection was selected by another conversation's text (observed: the main session asked about a plugin bug, a subagent mentioned a different project, and the main session's injection switched to that project's memories). The cache is now per session; each prompt assembly looks up its own session through the assembly context (scope.session.id), and the process-wide fallback only accepts top-level sessions. Deleting by number re-checks the target remember remove echoes the entry first and deletes on the second call with confirm=true. If a write in between shifted the numbering, the confirmed number pointed at a different entry and the wrong memory was deleted (reproduced: preview #2, delete #1, confirm #2 — the entry that had been #3 was deleted). The preview records the entry id per number; a confirm that resolves to a different id is refused with MEMORY_CONFIRM_MISMATCH and asks for a fresh preview. Deleting by id is unaffected. Smaller fixes - ephemeral entries were stored with an empty sessionId, so the category could never match the running session and was never injected. The session now comes from the tool call itself (exec.agent.session.id). - A request body over 128 KB ended in a connection reset instead of the intended 413, because the handler destroyed the socket before answering. It now answers 413 and drains the body. - One corrupted character in a server.js comment was replaced. Verification: core-layer and HTTP regression suites (39 + 30 checks) against an isolated store and port; four-process concurrency over three rounds (101 entries and revision 101 each round); database-loss, corrupt-database, JSON-migration, backup and numbering checks; memory page rendered in headless Edge with no untranslated keys and no XSS in entry content. The real store was left untouched (59 entries before and after; WAL 0 bytes). 0.4.2 maintenance fixes ----------------------- - POST /api/entry/delete requires confirm: true, the same rule the command line has enforced since 0.4.1. Without it the endpoint answers 409; deleting an entry that is already gone answers 404. - storageInfo reports the real revision, last backup time and vector count. It used to read them from a database handle that had not been opened yet, so a fresh process always reported zeros and the memory page always claimed there was no backup. - Bulk deletion stays intentionally absent. An unreleased draft of 0.4.2 had a WIPE-gated endpoint and a settings button; both were withdrawn before release. Delete entries one at a time — the page asks twice every time. - Regression pass over every endpoint and a 5000-entry performance check. 0.4.1 data-safety fixes ----------------------- 0.4.1 fixes a set of data-safety defects in 0.4.0's SQLite storage. Each item below changes behaviour you can observe. Checkpoint after every write Every write ends with PRAGMA wal_checkpoint(TRUNCATE), merging the WAL back into memory.sqlite. The main file now carries all the data instead of staying a 4 KB shell whose content sits in the -wal file. Copying or backing up memory.sqlite alone is enough to read every memory; under 0.4.0 the same copy produced 0 entries and no error. -wal / -shm still appear while a process has the database open. They are runtime files, not part of a backup. A failed read degrades instead of falling back If the SQLite database cannot be read, the plugin no longer falls back to the old memory.json snapshot. It enters a degraded state: the injected section starts with a visible warning naming the reason and the database path, the memory page shows the same warning, and writes are refused (MEMORY_DEGRADED), so a stale snapshot cannot overwrite the live database. Migration sentinel: memory.sqlite.migrated A completed migration writes a sentinel file next to the database. If that database is later lost, the plugin will not rebuild a store from the old JSON — the sentinel records that the store was already migrated and the JSON is history. To run a migration again on purpose, delete the sentinel file. Automatic backups On the first write of each day the plugin creates a snapshot with VACUUM INTO at \backups\memory-.sqlite. The file is self-contained (no -wal dependency) and the most recent 30 are kept. Optimistic locking on writes Writes replace the whole store, so the DSH host and the memory page writing at the same time used to lose one side's entry. Each write now checks the revision counter in the SQLite meta table; on a mismatch it re-reads and replays the change instead of overwriting it. remember tool fixes - remove requires a second confirmation: the first call only echoes the entry, and the second call with confirm=true performs the deletion. - search now prints global entry numbers. Until 0.4.1 it printed the hit's position in the result list, so reading an entry by that number returned a different one. - update enforces the 2048-byte content limit, as add already did. Behaviour fixes - status=archived stays archived instead of being silently rewritten to active. - A global entry can be unpinned; global no longer forces pinned. - A 画像 tag is no longer silently dropped from an entry's tags. ------------------------------------------------------------------------ Data ---- Storage is SQLite — see Storage, vectors and updates (#storage-vectors-and-updates) for the paths, the migration and the update rules. The JSON document below is the fallback format the plugin reads and writes when node:sqlite is unavailable, and the shape GET /api/export returns: { "version": 2, "entries": [ { "id": "mem-xxxx", "content": "", "category": "global", "project": "", "priority": "high", "tags": [""], "source": "user", "status": "active", "pinned": true, "expiresAt": null, "sessionId": "", "createdAt": "2026-01-01T00:00:00.000Z", "updatedAt": "2026-01-01T00:00:00.000Z" } ], "settings": { "enabled": true, "lang": "en" } } status: active · pending (inbox) · rejected. Version-1 files migrate automatically (a [全局] prefix becomes category: "global"). The SQLite schema is an implementation detail — read the store through the page or the API, not with SQL. ------------------------------------------------------------------------ HTTP API -------- * GET Path: / Purpose: memory page * GET Path: /api/state?q= Purpose: state + entries + dashboard * GET Path: /api/export Purpose: download JSON * POST Path: /api/entry/add · /update · /delete Purpose: entry CRUD (one delete at a time, by design) * POST Path: /api/inbox · /api/inbox/bulk Purpose: accept / reject * POST Path: /api/settings Purpose: write user-layer settings ({reset:true} restores defaults) * POST Path: /api/preview Purpose: render the injection for a given q Loopback only; POST requires application/json, which blocks cross-site form posts. ------------------------------------------------------------------------ Troubleshooting (short list — full table in the tutorial) --------------------------------------------------------- * Nothing happens after install Cause / fix: DSH was not restarted; bundles load at boot only * DSH Desktop exits with code 1 right after install, three times, then stops restarting Cause / fix: the profile's package.json carries a UTF-8 BOM; run attach.ps1 -Repair, then restart DSH * Memories "disappear" after updating the plugin Cause / fix: compare dataFile in the patch with dsh-memory-manager.exe --data: an old install keeps its own path, a new one defaults to %APPDATA%\dsh-thirdparty-memory\memory.sqlite * Vector matching feels random / unwanted matches Cause / fix: raise vectorMinCos (stricter), lower vectorWeight, or set vector: false for keyword-only * Plugin missing from Settings → Plugins Cause / fix: run attach.ps1 again and read its output * Memory page blank Cause / fix: the page process died; run dsh-memory-manager.exe --start * Port 7891 is used by another program Cause / fix: another app owns the port: --port 7900, or Change port… * The agent searches the plugin folder instead of just answering Cause / fix: the plugin was not loaded, so no memory tool existed; restart DSH * Defender flags a scheduled task Cause / fix: never self-install via Task Scheduler (heuristic trojan detection); use the exe's tray or the Startup folder * Memories look like regulations Cause / fix: switch to judgeMode: model and let the agent write persona notes; the inbox shows what it inferred ------------------------------------------------------------------------ Privacy ------- Everything stays in one local SQLite database (plus its WAL side files). No network calls, no upload, no telemetry — the vector index is computed and stored locally. Memories do enter the model context — never store secrets. Destructive actions require two confirmations, and bulk deletion is intentionally not implemented: delete entries one by one. ------------------------------------------------------------------------ Compatibility ------------- Host Status -------------------------------------------- DSH Desktop (MSIX, desktop 0.2.x) tested DeepSeek Harness portable (Electron) tested (remember list / search verified) CLI dsh web composition verified via --dump-config Storage needs node:sqlite (Node 22.5+). On an older host runtime the plugin automatically falls back to JSON storage and keeps working; only vector retrieval is unavailable. The plugin deliberately has no schemastery dependency (its Config implements the Standard Schema ~standard contract), so it survives across DSH release lines; the only peer it needs is @deepseek-ai/dsh-tools. ------------------------------------------------------------------------ Build from source ----------------- :: rebuild the single-file manager (needs .NET Framework 4.x csc.exe + a node.exe) powershell -ExecutionPolicy Bypass -File build-single-exe.ps1 :: package release\dsh-thirdparty-memory-.zip (includes a personal-path leak check) powershell -ExecutionPolicy Bypass -File build-release.ps1 :: regenerate the plain-text docs after editing the Markdown node tools/md2txt.mjs README.md README.zh-CN.md README.zh-TW.md docs/tutorial.md docs/tutorial.zh-CN.md docs/tutorial.zh-TW.md ------------------------------------------------------------------------ License ------- MIT — see LICENSE. The bundled Node.js runtime is redistributed under its own MIT license (see docs/NODE-LICENSE.txt).