# Memory · dsh-bundle-memory A [DeepSeek Harness](https://github.com/OraSkyC/dsh-bundle-memory) (DSH) plugin that gives the agent a memory store **that travels with each workspace**. Once enabled, memories live under **`./memory/` in the current working directory**, with an auto-maintained `MEMORY.md` index at its root. Anything that needs classifying goes into a subdirectory created on demand; anything that does not goes straight into the root. > English · [简体中文](README.md) --- ## What it looks like ``` / └── memory/ ← memory root (configurable) ├── MEMORY.md ← index, generated by the plugin ├── 用户偏好用-pnpm.md ← no classification needed: straight into the root ├── 部署注意事项.md └── 项目约定/ ← classified: a subdirectory, created on demand └── 提交信息用中文.md ``` `memory_recall` with no arguments returns the index: ```markdown # 记忆索引 > 本文件由 dsh-bundle-memory 自动生成,手工改动会在下次保存记忆时被覆盖。 - 工作区:`D:\OraSkyC\Project\Python` - 记忆根目录:`D:\OraSkyC\Project\Python\memory` - 共 3 条记忆 ## 未分类 - [用户偏好用 pnpm](./用户偏好用-pnpm.md) — 工具链, 偏好 · 2026-10-08 · 420 B - [部署注意事项](./部署注意事项.md) · 2026-10-08 · 1.1 KB ## 项目约定/ - [提交信息用中文](./项目约定/提交信息用中文.md) — 约定 · 2026-10-08 · 380 B ``` ## Why "per workspace" The memory root is resolved **relative to the session's working directory** (`exec.agent.session.header.cwd`), which means: - what you record in project A is **invisible** in project B — no cross-contamination; - memories travel with the code, so you can commit them if you want and find them again after a clone on another machine; - you can still centralise them by setting `root` to something like `notes/mem` (still relative to the working directory). This is a deliberate trade-off against "global memory": global is less work but mixes project details together; per-workspace costs a little disk and keeps the boundary clear. ## Four tools | Tool | What it does | | --- | --- | | `memory_save` | Store a memory. Takes `title` + `content`; `category` is optional (`项目约定` or `people/work`, at most 3 levels) — **omit it and the file lands in the memory root**. Saving the same title again errors instead of silently overwriting; pass `overwrite: true` to replace. | | `memory_recall` | No arguments → the index (look here first). With `name` → one memory. `name` may be a relative path (`项目约定/提交信息用中文.md`) or a unique suffix (`提交信息用中文`); more than one match errors and lists the candidates — it **never guesses**. | | `memory_search` | Full-text search across every memory (title, tags, body; case-insensitive), returning matching paths with an excerpt around the hit. Use it when you remember the thing but not where it lives. | | `memory_forget` | Delete one memory. Accepts a single `.md` file only — it **never deletes a directory**, and never touches anything outside `./memory/`. | ## Installation **Settings → Plugins → Add plugin**, and enter: ``` https://github.com/OraSkyC/dsh-bundle-memory ``` Or on the command line: ```bash dsh plugin --profile desktop add https://github.com/OraSkyC/dsh-bundle-memory ``` **Restart DSH** afterwards. ## Settings **Settings → Plugins → Memory.** Changes take effect **immediately, with no restart**. Two layers: `cordis.patch.yml` holds the deploy defaults (restart required), and user overrides land in the **sparse** override layer at `%USERPROFILE%\.dsh\state\dsh-bundle-memory\settings.json`. "Reset" deletes the key so the field falls through to the deploy default again. | Key | Default | Meaning | | --- | --- | --- | | `enabled` | `true` | When off, no memory tools are registered; the panel still opens. | | `root` | `memory` | Memory root, relative to the session working directory, at most 3 levels. | | `indexFile` | `MEMORY.md` | Index file name inside the memory root. | | `autoIndex` | `true` | Rebuild the index after every save/delete. | | `maxEntryBytes` | `262144` | Max body size per memory (1024–4194304 bytes), so one call cannot dump a huge file into a single memory. | | `searchMaxResults` | `20` | Default result count for `memory_search` (1–100). | ## Design notes ### The index is a projection, not a ledger `MEMORY.md` is **regenerated** every time from the files on disk (scan the directory → read each file's front matter → render grouped by category). That is why there is no such state as "the index disagrees with the content" — the thing most likely to rot. The cost: **anything you hand-write into `MEMORY.md` disappears on the next rebuild.** The file header says so. To add a memory, drop a `.md` file into the directory (the plugin will index it) or use `memory_save`. ### The shape of one memory ```markdown --- title: 用户偏好用 pnpm tags: 工具链, 偏好 created: 2026-10-08T08:31:24.000Z updated: 2026-10-08T08:31:24.000Z --- 不要在文档里写 npm。 ``` There is deliberately **no YAML parser**: the format is only `key: value` lines, so a hand-written parser is more predictable — and a line you break by hand is ignored rather than fatal. Files with no front matter are still indexed, with the title falling back to the file name. ### Path safety This is where the plugin has to be most careful: the `name` / `category` the agent supplies end up as filesystem paths. Four layers: 1. **Segment validation** (`safeSegment`): rejects `..`, `.`, absolute paths, path separators, Windows-illegal characters (`\ / : * ? " < > |`), control characters, reserved device names (`CON`/`NUL`/`COM1`…), trailing dot or space, and anything over 64 characters. 2. **Depth limits**: `category` and `root` are capped at 3 levels. 3. **Re-check after joining** (`resolveInside`): `relative()` must not start with `..`. Even if an earlier layer were bypassed, this one still refuses. 4. **Deletes accept `.md` files only**: `memory_forget` can never remove a directory, nor the index. All four are covered by tests, including nested `..` traversal attempts. ### Where "the current working directory" comes from ```js const cwd = exec.agent.session.header.cwd ?? process.cwd(); ``` A tool's second argument `exec` carries the agent, the agent carries the session, and the session header carries `cwd`. **`process.cwd()` must not be used as the source** — that is DSH's own installation directory, not your workspace. ## Requirements | Item | Requirement | | --- | --- | | DSH | a `web` or any profile whose Host provides the `tools` service | | Node | `>= 22.19.0` | | npm dependencies | **none** (only `node:` builtins) | | Build step | **none** | The plugin depends on the Host's `tools` and `webServer` services (`inject = ['tools', 'webServer']`). `webServer` is a **hard** dependency: Cordis's `ctx` is a restricted proxy, so reading an undeclared property **throws** rather than returning `undefined`, and missing it fails the whole `apply()` — the plugin then shows as "error" in the Plugins page. ## FAQ **Will memories end up in my repository?** `./memory/` sits inside the working directory, so it depends on your `.gitignore`. Leave it un-ignored if you want memories to travel with the code; add a `memory/` line if you do not. **Why does the agent not remember by itself?** This version does **not** inject the index into the context — the agent has to call `memory_recall` or `memory_search`. A good habit is one line in the project's `AGENTS.md`: "run `memory_recall` first to read the index". Auto-injection is a viable follow-up, but it means constructing session messages and splicing them into the message stream, which carries more risk, so it is a separate piece of work. **I broke the index** Delete `memory/MEMORY.md`; the next save or delete rebuilds it. Or just call `memory_recall` with no arguments to force a rebuild. **Can it store binaries or attachments?** No. This is a plain-Markdown memory store; it only recognises `.md`. Put other things elsewhere in the workspace. ## Layout ``` dsh-bundle-memory/ ├── package.json # manifest: dsh.bundle.patch / dsh.client / files ├── cordis.patch.yml # registers the entry (id: memory) + deploy defaults ├── lib/index.js # config contract, path safety, index generation, four tools, panel routes ├── client.js # browser half: the settings card in the Plugins page ├── icon.svg ├── locale/zh.json # meta.title / meta.description ├── locale/en.json ├── README.md # Chinese ├── README.en.md # this file ├── CHANGELOG.md ├── LICENSE ├── test-host.mjs # host side: 59 assertion groups └── test-client.mjs # browser side: 24 assertion groups ``` ## Development ```bash npm run check # syntax check npm test # 59 host + 24 client assertion groups ``` The host tests use real temporary directories and cover config normalisation, all four path-safety layers, front-matter round-trips, index generation, every behaviour of the four tools, workspace isolation and the panel routes. The client tests cover the registration path and component rendering, plus two regressions that fail **silently** in a browser: **are the field labels visible**, and **do the CSS variables actually exist**. ## License [MIT](./LICENSE)