sage-mem 0.9.21 → 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.md +406 -229
- package/README.zh.md +566 -0
- package/lib/autodream/engine.js +9 -0
- package/lib/autodream/llm-loop.js +99 -13
- package/lib/autodream/report.js +29 -4
- package/lib/memory/config.js +4 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,118 +1,117 @@
|
|
|
1
|
-
# sage-mem ·
|
|
1
|
+
# sage-mem · file-based memory for DeepSeek Harness _(sage-mem)_
|
|
2
2
|
|
|
3
|
-
[
|
|
4
|
-
[](https://www.npmjs.com/package/sage-mem)
|
|
5
|
-
[](https://github.com/gezi-wen/sage-mem/blob/main/LICENSE)
|
|
6
|
-
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
3
|
+
> 中文版见 [README.zh.md](https://github.com/gezi-wen/sage-mem/blob/main/README.zh.md)
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/sage-mem)
|
|
6
|
+
[](https://www.npmjs.com/package/sage-mem)
|
|
7
|
+
[](https://github.com/gezi-wen/sage-mem/actions/workflows/test.yml)
|
|
8
|
+
[](#compatibility)
|
|
9
|
+
[](#compatibility)
|
|
10
|
+
[](https://github.com/gezi-wen/sage-mem/blob/main/LICENSE)
|
|
11
|
+
[](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md)
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
**Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.**
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
**Current version 1.0.0 (the first stable release).** The public commitments, the capability boundaries and the list of things it explicitly cannot do are in the
|
|
16
|
+
[frozen contract](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md).
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
If it is useful to you, a Star is the most direct feedback.
|
|
15
19
|
|
|
16
|
-
##
|
|
20
|
+
## Table of Contents
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
- [What This Is](#what-this-is)
|
|
23
|
+
- [Why Files, Not a Database](#why-files-not-a-database)
|
|
24
|
+
- [Core Features](#core-features)
|
|
25
|
+
- [Quick Start](#quick-start)
|
|
26
|
+
- [UI Preview](#ui-preview)
|
|
27
|
+
- [Memory File Format](#memory-file-format)
|
|
28
|
+
- [Retrieval and Injection](#retrieval-and-injection)
|
|
29
|
+
- [Write Safety](#write-safety)
|
|
30
|
+
- [Autodream](#autodream)
|
|
31
|
+
- [Migrating from Claude Code](#migrating-from-claude-code)
|
|
32
|
+
- [Configuration Reference](#configuration-reference)
|
|
33
|
+
- [Stability and Versioning Policy](#stability-and-versioning-policy)
|
|
34
|
+
- [Permissions and Privacy](#permissions-and-privacy)
|
|
35
|
+
- [Compatibility](#compatibility)
|
|
36
|
+
- [Architecture](#architecture)
|
|
37
|
+
- [Contributing](#contributing)
|
|
38
|
+
- [FAQ](#faq)
|
|
39
|
+
- [Uninstall](#uninstall)
|
|
40
|
+
- [Maintainer](#maintainer)
|
|
41
|
+
- [Credits](#credits)
|
|
42
|
+
- [Related Projects](#related-projects)
|
|
43
|
+
- [Legacy](#legacy)
|
|
44
|
+
- [License](#license)
|
|
19
45
|
|
|
20
|
-
|
|
46
|
+
## What This Is
|
|
21
47
|
|
|
22
|
-
|
|
48
|
+
DSH has no native memory system. Every new session is a blank sheet: it does not remember who you are, where you got to yesterday, or what stage the project is at, so you have to brief it again every time.
|
|
23
49
|
|
|
24
|
-
|
|
50
|
+
sage-mem adds that layer, like this: **write down the things worth remembering as cards, and store them on your own computer**. Before DSH hands the question to the model, the plugin picks out the few most relevant cards and **puts them in front of the model**.
|
|
25
51
|
|
|
26
|
-
|
|
27
|
-
|---|---|
|
|
28
|
-
| **文件式存储** | 每条记忆一个 `.md`:frontmatter 存元数据,正文存内容。无数据库、无隐藏格式,VS Code 直接打开就能改 |
|
|
29
|
-
| **跨会话记忆** | 每轮提问自动扫记忆目录、按相关度挑出最相关的几条注入上下文。agent 第一轮就「想起来」,不需要它自己去翻文件 |
|
|
30
|
-
| **Claude Code 无损迁移** | CC 的记忆与人格都是 Markdown——**直接拷文件**就完成迁移,不写转换脚本。详见[下一节](#从-claude-code-无损迁移) |
|
|
31
|
-
| **零额外服务** | 无 worker、无 SQLite、无端口、无常驻进程,不需要额外的 API key 或云服务(运行时唯一的依赖是普通 npm 库 `zod`) |
|
|
32
|
-
| **防膨胀** | 靠规则引导 agent 判断「值不值得存」:无实质内容的不存,靠读代码 / git 能推出来的不存 |
|
|
33
|
-
|
|
34
|
-
界面上还有三块附加能力(都可以不用):
|
|
35
|
-
|
|
36
|
-
- **记忆管理页**(v0.4)——DSH 设置页里的「记忆管理」:浏览、查看、编辑、删除、新建记忆,不用离开 GUI
|
|
37
|
-
- **类型与标签筛选**(v0.6)——文件列表顶部一排 chip,按四类 + 自定义标签筛;标签从全库自动聚合计数,与搜索框是 AND 关系
|
|
38
|
-
- **记忆星图**(v0.4 起,v0.6 重做)——把记忆画成星空:按类型着色、按体积分级、按新鲜度分亮度,还支持时间线与「诞生回放」。渲染是**按需重绘**,画面静止时不发起任何绘制
|
|
39
|
-
|
|
40
|
-

|
|
41
|
-
|
|
42
|
-
*记忆星图:每颗星是一条记忆 —— 颜色对应类型、大小对应体积、亮度对应新鲜度,底部是四类计数与库总体积。画布上只画关系与分布;悬停或点开某颗星时才会显示那一条的文件名。*
|
|
43
|
-
|
|
44
|
-
## 界面预览
|
|
45
|
-
|
|
46
|
-
下面每张图都是**从仓库里的界面代码渲染出来的**(同一份 CSS、同一棵组件树,数据是演示用的假数据,不是真实记忆目录)。
|
|
47
|
-
|
|
48
|
-

|
|
49
|
-
|
|
50
|
-
*文件列表 —— 头部带常显「体检 N 个问题」与「待重启生效」,不用点开就知道该不该处理;灰底条目是已归档,随时可以恢复。*
|
|
51
|
-
|
|
52
|
-

|
|
53
|
-
|
|
54
|
-
*工具抽屉 —— 关着时只占一个按钮,展开后两项各带状态:归档候选 2 条 / 保留名 3 个。(体检不在这里:它自己有一页。)*
|
|
55
|
-
|
|
56
|
-

|
|
57
|
-
|
|
58
|
-
*锁定 —— 重要记忆(人设、偏好这类)点一下就锁上:**自动归档的候选里不再出现它,手动点归档也会被宿主拒绝**。锁存在状态目录里,不写进记忆自己的 frontmatter —— 那会被自动整理重写掉,锁一丢它又会回到候选里。*
|
|
59
|
-
|
|
60
|
-

|
|
61
|
-
|
|
62
|
-
*体检(独立成一个页签)—— 索引悬空、漏索引、断链这类硬问题按类报红;「指向已归档」只作提示、不计入问题,也不提供一键修补按钮(改哪条由你决定)。从档案馆恢复一条记忆时,插件会顺手把它补回索引 —— 以前只搬文件、不补条目,于是顶层有文件、`MEMORY.md` 里没有,体检一直报「漏索引」。*
|
|
63
|
-
|
|
64
|
-

|
|
65
|
-
|
|
66
|
-
*更新提示 —— 只在**真有新版本**时出现一条横幅,写明新版本与当前版本;「看更新内容」开新标签页,「×」只是这次不看。查不到 / 被限流 / 网络不通一律静默,不会因为查版本失败而冒红条。*
|
|
52
|
+
So the accurate statement is not "the model remembers you", but "**before it opens its mouth, somebody has handed it the relevant slips of paper**". Take the slips away and it is that amnesiac colleague again.
|
|
67
53
|
|
|
68
|
-
|
|
54
|
+
A memory takes the form of one `.md` file per memory (YAML frontmatter + body): openable, editable and deletable in an editor, and manageable with git directly. **This memory stays in your own hands the whole time, not in some database.**
|
|
69
55
|
|
|
70
|
-
|
|
56
|
+
## Why Files, Not a Database
|
|
71
57
|
|
|
72
|
-
|
|
58
|
+
Most memory schemes take the "SQLite + a long-running worker" route: once memories are written into a database they are **opaque** (to see what is stored you have to go query tables), capturing every event **inflates** the store (even "the user said hello" counts as a row), and you have to keep an extra process and an extra port alive.
|
|
73
59
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
60
|
+
| | File-based (sage-mem) | Database + worker |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Seeing what is stored | Open the directory | Query tables / write a script |
|
|
63
|
+
| Editing one by hand | Edit it in an editor | Go through an API or SQL |
|
|
64
|
+
| Migration / backup | Copy the directory (travels with git) | Export tooling |
|
|
65
|
+
| Extra process and port | None | Usually yes |
|
|
66
|
+
| Who decides what gets stored | The agent judges by rules | Commonly indiscriminate capture of everything |
|
|
77
67
|
|
|
78
|
-
|
|
68
|
+
## Core Features
|
|
79
69
|
|
|
80
|
-
|
|
70
|
+
| Feature | Description |
|
|
71
|
+
|---|---|
|
|
72
|
+
| **File-based storage** | One `.md` per memory: frontmatter holds metadata, the body holds content. No database, no hidden format |
|
|
73
|
+
| **Automatic injection** | Every turn automatically scans the memory directory and picks the most relevant few by relevance to inject into context. The model "recalls" on the first turn, without having to go and read files itself |
|
|
74
|
+
| **The model can look things up too** | Two retrieval tools are also given to the model (`search_memory` / `read_memory`), so it can go through memories on its own and read the original text by line |
|
|
75
|
+
| **Writes are not overwritten** | While you are editing and the model is changing things at the same time, the later write does not silently clobber the earlier one; the conflict is left as a pending record for you to decide which side to keep |
|
|
76
|
+
| **Zero extra services** | No worker, no SQLite, no port, no long-running process, and no extra API key or cloud service (the only runtime dependency is the ordinary npm library `zod`) |
|
|
77
|
+
| **Anti-bloat** | Rules guide the model in judging "is this worth storing": nothing without substance is stored, nothing derivable by reading the code / git is stored |
|
|
78
|
+
| **Tidyable** | The optional "autodream" reads your memories through once: merges duplicates, fixes the index and broken links, and creates a rollback point before changing anything |
|
|
79
|
+
| **Migration from Claude Code** | The persona (`CLAUDE.md` → `AGENTS.md`) is copied as-is; memories just need organizing into `.md` files under four types (see [the next section](#migrating-from-claude-code)) |
|
|
81
80
|
|
|
82
|
-
|
|
81
|
+
The UI also carries four more capabilities (all of them optional): the memory management page, type and tag filtering, the memory star map, and audit & archiving.
|
|
83
82
|
|
|
84
|
-
##
|
|
83
|
+
## Quick Start
|
|
85
84
|
|
|
86
|
-
|
|
85
|
+
Requires **DSH `0.1.2-rc.1` or higher** (see [Compatibility](#compatibility)) and **Node.js `>= 18`**.
|
|
87
86
|
|
|
88
87
|
```bash
|
|
89
|
-
#
|
|
88
|
+
# replace web with your own profile name
|
|
90
89
|
dsh plugin --profile web add sage-mem
|
|
91
90
|
```
|
|
92
91
|
|
|
93
|
-
`dsh plugin add`
|
|
92
|
+
`dsh plugin add` puts the package into this profile and registers its bundle — no hand-editing `package.json` (manual install below).
|
|
94
93
|
|
|
95
|
-
|
|
94
|
+
After installing, restart DSH and confirm it is in:
|
|
96
95
|
|
|
97
96
|
```bash
|
|
98
97
|
dsh plugin --profile web list
|
|
99
98
|
```
|
|
100
99
|
|
|
101
|
-
|
|
100
|
+
Then **open a new session** and try three steps:
|
|
102
101
|
|
|
103
|
-
1.
|
|
104
|
-
2.
|
|
105
|
-
3.
|
|
102
|
+
1. Tell the model "remember: my cat is called Zhima, and it likes sunbathing" — it writes this into the memory directory by its rules
|
|
103
|
+
2. Open another **new session** and ask "what is my cat called"
|
|
104
|
+
3. It should answer "Zhima" on the first turn — the memory was retrieved and injected automatically by the plugin; the model did not go and read files itself
|
|
106
105
|
|
|
107
106
|
<details>
|
|
108
|
-
<summary
|
|
107
|
+
<summary>Manual install (if you do not want the CLI, or want to install a local development build)</summary>
|
|
109
108
|
|
|
110
|
-
|
|
109
|
+
Add the dependency to the profile's `package.json` and write the package name into `dsh.profile.bundles`:
|
|
111
110
|
|
|
112
111
|
```json
|
|
113
112
|
{
|
|
114
113
|
"dependencies": {
|
|
115
|
-
"sage-mem": "^0.
|
|
114
|
+
"sage-mem": "^1.0.0"
|
|
116
115
|
},
|
|
117
116
|
"dsh": {
|
|
118
117
|
"profile": {
|
|
@@ -122,7 +121,7 @@ dsh plugin --profile web list
|
|
|
122
121
|
}
|
|
123
122
|
```
|
|
124
123
|
|
|
125
|
-
|
|
124
|
+
For local development, use the cloned source:
|
|
126
125
|
|
|
127
126
|
```json
|
|
128
127
|
{
|
|
@@ -132,223 +131,333 @@ dsh plugin --profile web list
|
|
|
132
131
|
}
|
|
133
132
|
```
|
|
134
133
|
|
|
135
|
-
|
|
134
|
+
Then run `pnpm install` and restart DSH.
|
|
136
135
|
|
|
137
136
|
</details>
|
|
138
137
|
|
|
139
|
-
###
|
|
138
|
+
### Where Memory Lives
|
|
140
139
|
|
|
141
|
-
|
|
140
|
+
By default `~/.sage-mem/memory`. To put it somewhere else (inside a project repository, so it travels with git, for example), set the environment variable:
|
|
142
141
|
|
|
143
142
|
```bash
|
|
144
143
|
SAGE_MEM_DIR=/path/to/your/memory
|
|
145
144
|
```
|
|
146
145
|
|
|
147
|
-
|
|
146
|
+
The state directory (configuration, ledger, rollback points, conflict queue) defaults to the parent of the memory directory: `<memory directory>/.sage-mem`. All environment variables are in [Configuration Reference](#configuration-reference).
|
|
148
147
|
|
|
149
|
-
|
|
148
|
+
## UI Preview
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
Every image below is **rendered from the UI code in this repository** (the same CSS, the same component tree; the data is demo data for illustration, not a real memory directory).
|
|
151
|
+
|
|
152
|
+

|
|
153
|
+
|
|
154
|
+
*File list — the header always shows 「体检 N 个问题」(audit: N issues) and 「待重启生效」(restart to apply), so you know whether you should deal with it without opening anything; greyed rows are archived and can be restored at any time.*
|
|
155
|
+
|
|
156
|
+

|
|
157
|
+
|
|
158
|
+
*Tools drawer — closed it takes up a single button; expanded, each of the two items carries its own status.*
|
|
159
|
+
|
|
160
|
+

|
|
161
|
+
|
|
162
|
+
*Lock — one click locks an important memory: **it no longer appears among the automatic archive candidates; the archive button in the UI is disabled, and even if you bypass the UI, the archive implementation refuses explicitly with `locked`**. The lock lives in the state directory and is not written into the memory's own frontmatter — that would be rewritten by automatic consolidation, and once the lock is lost it would be back among the candidates.*
|
|
163
|
+
|
|
164
|
+

|
|
165
|
+
|
|
166
|
+
*Audit — hard problems such as dangling index entries, missing index entries and broken links are reported in red by category; "points to an archived memory" is only a hint, does not count as a problem, and has no one-click fix (which one to change is your call).*
|
|
167
|
+
|
|
168
|
+

|
|
169
|
+
|
|
170
|
+
*Update notice — a single banner appears only when there **really is a new version**, naming the new version and the current one; 「看更新内容」(see what's new) opens a new tab, and "×" just means not this time.*
|
|
171
|
+
|
|
172
|
+

|
|
173
|
+
|
|
174
|
+
*Archive candidates — each states "why archiving is suggested" (idle days vs the threshold for that type); files are touched only after you confirm one by one or select all. Archiving moves a file into `archive/`, it **does not delete**.*
|
|
175
|
+
|
|
176
|
+

|
|
155
177
|
|
|
156
|
-
|
|
178
|
+
*Memory star map — colour maps to type, size to volume, brightness to freshness; dark stars are archived memories, and opening one shows the archive time and reason.*
|
|
157
179
|
|
|
158
|
-
|
|
180
|
+

|
|
159
181
|
|
|
160
|
-
|
|
182
|
+
*Autodream · settings — run / settings / history as secondary groups; the five injection parameters laid out in two columns, with both 「当前生效」(currently in effect) and 「重启后」(after restart) written out.*
|
|
161
183
|
|
|
162
|
-
-
|
|
163
|
-
- **摘要没写到的,正文能找回来**。摘要是精准的一句话,代价是正文细节天生检索不到;摘要一条都没命中时改由正文检索兜底——此前这种情况直接等于「没有记忆可用」。正文检索只在问题**至少 4 个字符**时才跑:`继续` 这种两个字的问题拿去做正文匹配,命中的全是「继续」这个词本身
|
|
164
|
-
- **换个说法也能命中**:打分是**词面重合**,不是语义的——问「我有什么待办」而文件里写的是「代办清单」,差一个字就完全不命中。给这类记忆加一行 `aliases: [待办, todo]` 就能补上
|
|
165
|
-
- **注入的是命中段落,不是文件开头**。命中段落标成 `### 项目_部署.md:40-52`,agent 据此能直接跳到那几行核对来源;段落前补一句该记忆的 `description`,补回「这段是从哪条记忆切出来的」。`baseline` 与没命中正文的文件仍注入开头。单条正文超 1500 字符截断并标 `…(截断)`
|
|
166
|
-
- **相邻的重复问题不会重复注入**(按会话只留一个签名),连着问两遍不会又追加一遍。注意这**不是**「整个会话同题只给一次」:问 A → 问 B → 再问 A,第三次会重新注入 —— 再次要求回忆时重新给证据未必是坏事
|
|
167
|
-
- **单场会话预算 60 KB,拆成两本账**(`MAX_SESSION_BYTES`):**正文池** = 总上限 − **状态卡预留**(5%,夹在 1–4 KB)。总上限的数字没变,变的是到顶之后的行为 —— 旧行为是「正文装不下就一个字都不注,连为什么都没有」;现在是「正文照旧不注,但缺口状态卡发得出来,告诉你漏了什么、下一步怎么查」。**压缩或清空之后两本账与卡的去重签名一起归零**,又能注进来
|
|
168
|
-
- **缺口状态卡只在真有缺口时说话**:有候选没读、或有候选被预算省略时才出现,说清范围、条数、最佳线索和**一个可做的动作**;一条都没命中时**只在你问自己的历史时**才说(否则提示会被当噪音忽略)。单张卡上限 512 字节,且按内容签名去重 —— 同一张卡不会每一步都追加一遍
|
|
184
|
+

|
|
169
185
|
|
|
170
|
-
|
|
186
|
+
*Auto-archive — it is not the same thing as autodream: archiving is a **deterministic rule** (computed from idle days in the access ledger, no model call), which is why it gets a page of its own. Thresholds take effect on save, no restart needed.*
|
|
171
187
|
|
|
172
|
-
|
|
188
|
+

|
|
173
189
|
|
|
174
|
-
|
|
190
|
+
*Memory star map in full — the canvas draws only relations and distribution; the file name of a single memory appears only when you hover over or open that star. Rendering is redrawn on demand; no drawing is initiated while the view is static.*
|
|
191
|
+
|
|
192
|
+
## Memory File Format
|
|
193
|
+
|
|
194
|
+
````markdown
|
|
195
|
+
---
|
|
196
|
+
name: optional short name
|
|
197
|
+
description: one sentence saying exactly what this memory is (retrieval depends on it, so write it accurately)
|
|
198
|
+
metadata:
|
|
199
|
+
type: user # user / feedback / project / reference
|
|
200
|
+
tags: [project, todo] # optional, for filtering on the list page (at most 20, 40 characters each)
|
|
201
|
+
aliases: [todo, backlog] # optional: an alternative wording that also retrieves it
|
|
202
|
+
baseline: true # optional: injected unconditionally on a new session's first step (limit 8)
|
|
203
|
+
baselinePriority: 10 # optional: among several baseline memories, which is injected first (higher = earlier)
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
The memory body.
|
|
207
|
+
````
|
|
208
|
+
|
|
209
|
+
The four memory types:
|
|
210
|
+
|
|
211
|
+
| Type | What it stores |
|
|
175
212
|
|---|---|
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
213
|
+
| `user` | who the user is |
|
|
214
|
+
| `feedback` | guidance on how to work (recommended to carry `**Why:**` and `**How to apply:**` lines, to make the boundaries easy to judge) |
|
|
215
|
+
| `project` | work in progress and its state |
|
|
216
|
+
| `reference` | pointers to where external information can be found |
|
|
217
|
+
|
|
218
|
+
Single-file limit 512 KB, **validated on both the read and the write side** (on reading, the size is checked first; if it is over, the file is not read in).
|
|
219
|
+
|
|
220
|
+
## Retrieval and Injection
|
|
221
|
+
|
|
222
|
+
Something transparent deserves a clear account of how it selects its material, because that directly determines how many tokens get injected.
|
|
223
|
+
|
|
224
|
+
**Two-stage retrieval: summary first, then body.** The first stage scores by `description` + file name + `aliases`, taking at most **10 candidates**; the second stage runs **body paragraph retrieval** over those candidates (BM25, with Chinese tokenized as two-character units) and injects the most relevant paragraph. What is compared is a **paragraph**, not a whole file, so one file contributes at most one paragraph, and a long file cannot hog candidate slots through sheer paragraph count.
|
|
225
|
+
|
|
226
|
+
**What the summary does not cover, the body can find again.** The summary is one precise sentence, at the cost that body details are inherently unretrievable by it; when not a single summary hits, body retrieval takes over as the fallback. Body retrieval only runs when the question is **at least 4 characters** long — a two-character question like `继续` ("continue") fed into body matching hits nothing but the word `继续` itself.
|
|
227
|
+
|
|
228
|
+
**A different wording hits too:** scoring is **lexical overlap**, not semantic — ask "我有什么待办" ("what todos do I have") while the file says "代办清单" ("todo list") and one differing character means no hit at all. Adding a line `aliases: [待办, todo]` to such a memory covers it.
|
|
178
229
|
|
|
179
|
-
|
|
180
|
-
没有为工具另开一条绕过闸门的入口。它们用 `ctx.inject(['tools'], …)` 挂子 fiber 注册 ——
|
|
181
|
-
**不是硬依赖**:宿主不提供工具服务时,插件照常工作,只是状态卡换一套「直接读记忆目录」的说法。
|
|
230
|
+
**What gets injected is the matching paragraph, not the start of the file.** A matching paragraph is labelled `### 项目_部署.md:40-52`, so the model can jump straight to those lines to check the source; a line with that memory's `description` is prepended, saying which memory the passage was cut from. `baseline` memories and files with no body hit are still injected from the start; a single body over the character limit (1500 characters by default) is truncated and marked `…(截断)`.
|
|
182
231
|
|
|
183
|
-
|
|
184
|
-
- **意图识别绝不在四类显式请求上抑制重复**:「上次决定用 A 对吧」(核实)、「不用 A 了」(更正)、「之前为什么没选 A」(问依据)必须给原文;判不出来就不判,不猜
|
|
232
|
+
**Adjacent repeated questions are not injected twice** (only one signature is kept per session), so asking twice in a row does not append it again. Note that this is **not** "the same question is given once for the whole session": ask A → ask B → ask A, and the third one injects again — when recall is requested again, supplying the evidence again is not necessarily a bad thing.
|
|
185
233
|
|
|
186
|
-
|
|
234
|
+
**A single session has a budget cap** (60 KB by default), split into two ledgers: the **body pool** = the total cap − the **status-card reservation** (5%, clamped between 1 and 4 KB). Hitting the top does not mean "not a single character is injected"; the body is simply not injected as before while the gap status card is still sent, telling you what was left out and how to look it up next. **After the context is compacted or cleared, both ledgers and the card signatures reset to zero together**, and injection can happen again.
|
|
187
235
|
|
|
188
|
-
|
|
189
|
-
`<状态根>/tasks/<任务 id>/WORKING.md` 存在,注入末尾会给一条短句柄,说清任务、revision
|
|
190
|
-
与去哪读。**压缩后账本归零会连去重一起清掉**,句柄因此重新出现 —— 这就是恢复路径。
|
|
236
|
+
> ⚠️ **This cap is soft, not a hard gate.** `baseline` memories matched by the question are injected regardless and take no part in the degradation, so the cumulative body over one session **may exceed the body pool** (9.6% overrun was measured in evaluation, all of it from this kind of memory). This is a deliberate tradeoff: the semantics of that kind of memory are "it should be present every session". Do not read it as "a clean cut-off at 60 KB".
|
|
191
237
|
|
|
192
|
-
|
|
193
|
-
原文去决定读哪个目录。** 检查点里「已完成」的条目如果没有任何证据标记(路径、命令、测试结果),
|
|
194
|
-
解析时会如实报出来 —— 格式完美、内容错误的交接材料仍然是错的。
|
|
238
|
+
**The gap status card speaks only when there really is a gap**: it appears only when there are candidates that were not read, or candidates omitted by the budget, and it states the scope, the count, the best clue and **one action you can take**; when not a single thing matched, it speaks only if you **asked about your own history**, or this task **has a checkpoint** (otherwise the hint would be ignored as noise). A single card is capped at 512 bytes and deduplicated by content signature.
|
|
195
239
|
|
|
196
|
-
|
|
240
|
+
**There is a relevance threshold on the summary side too.** In Chinese, units like "怎么" and "什么" appear in a great many `description`s, so an unrelated "今天天气怎么样" can hit a dozen memories off a single "怎么". The threshold is normalized against **the corpus's own IDF scale**, which brings the candidate count for unrelated questions down to about one seventh; what gets cleared out is mainly the weak summary candidates that should have lost anyway.
|
|
197
241
|
|
|
198
|
-
|
|
242
|
+
The comparison on the offline evaluation set (before and after the threshold): **Recall@5 84.1% → 87.0%, spurious injection (memories injected that are unrelated to the question) 14.1% → 5.6%**.
|
|
199
243
|
|
|
200
|
-
|
|
244
|
+
> ⚠️ **These are measurements, not promises**: what transfers is the **relative improvement**; the absolute level **cannot be extrapolated** to a real memory directory — the summaries of that 30-item corpus were written by hand precisely so as to "be retrievable", whereas real memories' summaries are often written casually or are missing entirely. Method and limitations are in the [evaluation report](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-eval-report.md).
|
|
245
|
+
|
|
246
|
+
**Intent detection never suppresses repetition on three kinds of explicit request**: "we settled on A last time, right?" (verification), "we are not using A any more" (correction), "why did we not pick A before" (asking for the basis) must be given the original text; if it cannot be judged, it does not judge, and it does not guess.
|
|
247
|
+
|
|
248
|
+
### The Model Can Browse Memories Too: Two Tools
|
|
249
|
+
|
|
250
|
+
The trigger right for retrieval is split in two: half with the plugin (automatic injection — it guesses what you want to see), half with the model (the two tools below — it looks things up on its own).
|
|
251
|
+
|
|
252
|
+
| Tool | Purpose |
|
|
201
253
|
|---|---|
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
|
|
254
|
+
| `search_memory(query, scope?, limit?, cursor?)` | Compares summaries first, then searches bodies by paragraph; returns candidates, matching passages, line numbers, and **coverage information** (how many files were scanned, how many paragraphs were filtered out for insufficient relevance, whether there is a next page) |
|
|
255
|
+
| `read_memory(name, startLine?, startByte?, maxBytes?, revision?)` | Reads the body by line, paginated. Pass the `revision` you last read, and if the content has been changed by someone it returns `revision_changed`, **and does not return the content** — otherwise you would read the wrong thing with stale line numbers |
|
|
256
|
+
|
|
257
|
+
Both tools go through **the same** safe read path (file name checks → symlink and reserved-name identity checks → size gate); no separate entrance that bypasses the gate was opened for the tools. They register through the tool service the host provides — that is **not a hard dependency**: when the host does not provide that service, the plugin works as normal, only the status card switches to a "read the memory directory directly" wording.
|
|
258
|
+
|
|
259
|
+
### Relaying the Baton on Long Tasks: Task Checkpoints
|
|
260
|
+
|
|
261
|
+
When the host compacts context is not something the plugin controls — so it **does not promise "save before compaction"**. What it can do is: if `<state directory>/tasks/<task id>/WORKING.md` exists, a short handle is appended to the injection, stating the task, the revision and where to read it. **After compaction the ledgers reset to zero and that clears the deduplication along with them**, so the handle reappears — that is the recovery path.
|
|
262
|
+
|
|
263
|
+
The task id comes from `SAGE_MEM_TASK_ID` (for the same task across sessions), otherwise the session id is used. **Your raw question text is never used to decide which directory to read.** If a "done" item in a checkpoint carries no evidence marker at all (path, command, test result), parsing reports it as such — handover material that is perfectly formatted but factually wrong is still wrong.
|
|
205
264
|
|
|
206
|
-
|
|
207
|
-
选了它,都不等于内容有用、更不等于内容正确。混成一个「热度」就再也分不开了。
|
|
265
|
+
## Write Safety
|
|
208
266
|
|
|
209
|
-
|
|
210
|
-
`cat 某记忆` 认不出来,那就不认 —— 我们不会把「看不到」说成「没读过」。
|
|
267
|
+
Memories are **files**, so "you are editing" and "the model is changing things" can perfectly well happen at the same time. The default behaviour is **the later write wins and the earlier one quietly disappears** — the worst kind, because you never find out.
|
|
211
268
|
|
|
212
|
-
|
|
269
|
+
sage-mem adds a **version guard** to writes into the memory directory: the current version is noted before writing, and the write requires that the version has not changed. If it has, the write is **explicitly refused** instead of being clobbered — turning "a silent bad result" into "an explicit failure".
|
|
270
|
+
|
|
271
|
+
The guard has two paths, with **different** coverage, spelled out:
|
|
272
|
+
|
|
273
|
+
| Who is writing | How it is protected | When refused |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| The model changing a memory with the **built-in `write` / `edit`** | The plugin supplies the "current version" in a pre-event, and the host refuses stale overwrites on that basis | The host refuses outright (reporting `FS_STALE_VERSION`). **A refusal on this path leaves no conflict record** — the write gate only supplies the version, it does not perform refusals |
|
|
276
|
+
| **The plugin itself** writing (autodream consolidation) | The caller passes the version it read at write time | A pending record `<state directory>/conflicts/<id>.json` is written, keeping **the version on disk** and **the version it wanted to write** — with only one side there is nothing to judge |
|
|
277
|
+
|
|
278
|
+
- **Conflicts are not merged automatically**; which side to keep is your decision (the record is JSON, open it directly when you need to). Silent retry and silent abandonment both decide on your behalf.
|
|
279
|
+
- Known boundary: **editing memories in a shell does not go through this path**. The write gate guards "memory writes that go through the tools", not "all modifications to files"; redirection and the like inside `pwsh` are not recognized. If it cannot be sure, it does not claim it — so that "cannot see it" is not mistakenly recorded as fact.
|
|
280
|
+
|
|
281
|
+
### Access Ledger: Three Kinds of "Used" Recorded Separately
|
|
282
|
+
|
|
283
|
+
`<state directory>/access.json` records three things:
|
|
284
|
+
|
|
285
|
+
| Field | Meaning |
|
|
286
|
+
|---|---|
|
|
287
|
+
| `injected` | the plugin **passively** injected this memory |
|
|
288
|
+
| `read` | the model **actively** read it |
|
|
289
|
+
| `use` | the number of times it was explicitly adopted (self-reported) |
|
|
213
290
|
|
|
214
|
-
|
|
215
|
-
先写的悄悄消失** —— 那是最糟的一种,因为你不会发现。
|
|
291
|
+
They are recorded separately because the three mean completely different things: **being injected** only says the plugin thought it was relevant, **being actively read** only says the model picked it, and neither means the content was useful, let alone that it was correct. Merge them into one "heat" score and they can never be separated again.
|
|
216
292
|
|
|
217
|
-
|
|
218
|
-
变了就**明确拒绝**,而不是盖掉。模型用内置 `write` / `edit` 改记忆时会自动带上这层守卫。
|
|
293
|
+
**This ledger is currently used only for diagnostics and the auto-archive threshold; it takes no part in retrieval ranking.** The observation has known blind spots: reading memories in a shell is not recognized, so it is not claimed.
|
|
219
294
|
|
|
220
|
-
|
|
221
|
-
**盘上那一版**和**想要写的那一版** —— 因为只有一边等于没得判。
|
|
295
|
+
### Freshness and Citation Discipline
|
|
222
296
|
|
|
223
|
-
-
|
|
224
|
-
- 同一条冲突重复上报时 id 相同,队列里不会长出重复项。
|
|
225
|
-
- 已知边界:**在 shell 里改记忆不走这条路**(`pwsh` 里 `cat > 某个记忆` 我们认不出来)。
|
|
226
|
-
写门守的是「经过工具的记忆写入」,不是「所有对文件的修改」。
|
|
227
|
-
- **每条记忆的标题带新鲜度**:`(保存于 N 天前)`,**优先读 frontmatter 的 `updated:`,缺失才回落文件 mtime** —— 手写的时间戳是权威,文件系统时间会被复制 / 备份还原 / checkout 整批改掉。理由是陈旧记忆被当事实引用时,「引用」这个动作本身会让它显得更权威而不是更可疑,所以注入文本开头还带一节**引用纪律**(提到路径先确认存在、提到函数先 grep、要动手先核实)。它**独立成节**而不是一条 bullet:Claude Code 实测同一句话埋成 bullet 命中 0/3、独立成节 3/3
|
|
228
|
-
- **`baseline: true` 的记忆每场会话第一步无条件注入**,最多 8 条——给「你是谁」「待办清单」这类每次开场都该在场的记忆用。⚠️ **「不吃段落替换」不等于整份**:它同样受单文件长度上限(默认 1500 字符)截断,**关键内容若落在尾部会看不到** —— baseline 适合放短摘要,完整正文留给工具去读。多条 baseline 之间的先后由 `baselinePriority` 决定(数字越大越前,缺省 0),**不按文件名排**
|
|
229
|
-
- 记忆目录读不到、权限错、frontmatter 格式坏,这些都**降级为不注入并写日志**,不会静默失效
|
|
297
|
+
Every memory's heading carries its freshness: `(保存于 N 天前)`, and **the frontmatter's `updated:` takes priority, falling back to the file mtime only when it is missing** — a hand-written timestamp is authoritative; filesystem times get changed wholesale by copying / restoring from a backup / checkout.
|
|
230
298
|
|
|
231
|
-
|
|
299
|
+
The reason is: when a stale memory is cited as fact, the act of "citing" itself makes it look more authoritative rather than more suspect. So the injected text also opens with a **citation discipline** section (confirm a path exists before mentioning it, grep before mentioning a function, verify the current code before acting), and it is **a section of its own** — what it constrains is how this batch of content is to be used, and compressing it into one list item makes it easy to skip over together with the content.
|
|
232
300
|
|
|
233
|
-
|
|
301
|
+
**Memories with `baseline: true` are injected unconditionally on the first step of every session**, at most 8 — for memories like "who you are" and "the todo list" that ought to be present at every opening. ⚠️ **"Not subject to paragraph replacement" does not mean the whole file**: it is equally truncated by the single-item character limit (1500 characters by default), **so key content that falls at the tail will not be seen**. baseline suits short summaries; leave the full body for the tools to read. The order among several baseline memories is decided by `baselinePriority` (higher number = earlier, default 0), not by file name.
|
|
234
302
|
|
|
235
|
-
|
|
303
|
+
If the memory directory cannot be read, or permissions are wrong → **it degrades to no injection and writes a log**; it does not fail silently. Broken frontmatter is **handled leniently**: fields that cannot be parsed fall back to defaults (`type` missing is recorded as `reference`), and the file itself is still readable — one wrong line does not make a whole memory disappear.
|
|
236
304
|
|
|
237
|
-
|
|
305
|
+
## Autodream
|
|
238
306
|
|
|
239
|
-
|
|
307
|
+
The optional memory-consolidation feature (identifier `autodream`): it reads through the memory directory once — merging duplicates, deleting the overturned, fixing the index and broken links, converting relative dates into absolute dates — so that the next session can find its bearings quickly.
|
|
308
|
+
|
|
309
|
+
**Off by default**; turn it on in the settings page under 「记忆管理 → 自动做梦」(Memory management → Autodream). Three things are each optional:
|
|
310
|
+
|
|
311
|
+
| Option | Values | Description |
|
|
240
312
|
|---|---|---|
|
|
241
|
-
|
|
|
242
|
-
|
|
|
243
|
-
|
|
|
313
|
+
| Trigger | Manual only / run automatically when conditions are met | Automatic mode goes through three levels of gating, **checking the cheapest first**: time gate (≥ N hours since last time, one read/write) → session gate (≥ M sessions with updates in between, one directory scan) → lock (against concurrency). A 10-minute scan throttle is sandwiched in between, so that while "the time gate passed but the session gate did not" it does not rescan for nothing every turn |
|
|
314
|
+
| Mode of change | Report only / rewrite directly | In "report only" mode **the write tools are simply not attached for the model at all** — it is not constrained by a sentence saying "please do not write", it has no hands |
|
|
315
|
+
| Input source | Memory directory only / memory + session records | The latter decompresses session records (zstd) for targeted keyword search; the cost is that private conversations enter the model context, and the token cost is higher too |
|
|
244
316
|
|
|
245
|
-
|
|
317
|
+
Automatic triggering runs on the plugin's own timer (checks the gates every 30 minutes by default). Consolidation **deliberately does not run through your session** — the tool set is given by the plugin, the step count is capped, and it is not written into your session records.
|
|
246
318
|
|
|
247
|
-
|
|
319
|
+
### The Consolidation Model Can Be Specified Separately
|
|
248
320
|
|
|
249
|
-
|
|
250
|
-
- 面板下方始终显示**实际生效的是哪条**(配置指定 / 跟随默认),配置里那条路线万一从目录里消失了,下拉框也会把它显示出来而不是悄悄跳回默认。
|
|
251
|
-
- 拿不到模型目录时(宿主差异),退化成手填 `provider` 与 `model`。
|
|
321
|
+
Consolidation is a background batch job and need not be the same model as the one used for chatting. The settings page under 「高级设置 → 模型」(Advanced settings → Model) lets you pick one from **the model routes currently configured** (the options come from the same catalog as in 「模型选择」/ model selection).
|
|
252
322
|
|
|
253
|
-
|
|
323
|
+
- Once chosen, **only that one is accepted**: if the route does not exist in the host, **it errors before starting work** (it compares only against the host's registered catalog and sends no request at all), so it does not fail halfway through, and it **does not silently fall back to the default model**.
|
|
324
|
+
- The panel always shows **which one is actually in effect**; if the route named in the configuration happens to disappear from the catalog, the dropdown still shows it rather than quietly jumping back to the default.
|
|
325
|
+
- When the model catalog cannot be obtained (host differences), it degrades to typing `provider` and `model` by hand.
|
|
254
326
|
|
|
255
|
-
|
|
327
|
+
### Rollback Points
|
|
256
328
|
|
|
257
|
-
-
|
|
258
|
-
- **回滚本身也能退**:动手之前会先把「现在」再存一份**保护快照**(它不参与普通快照的轮转淘汰)。
|
|
259
|
-
- **范围可选**:只恢复这次运行动过的文件,或整目录回到该时点。
|
|
260
|
-
- **全程不删文件**:回滚中「不该存在」的文件一律移进 `archive/`,人工可捞回。有同名冲突就跳过并计数,绝不覆盖。
|
|
261
|
-
- 每次回滚都留痕:报告目录里写一份回滚报告,`.sage-mem/autodream/rollbacks.json` 里追加一条记录。
|
|
329
|
+
**Whenever this run is a "direct rewrite", a rollback point is created before work starts** (a full snapshot of the top-level `.md` files in the memory directory, each with a sha256), and after the run you can see it in the settings page and roll back to it in one click.
|
|
262
330
|
|
|
263
|
-
|
|
331
|
+
- The rollback point is **bound** to that run: the report carries the runId, the rollback directory has the same name, and the two match up.
|
|
332
|
+
- **A rollback can itself be rolled back**: before acting, it first saves "now" once more as a **protection snapshot** (not part of the normal snapshot rotation).
|
|
333
|
+
- **Scope is selectable**: restore only the files that run touched, or take the whole directory back to that point in time.
|
|
334
|
+
- **No file is deleted at any point**: files that "should not exist" during a rollback are all moved into `archive/`, recoverable by hand; on a name collision it skips and counts, and never overwrites.
|
|
335
|
+
- Every rollback leaves a trace: a rollback report is written into the report directory, and a record is appended to the state directory.
|
|
264
336
|
|
|
265
|
-
|
|
337
|
+
### Consolidation Manifest: What Changed and Why
|
|
266
338
|
|
|
267
|
-
-
|
|
268
|
-
- **`reason` 应当填写。** `write_memory` 不带 `reason` **不会被拒绝**——改动照样生效,但整理声明里会把它标成「未自述」并附上宿主能追溯到的信息(第几轮 / 什么操作 / 字节变化)。**`archive_memory` 的 `reason` 仍必填**,缺了直接拒绝;被拒的动作单列一节「未能落地的改动」,不会静默消失。(不硬拒 `write_memory` 是有意的:为了一句解释而丢掉一次本来正确的修正,代价更大。)
|
|
269
|
-
- 面板的「整理记录」区列出每一趟(时间 / 模式 / 改了几条 / 是否已被回滚),点开看声明全文。
|
|
339
|
+
Every consolidation leaves a **manifest** behind, in two forms: the human-readable `memory/autodream/<runId>.md` (embedded in the report) and the machine-readable `.sage-mem/autodream/runs/<runId>/manifest.json`.
|
|
270
340
|
|
|
271
|
-
|
|
341
|
+
It records item by item: **file / operation (create · rewrite · archive) / reason / byte change**, plus a structural audit comparison before and after the run.
|
|
272
342
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
3. **跑完自动审计**:七查(索引悬空 / 漏索引 / `type` 与文件名前缀不一致 / CRLF / 双链断链 / frontmatter 里被 YAML 当注释吞掉的「空格 + #」/ 无 frontmatter),外加「读不动」单列一类,结果与前后对比写进报告。它**只报不改**
|
|
343
|
+
- **`reason` should be filled in.** A `write_memory` without `reason` **will not be refused** — the change takes effect all the same, but the manifest marks it as 「未自述」(not self-described) and attaches what the host can trace (which turn / what operation / byte change). The reason: losing an otherwise correct correction over one missing explanation is the greater cost.
|
|
344
|
+
- **`archive_memory`'s `reason` is still required**; without it the action is refused outright, and refused actions get a section of their own, 「未能落地的改动」(changes that could not land), rather than disappearing silently.
|
|
276
345
|
|
|
277
|
-
|
|
346
|
+
### Three Safety Nets
|
|
278
347
|
|
|
279
|
-
|
|
348
|
+
1. **The safety boundary is in the tool set, not in the prompt** — during autodream the host gives the model only read-only tools, and only rewrite mode adds the rewriting tools; and **there is no "delete" capability at all** — archiving moves a file into `archive/`, recoverable by hand.
|
|
349
|
+
2. **A rollback point is created automatically before rewriting**, under `<parent of the memory directory>/.sage-mem/autodream/snapshots/<runId>/`, keeping the most recent N. The reason: the memory directory is **not under any version control** and deletion has no recycle bin, so "rollback-ability" has to be built by hand.
|
|
350
|
+
3. **An audit runs automatically at the end** — dangling index entries, missing index entries, `type` disagreeing with the file name prefix, CRLF, broken wikilinks, "space + #" swallowed as a YAML comment inside frontmatter, missing frontmatter, temporary files left behind by an interrupted archive, plus "unreadable" as a category of its own, and the result and the before/after comparison are written into the report. It **only reports, it does not change**.
|
|
280
351
|
|
|
281
|
-
|
|
352
|
+
### Rename Note
|
|
282
353
|
|
|
283
|
-
|
|
354
|
+
This feature was originally called "dream" (identifier `dream`) and has been called **autodream** since 0.7.0. Old data is not dropped: the old configuration `.sage-mem/dream.json` is read out and migrated to `.sage-mem/autodream.json` (**the old file is left untouched**), old reports under `memory/dream/` are still listed in the panel and marked 「旧版」(legacy), and old snapshots can still be rolled back to.
|
|
284
355
|
|
|
285
|
-
|
|
356
|
+
## Migrating from Claude Code
|
|
286
357
|
|
|
287
|
-
|
|
358
|
+
If you have already raised an agent in Claude Code (CC), sage-mem is part of a "CC → DSH" migration path. **Our own memories were migrated exactly this way** — at the time the memories on both sides were already Markdown files, so they could simply be copied over.
|
|
288
359
|
|
|
289
|
-
|
|
290
|
-
---
|
|
291
|
-
name: 可选的短名
|
|
292
|
-
description: 一句话说清这条记忆是什么(检索靠它,务必写准)
|
|
293
|
-
metadata:
|
|
294
|
-
type: user # user / feedback / project / reference
|
|
295
|
-
tags: [项目, 待办] # 可选,供列表页筛选
|
|
296
|
-
aliases: [待办, todo] # 可选:换一种说法也能被检索到
|
|
297
|
-
baseline: true # 可选:新会话第一步无条件注入(上限 8 条)
|
|
298
|
-
baselinePriority: 10 # 可选:多条 baseline 之间谁先注入(越大越前)
|
|
299
|
-
---
|
|
360
|
+
The two things are moved **differently**, spelled out:
|
|
300
361
|
|
|
301
|
-
|
|
302
|
-
|
|
362
|
+
| What | From | To | How |
|
|
363
|
+
|---|---|---|---|
|
|
364
|
+
| **Persona** | CC's `CLAUDE.md` | DSH's `AGENTS.md` | Both are plain text loaded natively by DSH; copy the content as-is |
|
|
365
|
+
| **Memories** | cross-session material accumulated in CC | sage-mem's `memory/` directory | One `.md` per memory, with frontmatter, filed under the four types `user` / `feedback` / `project` / `reference` |
|
|
366
|
+
|
|
367
|
+
⚠️ **The premise is that the shape matches.** The four-type division and the frontmatter are sage-mem's own conventions: if your memories in CC were already "one Markdown file per memory + frontmatter" (which is what ours were at the time), just copy them over; **if they were not** (if they were all piled into a single `CLAUDE.md`, say), you need to organize them first — and that job can perfectly well be handed to an agent: have it read the old material and lay it out as files under the four types.
|
|
368
|
+
|
|
369
|
+
## Configuration Reference
|
|
370
|
+
|
|
371
|
+
### Environment Variables
|
|
372
|
+
|
|
373
|
+
| Variable | Default | Description |
|
|
374
|
+
|---|---|---|
|
|
375
|
+
| `SAGE_MEM_DIR` | `~/.sage-mem/memory` | memory directory |
|
|
376
|
+
| `SAGE_MEM_STATE_DIR` | `<parent of the memory directory>/.sage-mem` | state directory (configuration, ledger, locks, conflict queue, rollback points) |
|
|
377
|
+
| `SAGE_MEM_MAX_SESSION_BYTES` | `61440` (60 KiB, 4096–524288) | total injection cap per session |
|
|
378
|
+
| `SAGE_MEM_MAX_RESULTS` | `10` (1–30) | cap on the number of candidate items injected per turn |
|
|
379
|
+
| `SAGE_MEM_MAX_CHARS` | `1500` (200–20000) | character cap for a single memory's injection |
|
|
380
|
+
| `SAGE_MEM_MAX_BASELINE` | `8` (1–50) | number of `baseline` slots |
|
|
381
|
+
| `SAGE_MEM_STALE_DAYS` | `1` (0–365) | threshold for marking a heading 「保存于 N 天前」(saved N days ago); 0 = mark every one |
|
|
382
|
+
| `SAGE_MEM_TASK_ID` | empty | scope of task checkpoints; empty falls back to the session id |
|
|
383
|
+
| `SAGE_MEM_DEBUG` | off | debug logging |
|
|
384
|
+
|
|
385
|
+
Value semantics: **out-of-range values are clamped to the boundary, illegal values (including pure whitespace) fall back to the default.**
|
|
386
|
+
|
|
387
|
+
### What the Settings Page Can Change
|
|
388
|
+
|
|
389
|
+
The panel can change the five caps above (written to `<state directory>/settings.json`, precedence **settings page > environment variable > built-in default**), as well as reserved names (file names that are protected and that the settings page may not write or delete, written to `<state directory>/reserved.json`). **Some items take effect only after restarting DSH**, and the panel writes out both the value in effect now and the value after restart.
|
|
303
390
|
|
|
304
|
-
|
|
391
|
+
### Files in the State Directory
|
|
305
392
|
|
|
306
|
-
|
|
|
393
|
+
| File | Purpose |
|
|
307
394
|
|---|---|
|
|
308
|
-
| `
|
|
309
|
-
| `
|
|
310
|
-
| `
|
|
311
|
-
| `
|
|
395
|
+
| `access.json` | access ledger (`injected` / `read` / `use`) |
|
|
396
|
+
| `conflicts/<id>.json` | write conflicts awaiting manual handling |
|
|
397
|
+
| `tasks/<task id>/WORKING.md` | task checkpoints |
|
|
398
|
+
| `settings.json` / `reserved.json` / `locked.json` | settings, reserved names, locks |
|
|
399
|
+
| `autodream.json`, `autodream/` | autodream configuration, snapshots, run records |
|
|
400
|
+
| `dream.json` | the legacy name, read once only during migration |
|
|
312
401
|
|
|
313
|
-
|
|
402
|
+
Apart from `access.json`, `conflicts/` and `tasks/`, the rest is internal implementation state, and its **format may change in minor versions**; do not depend on it.
|
|
314
403
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
│ Node fs 直读
|
|
319
|
-
▼
|
|
320
|
-
memory/ 目录(markdown 文件,4 类)
|
|
321
|
-
├── user_*.md
|
|
322
|
-
├── feedback_*.md
|
|
323
|
-
├── project_*.md
|
|
324
|
-
├── reference_*.md
|
|
325
|
-
└── MEMORY.md ← 索引
|
|
326
|
-
```
|
|
404
|
+
## Stability and Versioning Policy
|
|
405
|
+
|
|
406
|
+
**1.0.0 is this project's stability line.** From this version on, "what you can rely on" is no longer a piece of self-assessment but **a list that can be refuted by a machine**:
|
|
327
407
|
|
|
328
|
-
|
|
408
|
+
- **[Frozen contract](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md)** — the public surface is characterized item by item, with only four wordings: **guaranteed** (backed by an assertion; regressions go red) / **best-effort** (designed that way with empirical basis, but with no regression assertion that would go red) / **explicitly cannot do** (a capability boundary, a tradeoff rather than a defect) / **unverified** (we do not know).
|
|
409
|
+
- **Every "guaranteed" item can point to one concrete assertion** (one entry in some test suite), and `test/contract-binding-test.mjs` checks on every `npm test` that "this assertion really ran". A commitment that cannot point to an assertion can only be marked "best-effort" or "unverified".
|
|
410
|
+
- The **[capability boundaries table](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-boundaries.md)** is **cross-referenced line by line** with the contract, and `test/doc-consistency-test.mjs` checks that both sides characterize things the same way.
|
|
329
411
|
|
|
330
|
-
|
|
412
|
+
**How version numbers go up** (semantic versioning):
|
|
331
413
|
|
|
332
|
-
|
|
414
|
+
| Change | Version |
|
|
415
|
+
|---|---|
|
|
416
|
+
| Changing any item marked "guaranteed" in the contract, changing a tool name or a required parameter, dropping read compatibility with the old ledger format, changing an injection section name | **major** |
|
|
417
|
+
| Changing wording, changing ordering details, changing internal state file formats, changing the panel, adding a new environment variable, changing performance | minor / patch |
|
|
418
|
+
|
|
419
|
+
**Upgrading from 0.9.x to 1.0.0**: **there are no breaking changes** — not one of the tool names, parameters, environment variable names and defaults, injection section names, or ledger format changed. The only **behaviour change** is: an integer environment variable filled with **pure whitespace** now falls back to the default (previously it was treated as 0 and clamped to the lower bound). The item-by-item list is in the 1.0.0 section of the [CHANGELOG](https://github.com/gezi-wen/sage-mem/blob/main/CHANGELOG.md).
|
|
420
|
+
|
|
421
|
+
**Review material** (for those who want to dig in):
|
|
422
|
+
|
|
423
|
+
| File | Content |
|
|
424
|
+
|---|---|
|
|
425
|
+
| [1.0.0-contract.md](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md) | public commitments characterized item by item + the assertion each guarantee points to |
|
|
426
|
+
| [1.0.0-boundaries.md](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-boundaries.md) | 35 capabilities characterized item by item (with basis and means of verification) |
|
|
427
|
+
| [1.0.0-eval-report.md](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-eval-report.md) | quantified results for retrieval quality and budget, and what these numbers **cannot** measure |
|
|
428
|
+
| [review-findings.md](https://github.com/gezi-wen/sage-mem/blob/main/docs/review-findings.md) | the permanent ledger of what external review found: each finding, its disposition, and **which assertion watches over it** |
|
|
429
|
+
| [release-checklist.md](https://github.com/gezi-wen/sage-mem/blob/main/docs/release-checklist.md) | release discipline (the four sanitization checks, evaluation exit codes, probes, the four anchors) |
|
|
430
|
+
|
|
431
|
+
## Permissions and Privacy
|
|
333
432
|
|
|
334
|
-
|
|
433
|
+
sage-mem does three things only, all of them on your own machine:
|
|
434
|
+
|
|
435
|
+
| What it does | Concrete scope |
|
|
335
436
|
|---|---|
|
|
336
|
-
|
|
|
337
|
-
|
|
|
338
|
-
|
|
|
437
|
+
| **Reading memories** | reads only the top-level `*.md` under `SAGE_MEM_DIR` (default `~/.sage-mem/memory`), 512 KB per file |
|
|
438
|
+
| **Writing memories** | writes to the same directory through the model's file tools or the settings page; reserved names such as `MEMORY.md` and `session-log.md` are protected and the settings page cannot touch them |
|
|
439
|
+
| **Injection** | hands the selected memories to the model as context (at most 10 relevant + 8 `baseline`, 60 KB cumulative per session — ⚠️ this cap is **soft**, see [Retrieval and Injection](#retrieval-and-injection)) |
|
|
440
|
+
|
|
441
|
+
**What it does not do**: it opens no port, starts no long-running process, and needs no API key; it **does not capture every event** (whether to store is judged by the model according to rules; it is not "everything you say gets recorded").
|
|
339
442
|
|
|
340
|
-
|
|
341
|
-
不做事件全捕获(存不存由 agent 按规则判断,不是"你说了什么都记")。
|
|
443
|
+
**Two places where it goes out to the network / reads beyond its bounds, spelled out**:
|
|
342
444
|
|
|
343
|
-
|
|
445
|
+
1. **Version check**: opening the 「记忆管理」(Memory management) page silently makes one request to
|
|
446
|
+
`https://api.github.com/repos/gezi-wen/sage-mem/releases/latest` (10-second timeout; any failure is reported nowhere).
|
|
447
|
+
The request carries only the plugin's own `User-Agent`; **it contains no memory content, file path or usage statistics**. When `GITHUB_TOKEN` / `GH_TOKEN` is present in the environment it is attached (raising GitHub's unauthenticated 60/hour to 5000). This is the **only** network request.
|
|
448
|
+
2. **Session records**: when "autodream"'s input source is set to "memory + session records", it additionally **reads** the session records under `$DSH_HOME/sessions` for targeted search — that decompresses your raw conversation text (including private content) and sends it into the model context. **Off by default**.
|
|
344
449
|
|
|
345
|
-
|
|
450
|
+
**The only feature that calls a model is "autodream", and it is off by default.** Its model follows the current session's default model by default, and can also be specified separately in the settings page — once specified, only that one is accepted, with no silent fallback.
|
|
346
451
|
|
|
347
|
-
|
|
452
|
+
Your memories are the files in that directory, openable at any time for confirmation — which is also why file-based was chosen.
|
|
348
453
|
|
|
349
|
-
|
|
454
|
+
## Compatibility
|
|
350
455
|
|
|
351
|
-
|
|
456
|
+
sage-mem is a pure DSH plugin and declares its host requirement through **`engines.dsh`** in `package.json`: `>=0.1.2-rc.1` — **a one-sided open interval with no upper bound**, so no future DSH version will be blocked by this declaration. `engines.node` requires `>= 18`.
|
|
457
|
+
|
|
458
|
+
Version-by-version verification record:
|
|
459
|
+
|
|
460
|
+
| DSH version | Status |
|
|
352
461
|
| --- | --- |
|
|
353
462
|
| 0.1.2-rc.1 | compatible |
|
|
354
463
|
| 0.1.3-alpha.1 | compatible |
|
|
@@ -361,24 +470,92 @@ sage-mem 是纯 DSH 插件,用 `package.json` 的 **`engines.dsh`** 声明宿
|
|
|
361
470
|
| 0.2.0-rc.1 | compatible |
|
|
362
471
|
| 0.2.0-rc.2 | compatible |
|
|
363
472
|
|
|
364
|
-
|
|
473
|
+
Two points worth spelling out:
|
|
474
|
+
|
|
475
|
+
- **A single build covers both the `0.1.x` and the `0.2.x` line.** Since DSH `0.1.6-alpha.2` the TypertCodec contract changed from "read `schema`" to "read the `create()` factory", and a plugin that still writes only `schema:` throws directly during registration and drags down the whole plugin tree. Every codec in this plugin carries both `schema` and `create: () => schema`, so DSH from both eras can load it. The ranges of the peer dependencies `@deepseek-ai/dsh-typert-protocol` and `@deepseek-ai/dsh-client-locale` drop their upper bound, so that some future major version is not mistakenly blocked by this declaration (between `0.1.7-rc.2` and `0.2.0-rc.1` these two packages added and removed zero exported symbols; compared byte by byte).
|
|
476
|
+
- **One layer of defensive tracing is included.** The event names and fields the injection chain depends on have been very stable across everyday upgrades; if DSH ever changes them to asynchronous or renames them, the plugin will **raise an alarm in the log**, rather than silently stopping injecting memories.
|
|
477
|
+
|
|
478
|
+
## Architecture
|
|
479
|
+
|
|
480
|
+
```
|
|
481
|
+
DSH (Cordis plugin host)
|
|
482
|
+
└─ sage-mem plugin (retrieval by question + injection) ← this repository
|
|
483
|
+
│ direct Node fs read / write
|
|
484
|
+
▼
|
|
485
|
+
memory/ directory (markdown files, 4 types)
|
|
486
|
+
├── user_*.md
|
|
487
|
+
├── feedback_*.md
|
|
488
|
+
├── project_*.md
|
|
489
|
+
├── reference_*.md
|
|
490
|
+
└── MEMORY.md ← index
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
No worker, no SQLite, no HTTP port, no long-running process.
|
|
494
|
+
|
|
495
|
+
## Contributing
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
git clone https://github.com/gezi-wen/sage-mem
|
|
499
|
+
cd sage-mem
|
|
500
|
+
npm install
|
|
501
|
+
npm test # all assertions (including the three guards)
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
- The assertion scripts depend only on `node:` built-in modules and **introduce no test framework**; each script hand-writes `ok()` and ends with a summary line.
|
|
505
|
+
- **Three guards** watch over "whether the tests themselves hold up": `suite-registry-test.mjs` (is there a suite that was written but never runs), `contract-binding-test.mjs` (is there a commitment that was made but never verified), `doc-consistency-test.mjs` (do the two documents characterize things identically line by line).
|
|
506
|
+
- CI runs twice on `ubuntu-latest` (the second time with `TZ=UTC`) — a case-sensitive volume catches defects that are invisible on Windows.
|
|
507
|
+
- When you change **user-visible behaviour**, please also update the corresponding line in the [contract](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md) and the CHANGELOG; a commitment you cannot write an assertion for can only be marked "best-effort".
|
|
508
|
+
|
|
509
|
+
**Questions and suggestions**: open an [Issue](https://github.com/gezi-wen/sage-mem/issues). Small, well-defined fixes are welcome as direct PRs; for changes with a larger surface, open an Issue first to align on direction, so that the work is not wasted.
|
|
510
|
+
|
|
511
|
+
## FAQ
|
|
512
|
+
|
|
513
|
+
**What do I do if my memories are not being injected?**
|
|
514
|
+
First confirm the memory directory is right (`SAGE_MEM_DIR`, default `~/.sage-mem/memory`), then check whether the files have frontmatter (broken frontmatter is **handled leniently** — the file is still read, with fields falling back to defaults). The 「体检」(audit) page in the panel reports structural problems such as dangling index entries, missing index entries and broken links.
|
|
515
|
+
|
|
516
|
+
**Why can it not find something when I phrase it differently?**
|
|
517
|
+
Retrieval is **lexical overlap**, not semantic. Add a line `aliases: [another wording]` to that memory. This is a deliberate tradeoff, not a defect waiting to be fixed.
|
|
518
|
+
|
|
519
|
+
**Will it record every word I say?**
|
|
520
|
+
No. Whether to store is judged by the model according to rules: nothing without substance is stored, nothing derivable by reading the code / git is stored.
|
|
521
|
+
|
|
522
|
+
**I want to see what it actually stored.**
|
|
523
|
+
Open the memory directory; it is all Markdown. The 「记忆管理」(Memory management) page in the GUI can also browse, edit and delete.
|
|
524
|
+
|
|
525
|
+
**Is it safe for several DSH instances to share one memory directory?**
|
|
526
|
+
No. Serialized writes guarantee only **in-process** ordering; there is no file lock across processes — using two instances to write to the same directory at the same time can still lose one write. This is a known boundary.
|
|
527
|
+
|
|
528
|
+
**Can I do without autodream?**
|
|
529
|
+
Yes, it is off by default. It and archiving are two different things: archiving is a deterministic rule (by idle days), calls no model and costs nothing.
|
|
530
|
+
|
|
531
|
+
## Uninstall
|
|
532
|
+
|
|
533
|
+
```bash
|
|
534
|
+
dsh plugin --profile web remove sage-mem
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Or manually delete the `sage-mem` dependency from the profile's `package.json` and remove `"sage-mem"` from `dsh.profile.bundles`; then `pnpm install` and restart DSH.
|
|
538
|
+
|
|
539
|
+
**The memory Markdown files stay in their original directory, unaffected.**
|
|
540
|
+
|
|
541
|
+
## Maintainer
|
|
365
542
|
|
|
366
|
-
-
|
|
367
|
-
- **0.5.3 起带一层防御留痕。** `deriveMessages()`(仍是同步)、`system-prompt/assemble` 事件、`context.agent.session` 这条链在升级中都没变;但如果哪天 DSH 把它们改成异步或改了名,插件会在**日志里报警**,而不是像以前那样静默地不再注入记忆
|
|
543
|
+
[@gezi-wen](https://github.com/gezi-wen)
|
|
368
544
|
|
|
369
|
-
##
|
|
545
|
+
## Credits
|
|
370
546
|
|
|
371
|
-
|
|
547
|
+
- [@bjvgukv25842-cmyk](https://github.com/bjvgukv25842-cmyk) — the earliest Web memory management UI (the starting point of the memory management page).
|
|
548
|
+
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — the plugin host and the Cordis plugin system.
|
|
372
549
|
|
|
373
|
-
##
|
|
550
|
+
## Related Projects
|
|
374
551
|
|
|
375
|
-
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — DSH
|
|
376
|
-
- [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) — DSH
|
|
552
|
+
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — DSH itself, everything is a plugin
|
|
553
|
+
- [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) — a catalog of the DSH plugin ecosystem
|
|
377
554
|
|
|
378
|
-
##
|
|
555
|
+
## Legacy
|
|
379
556
|
|
|
380
|
-
|
|
557
|
+
This repository was originally a SQLite + worker architecture, now archived on the [`sqlite-worker`](https://github.com/gezi-wen/sage-mem/tree/sqlite-worker) branch. The file-based version is the successor implementation.
|
|
381
558
|
|
|
382
559
|
## License
|
|
383
560
|
|
|
384
|
-
Apache-2.0
|
|
561
|
+
Apache-2.0 · see [LICENSE](https://github.com/gezi-wen/sage-mem/blob/main/LICENSE)
|