@yandy0725/pi-memory 1.4.0 → 2.0.0
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 +248 -140
- package/README.zh.md +266 -156
- package/index.ts +393 -95
- package/package.json +1 -1
- package/src/agent-runner.ts +17 -2
- package/src/config.ts +31 -6
- package/src/dream.ts +98 -56
- package/src/entry-file.ts +81 -0
- package/src/entry-index.ts +112 -0
- package/src/extract.ts +338 -57
- package/src/filename.ts +48 -0
- package/src/fs-lock.ts +251 -0
- package/src/index-source.ts +140 -0
- package/src/inject.ts +121 -73
- package/src/memory-store.ts +498 -0
- package/src/memory-tool.ts +244 -326
- package/src/migrate.ts +303 -0
- package/src/paths.ts +1 -14
- package/src/process-lock.ts +122 -0
- package/src/sanitize.ts +36 -0
- package/src/snapshot.ts +68 -0
- package/src/index-file.ts +0 -91
- package/src/topic-file.ts +0 -119
package/README.zh.md
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
# pi-memory
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
pi coding agent 的文件系统持久记忆层。把项目知识(事实、偏好、调试历史)以纯 Markdown 文件的形式存在 `~/.pi/memory/<git|local>/<project>/` 下,跨会话可用。
|
|
4
4
|
|
|
5
|
-
对齐 Claude Code
|
|
5
|
+
对齐 Claude Code 的自动记忆机制:**一条记忆 = 一个文件**、`MEMORY.md` 索引一行一条记忆、按相关性自动浮现、每轮自动提取、记忆分类。
|
|
6
|
+
|
|
7
|
+
> ## ⚠️ 2.0.0 是破坏性的存储变更
|
|
8
|
+
>
|
|
9
|
+
> - 索引现在**一行一条记忆**(1.x 是一行一个 topic 文件,文件里塞很多 `## entry`)。
|
|
10
|
+
> - 每条记忆**独占一个文件**,frontmatter 五个字段:`name`、`description`、`type`、`created`、`modified`(1.x 用的是 `updated`)。
|
|
11
|
+
> - 索引改为以 **system prompt section**(`memory_index`)注入,并在整个会话内冻结,而不是拼到 system prompt 字符串末尾。
|
|
12
|
+
>
|
|
13
|
+
> 升级后第一次 `session_start` 会**自动迁移**已有的 1.x 目录:先把整个目录快照到 `.backups/migrate-<ts>/`,把原 topic 文件复制到 `originals/`,再把每个 `## entry` 拆成独立文件、重建索引,**最后一步**才写 `.migrated` 标记。任一步失败就不写标记、保留备份,下次会话重试。详见[从 1.x 迁移](#从-1x-迁移)。
|
|
6
14
|
|
|
7
15
|
## 功能
|
|
8
16
|
|
|
9
|
-
- **一个 `memory`
|
|
10
|
-
-
|
|
11
|
-
- **`MEMORY.md`
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- **`/dream
|
|
17
|
-
-
|
|
18
|
-
- **`/memory`
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
17
|
+
- **一个 `memory` 工具,主 agent 五个 action**:`add`、`replace`、`remove`、`list`、`search`。另外两个 —— `rename` 与 `rebuild_index` —— **只存在于 `/dream` 自己的 headless 会话里**,绝不出现在主 agent 或 extract 的 schema 中。
|
|
18
|
+
- **一条记忆 = 一个文件** —— 不再有多条目 `## section` 块;`name` 是定位键,`add` 一个已存在的 `name` 就是幂等覆盖。
|
|
19
|
+
- **`MEMORY.md` 索引** —— 一行一条记忆:`- [Name](file.md) — description`。分隔符是 **em dash**(`—`,U+2014),两侧各一个半角空格。
|
|
20
|
+
- **`memory_index` section,会话内冻结** ⭐ —— 索引写进 `event.systemPromptOptions.sections["memory_index"]`,其值在**整个会话内不再变化**,只有 compaction 会从磁盘重读。pi 对 sections 做 diff,值没变就一条消息都不追加,于是 system prompt 逐轮逐字节相同,provider 的 prefix cache 一直命中。`resume` / `fork` / `reload` 用 transcript 里的**录制值**重放,而不是读磁盘,因此恢复会话不会改写它的头部。
|
|
21
|
+
- **注入净化** —— 所有注入内容(索引行、浮现的 entry 正文与 name)都会剥离不可见/bidi 字符并转义 `<` `>`,因此记忆无法伪造 `</relevant_memories>`、`<system>`、`<project_instructions>`、`<active_agent …>` 或 `<memory_index>`。净化**只发生在注入时**:磁盘上的文件永远不会被改写(保持可读、可手工编辑)。
|
|
22
|
+
- **自动浮现(auto-surfacing)** ⭐ —— 每个用户回合由一次轻量侧查询挑出至多 `maxFiles` 条 **entry**(只看 `description`),把正文注入 `<relevant_memories>`。同一会话内按文件名去重;清单来自进程内的 `mtime` 缓存,每回合只付一次 `readdir` + 每文件一次 `stat`。子 agent 中不启用。
|
|
23
|
+
- **自动提取(extract memories)** ⭐ —— 每轮结束后一个异步 headless agent 拿到的是**整轮对话的结构化渲染**(user 消息全文、assistant 文本与 tool_call、tool_result 及其错误标记),而不是两条消息。它经同一套 `memory` 原语写入,并且**从不排队等锁**:dream 或迁移正在整轮持锁时,本回合直接跳过。
|
|
24
|
+
- **`/dream`** —— headless 整理 agent(Orient → Gather Signal → Consolidate → Prune & Index),合并重复、消解矛盾、改名、重建索引。它**没有裸文件权限**:只有七个 `memory` action,整轮持有逻辑锁,进入时先对整个目录拍一次快照。
|
|
25
|
+
- **Dream 提醒** —— 距上次 dream 超过 N 个会话或 N 小时后提示 `/dream`。
|
|
26
|
+
- **`/memory`** —— 完整状态(开关、目录、索引容量、entry 数、上次 dream、迁移状态、锁状态含持有者),以及 `on` / `off` / `unlock`。
|
|
27
|
+
- **两级锁** —— 进程内逻辑锁承担**逻辑作用域**(单次原语,或 dream / 迁移的整轮);跨进程 `.lock` **只持毫秒**且**永不自动回收**。没有 TTL、没有心跳、没有接管,所以互斥是硬保证;代价是崩溃遗留的锁必须**人工**清除(`/memory unlock`)。
|
|
28
|
+
- **快照** —— 每次写入都在 `.backups/<ts>-<label>/` 留下回滚点,保留最近 `lock.snapshotKeep` 份(迁移备份以 `migrate-` 开头,永不裁剪)。`/dream` 是例外:它**进入时只对整个目录拍一次**快照,该轮内部的原语会跳过逐文件快照(一轮只留一个回滚点)。
|
|
29
|
+
- **会话检索** —— `memory search scope=sessions` 查历史会话。
|
|
30
|
+
- **可读、clone 友好的布局** —— git 仓库(http(s)/ssh/git remote,含 scp 写法与 `git+ssh`/`git+https`)存在 `~/.pi/memory/git/<host__owner__repo>/`,其余存在 `~/.pi/memory/local/<absolute-path>/`;同一仓库的 clone 与 worktree 共享记忆(fork 有自己的 remote,因此有独立目录)。
|
|
22
31
|
|
|
23
32
|
## 安装
|
|
24
33
|
|
|
@@ -26,7 +35,7 @@
|
|
|
26
35
|
pi install npm:@yandy0725/pi-memory
|
|
27
36
|
```
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
或加入 `~/.pi/agent/settings.json`:
|
|
30
39
|
|
|
31
40
|
```json
|
|
32
41
|
{
|
|
@@ -34,9 +43,71 @@ pi install npm:@yandy0725/pi-memory
|
|
|
34
43
|
}
|
|
35
44
|
```
|
|
36
45
|
|
|
46
|
+
## 存储布局
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
~/.pi/memory/git/github.com__owner__repo/
|
|
50
|
+
MEMORY.md — 索引:一行一条记忆(em dash 分隔)
|
|
51
|
+
SSH-port-on-staging.md
|
|
52
|
+
Test-command.md — 一条记忆一个文件
|
|
53
|
+
.lock — 跨进程写锁(只持毫秒,永不自动回收)
|
|
54
|
+
.backups/ — 回滚点:<ISO-ts>-<label>/,升级迁移是 migrate-<ts>/
|
|
55
|
+
.migrated — 1.x → 2.x 迁移的完成标记(最后一步才写)
|
|
56
|
+
.dream-meta.json — 上次 dream 的时间与会话数(提醒逻辑用它)
|
|
57
|
+
sessions/ — headless 会话落盘目录,仅在开启 sessionPersistence 时使用
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Entry 文件
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
---
|
|
64
|
+
name: SSH port on staging
|
|
65
|
+
description: staging SSH listens on 2222, not 22; key at ~/.ssh/staging
|
|
66
|
+
type: project
|
|
67
|
+
created: 2026-07-13
|
|
68
|
+
modified: 2026-10-02T08:14:03.120Z
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
staging 的 SSH 用 2222 端口,密钥在 ~/.ssh/staging。
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `name` —— 唯一、可读的标题;`replace` / `remove` / `rename` 的定位键。再次 `add` 同名记忆是**覆盖**,不会产生第二条。
|
|
75
|
+
- `description` —— **一行自包含的说明**。侧查询判断相关性时**只看得到它**,所以它必须脱离正文也能读懂。反例:`Debugging tips`;正例:`staging SSH listens on 2222, not 22`。
|
|
76
|
+
- `type` —— `user` | `feedback`(默认)| `project` | `reference`。
|
|
77
|
+
- `created` —— `YYYY-MM-DD`,只在新建时写入,覆盖时保留。
|
|
78
|
+
- `modified` —— ISO 8601,**一律由 store 写入**,调用方传不进来。
|
|
79
|
+
|
|
80
|
+
文件名由 `name` 派生(不安全字符替换、100 字节上限,只有**文件名**冲突时才追加 `-2` / `-3`)。任何 `name` 都写不到 `MEMORY.md` 上。
|
|
81
|
+
|
|
82
|
+
### MEMORY.md 索引
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
# Memory Index
|
|
86
|
+
|
|
87
|
+
- [SSH port on staging](SSH-port-on-staging.md) — staging SSH listens on 2222, not 22
|
|
88
|
+
- [Test command](Test-command.md) — run npm test, not npm run test
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
写入是**外科式**的:只改目标行,手写的标题、分组、注释逐字保留,行序稳定。唯一的例外是行尾:CRLF(以及单独的 CR)在解析前被归一为 LF,因此第一次写入 CRLF 文件会把它整体转成 LF。
|
|
92
|
+
|
|
93
|
+
### 记忆类型
|
|
94
|
+
|
|
95
|
+
| 类型 | 含义 | 示例 |
|
|
96
|
+
|------|------|------|
|
|
97
|
+
| `user` | 用户角色、偏好、知识背景 | "用户是聚焦可观测性的数据科学家" |
|
|
98
|
+
| `feedback` | 经验、纠正、确认(默认) | "集成测试用真库不要 mock —— 上季度栽过" |
|
|
99
|
+
| `project` | 项目状态、时间点、事故 | "移动端 2026-03-05 起封版" |
|
|
100
|
+
| `reference` | 外部系统的指针 | "缺陷跟踪 = Linear 的 INGEST 项目" |
|
|
101
|
+
|
|
102
|
+
### 容量:200 行索引 ≈ 199 条记忆
|
|
103
|
+
|
|
104
|
+
索引上限是 `memIndexMaxLines`(200)个非空行与 `memIndexMaxBytes`(25600)字节。这 200 行是**索引行,不是记忆条数**:`rebuildIndex` 总会写一行 `# Memory Index` 头,手写的标题、分组、注释同样占额度。因此重建后的索引最多约 **199 条记忆**(每个项目目录;若保留手写标题则更少)。超限时写入**不会失败**:写入照样成功,工具把一条可操作的警告回给模型,让它去合并或删除条目(超出上限的部分下次加载时不可见)。
|
|
105
|
+
|
|
106
|
+
这也是 `/dream` 不再是「可选的整理」而是**容量管理必需**的原因。在接近 199 条之前跑一次(或者接受提醒)。
|
|
107
|
+
|
|
37
108
|
## 配置
|
|
38
109
|
|
|
39
|
-
|
|
110
|
+
在 agent 目录(`~/.pi/agent/memory.json`)或项目 `.pi/` 目录(仅在项目被信任时)创建 `memory.json`:
|
|
40
111
|
|
|
41
112
|
```json
|
|
42
113
|
{
|
|
@@ -44,187 +115,219 @@ pi install npm:@yandy0725/pi-memory
|
|
|
44
115
|
"memoryDir": "~/.pi/memory",
|
|
45
116
|
"memIndexMaxLines": 200,
|
|
46
117
|
"memIndexMaxBytes": 25600,
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
51
|
-
"dream": {
|
|
52
|
-
|
|
53
|
-
"nudgeAfterHours": 24,
|
|
54
|
-
"model": "auto",
|
|
55
|
-
"thinkLevel": "high",
|
|
56
|
-
"sessionPersistence": { "enabled": false }
|
|
57
|
-
},
|
|
58
|
-
"sessionSearch": {
|
|
59
|
-
"maxSessions": 10,
|
|
60
|
-
"maxMatches": 5
|
|
61
|
-
},
|
|
118
|
+
"memIndexInjectMaxLines": 200,
|
|
119
|
+
"memIndexInjectMaxBytes": 25600,
|
|
120
|
+
"lock": { "timeoutMs": 5000, "snapshotKeep": 5 },
|
|
121
|
+
"defaults": { "sessionPersistence": { "enabled": false } },
|
|
122
|
+
"dream": { "nudgeAfterSessions": 5, "nudgeAfterHours": 24, "thinkLevel": "high" },
|
|
123
|
+
"sessionSearch": { "maxSessions": 10, "maxMatches": 5 },
|
|
62
124
|
"autoSurfacing": {
|
|
63
125
|
"enabled": true,
|
|
64
|
-
"model": "auto",
|
|
65
126
|
"thinkLevel": "off",
|
|
66
|
-
"maxFiles":
|
|
67
|
-
"
|
|
68
|
-
"maxInjectionBytes":
|
|
69
|
-
"sessionPersistence": { "enabled": false }
|
|
127
|
+
"maxFiles": 3,
|
|
128
|
+
"maxEntryBytes": 3072,
|
|
129
|
+
"maxInjectionBytes": 10240
|
|
70
130
|
},
|
|
71
131
|
"extractMemories": {
|
|
72
132
|
"enabled": true,
|
|
73
|
-
"model": "auto",
|
|
74
133
|
"thinkLevel": "high",
|
|
75
134
|
"maxContextTokens": 2000,
|
|
76
|
-
"
|
|
135
|
+
"maxToolResultChars": 500,
|
|
136
|
+
"maxAssistantChars": 2000
|
|
77
137
|
}
|
|
78
138
|
}
|
|
79
139
|
```
|
|
80
140
|
|
|
81
|
-
|
|
|
82
|
-
|
|
83
|
-
| `enabled` | `true` |
|
|
141
|
+
| 键 | 默认值 | 说明 |
|
|
142
|
+
|-----|---------|------|
|
|
143
|
+
| `enabled` | `true` | 整个记忆系统的总开关 |
|
|
84
144
|
| `memoryDir` | `~/.pi/memory` | 所有记忆数据的根目录 |
|
|
85
|
-
| `memIndexMaxLines` | `200` |
|
|
86
|
-
| `memIndexMaxBytes` | `25600` |
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `dream.
|
|
95
|
-
| `dream.
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `autoSurfacing.
|
|
102
|
-
| `autoSurfacing.
|
|
103
|
-
| `autoSurfacing.
|
|
104
|
-
| `autoSurfacing.
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `extractMemories.
|
|
109
|
-
| `extractMemories.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
145
|
+
| `memIndexMaxLines` | `200` | 写入口径:`MEMORY.md` 的最大非空行数(`# Memory Index` 头行与手写标题同样占额度,所以并不等于记忆条数) |
|
|
146
|
+
| `memIndexMaxBytes` | `25600` | 写入口径:`MEMORY.md` 的最大字节数 |
|
|
147
|
+
| `memIndexInjectMaxLines` | `200` | 注入口径:放进 `memory_index` section 的最大行数。**刻意与写入口径同量级** —— 预算更小会让「已经写成功」的记忆看不见 |
|
|
148
|
+
| `memIndexInjectMaxBytes` | `25600` | 注入口径:section 的最大字节数(超出则截断并带 `[truncated: …]` 标记) |
|
|
149
|
+
| `lock.timeoutMs` | `5000` | 单次原语等逻辑锁 / 等跨进程 `.lock` 的上限。迁移固定用 30s(它要在锁内重写整个目录)。同时也是 `session_shutdown` 等在途写入的上限 |
|
|
150
|
+
| `lock.snapshotKeep` | `5` | `.backups/` 保留的回滚点数量(`migrate-` 前缀的目录永不裁剪) |
|
|
151
|
+
| `defaults.model` | — | 所有子任务共享的模型回退;各任务自己的 `model` 优先,都没配则回退父会话模型 |
|
|
152
|
+
| `defaults.sessionPersistence.enabled` | `false` | 共享回退:headless 子会话(extract / dream / 侧查询)默认只在内存里跑 |
|
|
153
|
+
| `defaults.sessionPersistence.sessionDir` | `<项目记忆目录>/sessions/` | headless 会话的自定义落盘目录 |
|
|
154
|
+
| `dream.nudgeAfterSessions` | `5` | 距上次 dream 多少个会话后开始提醒 |
|
|
155
|
+
| `dream.nudgeAfterHours` | `24` | 距上次 dream 多少小时后开始提醒 |
|
|
156
|
+
| `dream.model` | — | dream 用的模型(`"provider/id"`)。回退 `defaults.model` → 父会话模型 |
|
|
157
|
+
| `dream.thinkLevel` | `"high"` | dream 的思考强度:`off` / `minimal` / `low` / `medium` / `high` / `xhigh` |
|
|
158
|
+
| `dream.sessionPersistence.*` | 继承 `defaults` | 把 dream 会话落盘(调试/审计用) |
|
|
159
|
+
| `sessionSearch.maxSessions` | `10` | `search scope=sessions` 扫描的最大会话数 |
|
|
160
|
+
| `sessionSearch.maxMatches` | `5` | 历史检索返回的最大命中数 |
|
|
161
|
+
| `autoSurfacing.enabled` | `true` | ⭐ 开启每回合的 entry 自动注入 |
|
|
162
|
+
| `autoSurfacing.model` | — | ⭐ 相关性侧查询用的模型。回退 `defaults.model` → 父会话模型 |
|
|
163
|
+
| `autoSurfacing.thinkLevel` | `"off"` | ⭐ 侧查询的思考强度(`"off"` 最省) |
|
|
164
|
+
| `autoSurfacing.maxFiles` | `3` | ⭐ 每回合最多注入几条 entry |
|
|
165
|
+
| `autoSurfacing.maxEntryBytes` | `3072` | ⭐ 单条 entry 正文的注入字节上限(超出截断)。取代 1.x 的 `maxTopicBytes`(旧键已失效) |
|
|
166
|
+
| `autoSurfacing.maxInjectionBytes` | `10240` | ⭐ 每回合注入内容的总字节上限 |
|
|
167
|
+
| `autoSurfacing.sessionPersistence.*` | 继承 `defaults` | 把侧查询会话落盘 |
|
|
168
|
+
| `extractMemories.enabled` | `true` | ⭐ 开启每轮自动提取 |
|
|
169
|
+
| `extractMemories.model` | — | ⭐ 提取 agent 用的模型。回退 `defaults.model` → 父会话模型 |
|
|
170
|
+
| `extractMemories.thinkLevel` | `"high"` | ⭐ 提取的思考强度 |
|
|
171
|
+
| `extractMemories.maxContextTokens` | `2000` | ⭐ 渲染后对话的预算(`× 4` 个字符;超出时先裁中段、首尾优先保留,user 消息最后才动) |
|
|
172
|
+
| `extractMemories.maxToolResultChars` | `500` | ⭐ 单条 `tool_result` 渲染的字符上限 |
|
|
173
|
+
| `extractMemories.maxAssistantChars` | `2000` | ⭐ 单条 assistant 文本渲染的字符上限(user 消息从不截断) |
|
|
174
|
+
| `extractMemories.sessionPersistence.*` | 继承 `defaults` | 把 extract 会话落盘 |
|
|
175
|
+
|
|
176
|
+
headless 会话默认落在 `<项目记忆目录>/sessions/` —— 在项目记忆目录里,不在你的工作副本里。
|
|
114
177
|
|
|
115
178
|
## 工作原理
|
|
116
179
|
|
|
117
|
-
###
|
|
118
|
-
|
|
119
|
-
MEMORY.md 是一个**紧凑指针索引** — 每个 topic 文件一行,而非每个条目一行:
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
- [Debugging](debugging.md) — SSH 使用 2222 端口;MySQL staging 上 30s 超时
|
|
123
|
-
- [API Conventions](api.md) — REST handlers 在 src/api/handlers/;使用标准错误格式
|
|
124
|
-
```
|
|
180
|
+
### 会话生命周期
|
|
125
181
|
|
|
126
|
-
|
|
182
|
+
| 事件 | pi-memory 做什么 |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `session_start` | 加载配置 → 解析记忆目录 → 需要则执行 1.x 迁移 → **确定索引值并冻结**(`startup`/`new` 读磁盘;`resume`/`fork`/`reload` 重放 transcript 取录制值)→ 注册 `memory` 工具(仅首次,5 个 action)→ 重建清单缓存 → dream 提醒检查 |
|
|
185
|
+
| `before_agent_start` | 把冻结值写进 `sections["memory_index"]`(**每一轮、无条件**),然后做 auto-surfacing(主会话且非子 agent) |
|
|
186
|
+
| `agent_end` | 触发异步 extract;写入成功通知 `Extracted N memories.`,失败通知 `Extract failed: …`(每会话一次) |
|
|
187
|
+
| `session_compact` | 清空已注入集合,**并从磁盘重读索引** —— 会话内唯一的刷新点 |
|
|
188
|
+
| `session_shutdown` | 等在途写入收尾(上限 `lock.timeoutMs`),避免退出时留下 stale 的 `.lock` |
|
|
127
189
|
|
|
128
|
-
###
|
|
190
|
+
### 为什么索引要冻结
|
|
129
191
|
|
|
130
|
-
|
|
192
|
+
pi 用有序的 sections 构建 system prompt,只对**值发生变化**的 section 追加一条 patch 消息;全部没变时一条都不追加。自定义 section 插在**最后**,所以 `memory_index` 是 system prompt 的最后一段,其后紧跟整段对话。一旦它的值在会话中途变化,折叠后的头部就会被改写,其后的全部内容失去 prefix cache。因此:一个会话一个值,只有 compaction 才刷新(那时对话中段本来就要被重写)。
|
|
131
193
|
|
|
132
|
-
|
|
133
|
-
---
|
|
134
|
-
name: 调试技巧
|
|
135
|
-
description: 常见调试模式、SSH 端口、MySQL 超时配置
|
|
136
|
-
type: feedback
|
|
137
|
-
updated: 2026-07-13
|
|
138
|
-
---
|
|
194
|
+
有意接受的代价:**本会话内写入的记忆不会出现在本会话的索引里**。工具返回值已经确认写入,下一个会话立刻可见。
|
|
139
195
|
|
|
140
|
-
|
|
141
|
-
staging 使用 2222 端口,密钥在 ~/.ssh/staging
|
|
196
|
+
如果宿主的 pi 还没有 sections API,pi-memory 回退为把索引拼到 system prompt 字符串末尾 —— 功能不变,只是缓存变差。
|
|
142
197
|
|
|
143
|
-
|
|
144
|
-
staging 上连接超时 30s
|
|
145
|
-
```
|
|
198
|
+
### 自动浮现
|
|
146
199
|
|
|
147
|
-
`
|
|
200
|
+
1. 从 store 的 `mtime` 缓存构建清单:`[type] file.md — description`,按 `modified` 降序,最多 200 条、4000 字符(description 预算按条数均分,每条不低于 80 字符)。
|
|
201
|
+
2. 侧查询(`maxTurns: 1`、零工具)从清单里返回至多 `maxFiles` 个文件名;不在清单里的一律丢弃。
|
|
202
|
+
3. 选中 entry 的**正文**(不含 frontmatter)按 `maxEntryBytes` 截断、净化,作为一条 `display: false` 的 custom 消息注入,外面包 `<relevant_memories>`;总量到 `maxInjectionBytes` 就停。
|
|
203
|
+
4. 注入过的文件名在本会话内记住,compaction 时清空。你会看到 `Recalled: N entries` 通知。
|
|
148
204
|
|
|
149
|
-
###
|
|
205
|
+
### 自动提取
|
|
150
206
|
|
|
151
|
-
|
|
152
|
-
|------|------|------|
|
|
153
|
-
| `user` | 用户角色、偏好、知识背景 | "用户是数据科学家,关注可观测性" |
|
|
154
|
-
| `feedback` | 教训/纠正/确认(默认) | "用真实 DB 不用 mock—上次踩过坑" |
|
|
155
|
-
| `project` | 项目状态、deadline、incident | "merge freeze 从 3 月 5 日开始" |
|
|
156
|
-
| `reference` | 外部系统指针 | "bug tracker = Linear INGEST project" |
|
|
207
|
+
extract 拿到的是结构化渲染,而不是有损的两条消息摘要:
|
|
157
208
|
|
|
158
|
-
|
|
209
|
+
```
|
|
210
|
+
=== Conversation ===
|
|
211
|
+
[1] user: <全文>
|
|
212
|
+
[2] assistant: <文本> | tool_call: memory({"action":"list"})
|
|
213
|
+
[3] tool_result: <摘要,按 maxToolResultChars 截断>
|
|
214
|
+
[4] user: <纠正>
|
|
215
|
+
```
|
|
159
216
|
|
|
160
|
-
|
|
161
|
-
1. 扫描所有 topic 文件,提取 frontmatter 元数据
|
|
162
|
-
2. Side-query LLM 根据用户问题选出最多 `maxFiles` 个相关 topic 文件
|
|
163
|
-
3. 已注入过的 topic 跳过(session 内去重)
|
|
164
|
-
4. 选中文件内容注入 context — agent 自动看到相关记忆
|
|
217
|
+
它只有主 agent 的五个 action(永远拿不到 `rename` / `rebuild_index`)、没有文件工具、`maxTurns: 5`、超时 120s。逻辑锁被占用时(dream 或迁移正在整轮持有)它**跳过本回合**而不是排队 —— 下一次 `agent_end` 还会来。
|
|
165
218
|
|
|
166
|
-
###
|
|
219
|
+
### 锁
|
|
167
220
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
4. 子 agent 独立运行,结果为未来会话所用
|
|
221
|
+
| 层级 | 作用域 | 行为 |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| 进程内逻辑锁(按记忆目录分键) | 单次原语;或 dream / 迁移的整轮 | 最多等 `lock.timeoutMs`(迁移 30s),超时抛一条写明目录的可读错误。`extract` 用不等待的形态,直接跳过本回合 |
|
|
224
|
+
| 跨进程 `.lock` | 毫秒级,只包住物理写入 | 用 `link` 原子获取,**永不自动回收**:没有 TTL、没有心跳、没有接管 |
|
|
173
225
|
|
|
174
|
-
|
|
226
|
+
因此写入中途崩溃可能留下一个 `.lock`,而且**没有任何进程会替你删掉它** —— 这是「互斥是硬保证」的刻意代价。错误文案会写明 pid、op、开始时间与路径;`/memory unlock` 是唯一被认可的清除方式。
|
|
175
227
|
|
|
176
228
|
## 工具参考
|
|
177
229
|
|
|
178
230
|
```
|
|
179
|
-
memory(action: "add" | "remove" | "search",
|
|
180
|
-
|
|
181
|
-
entry?, query?, scope?)
|
|
231
|
+
memory(action: "add" | "replace" | "remove" | "list" | "search",
|
|
232
|
+
name?, description?, content?, type?, query?, scope?)
|
|
182
233
|
```
|
|
183
234
|
|
|
235
|
+
`description` 是未来会话唯一能拿到的相关性信号 —— `add` 与 `replace` 请务必带上自包含的一行。
|
|
236
|
+
|
|
184
237
|
### `add`
|
|
185
238
|
|
|
186
|
-
|
|
239
|
+
新建一条记忆,或**覆盖 `name` 完全相同的那一条**(幂等;`created` 保留)。
|
|
240
|
+
|
|
241
|
+
- `name`(必填)—— 唯一、可读的标题
|
|
242
|
+
- `content`(必填)—— 记忆正文,它会成为整个 entry 文件
|
|
243
|
+
- `description`(可选)—— 一行自包含说明;缺省取 `content` 的第一句
|
|
244
|
+
- `type`(可选)—— `user` / `feedback`(默认)/ `project` / `reference`
|
|
187
245
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
- **`type`**(可选)— 记忆分类:`"user"`、`"feedback"`(默认)、`"project"`、`"reference"`
|
|
246
|
+
### `replace`
|
|
247
|
+
|
|
248
|
+
重写已有记忆的 `content` / `description` / `type`,按 `name` 定位。改名是 `rename`,dream 专属。
|
|
192
249
|
|
|
193
250
|
### `remove`
|
|
194
251
|
|
|
195
|
-
|
|
252
|
+
删除 `name` 匹配的记忆 —— 文件、索引行一起删。找不到条目或删不掉文件时**明确报错**,不会假装删成功。
|
|
253
|
+
|
|
254
|
+
### `list`
|
|
196
255
|
|
|
197
|
-
|
|
256
|
+
每条记忆一行:`- name (type, modified …) — description [file]`。
|
|
198
257
|
|
|
199
258
|
### `search`
|
|
200
259
|
|
|
201
|
-
|
|
260
|
+
- `query`(必填)
|
|
261
|
+
- `scope`(可选)—— `memory`(默认:在所有 entry 的 name / description / 正文里找)或 `sessions`(查历史会话)
|
|
202
262
|
|
|
203
|
-
|
|
204
|
-
|
|
263
|
+
### dream 专属 action
|
|
264
|
+
|
|
265
|
+
`rename`(`name` + `new_name`;文件名与索引行跟着走,并保持索引行的位置)与 `rebuild_index`(从磁盘全量重建,保留手写头部)。它们**只**注册在 `/dream` 的 headless 会话里,主 agent 与 extract 都调不到。
|
|
205
266
|
|
|
206
267
|
## 命令
|
|
207
268
|
|
|
208
269
|
### `/memory`
|
|
209
270
|
|
|
210
|
-
|
|
271
|
+
```
|
|
272
|
+
/memory — 查看状态
|
|
273
|
+
/memory on — 开启
|
|
274
|
+
/memory off — 关闭
|
|
275
|
+
/memory unlock — 清除遗留的 .lock(会先要求确认)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
状态输出:
|
|
211
279
|
|
|
212
280
|
```
|
|
213
|
-
|
|
214
|
-
/memory
|
|
215
|
-
/
|
|
281
|
+
Memory: enabled
|
|
282
|
+
Dir: /home/you/.pi/memory/git/github.com__owner__repo
|
|
283
|
+
Index: 38/200 lines, 2841/25600 bytes, 1 unrecognized lines
|
|
284
|
+
Entries: 37
|
|
285
|
+
Last dream: 2026-10-01T22:10:04.882Z
|
|
286
|
+
Migration: migrated at 2026-09-30T09:12:44.120Z (18 entries from 4 files)
|
|
287
|
+
Lock: free
|
|
216
288
|
```
|
|
217
289
|
|
|
290
|
+
- `Index` 用**写入**口径(`memIndexMax*`),并报告索引里有多少非空行解析不出(`# Memory Index` 头行与手写标题会计入)。CRLF(以及单独的 CR)行尾在解析前就被归一为 LF,下一次写入也一律输出 LF,因此被 Windows 编辑器改过行尾的 `MEMORY.md` **不会**推高这个计数。
|
|
291
|
+
- `Migration` 有三种:`migrated at …`、`not needed`(标记显示当时没东西要迁)、`pending`(没有标记或标记读不懂 → 下次 `session_start` 重试)。
|
|
292
|
+
- `Lock` 有三种:`free`、`held by <op> (pid N, started <ISO>)`、`unreadable — run /memory unlock`。
|
|
293
|
+
- 以 `enabled: false` 启动的会话里,store 从未初始化,因此 `/memory on` 会回答 `Memory not initialized.`(并不会真的打开开关)—— 本 session 的 `/memory unlock` 也不可达。两者都需要重开会话(或重启 pi)后才可用。
|
|
294
|
+
|
|
218
295
|
### `/dream`
|
|
219
296
|
|
|
220
|
-
|
|
297
|
+
先要求确认,然后对整个目录拍快照,再跑一个 headless agent 走完四个阶段:
|
|
298
|
+
|
|
299
|
+
1. **Orient** —— `list`,读相关 entry
|
|
300
|
+
2. **Gather Signal** —— 找重复(同一事实的多条 entry)、矛盾、过时内容
|
|
301
|
+
3. **Consolidate** —— 用 `replace` + `remove` 合并,用 `rename` 改名
|
|
302
|
+
4. **Prune & Index** —— 用 `rebuild_index` 兜底重建
|
|
221
303
|
|
|
222
|
-
|
|
223
|
-
2. **Gather Signal** — 发现重复、矛盾、过时条目
|
|
224
|
-
3. **Consolidate** — 合并重复、解决矛盾、更新日期
|
|
225
|
-
4. **Prune & Index** — 重建 frontmatter、生成 hook、重建 MEMORY.md
|
|
304
|
+
它碰不到文件:只有七个 `memory` action。完成时通知摘要,失败时通知 `Dream failed: …`。模型可用 `dream.model` 配置。
|
|
226
305
|
|
|
227
|
-
|
|
306
|
+
## 从 1.x 迁移
|
|
307
|
+
|
|
308
|
+
升级后第一次 `session_start` 自动执行:
|
|
309
|
+
|
|
310
|
+
1. 整轮取逻辑锁(30s);
|
|
311
|
+
2. 把整个目录快照到 `.backups/migrate-<ts>/`,并把原 topic 文件复制到 `.backups/migrate-<ts>/originals/`;
|
|
312
|
+
3. 对每个 legacy 文件(含 ≥ 2 个 `## ` 段,或 frontmatter 里有旧字段 `updated`):把每个 `## entry` 拆成独立文件,沿用 `type`,把 `updated` 归一成 `created` / `modified`;跨文件重名的第二条起追加 ` (2)`、` (3)`;
|
|
313
|
+
4. `rebuildIndex()`;
|
|
314
|
+
5. 删除原 topic 文件(它们仍在 `originals/` 里);
|
|
315
|
+
6. 写 `.migrated`;
|
|
316
|
+
7. 通知 `Migrated N memories from M topic files. Backup at <path>`。
|
|
317
|
+
|
|
318
|
+
frontmatter 里已含 `modified` 字段的文件**永远不**会被当成 legacy topic 文件(即使正文里有多个 `## ` 小标题)—— 这道守卫正是为了避免正常的 v2 记忆被再次拆碎。
|
|
319
|
+
|
|
320
|
+
重跑是安全的:标记只在最后写,而**同名且同正文**的条目会被复用而不是复制一份。任一步失败就不写标记、保留备份、上报错误 —— 下次会话重试。
|
|
321
|
+
|
|
322
|
+
**手工回滚:**
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
cd ~/.pi/memory/git/github.com__owner__repo # /memory 打印的那个目录
|
|
326
|
+
ls .backups/migrate-*/originals/ # 找到要撤销的那一次
|
|
327
|
+
cp .backups/migrate-<ts>/originals/*.md . # 恢复 1.x 的 topic 文件
|
|
328
|
+
rm .migrated # 让迁移可以重新跑
|
|
329
|
+
rm <生成的 entry 文件> # 即 /memory 的 Entries 列出、但不在 originals/ 里的那些
|
|
330
|
+
```
|
|
228
331
|
|
|
229
332
|
## 文件布局
|
|
230
333
|
|
|
@@ -232,31 +335,38 @@ memory(action: "add" | "remove" | "search",
|
|
|
232
335
|
~/.pi/memory/
|
|
233
336
|
git/
|
|
234
337
|
github.com__yandy__pi-packages/ ← https://github.com/yandy/pi-packages.git
|
|
235
|
-
MEMORY.md —
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
...
|
|
338
|
+
MEMORY.md — 索引:一行一条记忆
|
|
339
|
+
SSH-port-on-staging.md
|
|
340
|
+
Test-command.md — 一条记忆一个文件
|
|
341
|
+
.lock .backups/ .migrated .dream-meta.json
|
|
240
342
|
local/
|
|
241
343
|
home__yandy__workspace__scratch/ ← 非 git 目录 /home/yandy/workspace/scratch
|
|
242
344
|
```
|
|
243
345
|
|
|
244
|
-
|
|
346
|
+
目录名的推导规则:
|
|
245
347
|
|
|
246
|
-
- remote
|
|
247
|
-
- remote URL
|
|
248
|
-
- scheme
|
|
249
|
-
- 其余情况 —— 非 git
|
|
250
|
-
- `/`
|
|
251
|
-
-
|
|
252
|
-
-
|
|
348
|
+
- remote 是 http(s)、ssh(含 scp 写法 `[user@]host:owner/repo`,user 可省略)或 `git://`,以及 `git+ssh://` / `git+https://` 别名的 git 仓库 → `git/<host>__<owner>__<repo>`;端口、凭据、结尾的 `/` 与 `.git` 会被剥掉,host 转小写
|
|
349
|
+
- remote URL 从原始 git config 读取(`remote.<name>.url`;先 `origin`,再按字母序,第一个可用的胜出),所以 `url.*.insteadOf` 重写不会影响映射
|
|
350
|
+
- scheme 形式走 WHATWG URL 规范化(IDN host 转 punycode,百分号编码与 `.`/`..` 折叠生效,凭据/query/fragment 被丢弃),scp 形式保留原样路径 —— 等价但写法不同的 remote 可能映射到不同目录
|
|
351
|
+
- 其余情况 —— 非 git 目录、没有 remote 的 git 仓库、`file://` 或本地路径 remote → `local/<absolute-path>`(git 仓库用仓库根;Windows 盘符形式的 remote 如 `C:/repos/foo.git` 在 POSIX 上按 scp 写法处理,与 git 一致)
|
|
352
|
+
- `/` 变成 `__`;文件名里不可移植的字符(`<>:"|?*`、控制字符)变成 `_XX` 十六进制转义
|
|
353
|
+
- 超过 120 UTF-8 字节的名字在码点边界截断到 100 字节,再加 `__<hash8>` 后缀
|
|
354
|
+
- 名字面向 POSIX 文件系统:反斜杠是普通字符,不做 Windows 设备名与结尾句点处理
|
|
253
355
|
|
|
254
|
-
|
|
356
|
+
映射不是单射:下划线原样保留,所以 `/home/a__b` 与 `/home/a/b` 都映射到 `home__a__b`(共享同一个记忆目录)。改动或重命名 remote、新增一个排序更靠前的 remote、移动本地目录,都会改变记忆目录,旧目录会被孤立。
|
|
255
357
|
|
|
256
|
-
|
|
358
|
+
**更老的布局:** 1.x 之前的版本把记忆存在 `~/.pi/memory/<12-char-sha256>/`;这些目录不再被读写。要手工迁移,用 `printf '%s' "$(git rev-parse --show-toplevel)" | sha256sum | cut -c1-12` 算出旧 hash(不在 git 仓库里就用 `$PWD`),把那个目录 `mv` 到新位置(在项目里跑 `/memory` 可以看到新路径),然后让 1.x → 2.x 的迁移去拆它的 topic 文件。
|
|
257
359
|
|
|
258
|
-
##
|
|
360
|
+
## 通知
|
|
259
361
|
|
|
260
|
-
|
|
362
|
+
| 时机 | 通知 |
|
|
363
|
+
|---|---|
|
|
364
|
+
| `memory add` 成功(有 UI 的会话) | `Saved: <name>` |
|
|
365
|
+
| 自动浮现注入了 entry | `Recalled: <N> entries` |
|
|
366
|
+
| extract 写入了记忆 | `Extracted <N> memory.` / `Extracted <N> memories.` |
|
|
367
|
+
| extract 失败 | `Extract failed: <message>` —— 每会话最多一次 |
|
|
368
|
+
| 1.x 迁移完成 | `Migrated <N> memories from <M> topic files. Backup at <path>` |
|
|
369
|
+
| 迁移失败 | `Memory migration failed: <message>` |
|
|
370
|
+
| `/dream` 结束 / 失败 | headless agent 的摘要 / `Dream failed: <message>` |
|
|
261
371
|
|
|
262
|
-
|
|
372
|
+
headless 会话(`hasUI === false`)不发任何通知。
|