dsh-doubao-memory — Complete tutorial
=====================================
English root copy. 简体中文: tutorial.zh-CN.md · 繁體中文: tutorial.zh-TW.md.
Plain-text versions ship alongside every language (tutorial.txt, tutorial.zh-CN.txt, tutorial.zh-TW.txt).
This tutorial is written to be the only document you need: install, configure, use it every day, and fix it when something breaks.
------------------------------------------------------------------------
0. What you are actually running
--------------------------------
Four moving parts. Knowing which one is broken is 90% of troubleshooting.
* Plugin (index.js + core.js)
Lives where: inside a DSH profile, loaded by the Cordis loader
What it does: injects the 【long-term memory】 section, registers the remember tool, listens to conversation events
How to restart: restart DSH (bundles load at boot only)
* Memory page server (server.js + ui.html + core.js)
Lives where: a standalone node process
What it does: serves , reads/writes the same JSON
How to restart: dsh-memory-manager.exe --restart, or tray ▸ Restart
* Manager exe (dsh-memory-manager.exe)
Lives where: wherever you put it
What it does: extracts the bundled runtime, starts/stops the page server, tray menu, storage & port & language settings
How to restart: just relaunch it
* Store (memory.json)
Lives where: dataFile (default /data/memory.json, or your chosen storage folder)
What it does: the single source of truth both the plugin and the page read
How to restart: no restart needed; every operation re-reads the file
Two rules that explain most confusion:
1. The page and the plugin are independent processes. The page can be up while the plugin is not loaded (the agent then has no memory tool), and the plugin can be loaded while the page is down (the agent still has memories; only the UI is missing).
2. Only the store is shared state. If the page shows 0 entries while the agent "remembers things", they are reading two different files — see §3.4.
------------------------------------------------------------------------
1. Install
----------
1.1 Before you start
- Windows 10/11.
- Any DSH host already installed: DSH Desktop (MSIX), the DeepSeek Harness portable build, or the CLI (dsh).
- Nothing else. No Node install needed if you use the exe (it ships Node 24); with the script/manual route, Node 18+ must be resolvable.
Find your profile(s) first — the plugin attaches to a profile, not to "DSH" in the abstract:
%APPDATA%\com.yeagoo.dsh-desktop\harness\profiles\web ← DSH Desktop (MSIX), booted by `dsh web`
%USERPROFILE%\.dsh\profiles\web ← CLI / portable builds
%USERPROFILE%\.dsh\profiles\desktop ← older desktop-style profile
A profile is just a folder containing package.json, cordis.patch.yml, cordis.yml and node_modules. dsh web means "boot the profile named web".
1.2 Route A — the manager exe (recommended)
1. Put dsh-memory-manager.exe anywhere writable (e.g. D:\tools\). It is a single ~34 MB file; nothing else is required.
2. Double-click it.
- First run asks for the UI language → English / 简体中文 / 繁體中文. The choice is written to %LOCALAPPDATA%\dsh-doubao-memory\config.json and stays until you change it (tray ▸ Language).
- It extracts the bundled Node runtime and the app files into %LOCALAPPDATA%\dsh-doubao-memory\, starts the page server and opens .
- A tray icon appears: Open memory page, Copy address, Storage location…, Open storage folder, Change port…, Restart service, Stop service, Attach to DSH…, Open install folder, Language, Quit.
3. Tray ▸ Attach to DSH… (or run dsh-memory-manager.exe --install). For every profile it finds it creates a junction, registers the bundle, writes the patch entry and points the plugin's dsh-tools peer at the host's copy.
4. Restart DSH.
5. Verify:
- DSH ▸ Settings → Plugins lists dsh-doubao-memory (entry id doubao-memory, mounted);
- open a new chat and ask "what do you remember about me?" — it should answer from the injected section rather than hunting the disk;
- the memory page shows the same entries the agent can see.
CLI equivalents:
dsh-memory-manager.exe --install :: attach + start + open
dsh-memory-manager.exe --start :: start the page server only
dsh-memory-manager.exe --stop :: stop the page server
dsh-memory-manager.exe --status :: 0 healthy · 1 not running · 3 port owned by another app
dsh-memory-manager.exe --port 7900 :: remember a new port (refused if taken)
dsh-memory-manager.exe --store "D:\dsh-memory-data" :: remember a new storage folder
dsh-memory-manager.exe --lang zh-TW :: switch UI language
dsh-memory-manager.exe --dir / --data / --version :: show install dir / data file / version
1.3 Route B — attach.ps1
# every profile it can find (MSIX + portable + CLI homes)
powershell -ExecutionPolicy Bypass -File .\attach.ps1
# also create a `web` profile in any home that lacks one
powershell -ExecutionPolicy Bypass -File .\attach.ps1 -CreateWeb
# one specific profile
powershell -ExecutionPolicy Bypass -File .\attach.ps1 -ProfileDir "C:\Users\you\.dsh\profiles\web"
# keep the store outside the plugin folder (recommended: an ASCII path)
powershell -ExecutionPolicy Bypass -File .\attach.ps1 -DataFile "D:/dsh-memory-data/memory.json"
# point the plugin's dsh-tools peer at a specific runtime (multi-release setups)
powershell -ExecutionPolicy Bypass -File .\attach.ps1 -RuntimeDir "D:\harness\node_modules\@deepseek-ai" -Force
The script is idempotent: [skip] means "already fine". It prints one line per change, and ends with done: N/M profile(s) attached.
1.4 Route C — manual, four steps
Given a profile directory P:
1. Link the plugin:
mklink /J "P\node_modules\dsh-doubao-memory" "D:\plugins\dsh-doubao-memory"
(copying the folder works too; a junction just keeps edits in one place.)
2. Register the bundle — in P\package.json, dsh.profile.bundles must contain "dsh-doubao-memory".
3. Write the loader patch — append to P\cordis.patch.yml:
- id: doubao-memory
name: dsh-doubao-memory
config:
enabled: true
judgeMode: model
inbox: true
If the file only contains [], replace that line — a bare [] plus a mapping is invalid YAML.
4. Peer dependency — the plugin imports @deepseek-ai/dsh-tools from the host:
mklink /J "\node_modules\@deepseek-ai\dsh-tools" "\node_modules\@deepseek-ai\dsh-tools"
Then restart DSH.
> Why step 4 exists: the plugin's real path may sit on another drive, so Node's parent-directory walk never reaches $DSH_HOME\profiles\node_modules (the fallback DSH maintains for in-box packages). The junction makes the peer resolvable without duplicating it.
1.5 First-run checklist
[ ] memory page opens on http://127.0.0.1:7891/ (and http://127.0.0.1:7891/)
[ ] Settings → Plugins shows dsh-doubao-memory as mounted
[ ] a new chat can list memories (ask: "list my memories")
[ ] the page and the agent see the same number of entries
[ ] tray ▸ Storage location shows the folder you expect
------------------------------------------------------------------------
2. Configure
------------
2.1 Two configuration layers
Layer File Wins over
---------------------------------------------------------------------------
Deployment profile cordis.patch.yml (- id: doubao-memory block) the plugin's own defaults
User memory.json → settings (written by the memory page) deployment, for whitelisted keys
Whitelisted keys a user-layer setting may override: injectMode, judgeMode, maxRelevant, perCluster, minScore, showIndex, indexChars, maxBytes, maxEntries, recallMaxBytes, maxRecall, currentProject. Switches (enabled, autoExtract) are also layered: YAML off = hard off (the page switch cannot turn it back on), page off = soft off.
2.2 Every key, and when to change it
* enabled
Default: true
Change it when: You want a global kill switch (e.g. a sensitive session)
* judgeMode
Default: model
Change it when: rules if you want zero-token extraction; off if you want only explicit requests
* autoExtract
Default: true
Change it when: Only meaningful with judgeMode: rules
* inbox
Default: true
Change it when: false to let rule-extracted persona facts go live immediately
* injectMode
Default: relevant
Change it when: all if you have very few memories and want them always visible
* maxBytes
Default: 8192
Change it when: Lower for cheap models / long contexts; raise if you have many project facts
* maxRelevant
Default: 6
Change it when: Raise when your questions span many topics
* perCluster
Default: 2
Change it when: Raise when one sentence names 3–4 things
* minScore
Default: 6
Change it when: Raise (8–10) if unrelated memories keep showing up; lower (4) if relevant ones are missed
* recallMaxBytes
Default: 16384
Change it when: Raise if "extract everything about X" still truncates
* maxRecall
Default: 40
Change it when: Raise for very large libraries
* showIndex
Default: true
Change it when: false saves bytes but the model can no longer discover unselected entries
* indexChars
Default: 18
Change it when: 10–12 for cheaper indexes, 24+ for more recognisable titles
* currentProject
Default: ""
Change it when: Set it while you work inside one project so its memories are eligible without naming it
* dataFile
Default: ""
Change it when: Keep the store outside the plugin folder (recommended)
* pageServer
Default: true
Change it when: false if you manage the page yourself
* pageHost / pagePort
Default: 127.5.0.1 / 7891
Change it when: Port collisions, or you prefer 127.0.0.1
Three ready-made profiles:
# Minimal — few memories, cheapest prompt
config: { enabled: true, judgeMode: model, injectMode: all, maxBytes: 4096, showIndex: false }
# Balanced (default)
config: { enabled: true, judgeMode: model, injectMode: relevant, maxBytes: 8192, maxRelevant: 6, minScore: 6, showIndex: true }
# Long-context / many projects
config: { enabled: true, judgeMode: model, injectMode: relevant, maxBytes: 16384, maxRelevant: 10, perCluster: 3, minScore: 5,
recallMaxBytes: 32768, indexChars: 24, currentProject: 'my-project' }
2.3 Port
- Default 127.5.0.1:7891, and the server also binds 127.0.0.1 so localhost:7891 works.
- Change it: tray ▸ Change port…, or --port 7900, or pagePort in the patch, then restart the page.
- Occupied ports are refused, not hijacked. The manager distinguishes "my own server" (reuses it) from "someone else's" (exit code 3 + a message). If you point it at a port a different app owns, nothing starts and nothing is killed.
- Changing the port restarts only the page server; the DSH plugin is unaffected (it does not use the port).
2.4 Storage folder
Rules (enforced by the manager):
1. the drive must exist;
2. every folder name must be ASCII: A-Z a-z 0-9 plus space _ - .;
3. the target must be empty, or already be your current store;
4. a bare drive root (D:\) is rejected.
Change it with tray ▸ Storage location… or --store . If the old folder still has memories you are asked:
The old folder still holds N memories:
D:\old\memory.json
Copy them into the new folder?
Yes = copy No = switch only (old data stays) Cancel = abort
After a successful switch the manager writes dataFile into every attached profile's cordis.patch.yml — restart DSH so the plugin follows. The page server is restarted automatically.
> Keep the store in an ASCII path. Windows tolerates Chinese/Unicode folders, but wscript, PowerShell 5.1 script files and some launchers mangle them; keeping the data path plain ASCII removes a whole class of failures.
2.5 Language
- Chosen once at install time (first launch modal), stored in config.json.lang.
- Switch any time: tray ▸ Language, or --lang en|zh-CN|zh-TW, or the memory page ▸ Settings ▸ Language.
- The manager mirrors the choice into memory.json.settings.lang; the page server injects it into the HTML (window.__DSH_MEMORY_LANG__), so both stay in sync.
- Docs follow the same set: README.md (English root), README.zh-CN.md, README.zh-TW.md, and the tutorial in all three, each as .md and .txt.
------------------------------------------------------------------------
3. Daily use
------------
3.1 The remember tool
* list
Required: —
Optional: —
Effect: every entry with its number, category, priority, status
* read
Required: id or index
Optional: —
Effect: full text + metadata of one entry
* add
Required: content
Optional: category, priority, project, tags, ttlDays
Effect: create; refuses duplicates
* update
Required: id or index
Optional: content, category, priority, status, project, tags, ttlDays
Effect: rewrite in place, id preserved, updatedAt refreshed
* remove
Required: id or index
Optional: —
Effect: delete one entry
* search
Required: keyword
Optional: —
Effect: match over content, tags and project
Numbering is insertion order and identical in list, in the injected section and on the page — so "entry 3" always means the same thing.
The agent is instructed to search first and update rather than add: one fact, one entry, always the latest text.
3.2 Choosing the right category
* a rule that has to hold on every turn
Use: global
Why: always injected
* how they react, what they care about, what they push back on
Use: persona
Why: always injected, and it is what makes the assistant feel like it knows them
* a path / script / pitfall bound to one repository
Use: project (+ project name)
Why: only eligible while that project is active
* scratch context that only matters today
Use: ephemeral
Why: dies with the session
* anything else worth keeping
Use: general
Why: keyword-selected
If you want "always injected" without thinking about categories, start the text with [全局] — the plugin strips the marker and treats the entry as global.
3.2b What a persona entry has to look like
persona is the category that makes the assistant feel like it knows you — and the one that turns creepy when written badly. Third-person verdicts about a person read like a file being kept on them behind their back; that reaction ("why are you keeping notes on me?") is a signal you have written a profile instead of a record.
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. That profile is what makes a memory file read like a dossier — and it is the one users ask to have deleted.
Template (structure only, no content): YYYY-MM-DD — "" — .
If you cannot recall a scene and the wording, write nothing — an empty persona section is better than an invented one.
Practical consequences:
- Persona entries age. Add the date; when the observation stops matching reality, update it instead of stacking a contradicting twin.
- Never store a persona entry that only restates a global rule in psychological language — the rule itself belongs in global.
- Persona entries are always injected, so they are the most expensive bytes you own. Every sentence must earn its place.
3.3 What gets injected, and why the numbers matter
Order: global + persona (all, subject to budget) → project (eligible ones that also match) → ephemeral (same session) → general (keyword-matched) → a numbered index of everything else.
- A sentence with several topics is split into coverage units — mentioned tags first, then punctuation-separated clauses — and each unit contributes its own entries, so one loud topic cannot monopolise the budget.
- Explicit recall ("extract every memory about X") bypasses minScore/maxRelevant and widens the budget to recallMaxBytes.
- Nothing that is pending, rejected or expired is ever injected.
Read the dashboard before you complain about cost. It shows the byte/token split of the current injection, and warns when always-on memories exceed 40–90 % of the budget — that is your cue to demote project facts or trim globals.
3.4 Workflows worth knowing
Project switching. Keep project facts as project entries, then set currentProject when you switch (page ▸ Settings, or edit the patch). Their entries are then eligible by keyword even when you don't name the project; unset it and they go dormant.
Recording a pitfall. Just hit the problem in chat. Under judgeMode: model the agent decides it is worth keeping, searches for an existing note on the same topic, and writes or updates it — usually with tags like pitfall / lesson. You do not have to say "remember".
Fixing a wrong memory. Page ▸ Memory Hub ▸ click the entry ▸ edit the text, change category/priority/TTL ▸ Save, or Delete (two-step confirmation: click, then type the confirmation word). Edits take effect on the agent's next turn; no restart needed.
Inbox discipline. Rule-extracted persona facts (only with judgeMode: rules) land in the inbox as pending. Accept the ones that are true, reject the rest; a rejected entry never reaches the prompt.
Backups. GET /api/export (page ▸ Export JSON) writes the whole store. The manager also leaves memory.json.bak_* files next to the store whenever it migrates or rewrites.
------------------------------------------------------------------------
4. Troubleshooting
------------------
4.1 The agent has no memory at all
* DSH restarted after install?
Command / place: —
Expected: bundles load at boot only; restart is mandatory
* Plugin mounted?
Command / place: Settings → Plugins
Expected: dsh-doubao-memory present, mounted
* Bundle registered?
Command / place: profile package.json → dsh.profile.bundles
Expected: contains dsh-doubao-memory
* Patch applied?
Command / place: dsh web --dump-config (CLI) or profile cordis.patch.yml
Expected: contains the - id: doubao-memory block
* Junction healthy?
Command / place: \node_modules\dsh-doubao-memory
Expected: points at the plugin dir
* Peer resolvable?
Command / place: \node_modules\@deepseek-ai\dsh-tools\package.json
Expected: exists
Re-run attach.ps1; every line is either [ok], [skip] or [fail] — [fail] tells you which link is wrong.
4.2 The memory page does not open
dsh-memory-manager.exe --status :: 0 = healthy, 1 = not running, 3 = port owned by another app
dsh-memory-manager.exe --start :: start it
> Reading that exit code: the manager is a GUI-subsystem executable, so PowerShell's & returns before it finishes and $LASTEXITCODE stays empty. Use cmd /c "dsh-memory-manager.exe --status & echo %ERRORLEVEL%" or Start-Process -Wait -PassThru … when you need the number.
- exit 3 → someone else owns the port. --port 7900, or tray ▸ Change port…
- Start succeeds but the browser shows nothing → check http://127.0.0.1:7891/ as well as 127.5.0.1.
- The page worked and then went blank → the process died (killed by a cleanup script, a reboot, or the OOM killer). --start brings it back; the plugin also spawns it at DSH boot.
4.3 "My memories are gone!"
Almost never true. Check in this order:
1. Different store. Page ▸ Settings shows the data path; compare with what the plugin's patch says. A --store switch or an old dataFile value explains "0 entries" instantly.
2. Different host. Two DSH installations (MSIX + portable) can each have their own profile. Run attach.ps1 for both.
3. Pending / rejected. The dashboard counts them separately; the inbox is where unapproved facts wait.
4. Expired. expiresAt in the past hides an entry silently — filter chips ▸ ⏰ Expired.
5. The JSON is actually damaged (rare: only interrupted writes). Restore the newest memory.json.bak_* next to it, or re-import an export.
6. The file is fine but unreadable — check it parses: node -e "JSON.parse(require('fs').readFileSync('memory.json','utf8'))".
4.4 Bad memories keep getting in
- Switch to judgeMode: model (the agent judges) or off (only explicit requests) — the regex fallback is the noisy path.
- Keep inbox: true so rule-extracted persona facts need approval.
- Delete the offending entries (page, two-step confirm). The extractor also refuses sentences that are negated or meta ("when the user says remember…"), and technical instructions.
4.5 Prompt cost too high / model gets distracted
- Dashboard ▸ the bar tells you which section is expensive; always-on (global + persona) is the usual culprit.
- Demote project facts to project and set currentProject only when needed.
- Lower maxBytes, maxRelevant, perCluster; raise minScore.
- Turn showIndex off for the leanest prompt (you lose discoverability).
- Persona notes are cheap and useful — trim rule-like globals first, not persona.
4.6 Windows-specific traps
* Task Scheduler auto-start
Symptom: Windows Defender may treat a self-created scheduled task as a heuristic trojan (task registration fails with 0x800700E1)
Fix: do not create one; put the exe in the Startup folder, or start it from the tray after logon
* PowerShell 5.1 + UTF-8 scripts
Symptom: attach.ps1 fails to parse, Chinese messages turn into mojibake
Fix: scripts ship as UTF-8 with BOM; do not re-save them as BOM-less UTF-8
* wscript + non-ASCII paths
Symptom: the launcher cannot find server.js
Fix: launchers are pure ASCII and derive paths at runtime; keep the plugin/store in ASCII paths
* Antivirus slows or blocks the first run
Symptom: extraction takes long, or node.exe is quarantined
Fix: allow the exe / its %LOCALAPPDATA% folder
* Junction vs. copy
Symptom: edits to the source don't appear
Fix: a junction reflects edits immediately; a copy does not — re-copy or switch to a junction
* Two DSH hosts at once
Symptom: page serves one store, the other host uses another
Fix: point both profiles at the same dataFile
4.7 Uninstall
1. Delete the - id: doubao-memory block from each profile's cordis.patch.yml and dsh-doubao-memory from dsh.profile.bundles.
2. Remove \node_modules\dsh-doubao-memory (and the dsh-tools junction inside the plugin if you created it).
3. Restart DSH.
4. Delete the page process (--stop), the plugin folder, and — if you want the memories gone — the memory.json store and %LOCALAPPDATA%\dsh-doubao-memory.
5. Memories live only in that JSON file; nothing is uploaded anywhere.
------------------------------------------------------------------------
Appendix A — file map
---------------------
dsh-doubao-memory/
├── index.js plugin entry (system prompt section/context, remember tool, events, page spawn)
├── core.js pure logic: model, migration, tokenizer, selection, rendering, token estimate
├── server.js memory page server (node:http, loopback only)
├── ui.html memory page UI (single file, no external assets, 3 languages)
├── cordis.patch.yml bundle patch (inserts the doubao-memory row)
├── package.json package manifest (dsh.bundle.patch, peer: dsh-tools)
├── attach.ps1 multi-host installer (junction + bundles + patch + peer + dataFile)
├── build-single-exe.ps1 rebuilds the single-file manager
├── build-release.ps1 packages release\dsh-doubao-memory-.zip (+ leak check)
├── tools/md2txt.mjs regenerates the .txt docs from the .md sources
├── manager/
│ ├── DshMemoryManager.cs single-file manager source (embeds Node + docs)
│ └── Program.cs legacy "same-folder" manager source (not shipped)
├── start-memory-page.vbs / .bat, stop-memory-page.bat, 启动记忆页.bat, 停止记忆页.bat
├── docs/ tutorial.md/.txt (en, zh-CN, zh-TW), NODE-LICENSE.txt
├── examples/ cordis.patch.yml.example
├── data/memory.json default store (auto-created)
├── README.md / README.zh-CN.md / README.zh-TW.md (+ .txt)
└── LICENSE
Appendix B — endpoints
----------------------
* GET
Path: /
Body: —
Notes: memory page HTML (language injected server-side)
* GET
Path: /api/state?q=&session=
Body: —
Notes: config, settings, entries, dashboard, preview
* GET
Path: /api/export
Body: —
Notes: full JSON
* POST
Path: /api/entry/add
Body: {content, category?, project?, priority?, tags?, ttlDays?}
Notes: 400 empty, 409 duplicate
* POST
Path: /api/entry/update
Body: {id, ...fields}
Notes: ttlDays, clearExpiry
* POST
Path: /api/entry/delete
Body: {id}
Notes: one at a time by design
* POST
Path: /api/inbox
Body: `{id, action: accept\
Notes: reject, content?}`
* POST
Path: /api/inbox/bulk
Body: {action}
* POST
Path: /api/settings
Body: {enabled?, autoExtract?, judgeMode?, injectMode?, showIndex?, lang?, maxBytes?, …}
Notes: {reset:true} restores defaults
* POST
Path: /api/preview
Body: {q?, session?}
Notes: renders the injection
All POSTs require Content-Type: application/json.
Appendix C — quick reference
----------------------------
:: manager
dsh-memory-manager.exe --install / --start / --stop / --restart / --status
dsh-memory-manager.exe --port 7900 --store D:\dsh-memory-data --lang zh-CN
:: attach to every detected profile, store outside the plugin
powershell -ExecutionPolicy Bypass -File attach.ps1 -DataFile "D:/dsh-memory-data/memory.json"
:: rebuild the single-file exe (needs .NET Framework 4.x csc.exe)
powershell -ExecutionPolicy Bypass -File build-single-exe.ps1
:: package the release zip (leak-checks personal paths, never includes real memories)
powershell -ExecutionPolicy Bypass -File build-release.ps1
:: regenerate the .txt docs from the Markdown sources
node tools/md2txt.mjs README.md docs/tutorial.md
License: MIT. The bundled Node runtime is MIT as well — see docs/NODE-LICENSE.txt.