dsh-engramory 0.3.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md ADDED
@@ -0,0 +1,213 @@
1
+ [简体中文](README.md) | **English**
2
+
3
+ # dsh-engramory
4
+
5
+ [![dsh-xray](https://img.shields.io/endpoint?url=https%3A%2F%2Funstone.github.io%2Fdsh-xray%2Fbadge%2Ftinqiao-oss__engramory.json)](https://unstone.github.io/dsh-xray/registry.html#tinqiao-oss__engramory)
6
+
7
+ Long-term memory for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
8
+ (dsh). What the AI remembers is kept as plain markdown files you can open and edit
9
+ at any time. Part of [Engramory](https://github.com/tinqiao-oss/engramory).
10
+
11
+ ![The memory page](https://raw.githubusercontent.com/tinqiao-oss/engramory/master/adapters/dsh/plugin/screenshots/memory-page.png)
12
+
13
+ ## What it does
14
+
15
+ - **The memory index rides along on every step.** The index, `MEMORY.md`, lists the
16
+ notes by name. The plugin puts it into dsh's context on every step, so the AI knows
17
+ what it has saved without reading a file first, and opens a note when it needs the
18
+ detail. A write shows up on the next request; whatever is past the cap is left
19
+ out, and the AI is told to compact.
20
+ - **The index has a hard cap.** 200 lines / 25 KB by default. When the AI writes the
21
+ index with dsh's file tools and the plugin can work out the result, a write that
22
+ would grow it past the cap is refused before it happens, and the AI is told to
23
+ compact; compaction still passes. Details in [How the cap decides](#how-the-cap-decides).
24
+ - **The rules come with it.** What to save, how, and when to clean up: the rules
25
+ (a skill) are registered by the plugin, with no files to install.
26
+ - **A Memory page in the sidebar.** Read-only; it lists the notes by name and opens
27
+ one in your computer's default app.
28
+
29
+ ## Install
30
+
31
+ Needs Node.js 18+ and `pnpm` on PATH (dsh's plugin command uses it). In dsh's desktop
32
+ app you can also search for `dsh-engramory` on the Plugins page.
33
+
34
+ ```sh
35
+ dsh plugin --profile <name> add dsh-engramory
36
+ ```
37
+
38
+ To uninstall, use `remove` instead of `add`. Compatible dsh releases are listed in
39
+ `dsh.compatibility.dshReleases` in [`package.json`](package.json); an unlisted release
40
+ is unverified.
41
+
42
+ **Where the memory lives:**
43
+
44
+ - **The global store**, `~/.dsh/.engramory-memory/` (under `$DSH_HOME` when it is set).
45
+ With the plugin alone this is the only store; the AI creates it on its first save.
46
+ - **A project store** (optional), `.engramory-memory/` in a project's root, is created
47
+ by you only: run `python tools/engramory_init.py dsh --project-root <project>` from
48
+ the Engramory repo, which also git-ignores it. Where one exists, what holds only in
49
+ that project goes there; everything else stays in the global store.
50
+
51
+ Only one AI may write a store; two writers overwrite each other. To share it, let the
52
+ others read it only: see [one writer, many readers](../../reader/README.md). Memories are plain text and carry machine-local detail. Inside a project's git repo
53
+ the store must be git-ignored; a dedicated private repo for the store is fine.
54
+
55
+ **Without the plugin:** `python tools/engramory_init.py dsh --install-skill` writes the
56
+ rules into `$DSH_HOME/AGENTS.md`, installs the skill in `$DSH_HOME/skills/engramory/`
57
+ (`$DSH_HOME` defaults to `~/.dsh`) and creates the global store. The AI still keeps memory, but reads the index itself, and
58
+ the cap holds only if it runs `engramory_check.py` after each write.
59
+
60
+ ## Companion mode (大肥鱼模式)
61
+
62
+ For a character you chat with: the community's 大肥鱼, or any persona. Turn on the
63
+ companion switch on the settings page, or set `companion: true`.
64
+
65
+ ![The memory page in companion mode](https://raw.githubusercontent.com/tinqiao-oss/engramory/master/adapters/dsh/plugin/screenshots/companion-mode.png)
66
+
67
+ - **Three more types for the character's own life** ([SKILL.md §2.2](../../../SKILL.md)):
68
+ `moment`, something you shared, with its date, folded into a monthly archive when
69
+ the index fills; `upcoming`, something coming up in your life, asked about once its
70
+ date passes and rewritten as a moment once you say how it went; `self`, a change in
71
+ the character itself, such as a nickname. A moment or self note is deleted only when
72
+ you ask it to forget.
73
+ - **Recall in character.** Even a greeting may call for recall, and what it remembered,
74
+ or failed to save, is told in the character's voice, without naming files or tools.
75
+ - **The persona preset needs the file tools.** The memory arrives even under a preset
76
+ that turns optional context off (dsh's `minimal`, for one). To save notes, though, the
77
+ preset needs dsh's file tools and the skill tool; a preset with only a shell reads the
78
+ memory, and its writes go past the cap.
79
+
80
+ ## Social mode
81
+
82
+ A choice inside companion mode, for a character other people can talk to as well, such
83
+ as one wired to QQ, WeChat, Douyin or Bilibili through a bridge plugin. Choose *Social*
84
+ under *Memory access* on the settings page, or set `audience: social`. If only you talk
85
+ to this dsh, keep the default, *Private*.
86
+
87
+ With it on, **only your own conversations get the memory**: one is yours when you have
88
+ typed in it in dsh's own window (the desktop app or `dsh web`) and nobody else has.
89
+ A subagent goes with its parent. Messages from a bridge plugin (unless it marks them as
90
+ the owner's, below), `dsh headless`, other programs, and terminal or editor clients
91
+ count as someone else's. Once anyone else has written in a conversation it stays
92
+ theirs, and the memory already put into it stays in its history.
93
+
94
+ Someone else's conversation gets no index and is not told where the store is; dsh's
95
+ file tools there may name files only (every directory is refused), and any path into
96
+ a store is refused however it is spelled. This is **not** a security boundary: a shell
97
+ tool reads any file, and another profile on the same account can read the store too.
98
+ Real isolation needs filesystem permissions or a sandbox.
99
+
100
+ ### For bridge authors
101
+
102
+ With social mode on, every message a bridge submits counts as someone else's. For a
103
+ message from the owner's own account (their private QQ chat with the character, say),
104
+ put a `speaker` on its source:
105
+
106
+ ```js
107
+ agent.followup(createUserMessage({
108
+ content: [{ type: 'text', text }],
109
+ source: { kind: 'plugin:your-bridge', speaker: { role: 'owner' } },
110
+ }))
111
+ ```
112
+
113
+ | `speaker.role` | Meaning |
114
+ |---|---|
115
+ | `owner` | The owner wrote it: it comes from the account the user set as theirs in your bridge. |
116
+ | `guest` | Someone else wrote it, the same as no `speaker` at all. |
117
+ | `none` | Nobody wrote it: your plugin's own report, reminder or notice. It does not make the owner's conversation someone else's. |
118
+
119
+ A group conversation stays someone else's once anyone else in it speaks. A program
120
+ that talks to dsh only through dsh web's API cannot set a source, so its messages count
121
+ as someone else's; if it sends `clientTimeZone` with a prompt it is taken for the
122
+ owner, so a program forwarding other people's messages must leave it out.
123
+
124
+ ## Settings page and memory page
125
+
126
+ ![The settings page](https://raw.githubusercontent.com/tinqiao-oss/engramory/master/adapters/dsh/plugin/screenshots/settings-page.png)
127
+
128
+ - **Settings page:** on dsh's *Plugins* page, open *Engramory memory* to set companion
129
+ mode, memory access and the index cap. Saving goes through dsh's own profile editor,
130
+ which writes your profile's `cordis.patch.yml` and reloads the plugin. Only you, signed
131
+ in, can call it, so an agent cannot use it to switch social mode off.
132
+ - **Memory page:** *Memory* in the sidebar. It lists the global store and each
133
+ workspace's project store (with an absolute `indexPath`, that store alone; with a
134
+ relative one, none), with each index's size against the cap, its notes' names
135
+ by type, and its archive. Read-only, never a note's text; a name, *Open the index* and
136
+ *Open the folder* open on the machine dsh runs on. The two switches at the top save
137
+ as you flip them.
138
+
139
+ ## Configuration
140
+
141
+ Settings the page does not cover go in your profile's `cordis.patch.yml`. Patch the
142
+ EXISTING row by id; a second `- insert:` adds another engramory row and leaves the old
143
+ caps in force. A patch replaces the row's whole `config`, so list every key you keep:
144
+
145
+ ```yaml
146
+ - id: engramory
147
+ config:
148
+ indexName: MEMORY.md
149
+ maxLines: 200
150
+ maxBytes: 25600
151
+ ```
152
+
153
+ | Field | Default | Meaning |
154
+ |---|---|---|
155
+ | `indexName` | `MEMORY.md` | The file name treated as the index, case-insensitive. |
156
+ | `maxLines` | `200` | Line cap for the index. |
157
+ | `maxBytes` | `25600` | UTF-8 byte cap for the index (25 KB). |
158
+ | `indexPath` | unset | The one index to use: absolute, or relative to the session's directory. Unset, a `MEMORY.md` in any project is capped as an index; a store outside the default places also needs it to be found. |
159
+ | `registerSkill` | `true` | `false` keeps the cap but skips the built-in rules. |
160
+ | `injectIndex` | `true` | `false` stops putting the index into context; the AI reads it instead. |
161
+ | `language` | dsh's | `zh` (or `zh-CN` and other regional forms) writes the store in Chinese: note names, headings such as `## 用户`, the Why/How labels. Any other value is English. Unset, it follows dsh's language; a change applies once dsh restarts. Frontmatter keys and `type:` values stay English, so every host reads the store. |
162
+ | `companion` | `false` | `true` turns companion mode on. |
163
+ | `audience` | `private` | With companion mode on, `social` turns social mode on. |
164
+ | `skill` | built-in | Your own markdown in place of the built-in rules; it must then say where the store is. |
165
+
166
+ ## How the cap decides
167
+
168
+ | Case | Decision |
169
+ |---|---|
170
+ | The index ends within the caps | allow |
171
+ | The write grows the index past a cap | **deny**, with the numbers and what to compact |
172
+ | The index is over a cap and the write shrinks it or keeps its size | allow, so compaction can go step by step (210 → 205 → 198) |
173
+ | An edit with `old_str`/`new_str` | its result is worked out first, then judged as above |
174
+ | A partial edit whose result cannot be worked out (e.g. `insert`) | deny on an over-cap index, else allow |
175
+ | The index is missing, unreadable, or not a regular file | a whole-file write is judged as if it were empty; a partial edit passes |
176
+ | The index is past the read limit (`min(16 MiB, max(1 MiB, 4 × maxBytes))`) | not read whole: a whole-file write is judged on bytes alone; a partial edit passes |
177
+ | Reads, writes to other files, unknown tools or arguments | allow; reads are never blocked, even over the cap |
178
+
179
+ Lines and bytes are judged apart: only a dimension that is over its cap AND grew is
180
+ refused, so a write with fewer bytes but lines grown past the cap is still refused.
181
+ The line count ignores a trailing newline, so an index exactly at the cap stays
182
+ writable. This table is pinned by `node --test` in CI.
183
+
184
+ ## Known limits
185
+
186
+ - **Only dsh's own file tools are capped** (`write`, `edit`, `str_replace_editor` and
187
+ the like). Shell tools, MCP file tools and external editors go past it: run
188
+ `engramory_check.py` after editing the index and `engramory_doctor.py` now and then.
189
+ - **Stores are found only where the installer puts them.** Elsewhere, or for a project
190
+ store that links out of the project, set `indexPath`. A project's root is found by
191
+ its `.git` only, and a project store created mid-session can take ten seconds to appear.
192
+ - **Social mode knows you by what dsh's window sends.** Apart from a bridge's owner
193
+ messages, only what you type in dsh's own window is yours, and not when the browser's
194
+ time zone cannot be read. A kind of
195
+ message a later dsh adds counts as someone else's until this plugin knows it: your
196
+ conversation may lose the memory, never the other way round.
197
+ - **What the memory page opens is checked by path.** dsh's opener resolves the path once
198
+ more before the desktop opens it, so a process changing the store's files at that
199
+ moment could redirect what opens (as with dsh's own opener for any workspace file).
200
+ - **Experimental:** companion mode, social mode, and the settings and memory pages may
201
+ change in a minor version.
202
+
203
+ ## Versions
204
+
205
+ This package carries Engramory's version: `dsh-engramory@X.Y.Z` is the repo's
206
+ `vX.Y.Z`. The cap's decisions (the table above) and the settings outside the
207
+ experimental features change incompatibly only in a major version. See Engramory's
208
+ [Versioning](https://github.com/tinqiao-oss/engramory/blob/master/README.en.md#versioning); history is in the
209
+ [CHANGELOG](../../../CHANGELOG.md).
210
+
211
+ ## License
212
+
213
+ MIT — see the [Engramory repository](https://github.com/tinqiao-oss/engramory).
package/README.md CHANGED
@@ -1,160 +1,60 @@
1
- [English](README.md) | [简体中文](README.zh-CN.md)
1
+ **简体中文** | [English](README.en.md)
2
2
 
3
3
  # dsh-engramory
4
4
 
5
5
  [![dsh-xray](https://img.shields.io/endpoint?url=https%3A%2F%2Funstone.github.io%2Fdsh-xray%2Fbadge%2Ftinqiao-oss__engramory.json)](https://unstone.github.io/dsh-xray/registry.html#tinqiao-oss__engramory)
6
6
 
7
- Curated, file-based long-term memory for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) —
8
- plain markdown notes you can open with anything,
9
- [one writer and many readers](../../reader/README.md) across hosts, and a pre-write
10
- index guard for matched file tools. (If the store
11
- lives inside a project's git repo it must be git-ignored — memories carry machine-local
12
- detail; see SKILL.md §1. A *dedicated private* repo for the store itself is fine.)
7
+ 给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)用的长期记忆插件。AI 记住的东西存成一个个普通 markdown 文件,你随时能打开看、能改。属于 [Engramory](https://github.com/tinqiao-oss/engramory)。
13
8
 
14
- Part of [Engramory](https://github.com/tinqiao-oss/engramory).
9
+ ![记忆页](https://raw.githubusercontent.com/tinqiao-oss/engramory/master/adapters/dsh/plugin/screenshots/memory-page.png)
15
10
 
16
- ## Why a plugin, when an `AGENTS.md` block already works
11
+ ## 它做什么
17
12
 
18
- The block carries the discipline. Three things it cannot do:
13
+ - **每轮自动带上记忆索引。** 索引 `MEMORY.md` 只列笔记名字,插件每一步都把它放进上下文,AI 不用先读文件就知道自己记过什么,要细节再打开那条笔记。写入后下一轮就能看到;超出上限的部分不带上,并提醒 AI 压缩。
14
+ - **索引有硬上限。** 默认 200 行 / 25 KB。AI 用 dsh 的文件工具写索引时,只要插件算得出写完的样子,会超上限就在写入前拒绝,让它先压缩;压缩照样放行。细节见[上限怎么判](#上限怎么判)。
15
+ - **自带记忆规则。** 记什么、怎么记、什么时候清理,这套规则(skill)随插件注册,不用另外装文件。
16
+ - **侧栏多一个「记忆」页。** 只读,列出笔记名字,点名字用电脑上的默认程序打开。
19
17
 
20
- **The index is already in context — recall needs no read.** On every step the plugin
21
- puts the memory index into dsh's runtime context: the project store
22
- (`<project>/.engramory-memory/MEMORY.md`, the nearest one between the session's
23
- directory and its git root — the way dsh finds a project's `AGENTS.md`) and the global one
24
- (`$DSH_HOME/.engramory-memory/MEMORY.md`, `~/.dsh` when `$DSH_HOME` is unset) —
25
- where `tools/engramory_init.py dsh` puts them — or, when set, `indexPath` alone. It is
26
- re-read whenever it changes, so a write shows on the next request, and cut at the cap:
27
- lines past it are left out, and the model is told to compact. Turn it off with
28
- `injectIndex: false`.
18
+ ## 安装
29
19
 
30
- **The cap becomes deterministic for matched tools.** `ctx.tools.guard()` is a
31
- synchronous, **monotonic** refusal: once a guard returns a reason, later listeners
32
- cannot allow the call. This keeps the index within its configured growth limits
33
- without relying on the agent to run a checker. Use **`dsh-engramory >= 0.2.7`**;
34
- [PORTING.md](../../../PORTING.md) records which hosts have shipped guards, and
35
- [CHANGELOG.md](../../../CHANGELOG.md) holds release history.
20
+ 需要 Node.js 18+,PATH 里要有 `pnpm`(dsh 的插件命令要用)。dsh 桌面版也可以在插件页搜 `dsh-engramory` 安装。
36
21
 
37
- **The protocol arrives with the plugin.** The skill is registered at runtime through
38
- dsh's skill registry — via a reactive `ctx.inject(['skills'], …)` child, so a profile
39
- without a registry still gets the cap and one whose registry activates late still gets
40
- the skill — which means it does not depend on landing files in one of the five skill
41
- roots dsh scans, a path that is easy to get subtly wrong and fails silently when you do.
22
+ ```sh
23
+ dsh plugin --profile <名字> add dsh-engramory
24
+ ```
42
25
 
43
- ## Install
26
+ 卸载把 `add` 换成 `remove`。兼容的 dsh 版本见 [`package.json`](package.json) 里的 `dsh.compatibility.dshReleases`,没列出的版本未经验证。
44
27
 
45
- Zero-dependency ESM with no build step; requires Node.js 18+ and `pnpm` on PATH
46
- for dsh's plugin command.
28
+ **记忆存在哪:**
47
29
 
48
- ```sh
49
- dsh plugin --profile <name> add dsh-engramory
50
- ```
30
+ - **全局库** `~/.dsh/.engramory-memory/`(设了 `$DSH_HOME` 就在它下面)。只装插件就用这一个,AI 第一次保存时自己建。
31
+ - **项目库**(可选)是项目根目录下的 `.engramory-memory/`,只能由你来建:在 Engramory 仓库里运行 `python tools/engramory_init.py dsh --project-root <项目>`,它会顺手加进 `.gitignore`。有项目库时,只在这个项目成立的事记在那里,其余仍记全局库。
51
32
 
52
- Check `dsh.compatibility.dshReleases` in [`package.json`](package.json) for the
53
- exact compatible releases, backed by [`results.json`](../../../tests/dsh_e2e/results.json).
54
- Use the [E2E guide](../../../tests/dsh_e2e/README.md) to verify another release.
33
+ 一个库只能有一个 AI 写,同时写会互相覆盖;想让别的 AI 也用,让它只读,见[单写者、多读者](../../reader/README.md)。记忆是明文,带着本机细节。放在项目的 git 仓库里必须 git-ignore;给记忆库单独开一个私有仓没问题。
55
34
 
56
- The package ships a `dsh.bundle` manifest pointing at its own
57
- `cordis.patch.yml`, so the row is inserted into the profile's plugin tree for you — no
58
- hand-editing. To change a default, patch the EXISTING row by id in your profile's own
59
- patch layer — do **not** use a second `- insert:` (insert always appends, so you would
60
- end up with two engramory rows and the original caps still enforced):
35
+ **不装插件也能用:** `python tools/engramory_init.py dsh --install-skill` 把记忆规则写进 `$DSH_HOME/AGENTS.md`、把 skill 装进 `$DSH_HOME/skills/engramory/`(`$DSH_HOME` 默认 `~/.dsh`),并建好全局库。AI 照样会记,但索引要它自己去读,上限只靠它写完后自觉运行 `engramory_check.py`。
61
36
 
62
- ```yaml
63
- - id: engramory
64
- config:
65
- indexName: MEMORY.md
66
- maxLines: 200
67
- maxBytes: 25600
68
- ```
37
+ ## 大肥鱼模式
69
38
 
70
- A patch replaces the targeted row's whole `config`, so list every key you mean to keep.
39
+ 给陪你聊天的角色用(社区里的大肥鱼,或任何人设)。在设置页打开「大肥鱼模式」,或在配置里写 `companion: true`。
71
40
 
72
- With the plugin alone, the memory is one global store, `$DSH_HOME/.engramory-memory/`
73
- (`$DSH_HOME` defaults to `~/.dsh`): the skill tells the AI where it is, and the AI
74
- creates it on its first save. A project can have a store of its own,
75
- `.engramory-memory/` in its root, which only you create — the installer does it and
76
- git-ignores it (`python tools/engramory_init.py dsh --project-root <project>`). The AI
77
- never creates one; where one exists, what holds only in that project goes there, and
78
- everything else stays in the global store. The installer also writes the always-on block
79
- into `AGENTS.md` (`python tools/engramory_init.py dsh --install-skill`).
41
+ ![大肥鱼模式下的记忆页](https://raw.githubusercontent.com/tinqiao-oss/engramory/master/adapters/dsh/plugin/screenshots/companion-mode.png)
80
42
 
81
- To uninstall:
43
+ - **多记三类角色自己的事**([SKILL.md §2.2](../../../SKILL.md)):`moment`(经历)是和你一起经历的事,带日期,索引满了按月归档;`upcoming`(约定)是你将要发生的事,日子过了会问起结果,问到了再改写成经历;`self`(我自己)是角色自己的变化,比如外号。经历和我自己只有你说忘掉才删。
44
+ - **用角色口吻回忆。** 打招呼也可能让它想起点什么;记住了、没记上,都用角色的话说,不提文件和工具。
45
+ - **人设预设要留着文件工具。** 记忆总会送到,哪怕预设关掉了可选上下文(比如 dsh 的 `minimal`)。但角色要保存记忆,预设得带上 dsh 的文件工具和 skill 工具;只给了 shell 的预设读得到记忆,写入却绕过上限。
82
46
 
83
- ```sh
84
- dsh plugin --profile <name> remove dsh-engramory
85
- ```
47
+ ## 社交模式
48
+
49
+ 大肥鱼模式里的一个选项,给别人也能聊的角色用,比如通过接入插件接到 QQ、微信、抖音、B 站的角色。在设置页「记忆权限」选「社交」,或写 `audience: social`。只有你自己跟这个 dsh 聊,就保持默认的「专属」。
50
+
51
+ 打开后,**只有你本人的会话拿得到记忆**:你在 dsh 自己的窗口(桌面版或 `dsh web`)里亲手发过消息、且没有别人说过话的会话才算你的。子 agent 跟着它的父会话算。接入插件转来的消息(标明是主人的除外,见下文)、`dsh headless`、外部程序、终端和编辑器客户端发的都算别人的;一个会话只要有别人说过话,就一直算别人的,之前已经带进去的记忆仍留在对话记录里。
86
52
 
87
- ## Companion mode (大肥鱼模式)
88
-
89
- For a character the user chats with — the community's 大肥鱼, or any persona. Turn it on
90
- with `companion: true`, or the 大肥鱼模式 switch on the settings page (below).
91
-
92
- - **Three more types for the character's own life** ([SKILL.md §2.2](../../../SKILL.md)):
93
- `moment` — something the character and the user shared, with its date; kept, and
94
- folded into a monthly archive when the index fills. `upcoming` — something coming up
95
- in the user's life; asked about once its date passes, and rewritten as a `moment`
96
- once the user has said how it went. `self` —
97
- a change in the character itself, such as a nickname; updated in place, never folded.
98
- A `moment` or `self` note is deleted only when the user asks to forget. In a Chinese
99
- store their headings are `## 经历`, `## 约定` and `## 我自己`. The other four types
100
- keep their meaning.
101
- - **Recall in character.** The agent is told that, when it plays a character for you,
102
- any message, a greeting included, may call for recall, and to speak of what it
103
- remembered — or failed to save — in character, without naming files or tools.
104
- - **The persona preset decides what it can do with memory.** With companion mode or
105
- social mode on, what a conversation is given — the memory and its rules, or social
106
- mode's note — is a *required* runtime context, so it arrives even under a preset that
107
- turns optional context off (dsh-persona's `includeRuntimeContext: false`, as dsh's
108
- `minimal` preset does). Saving is another matter: the preset needs dsh's file tools and
109
- the skill tool for the character to write notes the way the skill describes. A preset
110
- with only a shell can read the memory it is given, and its writes go past the guard.
111
-
112
- ## Social mode
113
-
114
- A choice inside companion mode, for a character other people can talk to as well — one
115
- wired to QQ, WeChat, Douyin or Bilibili through a bridge plugin. With companion mode on,
116
- choose *Social* (社交) under *Memory access* (记忆权限) on the settings page, or set
117
- `audience: social`. The default, *Private* (专属), gives every conversation the memory:
118
- if only you talk to this dsh, keep it. Without companion mode, `audience` changes
119
- nothing.
120
-
121
- - **Only your own conversations get the memory.** A conversation is yours when you have
122
- written in it in dsh's own window — the desktop app or `dsh web` — and nobody else has.
123
- dsh stamps every user message with where it came from, and its browser client sends
124
- each prompt with a request id and the browser's time zone: that is how a message you
125
- typed there is told apart. Everything else is someone else's:
126
- - a message a bridge plugin submits, under a kind of its own (`plugin:<name>`, as
127
- dsh-onebot writes, or any other kind dsh's own producers do not use) or dsh's plain
128
- `user` kind, unless the bridge marks it as yours (the standard below);
129
- - a program calling dsh web's API, `dsh headless`, an editor or terminal client;
130
- - a conversation an automation or a plugin opened, until you write in it;
131
- - a caller dsh itself names as someone other than the owner.
132
-
133
- Once anyone else has written in a conversation it stays theirs, even after you write
134
- in it too: what the memory put into it earlier stays in its history. Messages dsh
135
- adds itself (runtime context, skills, compaction, agent-to-agent messages) decide
136
- nothing.
137
- - **Someone else's conversation gets no memory:** no index, a note saying so, no full path
138
- to the store (the skill, which every conversation reads, gives none in social mode; your
139
- own conversations are told it in their runtime context), and file tools that may name
140
- files only. A directory is refused (so grep, glob and directory
141
- views are — any tree may hold a store), and so is any path into a store, however it is
142
- spelled or linked, including a store that does not exist yet, so none can be planted
143
- for you. A subagent is yours while its live parent is — read afresh on every call —
144
- and nobody else has written to it directly; a forked conversation is judged by its own
145
- history, the part it inherited included. A call with no session, a
146
- dsh without session projections, or a network path counts as someone else's.
147
-
148
- This keeps the user's memory out of others' context by the routes dsh's own file tools
149
- take. It is **not** a security boundary: a shell tool reads any file, and a different
150
- profile on the same account can still read the store's files. Keeping others away from
151
- the store needs filesystem permissions or a sandbox.
152
-
153
- ### For bridge authors: marking who wrote a message
154
-
155
- With social mode on, every message a bridge submits counts as someone else's. For the
156
- owner's own chat to get the memory — their private QQ chat with the character, say —
157
- put a `speaker` on the message's source:
53
+ 别人的会话拿不到索引,也不知道记忆库在哪;dsh 的文件工具只能指名文件(任何目录都不让列),指向记忆库的路径不管怎么写都拒绝。这**不是**安全边界:shell 工具照样能读任何文件,同一账户下的另一个 profile 也读得到。要真正隔离,得靠文件权限或沙箱。
54
+
55
+ ### 给接入插件作者
56
+
57
+ 社交模式下,接入插件转进来的消息默认都算别人的。来自主人自己账号的消息(比如主人和角色的 QQ 私聊),在来源章上标 `speaker`:
158
58
 
159
59
  ```js
160
60
  agent.followup(createUserMessage({
@@ -163,154 +63,73 @@ agent.followup(createUserMessage({
163
63
  }))
164
64
  ```
165
65
 
166
- | `speaker.role` | Meaning |
66
+ | `speaker.role` | 含义 |
167
67
  |---|---|
168
- | `owner` | The dsh owner wrote it: a message from the account the user set as theirs in your bridge. |
169
- | `guest` | Someone else wrote it — the same as no `speaker` at all. |
170
- | `none` | Nobody wrote it: your plugin's own report, reminder or notice. It does not turn the owner's conversation into someone else's. |
171
-
172
- Mark a message `owner` only when it comes from the owner's own account. A group's
173
- conversation stays someone else's once anyone else in it speaks. dsh keeps unknown
174
- source fields in the session log, so the role survives a restart.
175
-
176
- A program that talks to dsh only through dsh web's API cannot set a source, and its
177
- messages count as someone else's — unless it sends `clientTimeZone` with a prompt: dsh
178
- then records exactly what its own page sends, and the message counts as the owner's. A
179
- program forwarding other people's messages must leave `clientTimeZone` out.
180
-
181
- ## Settings page
182
-
183
- On dsh's **Plugins** page, open *Engramory memory*: its page sets companion mode (a
184
- switch) and, while it is on, its memory access (private or social), and the index cap.
185
- The memory's language follows dsh's own (see `language` below).
186
-
187
- Saving goes through dsh's own profile editor: it validates the whole config, writes it
188
- to your profile's `cordis.patch.yml`, and reloads the plugin with it — or leaves the
189
- file as it was. A save made against settings that changed elsewhere in the meantime is
190
- refused, and the page reads them again. Settings put back to their defaults remove
191
- their override instead of pinning it. Other fields (`indexName`, `indexPath`, …) stay
192
- in the file.
193
-
194
- Only you reach the page's route — your signed-in browser, or a program holding your
195
- login: dsh answers 401 to a request without it and 403 to a cross-site one, so an agent
196
- cannot call it to switch social mode off. A dsh without the profile editor shows the page read-only.
197
- Companion mode and its memory access are also at the top of the memory page (below).
198
-
199
- ## Memory page
200
-
201
- dsh's sidebar gets a **Memory** entry. Its page lists the stores recall can use — the
202
- global store under `$DSH_HOME` and each workspace's project store, found as recall finds
203
- one, or, with `indexPath` pinned, that index's directory alone — with each index's size
204
- against the cap, its notes' names by type, and its archive. It is read-only and shows
205
- names only, never a note's text: a name opens the note in your computer's default app,
206
- through dsh's own opener, on the machine dsh runs on. Where dsh says it cannot open there
207
- (over SSH, say), the names are plain text and the page shows where the files are.
208
- *Open the index* opens the index the same way; *Open the folder* shows the index in your
209
- file manager (Explorer and Finder select it in the store's folder; on Linux the folder
210
- opens), so a store with no index has no such button. At the top, companion mode and its
211
- memory access save as they are switched; what they mean, and the index cap, are on the
212
- settings page.
213
-
214
- The listing is bounded — 2000 entries looked at per folder, 200 workspaces, 5000 names in
215
- all — and never looks into a workspace on a network share path (`\\server\share\…`), or
216
- one whose folder has since become a link to somewhere else. A note that is a link is not
217
- listed; what is opened is a note or the index found again, by name, directly in its store,
218
- and refused if it has become a link. A relative `indexPath` follows each conversation's
219
- directory, so the page cannot list it. Only you reach the page's route, as the settings
220
- page's.
221
-
222
- ## Configuration
223
-
224
- | Field | Default | Meaning |
68
+ | `owner` | 主人发的:来自用户在你插件里设为"自己"的那个账号。 |
69
+ | `guest` | 别人发的,和不写 `speaker` 一样。 |
70
+ | `none` | 不是人发的:你插件自己的汇报、提醒或通知,不会让主人的会话变成别人的。 |
71
+
72
+ 群聊里只要有别人说话,那个会话就一直算别人的。只通过 dsh web 接口发消息的程序设不了来源章,消息都算别人的;它若带上 `clientTimeZone`,会被当成主人,所以替别人转发消息的程序不要带。
73
+
74
+ ## 设置页与记忆页
75
+
76
+ ![设置页](https://raw.githubusercontent.com/tinqiao-oss/engramory/master/adapters/dsh/plugin/screenshots/settings-page.png)
77
+
78
+ - **设置页**:dsh 的「插件」页里点开「Engramory 记忆」,能改大肥鱼模式、记忆权限和索引上限。保存走 dsh 自己的配置服务,写进你 profile 的 `cordis.patch.yml` 并重新加载插件。这个接口只有登录了的你能调,AI 没法调用它关掉社交模式。
79
+ - **记忆页**:左侧栏「记忆」。列出全局库和各工作区的项目库(设了绝对路径的 `indexPath` 时只列那一个,相对路径则列不出),显示索引离上限还有多远、按类型分组的笔记名字和归档。只读,不显示正文;点名字、「打开索引」「打开文件夹」都在运行 dsh 的那台机器上打开。顶部的两个开关拨了就保存。
80
+
81
+ ## 配置
82
+
83
+ 设置页没有的配置,写在你 profile 的 `cordis.patch.yml` 里:按 id 改已有的那一行,别再写一个 `- insert:`(那会多出第二行 engramory,旧上限照样生效)。一次 patch 会整体替换 `config`,要保留的键都列上:
84
+
85
+ ```yaml
86
+ - id: engramory
87
+ config:
88
+ indexName: MEMORY.md
89
+ maxLines: 200
90
+ maxBytes: 25600
91
+ ```
92
+
93
+ | 字段 | 默认值 | 含义 |
225
94
  |---|---|---|
226
- | `indexName` | `MEMORY.md` | Basename treated as the memory index (case-insensitive; both `file_path` and `str_replace_editor`'s `path` are checked) — the name a write is called by, or the name of the file it lands on, so a symlink under another name is caught too. An empty or non-string value falls back to the default, and so does one that is not a plain file name (a path separator, a `:`, `.` or `..`). |
227
- | `maxLines` | `200` | Hard line cap. Non-positive or non-finite values fall back to the default. |
228
- | `maxBytes` | `25600` | Hard UTF-8 byte cap (25 KB). |
229
- | `indexPath` | unset | Path of the ONE index to guard: absolute, or relative to each session's working directory (resolved there by the guard as by recall; the memory page lists only an absolute pin). Without it the guard matches on basename alone, so an unrelated `MEMORY.md` in another project is gated too; set this and only that exact file is. Compared by identity — the file the write lands on, whatever it is called (symlinks and `..` resolved; case-folded on Windows only) — and a path that does not exist yet still has its first write guarded. |
230
- | `registerSkill` | `true` | Set `false` to keep the cap but skip the runtime skill. |
231
- | `injectIndex` | `true` | Put the memory index into dsh's runtime context on every step (see above). Set `false` to recall by reading the file instead. |
232
- | `language` | dsh's | Unset (or empty), the memory follows dsh's language — the one chosen in dsh's Settings → General, else the language of the system dsh runs on (dsh's page, with no choice made, follows the browser's, so on a remote `dsh web` the two can differ); a change applies once dsh restarts. Set it to pin one: `zh` writes the store in Chinese (SKILL.md §1.1): Chinese note names, `## 用户` / `## 反馈` / `## 项目` / `## 参考` headings, `**为什么:**` / `**怎么用:**` labels, and the skill and the injected index in Chinese. Frontmatter keys and `type:` values stay English, so every host still reads the store. The cap's refusals stay English. `zh` and its regional forms (`zh-CN`, `zh-TW`, …) select Chinese; any other string selects `en`. |
233
- | `companion` | `false` | `true` turns companion mode on (above): three more types for the character's own life, and recall in character. Only the boolean `true` counts. |
234
- | `audience` | `private` | With companion mode on, `social` turns social mode on (above): only your own conversations get the memory. Any other value is `private`, where every conversation does. Without companion mode it changes nothing. |
235
- | `skill` | built-in | Replace the skill body with your own markdown — which must then say where the store is itself. |
236
-
237
- ## What the guard actually does
238
-
239
- | Call | Decision |
95
+ | `indexName` | `MEMORY.md` | 哪个文件名算索引,不分大小写。 |
96
+ | `maxLines` | `200` | 索引行数上限。 |
97
+ | `maxBytes` | `25600` | 索引字节上限(25 KB)。 |
98
+ | `indexPath` | 不设 | 只认这一个索引:绝对路径,或相对会话目录。不设时,任何项目里叫 `MEMORY.md` 的文件都按索引卡上限;记忆库不在默认位置时也要设它,否则不会自动带上。 |
99
+ | `registerSkill` | `true` | `false`:不注册内置的记忆规则,只留上限。 |
100
+ | `injectIndex` | `true` | `false`:不再每轮带上索引,改由 AI 自己读。 |
101
+ | `language` | 跟随 dsh | `zh`(`zh-CN` 等也算)用中文写记忆(笔记名、`## 用户` 这类标题、**为什么:**/**怎么用:**),其他值用英文。不设就跟 dsh 的语言,改了要重启 dsh。frontmatter 的键和 `type:` 取值始终是英文,别的宿主照样能读。 |
102
+ | `companion` | `false` | `true` 打开大肥鱼模式。 |
103
+ | `audience` | `private` | 大肥鱼模式下设 `social` 打开社交模式。 |
104
+ | `skill` | 内置 | 用你自己的 markdown 换掉内置规则,这时得自己写明记忆库在哪。 |
105
+
106
+ ## 上限怎么判
107
+
108
+ | 情况 | 结果 |
240
109
  |---|---|
241
- | `write` (or `str_replace_editor` `create`) ending within caps | allow |
242
- | `write`/`create` that GROWS the index past a cap | **deny**, naming the numbers and what to compact |
243
- | `write`/`create` that SHRINKS or keeps an over-cap index | allow — incremental compaction (210 → 205 → 198) must stay possible |
244
- | Edit carrying `old_str`/`new_str`: result simulated | judged by the RESULT, same grow/shrink rule as a write — sized before it is built, so a `replace_all` whose result would be too long to build is judged on lower bounds of its bytes and lines |
245
- | Unsimulable partial (e.g. `insert`) on an over-cap index | **deny**, telling the agent a shrinking whole-file write passes |
246
- | Unsimulable partial on a healthy index | allow |
247
- | `read`, `view`, or any unknown tool | allow — recall must never be blocked, even over cap |
248
- | Any write to any other file | allow — not the guard's business |
249
- | Missing/unreadable current index on a whole-file write | treated as EMPTY (mirrors the Python guard) — a within-caps write passes, an over-cap first write is refused |
250
- | Missing/unreadable index on a partial edit | allow — a guard must never block work over a path it cannot read |
251
- | A FIFO, device, or directory under the index's name | never read — counts as unreadable (a read there would hang or flood the host, which runs guards on its main thread) |
252
- | An index past the read limit (`min(16 MiB, max(1 MiB, 4 × maxBytes))`) | not read whole: a whole-file write is judged on bytes against the filesystem's size (every shrink passes; its line count is unknown, so lines are not capped there), and an edit passes unmeasured, as on an unreadable index |
253
- | Malformed execution (no arguments, non-string path/content) | allow — not recognisably a write |
254
-
255
- For measured results, deny only a dimension that is over its cap AND grew past
256
- the current file. The line count ignores a trailing newline, so an index exactly
257
- at the cap stays writable. This decision table is pinned by `node --test` in CI.
258
-
259
- Relative paths use the session's `workingDirectory` service when available,
260
- falling back to the process cwd. Edits match LF-normalised text and preserve the
261
- file's line endings when sizing the result, including CRLF bytes.
262
-
263
- ## Known limits
264
-
265
- - **Unsimulable partial edits can cross the cap from below.** On a readable,
266
- already-over-cap index they are denied; use a shrinking whole-file write to
267
- compact. Missing/unreadable files and the read-limit exceptions are listed above.
268
- Run `engramory_check.py` after index edits and `engramory_doctor.py` periodically.
269
- - **Coverage is limited to known file tools:** `write`/`edit` with `file_path`,
270
- `str_replace`/`insert`, and mutating `str_replace_editor` commands with `path`.
271
- Unknown tools pass. **Shell tools, MCP file tools, and external editors bypass
272
- the guard.**
273
- - **Injection finds the stores where the installer puts them.** A store elsewhere
274
- (`--memory-root`) needs `indexPath`. The project root is found by dsh's default `.git`
275
- marker only (a profile's custom `projectRootMarkers` are not visible to a plugin), and
276
- a project store created mid-session can take up to ten seconds to appear. A project
277
- store whose real path leaves the project — a symlink out of the checkout — is not
278
- read; point `indexPath` at it if that is deliberate. The global store's location comes
279
- from `$DSH_HOME`, resolved the way dsh resolves it. What is injected is framed as notes,
280
- not instructions, that may be stale; a host without runtime context
281
- (no `ctx.systemPrompt.context`) goes without it.
282
- - **Social mode knows your typing by what dsh's browser client sends.** A program that
283
- sends the same request id and time zone as dsh's own page is taken for you, and a
284
- browser whose time zone cannot be read leaves your messages unmarked. dsh's terminal,
285
- editor (ACP) and SDK clients do not mark them either: with social mode on, only
286
- dsh's own window counts.
287
- - **Some of dsh's messages carry no author.** An answer to the agent's question, a
288
- scheduled prompt and a message a slash command adds (`/plan <text>`, `/goal`'s
289
- attachments) count as nobody's, and a queued message edited through dsh's API keeps the
290
- author it had. On dsh 0.2.1 only the owner can answer, schedule or edit — in dsh's
291
- window, or through a program with the owner's login, which can read the store's files
292
- through dsh's own file API anyway — and a chat bridge that forwards a command runs it in
293
- its own conversation, which its other messages already make someone else's.
294
- - **A kind of message a later dsh adds counts as someone else's** until this plugin knows
295
- it: a conversation of yours may lose the memory, never the other way round.
296
- - **The memory page reads the disks as they answer.** On a filesystem whose directory
297
- entries carry no type (some network mounts), Node stats each listed entry on dsh's main
298
- thread, at most 2000 per folder; and a listing stuck on a drive that never answers keeps
299
- reporting a timeout until it does, rather than starting another that would get stuck too.
300
- - **What the memory page opens is checked by path.** dsh's opener takes a path, which it
301
- resolves once more before the desktop opens it; a process changing a store's files at
302
- that very moment could still redirect what the desktop opens — as with dsh's own opener
303
- for any workspace file — though not to a link the page saw.
304
- - dsh's plugin API can change. The plugin uses `ctx.tools.guard()` and reactive
305
- `ctx.inject()` children for the skill registry, the session working directory, the
306
- prompt registry, and — in social mode — dsh's session projections, where it folds
307
- who has written in each session; a compatible-release declaration needs recorded host
308
- evidence, not just unit tests. The recorded end-to-end runs cover the cap, the skill
309
- and the injected index; companion mode, social mode, the settings page and the memory page were checked by hand on
310
- a real dsh (0.2.1-rc.5) — messages typed in dsh's window, sent by `dsh headless` and by
311
- a program through dsh web's API, a save through the page, and the memory page's listing,
312
- a switch saved from it and a note opened from it — but are not part of that record yet.
313
-
314
- ## License
315
-
316
- MIT — see the [Engramory repository](https://github.com/tinqiao-oss/engramory).
110
+ | 写完后索引在上限内 | 放行 |
111
+ | 写入让索引变大,并超过上限 | **拒绝**,报出数字,提示压缩什么 |
112
+ | 索引已超限,写入让它变小或不变 | 放行(压缩可以一步步来:210 → 205 → 198) |
113
+ | 带 `old_str`/`new_str` 的编辑 | 先算出改完的结果,再按上面三条判 |
114
+ | 算不出结果的局部编辑(如 `insert`) | 索引已超限时拒绝,否则放行 |
115
+ | 索引不存在、读不到,或不是普通文件 | 整文件写入当它是空的来判;局部编辑放行 |
116
+ | 索引大到超过读取上限(`min(16 MiB, max(1 MiB, 4 × maxBytes))`) | 不整份读:整文件写入只按字节比大小;局部编辑放行 |
117
+ | 读文件、写别的文件、不认识的工具或参数 | 放行;索引超限也不挡读取 |
118
+
119
+ 行数和字节分开判:只拒绝既超上限、又比原来变大的那一项,所以字节变少但行数涨过上限的写入照样被拒。行数不算末尾换行,正好卡在上限的索引还能写。这张表由 CI 里的 `node --test` 固定。
120
+
121
+ ## 已知限制
122
+
123
+ - **只拦 dsh 自带的文件工具**(`write`、`edit`、`str_replace_editor` 等)。shell、MCP 文件工具和外部编辑器写入都绕过上限;改完索引跑 `engramory_check.py`,定期跑 `engramory_doctor.py`。
124
+ - **记忆库只在默认位置自动找到。** 放在别处,或项目库是指向项目外的链接,要设 `indexPath`;项目根只按 `.git` 认;会话中途新建的项目库最多十秒后才出现。
125
+ - **社交模式靠 dsh 窗口发来的信息认你。** 除了接入插件标明的主人消息,只有 dsh 自己窗口里打的字算你的;浏览器读不出时区时也认不出。dsh 以后新加的消息种类,插件认识之前都算别人的:你的会话可能拿不到记忆,但不会把记忆漏给别人。
126
+ - **记忆页打开文件前按路径检查。** dsh 的打开功能拿到路径后还会再解析一次,如果恰好在这一刻有程序改动记忆库里的文件,桌面最终打开的可能是别的东西(dsh 自己打开工作区文件也是这样)。
127
+ - **实验性功能**:大肥鱼模式、社交模式、设置页和记忆页在小版本里也可能改。
128
+
129
+ ## 版本
130
+
131
+ 本包和 Engramory 用同一个版本号:`dsh-engramory@X.Y.Z` 就是仓库的 `vX.Y.Z`。上限判定(上面那张表)和实验性功能以外的配置,只在主版本做不兼容改动。详见[版本规则](https://github.com/tinqiao-oss/engramory#版本规则),改动历史见 [CHANGELOG](../../../CHANGELOG.md)。
132
+
133
+ ## 许可
134
+
135
+ MIT,见 [Engramory 仓库](https://github.com/tinqiao-oss/engramory)。
package/index.js CHANGED
@@ -2090,7 +2090,7 @@ function languageOf(value) {
2090
2090
 
2091
2091
  /**
2092
2092
  * Everything this plugin says to the model, per language. The two must state the same
2093
- * rules — adapters/dsh/plugin/test.js asserts each rule in both — and the store
2093
+ * rules — adapters/dsh/plugin/index.test.js asserts each rule in both — and the store
2094
2094
  * labels in `labels` must be the ones the skill tells the model to look for.
2095
2095
  */
2096
2096
  const TEXT = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-engramory",
3
- "version": "0.3.0",
4
- "description": "Curated file-based long-term memory for DeepSeek Harness — human-readable markdown notes, one plain-file store shared across hosts (git-ignored when it lives inside a project repo), and a deterministic index cap enforced by ctx.tools.guard()",
3
+ "version": "1.0.1",
4
+ "description": "DeepSeek Harness 的长期记忆插件:AI 记住的东西存成普通 markdown 文件,每轮自动带上记忆索引,索引有大小上限。",
5
5
  "keywords": [
6
6
  "dsh-plugin",
7
7
  "deepseek-harness",
@@ -59,13 +59,14 @@
59
59
  "README.md",
60
60
  "LICENSE"
61
61
  ],
62
+ "author": "tinqiao-oss",
62
63
  "license": "MIT",
63
64
  "repository": {
64
65
  "type": "git",
65
66
  "url": "git+https://github.com/tinqiao-oss/engramory.git",
66
67
  "directory": "adapters/dsh/plugin"
67
68
  },
68
- "homepage": "https://github.com/tinqiao-oss/engramory/tree/master/adapters/dsh#readme",
69
+ "homepage": "https://github.com/tinqiao-oss/engramory/tree/master/adapters/dsh/plugin#readme",
69
70
  "bugs": {
70
71
  "url": "https://github.com/tinqiao-oss/engramory/issues"
71
72
  },
package/README.zh-CN.md DELETED
@@ -1,196 +0,0 @@
1
- [English](README.md) | **简体中文**
2
-
3
- # dsh-engramory
4
-
5
- [![dsh-xray](https://img.shields.io/endpoint?url=https%3A%2F%2Funstone.github.io%2Fdsh-xray%2Fbadge%2Ftinqiao-oss__engramory.json)](https://unstone.github.io/dsh-xray/registry.html#tinqiao-oss__engramory)
6
-
7
- 给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 用的**策展式、文件式**长期记忆 ——
8
- 纯 markdown 笔记,随便什么工具都能打开;跨宿主共享遵循
9
- [一个写入者、多个只读者](../../reader/README.md),索引守卫会在匹配的文件工具写入前检查上限。(记忆库若位于某个项目的 git 仓库内,必须 git-ignore ——
10
- 记忆里带着本机独有的细节,见 SKILL.md §1。给记忆库单独开一个*私有仓*则完全可以。)
11
-
12
- 隶属于 [Engramory](https://github.com/tinqiao-oss/engramory)。
13
-
14
- ## 已经有 `AGENTS.md` 常驻块了,为什么还要插件
15
-
16
- 那个常驻块承载了纪律。但有三件事它做不到:
17
-
18
- **索引已经在上下文里 —— 召回不用再读文件。** 插件在每一步都把记忆索引放进 dsh 的运行时
19
- 上下文:项目库(`<项目>/.engramory-memory/MEMORY.md`,取会话目录到 git 根目录之间离会话最近的那个,和 dsh 找项目 `AGENTS.md` 的方式一致)和全局库
20
- (`$DSH_HOME/.engramory-memory/MEMORY.md`,未设 `$DSH_HOME` 时为 `~/.dsh`)—— 正是
21
- `tools/engramory_init.py dsh` 建库的位置;设了 `indexPath` 时则只注入它。内容一变就重新读取,
22
- 所以写入后下一轮请求就能看到;超过上限的行会被截掉,并提示模型去压缩。用
23
- `injectIndex: false` 关闭。
24
-
25
- **匹配工具的上限由守卫确定性执行。** `ctx.tools.guard()` 提供同步、**单调**的否决:
26
- 守卫一旦返回理由,后续监听器就不能再放行。索引的增长受配置上限约束,无需依赖 agent
27
- 记得运行校验器。请用 **`dsh-engramory >= 0.2.7`**;
28
- [PORTING.md](../../../PORTING.md) 记录各宿主是否已有守卫实现,
29
- [CHANGELOG.md](../../../CHANGELOG.md) 保存版本历史。
30
-
31
- **协议随插件一起到达。** skill 是在运行时通过 dsh 的 skill 注册表注册的 —— 走一个响应式的
32
- `ctx.inject(['skills'], …)` 子上下文,所以没有注册表的 profile 照样拿到上限,而注册表晚一步
33
- 挂载的 profile 也照样拿得到 skill。这意味着它**不依赖**把文件放进 dsh 扫描的那五个 skill 根
34
- 目录之一 —— 那条路很容易在细微处放错,而一旦放错就是**静默失败**。
35
-
36
- ## 安装
37
-
38
- 零依赖 ESM,不需要构建;需要 Node.js 18+,dsh 的插件命令还要求 PATH 中有 `pnpm`。
39
-
40
- ```sh
41
- dsh plugin --profile <名字> add dsh-engramory
42
- ```
43
-
44
- 确切的兼容版本以 [`package.json`](package.json) 的 `dsh.compatibility.dshReleases` 为准,
45
- 由 [`results.json`](../../../tests/dsh_e2e/results.json) 的记录支持。
46
- 验证其他版本请看[端到端验证指南](../../../tests/dsh_e2e/README.md)。
47
-
48
- 这个包自带 `dsh.bundle` manifest 指向它自己的 `cordis.patch.yml`,所以那一行
49
- 会被自动插进 profile 的插件树里 —— 不需要手改配置。要改默认值,请在你自己 profile 的 patch
50
- 层里**按 id 修改那一行已存在的配置**,**不要**再写一个 `- insert:`(insert 永远是追加,你会得到
51
- 两行 engramory,而原来那行的上限依旧在生效):
52
-
53
- ```yaml
54
- - id: engramory
55
- config:
56
- indexName: MEMORY.md
57
- maxLines: 200
58
- maxBytes: 25600
59
- ```
60
-
61
- 一次 patch 会**整体替换**目标行的 `config`,所以你想保留的键要逐个列全。
62
-
63
- 只装插件时,记忆就是一个全局库 `$DSH_HOME/.engramory-memory/`(`$DSH_HOME` 默认是 `~/.dsh`):skill 会告诉 AI 它在哪,AI 第一次保存时自己建好。项目也可以有自己的库——项目根目录下的 `.engramory-memory/`——但只能由你来建:安装器会建好并加进 .gitignore(`python tools/engramory_init.py dsh --project-root <项目>`)。AI 绝不自己建项目库;有项目库时,只在这个项目成立的事记在那里,其余的仍记在全局库。安装器还会把常驻块写进 `AGENTS.md`(`python tools/engramory_init.py dsh --install-skill`)。
64
-
65
- 卸载:
66
-
67
- ```sh
68
- dsh plugin --profile <名字> remove dsh-engramory
69
- ```
70
-
71
- ## 大肥鱼模式(陪伴模式)
72
-
73
- 给用户聊天陪伴的角色用——社区里的大肥鱼,或任何人设。写 `companion: true` 打开,或者在设置页(见下文)里打开「大肥鱼模式」开关。
74
-
75
- - **再加三类,记角色自己的人生**([SKILL.md §2.2](../../../SKILL.md)):`moment`(经历)——和用户一起经历过的事,带日期,默认保留,索引写满时按月收进归档;`upcoming`(约定)——用户生活里将要发生的事,过了日子会问起,问到结果才改写成一条经历,绝不自己编结果;`self`(我自己)——角色自己的变化,比如外号,原地更新,不归档。经历和我自己只有用户说忘掉才删。其余四类意思不变。
76
- - **用角色口吻回忆。** AI 会被告知:它为你扮演角色时,用户的每句话,包括打招呼,都可能该想起点什么;记住了什么、没记上什么,都用角色口吻说,不提文件和工具。
77
- - **人设预设决定它能拿记忆做什么。** 大肥鱼模式或社交模式打开时,会话收到的内容——记忆和陪伴规则,或者社交模式的那条说明——是*必需*的运行时上下文,所以即使预设关掉了可选上下文(dsh-persona 的 `includeRuntimeContext: false`,dsh 自带的 `minimal` 预设就是这样),它也照样送到。保存记忆是另一回事:预设得带上 dsh 的文件工具和 skill 工具,角色才能照 skill 的格式写笔记。只有 shell 的预设读得到给它的记忆,但它写的东西绕过守卫。
78
-
79
- ## 社交模式
80
-
81
- 大肥鱼模式里的一个选项,给"别人也能跟它聊"的角色用——比如经接入插件接到 QQ、微信、抖音、B 站的角色。打开大肥鱼模式后,在设置页的「记忆权限」里选「社交」,或者写 `audience: social`。默认的「专属」让每个会话都能用记忆:只有你自己跟这个 dsh 聊的话,保持专属就行。大肥鱼模式没打开时,`audience` 不起作用。
82
-
83
- - **只有你本人的会话拿得到记忆。** 你在 dsh 自己的窗口(桌面版或 `dsh web`)里亲手发过消息、而且没有别人在里面说过话的会话,才算你的。dsh 给每条用户消息都盖了来源章,它的浏览器界面发消息时会带上请求编号和浏览器的时区,靠这个认出是你在界面里打的字。其余一律算别人的:
84
- - 接入插件转进来的消息,不管用的是它自己的章(`plugin:<包名>`,dsh-onebot 就是这样;或者别的任何 dsh 自己不用的章)还是 dsh 的 `user` 章——除非插件按下面的标准标明是你;
85
- - 通过 dsh web 接口调用的外部程序、`dsh headless`、编辑器或终端客户端;
86
- - 自动化任务或插件开的会话,在你亲手发消息之前;
87
- - dsh 自己标明不是主人的调用方。
88
-
89
- 一段会话只要有别人说过话,就一直算别人的,你之后再在里面发消息也一样:之前放进去的记忆还留在对话记录里。dsh 自己加的消息(运行时上下文、技能、压缩、agent 之间的消息)不影响判断。
90
- - **别人的会话拿不到记忆:** 没有索引,只收到一条说明,也拿不到记忆库的完整路径(社交模式下,所有会话都读的 skill 里不写完整路径;你本人的会话会在运行时上下文里得知它),而且文件工具只能指名文件:目录一律拒绝(所以 grep、glob 和目录查看都不行——任何目录树里都可能藏着记忆库),指向记忆库的路径不管怎么写、怎么链接也一律拒绝,还不存在的记忆库也算,所以没人能替你预先种下一个。子 agent 在它的父会话(每次都重新查在线的父会话)算你的、而且没有别人直接给它发过消息时,才算你的;分叉出来的会话按它自己的记录判断,继承来的那部分也算。没有会话的调用、没有会话投影服务的 dsh、网络路径,都按别人算。
91
-
92
- 这只是让用户的记忆不经 dsh 自带文件工具的路径进入别人的上下文。它**不是**安全边界:shell 工具能读任何文件,同一账户下的另一个配置也照样读得到记忆库的文件。要让别人真正碰不到,得靠文件系统权限或沙箱。
93
-
94
- ### 给接入插件作者:标明消息是谁发的
95
-
96
- 社交模式打开时,接入插件转进来的每条消息都算别人的。想让主人自己的聊天(比如主人和角色的 QQ 私聊)用上记忆,就在消息的来源章上加一个 `speaker`:
97
-
98
- ```js
99
- agent.followup(createUserMessage({
100
- content: [{ type: 'text', text }],
101
- source: { kind: 'plugin:your-bridge', speaker: { role: 'owner' } },
102
- }))
103
- ```
104
-
105
- | `speaker.role` | 含义 |
106
- |---|---|
107
- | `owner` | dsh 的主人发的:来自用户在你插件里设为"自己"的那个账号。 |
108
- | `guest` | 别人发的——和不写 `speaker` 一样。 |
109
- | `none` | 不是人发的:你插件自己的汇报、提醒或通知。它不会让主人的会话变成别人的。 |
110
-
111
- 只有确认来自主人自己账号的消息才标 `owner`。群聊里只要有别人说话,那个会话就一直算别人的。dsh 会把来源章里它不认识的字段原样存进会话记录,所以重启后标注还在。
112
-
113
- 只通过 dsh web 接口和 dsh 交互的程序设不了来源章,它发的消息都算别人的——除非它发消息时带了 `clientTimeZone`:那样 dsh 记下的章和它自己页面发的一模一样,消息会被当成主人的。替别人转发消息的程序不要带 `clientTimeZone`。
114
-
115
- ## 设置页
116
-
117
- 在 dsh 的**插件**页里打开「Engramory 记忆」:它的页面上设置大肥鱼模式(一个开关)以及打开时的记忆权限(专属或社交)、索引上限。记忆用什么语言写跟随 dsh 自己的语言(见下文 `language`)。
118
-
119
- 保存走 dsh 自己的配置编辑服务:它校验整份配置,写进你这个配置的 `cordis.patch.yml`,再用新配置重新加载插件——校验不过就保持文件原样。如果你打开页面之后设置在别处被改过,这次保存会被拒绝,页面重新读取。改回默认值的设置会删掉对应的覆盖,而不是把默认值钉死。其他字段(`indexName`、`indexPath` 等)仍留在文件里。
120
-
121
- 只有你能访问这一页用的接口——你已登录的浏览器,或者带着你登录凭据的程序:没有凭据的请求 dsh 回 401,跨站请求回 403,所以 agent 没法调用它关掉社交模式。没有配置编辑服务的 dsh 上,这一页只读。大肥鱼模式和记忆权限在记忆页(见下文)顶部也有。
122
-
123
- ## 记忆页
124
-
125
- dsh 左侧栏多一个「记忆」入口。点开的页面列出召回能用到的记忆库——`$DSH_HOME` 下的全局库,以及每个工作区的项目库(找法和召回一样);设了 `indexPath` 时只列那个索引所在的目录——每个库显示索引相对上限的大小、按类型分组的笔记名字,以及归档。页面只读,只显示名字,不显示笔记正文:点名字会用电脑上的默认程序打开那条笔记,走的是 dsh 自带的打开功能,在运行 dsh 的那台机器上打开。dsh 说那台机器上打不开时(比如经 SSH 访问),名字就只是文字,页面会写出文件在哪。「打开索引」同样用默认程序打开索引;「打开文件夹」在文件管理器里显示索引(资源管理器和访达会在记忆库文件夹里选中它;Linux 上是打开那个文件夹),所以没有索引的记忆库没有这个按钮。页面顶部的大肥鱼模式和记忆权限拨了就保存;它们是什么意思、索引上限,见设置页。
126
-
127
- 列出的东西有上限——每个文件夹最多看 2000 个条目、最多 200 个工作区、总共 5000 个名字——而且不去看网络共享路径(`\\server\share\…`)上的工作区,也不去看文件夹后来被换成指向别处的链接的工作区。本身是链接的笔记不列出;打开的是按名字在记忆库里重新找到的笔记或索引,这时它若已变成链接就拒绝。相对路径的 `indexPath` 跟着每个会话的目录走,所以页面列不出来。页面用的接口和设置页一样只有你能访问。
128
-
129
- ## 配置
130
-
131
- | 字段 | 默认值 | 含义 |
132
- |---|---|---|
133
- | `indexName` | `MEMORY.md` | 被当作记忆索引的文件名(不区分大小写;`file_path` 与 `str_replace_editor` 的 `path` 都会检查)—— 看写入时用的文件名,或写入实际落到的那个文件的文件名,所以换了名字的 symlink 也会被拦。空值或非字符串会回退到默认值,不是单纯文件名的(含路径分隔符、`:`,或是 `.`、`..`)也回退。 |
134
- | `maxLines` | `200` | 硬性行数上限。非正数或非有限值回退到默认值。 |
135
- | `maxBytes` | `25600` | 硬性 UTF-8 字节上限(25 KB)。 |
136
- | `indexPath` | 未设置 | 要守护的**那一个**索引的路径:绝对路径,或相对每个会话工作目录的路径(守卫和召回都按会话目录解析;记忆页只列绝对路径的固定索引)。不设它时,守卫只按文件名匹配,于是**别的项目里一个无关的 `MEMORY.md` 也会被拦**;设了它就只有这个确切的文件会被守。比较走的是身份而非拼写 —— 看写入实际落到的那个文件,不管它叫什么名字(symlink 与 `..` 都会解析;仅在 Windows 上折叠大小写),而且即使文件尚不存在,它的第一次写入照样受守护。 |
137
- | `registerSkill` | `true` | 设为 `false` 则保留上限、但跳过运行时 skill。 |
138
- | `injectIndex` | `true` | 每一步都把记忆索引放进 dsh 的运行时上下文(见上文)。设为 `false` 则改回由 agent 读文件召回。 |
139
- | `language` | 跟随 dsh | 不设(或留空)时跟随 dsh 的语言——你在 dsh「设置 → 通用」里选的那个,没选过就用运行 dsh 那台机器的系统语言(dsh 页面没选过时看的是浏览器语言,远程用 `dsh web` 时两者可能不同);改了要重启 dsh 才生效。设了就固定:`zh` 用中文写记忆库(SKILL.md §1.1):中文笔记名,`## 用户` / `## 反馈` / `## 项目` / `## 参考` 标题,`**为什么:**` / `**怎么用:**` 标签,skill 和注入的索引也换成中文。frontmatter 的键和 `type:` 取值保持英文,所以各宿主照样能读。超上限的拒绝提示仍是英文。`zh` 及其地区写法(`zh-CN`、`zh-TW` 等)选中文;其他字符串一律按 `en`。 |
140
- | `companion` | `false` | 设为 `true` 打开大肥鱼模式(见上文):多记三类角色人生,用角色口吻回忆。只认布尔值 `true`。 |
141
- | `audience` | `private` | 大肥鱼模式打开时,设为 `social` 打开社交模式(见上文):只有你本人的会话拿得到记忆。其他取值一律按 `private`(专属),每个会话都能用记忆。大肥鱼模式没打开时不起作用。 |
142
- | `skill` | 内置 | 用你自己的 markdown 替换 skill 正文——这时记忆库在哪也得由它自己写明。 |
143
-
144
- ## 守卫到底做了什么
145
-
146
- | 调用 | 判决 |
147
- |---|---|
148
- | `write`(或 `str_replace_editor` 的 `create`)且结果在上限内 | 放行 |
149
- | `write`/`create` 会让索引**增长**并越过上限 | **拒绝**,并报出具体数字和该压缩什么 |
150
- | `write`/`create` 让一个已超限的索引**缩小或持平** | 放行 —— 增量压缩(210 → 205 → 198)必须走得通 |
151
- | 带 `old_str`/`new_str` 的 edit:结果可模拟 | 按**结果**判定,增长/缩小规则与整文件写入相同 —— 先算大小再构建,结果长到没法构建的 `replace_all` 按字节与行数的下界判定 |
152
- | 无法模拟的部分写入(如 `insert`)落在已超限的索引上 | **拒绝**,并告诉 agent:缩小的整文件写入是放行的 |
153
- | 无法模拟的部分写入落在健康的索引上 | 放行 |
154
- | `read`、`view`,以及任何未知工具 | 放行 —— 哪怕已超限,召回也绝不能被挡住 |
155
- | 对任何其他文件的写入 | 放行 —— 不关守卫的事 |
156
- | 整文件写入时当前索引缺失/读不到 | 视为**空**(与 Python 版守卫一致)—— 上限内的写入放行,而首次就超限的写入被拒 |
157
- | 部分编辑时索引缺失/读不到 | 放行 —— 守卫绝不能因为一个自己读不了的路径而挡住工作 |
158
- | 用索引名字的 FIFO、设备或目录 | 一律不读,按读不到处理(在那里读会把宿主卡死或撑爆内存,而宿主在主线程上跑守卫) |
159
- | 超过读取上限的索引(`min(16 MiB, max(1 MiB, 4 × maxBytes))`) | 不整份读入:整文件写入按文件系统报告的大小比较字节(缩小一律放行;行数未知,这时不卡行数),edit 无法测量,按读不到的索引处理,放行 |
160
- | 畸形的执行(没有 arguments、路径/内容不是字符串) | 放行 —— 它压根不像一次写入 |
161
-
162
- 对可测量的结果,只拒绝某个维度既超出上限、又比当前文件增长的写入。行数统计忽略结尾换行,
163
- 所以正好卡在上限的索引仍然可写。此判决表由 CI 中的 `node --test` 固定验证。
164
-
165
- 相对路径优先通过会话的 `workingDirectory` 服务解析,服务不存在时退回进程 cwd。
166
- edit 在统一为 LF 的文本上匹配,计算结果大小时保留文件原有换行,包括 CRLF 的字节数。
167
-
168
- ## 已知限制
169
-
170
- - **无法模拟的部分编辑可能从上限之下直接越界。** 若索引可读且已经超限,则拒绝这种编辑;
171
- 可以用缩小的整文件写入来压缩。文件缺失、不可读和超过读取上限的例外见上表。
172
- 编辑索引后运行 `engramory_check.py`,定期运行 `engramory_doctor.py`。
173
- - **只覆盖已知文件工具:**使用 `file_path` 的 `write`/`edit`、`str_replace`/`insert`,
174
- 以及使用 `path` 的 `str_replace_editor` 修改命令。未知工具放行。
175
- **Shell 工具、MCP 文件工具和外部编辑器都绕过守卫。**
176
- - **注入只在安装器建库的位置找记忆库。** 库放在别处(`--memory-root`)时要设 `indexPath`。
177
- 项目根只按 dsh 默认的 `.git` 标记判断(profile 里自定义的 `projectRootMarkers` 插件看不到);
178
- 会话中途新建的项目库最多要十秒才会出现。真实路径跑出项目之外的项目库(比如指向仓库外的符号
179
- 链接)不会被读取;如果是有意为之,请把 `indexPath` 指向它。全局库的位置来自 `$DSH_HOME`,
180
- 解析方式与 dsh 自己一致。注入的内容被标注为"笔记而非指令、可能过时";没有运行时上下文
181
- (`ctx.systemPrompt.context`)的宿主则不注入。
182
- - **社交模式靠 dsh 浏览器界面发来的信息认出你。** 外部程序如果带上和 dsh 页面一样的请求编号和时区,会被当成你;浏览器读不出时区时,你发的消息也不会被认出。dsh 的终端、编辑器(ACP)和 SDK 客户端发的消息同样不带这些信息:社交模式打开时,只有 dsh 自己的窗口算数。
183
- - **dsh 有些消息不带发送者。** 对 AI 提问的回答、定时任务发起的消息、斜杠命令附带的消息(`/plan <内容>`、`/goal` 的附件)都算"没人发的";通过 dsh 接口编辑过的排队消息,沿用原来的发送者。在 dsh 0.2.1 上,回答、定时、编辑只有主人做得了——在 dsh 窗口里,或者通过登录了主人身份的程序,而这样的程序本来就能通过 dsh 自己的文件接口读到记忆库;接入插件转发的命令跑在它自己的会话里,那个会话已经因为它的其他消息算别人的了。
184
- - **以后的 dsh 新加的消息种类,在本插件认识它之前都算别人的:** 你自己的会话可能因此拿不到记忆,但不会反过来把记忆漏给别人。
185
- - **记忆页按磁盘的响应来读。** 在目录条目不带类型的文件系统上(部分网络挂载),Node 会在 dsh 主线程上逐个 stat 列出的条目,每个文件夹最多 2000 个;卡在永不响应的盘上的那次读取会一直报超时,直到它结束,而不是再起一次同样会卡住的读取。
186
- - **记忆页打开什么是按路径检查的。** dsh 的打开功能接收的是路径,它在交给桌面打开前会再解析一次;如果恰好在那一刻有进程改动记忆库里的文件,桌面最终打开的东西仍可能被换掉——dsh 自己打开任何工作区文件也是如此——但换不成页面检查时看到的链接。
187
- - dsh 的插件 API 可能变化。本插件使用 `ctx.tools.guard()` 和响应式 `ctx.inject()`
188
- 子上下文,分别接入 skill 注册表、会话工作目录、提示词注册表,以及(社交模式下)dsh 的
189
- 会话投影,在那里累计每个会话里谁说过话;兼容声明需要真实宿主的运行记录,不能只凭单元测试。
190
- 记录下来的端到端运行覆盖上限、skill 和注入的索引;大肥鱼模式、社交模式、设置页和记忆页已在真实 dsh
191
- (0.2.1-rc.5)上手动验证过——在 dsh 窗口里打字、用 `dsh headless` 和外部程序经 dsh web
192
- 接口发消息、在设置页里保存,以及记忆页的列表、在记忆页上拨开关保存、从记忆页打开笔记——但还不在那份记录里。
193
-
194
- ## 许可
195
-
196
- MIT —— 见 [Engramory 仓库](https://github.com/tinqiao-oss/engramory)。