# dsh-doubao-memory

**Long-term memory for DeepSeek Harness (DSH) — the way Doubao's memory page feels.**

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](docs/tutorial.md) (also as `.txt`, with zh-CN / zh-TW versions in `docs/`).

```
Platforms : DSH Desktop (MSIX) · DeepSeek Harness portable (Electron) · CLI `dsh web`
Runtime   : Node.js 18+ (the exe ships its own Node 24)
Storage   : one local JSON file — no database, no vector store, no cloud, no telemetry
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-doubao-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. |
| **Zero runtime deps** | The server is `node:http`; there is nothing to `npm install`. |

---

## The five kinds of memory

| Category | Meaning | When injected |
| --- | --- | --- |
| 🌍 `global` | Language preference, hard prohibitions, workflow rules | **every turn** |
| 📁 `project` | Project-bound paths, pitfalls, scripts, specs | only while that project is active (named in the message, or `currentProject`) |
| 👤 `persona` | What you care about, what annoys you, taste, habits | **every turn** |
| ⏳ `ephemeral` | Scratch context for this conversation | same session only |
| 📝 `general` | Everything else | 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:

| Property | What it means | Why it matters |
| --- | --- | --- |
| **dated** | it carries the date of the observation | an undated observation cannot expire, and persona notes age |
| **scene-bound** | it names what actually happened | a scene can be confirmed; a character trait cannot |
| **anchored** | it keeps their own words or a concrete action | paraphrasing is where invention enters |
| **one line of reading** | judgement comes last and stays short | 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  <what happened>  — "<their words>"  — <one line of reading>`.

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-doubao-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
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 -ProfileDir "C:\Users\you\.dsh\profiles\web"
powershell -ExecutionPolicy Bypass -File .\attach.ps1 -DataFile "D:/dsh-mem/memory.json"
```

### 3. Manual (four steps)

For a profile directory such as `%APPDATA%\com.yeagoo.dsh-desktop\harness\profiles\web`:

1. `mklink /J "<profile>\node_modules\dsh-doubao-memory" "<plugin dir>"` (or copy the folder)
2. add `"dsh-doubao-memory"` to `dsh.profile.bundles` in the profile's `package.json`
3. append to `cordis.patch.yml`:
   ```yaml
   - id: doubao-memory
     name: dsh-doubao-memory
     config:
       enabled: true
       judgeMode: model
       inbox: true
   ```
4. give the plugin its peer: junction `<plugin>\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-doubao-memory`, and the memory page is up at <http://127.0.0.1:7891/>.

---

## Daily use

```
You:  remember: <one fact, plus the reason when the reason is not obvious>
      → stored immediately (explicit request), no review needed

You:  [全局] <a rule that has to hold on every single turn>
      → injected on every turn from now on; global entries are the priciest bytes

You:  <a task about topic A>, then <a task about topic B>
      → only the A and B entries are injected; everything else collapses into the index

You:  extract every memory about <topic>
      → explicit recall: relevance threshold off, budget widened, whole batch delivered

You:  forget entry 3 / update entry 9 to: <new text>
      → 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

<http://127.0.0.1:7891/> (served on `127.0.0.1` as well)

| Panel | What it does |
| --- | --- |
| 🧠 **Memory Hub** | All entries with category filters, pin/unpin, delete (two-step confirm); click an entry → workbench drawer |
| 📥 **Inbox** | Auto-extracted persona facts; accept / edit / reject, individually or in bulk |
| 📊 **Token Dashboard** | Byte/token split per section, budget warnings, library KPIs |
| 🔍 **Search** | Keyword search over content, tags and project |
| ⚙️ **Settings** | Master switch, auto extraction, **who decides what to remember**, index on/off, injection parameters, language, export JSON |
| 👁 **Prompt preview** | The exact prompt a new chat would receive |

---

## Configuration

Two layers, later wins: `cordis.patch.yml` (deployment) → memory page settings stored in `memory.json`.

| Key | Default | Meaning |
| --- | --- | --- |
| `enabled` | `true` | Hard switch. Off = no injection, no extraction, `add` fails. |
| `judgeMode` | `model` | `model` = the agent decides what to remember · `rules` = built-in regex fallback · `off` = never auto-record |
| `autoExtract` | `true` | Enables rule extraction when `judgeMode: rules` |
| `inbox` | `true` | Rule-extracted persona facts require review |
| `injectMode` | `relevant` | `relevant` = keyword-selected · `all` = everything |
| `maxBytes` | `8192` | Injection byte budget |
| `maxRelevant` | `6` | Max matched entries per turn |
| `perCluster` | `2` | Entries per topic when several topics appear |
| `minScore` | `6` | Relevance threshold (higher = stricter) |
| `recallMaxBytes` | `16384` | Widened budget for explicit recall |
| `maxRecall` | `40` | Max entries returned by explicit recall |
| `showIndex` | `true` | Include the numbered index of unselected entries |
| `indexChars` | `18` | Title length in the index |
| `currentProject` | `""` | Active project name (project entries become eligible) |
| `dataFile` | `""` | Memory file path, empty = `<plugin>/data/memory.json` |
| `pageServer` / `pageHost` / `pagePort` | `true` / `127.5.0.1` / `7891` | 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.

---

## Data

One JSON file (`memory.json`, version 2):

```json
{
  "version": 2,
  "entries": [
    {
      "id": "mem-xxxx", "content": "<the fact, one self-contained sentence>",
      "category": "global", "project": "", "priority": "high",
      "tags": ["<tag>"], "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"`).

---

## HTTP API

| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/` | memory page |
| GET | `/api/state?q=` | state + entries + dashboard |
| GET | `/api/export` | download JSON |
| POST | `/api/entry/add` · `/update` · `/delete` | entry CRUD (one delete at a time, by design) |
| POST | `/api/inbox` · `/api/inbox/bulk` | accept / reject |
| POST | `/api/settings` | write user-layer settings (`{reset:true}` restores defaults) |
| POST | `/api/preview` | 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)

| Symptom | Cause / fix |
| --- | --- |
| Nothing happens after install | DSH was not restarted; bundles load at boot only |
| Plugin missing from *Settings → Plugins* | run `attach.ps1` again and read its output |
| Memory page blank | the page process died; run `dsh-memory-manager.exe --start` |
| `Port 7891 is used by another program` | another app owns the port: `--port 7900`, or *Change port…* |
| The agent searches the plugin folder instead of just answering | the plugin was not loaded, so no memory tool existed; restart DSH |
| Defender flags a scheduled task | never self-install via Task Scheduler (heuristic trojan detection); use the exe's tray or the Startup folder |
| Memories look like regulations | switch to `judgeMode: model` and let the agent write persona notes; the inbox shows what it inferred |

---

## Privacy

Everything stays in one local JSON file. No network calls, no upload, no telemetry. 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` |

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

```bat
:: 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-doubao-memory-<version>.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](LICENSE). The bundled Node.js runtime is redistributed under its own MIT license (see `docs/NODE-LICENSE.txt`).
