@yandy0725/pi-memory 0.3.1 → 1.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 +110 -16
- package/README.zh.md +120 -28
- package/index.ts +128 -18
- package/package.json +3 -2
- package/src/agent-types.ts +44 -0
- package/src/config.ts +36 -3
- package/src/dream.ts +116 -101
- package/src/extract.ts +110 -0
- package/src/index-file.ts +66 -57
- package/src/inject.ts +233 -1
- package/src/memory-tool.ts +162 -55
- package/src/nudge.ts +9 -6
- package/src/paths.ts +3 -6
- package/src/session-search.ts +19 -4
- package/src/topic-file.ts +87 -64
package/README.md
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
File-system driven persistent memory layer for pi coding agent. Stores project knowledge across sessions — facts, preferences, debugging history — in plain Markdown files under `~/.pi/memory/<project-hash>/`.
|
|
4
4
|
|
|
5
|
+
Aligned with Claude Code's auto memory mechanism: per-topic MEMORY.md index, automatic topic file surfacing based on relevance, per-turn memory extraction, and typed memory categories.
|
|
6
|
+
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
9
|
- **One `memory` tool**, four actions: `add` (append entry), `remove` (delete entry by title), `read` (load topic or entry), `search` (query memory or session history)
|
|
8
|
-
- **Topic-based file organization**: each `memory add` writes to a named `.md` file under the project's memory directory
|
|
9
|
-
- **`MEMORY.md` index** —
|
|
10
|
-
- **
|
|
11
|
-
-
|
|
10
|
+
- **Topic-based file organization**: each `memory add` writes a `## entry` block to a named `.md` file under the project's memory directory
|
|
11
|
+
- **`MEMORY.md` index** — one compact line per topic file with a relevance hook: `- [Name](file.md) — summary`
|
|
12
|
+
- **Memory types**: four categories — `user`, `feedback` (default), `project`, `reference` — stored in topic file frontmatter
|
|
13
|
+
- **Auto-surfacing** ⭐: on every user message, a side-query LLM selects up to N relevant topic files and injects their content into the agent's context. No manual `memory read` needed. Session-level deduplication prevents re-injecting the same topic.
|
|
14
|
+
- **Extract memories** ⭐: after each agent run, an async subagent analyzes the conversation and automatically writes learnings to memory — preferences, conventions, debugging insights
|
|
15
|
+
- **Snapshot injection**: on every new session, the MEMORY.md index is appended to the system prompt, keeping the agent aware of past work
|
|
16
|
+
- **`/dream` command**: launches a headless agent with a four-phase consolidation (Orient → Gather → Consolidate → Prune) to deduplicate, merge, and rebuild all memory files
|
|
12
17
|
- **Dream nudge**: after N sessions or N hours, a gentle notification suggests running `/dream`
|
|
13
18
|
- **`/memory` command**: show status, toggle on/off, inspect index and topic files
|
|
14
19
|
- **Session search**: the `memory search scope=sessions` action queries past conversation history
|
|
@@ -31,7 +36,7 @@ Or add to `~/.pi/agent/settings.json`:
|
|
|
31
36
|
|
|
32
37
|
## Configuration
|
|
33
38
|
|
|
34
|
-
Create `
|
|
39
|
+
Create `memory.json` in the agent directory (`~/.pi/agent/memory.json`) or the project `.pi/` directory (if trusted):
|
|
35
40
|
|
|
36
41
|
```json
|
|
37
42
|
{
|
|
@@ -47,6 +52,18 @@ Create `pi-memory.json` in the agent directory (`~/.pi/agent/pi-memory.json`) or
|
|
|
47
52
|
"sessionSearch": {
|
|
48
53
|
"maxSessions": 10,
|
|
49
54
|
"maxMatches": 5
|
|
55
|
+
},
|
|
56
|
+
"autoSurfacing": {
|
|
57
|
+
"enabled": true,
|
|
58
|
+
"model": "auto",
|
|
59
|
+
"maxFiles": 5,
|
|
60
|
+
"maxTopicBytes": 4096,
|
|
61
|
+
"maxInjectionBytes": 20480
|
|
62
|
+
},
|
|
63
|
+
"extractMemories": {
|
|
64
|
+
"enabled": true,
|
|
65
|
+
"model": "auto",
|
|
66
|
+
"maxContextTokens": 2000
|
|
50
67
|
}
|
|
51
68
|
}
|
|
52
69
|
```
|
|
@@ -62,28 +79,98 @@ Create `pi-memory.json` in the agent directory (`~/.pi/agent/pi-memory.json`) or
|
|
|
62
79
|
| `dream.model` | `"auto"` | Model for dream consolidation (`"auto"` = same as current, or `"provider/id"`) |
|
|
63
80
|
| `sessionSearch.maxSessions` | `10` | Max sessions to scan when searching history |
|
|
64
81
|
| `sessionSearch.maxMatches` | `5` | Max matches to return from history search |
|
|
82
|
+
| `autoSurfacing.enabled` | `true` | ⭐ Enable per-turn topic file auto-injection |
|
|
83
|
+
| `autoSurfacing.model` | `"auto"` | ⭐ Model for side-query relevance selection |
|
|
84
|
+
| `autoSurfacing.maxFiles` | `5` | ⭐ Max topic files to inject per turn |
|
|
85
|
+
| `autoSurfacing.maxTopicBytes` | `4096` | ⭐ Max bytes per injected topic file (truncated) |
|
|
86
|
+
| `autoSurfacing.maxInjectionBytes` | `20480` | ⭐ Max total bytes of injected content per turn |
|
|
87
|
+
| `extractMemories.enabled` | `true` | ⭐ Enable per-turn memory extraction |
|
|
88
|
+
| `extractMemories.model` | `"auto"` | ⭐ Model for the extraction subagent |
|
|
89
|
+
| `extractMemories.maxContextTokens` | `2000` | ⭐ Max tokens of conversation to analyze |
|
|
90
|
+
|
|
91
|
+
Project-level config (`.pi/memory.json`) is only loaded when the project is trusted.
|
|
92
|
+
|
|
93
|
+
## How it works
|
|
94
|
+
|
|
95
|
+
### MEMORY.md index
|
|
96
|
+
|
|
97
|
+
MEMORY.md is a compact **pointer index** — one line per topic file, not per entry:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
- [Debugging](debugging.md) — SSH uses port 2222; MySQL 30s timeout on staging
|
|
101
|
+
- [API Conventions](api.md) — REST handlers in src/api/handlers/; standard error format
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Only the index is injected into the system prompt on every session (first 200 lines / 25KB). Topic file content is **not** loaded at session start — it's surfaced on demand via auto-surfacing or explicit `memory read`.
|
|
105
|
+
|
|
106
|
+
### Topic file format
|
|
107
|
+
|
|
108
|
+
Each topic file uses YAML frontmatter with four fields:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
---
|
|
112
|
+
name: Debugging Tips
|
|
113
|
+
description: Common debugging patterns, SSH ports, MySQL timeout configs
|
|
114
|
+
type: feedback
|
|
115
|
+
updated: 2026-07-13
|
|
116
|
+
---
|
|
65
117
|
|
|
66
|
-
|
|
118
|
+
## SSH Gotcha
|
|
119
|
+
staging uses port 2222, key at ~/.ssh/staging
|
|
120
|
+
|
|
121
|
+
## MySQL Timeout
|
|
122
|
+
connection timeout after 30s on staging
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The `description` field is critical — the auto-surfacing side-query uses it to determine relevance. Make it specific.
|
|
126
|
+
|
|
127
|
+
### Memory types
|
|
128
|
+
|
|
129
|
+
| Type | Meaning | Example |
|
|
130
|
+
|------|---------|---------|
|
|
131
|
+
| `user` | User role, preferences, knowledge | "User is a data scientist focused on observability" |
|
|
132
|
+
| `feedback` | Lessons, corrections, confirmations (default) | "Use real DB not mocks — burned last quarter" |
|
|
133
|
+
| `project` | Project state, deadlines, incidents | "Merge freeze starts 2026-03-05 for mobile release" |
|
|
134
|
+
| `reference` | Pointers to external systems | "Bug tracker = Linear INGEST project" |
|
|
135
|
+
|
|
136
|
+
### Auto-surfacing
|
|
137
|
+
|
|
138
|
+
On every user message (`before_agent_start` hook):
|
|
139
|
+
1. Scan all topic files, extract their frontmatter metadata
|
|
140
|
+
2. A side-query LLM selects up to `maxFiles` relevant topic files based on the user's query
|
|
141
|
+
3. Already-injected topics are skipped (session-level dedup)
|
|
142
|
+
4. Selected topic file content is injected as context — the agent sees relevant memories automatically
|
|
143
|
+
|
|
144
|
+
### Extract memories
|
|
145
|
+
|
|
146
|
+
After each agent run (`agent_end` hook):
|
|
147
|
+
1. An async subagent is forked with the conversation transcript
|
|
148
|
+
2. It analyzes whether there are learnings worth persisting
|
|
149
|
+
3. If yes, it writes directly to memory files — preferences, conventions, debugging insights
|
|
150
|
+
4. The subagent runs independently; its results benefit future sessions
|
|
151
|
+
|
|
152
|
+
Memory extraction is selective: it ignores one-time tasks, code snippets derivable from the project, and anything already in CLAUDE.md.
|
|
67
153
|
|
|
68
154
|
## Tool reference
|
|
69
155
|
|
|
70
156
|
```
|
|
71
157
|
memory(action: "add" | "remove" | "search" | "read",
|
|
72
|
-
content?, topic?, title?,
|
|
158
|
+
content?, topic?, title?, type?,
|
|
73
159
|
entry?, query?, scope?)
|
|
74
160
|
```
|
|
75
161
|
|
|
76
162
|
### `add`
|
|
77
163
|
|
|
78
|
-
Appends
|
|
164
|
+
Appends a `## entry` block to a topic file. If the topic already exists in the index, the MEMORY.md hook is updated to summarize all entries. For new topics, a new index line is created.
|
|
79
165
|
|
|
80
166
|
- **`content`** (required) — knowledge text to persist
|
|
81
167
|
- **`topic`** (required) — target filename, e.g. `"debugging.md"`. Auto-created if new
|
|
82
|
-
- **`title`** (required) —
|
|
168
|
+
- **`title`** (required) — descriptive, self-contained title for the entry. Only the MEMORY.md index line is injected into future prompts (topic file content is NOT), so the title alone must convey what was learned
|
|
169
|
+
- **`type`** (optional) — memory category: `"user"`, `"feedback"` (default), `"project"`, `"reference"`
|
|
83
170
|
|
|
84
171
|
### `remove`
|
|
85
172
|
|
|
86
|
-
Deletes an entry by
|
|
173
|
+
Deletes an entry by title. Searches across all topic files for the matching `##` block. Updates the MEMORY.md hook for the affected topic. When the last entry in a topic is removed, the topic file and its index line are deleted.
|
|
87
174
|
|
|
88
175
|
- **`entry`** (required) — exact entry title to remove
|
|
89
176
|
|
|
@@ -115,20 +202,25 @@ Show memory status (enabled/disabled, directory, index line count, topic files,
|
|
|
115
202
|
|
|
116
203
|
### `/dream`
|
|
117
204
|
|
|
118
|
-
Launch a headless agent that reads all memory files
|
|
205
|
+
Launch a headless agent that reads all memory files and consolidates them in four phases:
|
|
119
206
|
|
|
120
|
-
|
|
207
|
+
1. **Orient** — list files, read MEMORY.md, skim topic files
|
|
208
|
+
2. **Gather Signal** — find duplicates, contradictions, outdated entries
|
|
209
|
+
3. **Consolidate** — merge duplicates, resolve contradictions, update dates
|
|
210
|
+
4. **Prune & Index** — rebuild frontmatter, generate hooks, rebuild MEMORY.md
|
|
121
211
|
|
|
122
|
-
|
|
212
|
+
The model used can be configured via `dream.model` in `memory.json`.
|
|
213
|
+
|
|
214
|
+
A confirmation dialog is shown before the consolidation begins. The result summary is shown as a notification when done.
|
|
123
215
|
|
|
124
216
|
## File layout
|
|
125
217
|
|
|
126
218
|
```
|
|
127
219
|
~/.pi/memory/
|
|
128
220
|
<12-char-sha256>/
|
|
129
|
-
MEMORY.md — index: one line per topic file
|
|
221
|
+
MEMORY.md — compact index: one line per topic file
|
|
130
222
|
.dream-meta.json — last dream timestamp + session count
|
|
131
|
-
debugging.md —
|
|
223
|
+
debugging.md — topic files with frontmatter + ## entries
|
|
132
224
|
preferences.md
|
|
133
225
|
...
|
|
134
226
|
```
|
|
@@ -137,4 +229,6 @@ The hash is derived from the project's git root (or absolute path), ensuring eac
|
|
|
137
229
|
|
|
138
230
|
## Snapshot semantics
|
|
139
231
|
|
|
140
|
-
On every `session_start`, the `MEMORY.md` index is read and appended to the system prompt via `before_agent_start`. If the index exceeds `memIndexMaxLines` or `memIndexMaxBytes`, it is truncated with a `[truncated]` marker — the agent still gets the most relevant portion. This snapshot is a static copy at the start of the session
|
|
232
|
+
On every `session_start`, the `MEMORY.md` index is read and appended to the system prompt via `before_agent_start`. If the index exceeds `memIndexMaxLines` or `memIndexMaxBytes`, it is truncated with a `[truncated]` marker — the agent still gets the most relevant portion. This snapshot is a static copy at the start of the session.
|
|
233
|
+
|
|
234
|
+
Topic file content is surfaced separately via **auto-surfacing** (automatic, per-turn, relevance-based) or explicit `memory read`.
|
package/README.zh.md
CHANGED
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
基于文件系统的持久化记忆层,为 pi 编程代理提供跨会话的项目记忆。事实、偏好、调试历史等知识以纯 Markdown 文件形式存储在 `~/.pi/memory/<项目哈希>/` 下。
|
|
4
4
|
|
|
5
|
+
对齐 Claude Code 的 auto memory 机制:每 topic 一行的 MEMORY.md 紧凑索引、基于相关性的 topic 文件自动注入、per-turn 记忆自动提取、类型化记忆分类。
|
|
6
|
+
|
|
5
7
|
## 功能
|
|
6
8
|
|
|
7
9
|
- **一个 `memory` 工具**,四种操作:`add`(追加条目)、`remove`(按标题删除条目)、`read`(加载主题或条目)、`search`(查询记忆或会话历史)
|
|
8
|
-
- **基于主题的文件组织**:每次 `memory add`
|
|
9
|
-
- **`MEMORY.md`
|
|
10
|
-
-
|
|
11
|
-
-
|
|
10
|
+
- **基于主题的文件组织**:每次 `memory add` 向指定名称的 `.md` 文件写入 `## 条目` 区块
|
|
11
|
+
- **`MEMORY.md` 索引**:每个 topic 文件一行紧凑指针 `- [名称](文件.md) — 摘要`,同 topic 自动合并
|
|
12
|
+
- **记忆类型系统**:四种分类 — `user`(用户)、`feedback`(反馈,默认)、`project`(项目)、`reference`(引用)— 存储在 topic 文件 frontmatter 中
|
|
13
|
+
- **Auto-surfacing** ⭐:每次用户发消息时,side-query LLM 自动选出最多 N 个相关 topic 文件并将其内容注入 agent context。无需手动 `memory read`。Session 内去重防止同一 topic 重复注入
|
|
14
|
+
- **Extract memories** ⭐:每次 agent 运行结束后,异步子 agent 分析对话内容,自动将 learnings 写入 memory — 偏好、约定、调试心法等
|
|
15
|
+
- **快照注入**:每个新会话启动时,MEMORY.md 索引追加到系统提示中
|
|
16
|
+
- **`/dream` 命令**:四阶段(Orient → Gather → Consolidate → Prune)无头代理整理,去重、合并、重建全部记忆文件
|
|
12
17
|
- **梦醒提醒**:经过 N 个会话或 N 小时后,温和通知建议运行 `/dream`
|
|
13
18
|
- **`/memory` 命令**:查看状态、开关记忆、检查索引和主题文件
|
|
14
19
|
- **会话搜索**:`memory search scope=sessions` 操作可检索过往对话历史
|
|
15
|
-
- **分支安全**:记忆目录以 git
|
|
20
|
+
- **分支安全**:记忆目录以 git 根目录为键,分叉仓库自然共享记忆
|
|
16
21
|
- **路径穿越防护**:主题文件路径会验证是否逃逸记忆目录
|
|
17
22
|
|
|
18
23
|
## 安装
|
|
@@ -31,7 +36,7 @@ pi install npm:@yandy0725/pi-memory
|
|
|
31
36
|
|
|
32
37
|
## 配置
|
|
33
38
|
|
|
34
|
-
在代理目录(`~/.pi/agent/
|
|
39
|
+
在代理目录(`~/.pi/agent/memory.json`)或项目的 `.pi/` 目录(需受信任)中创建 `memory.json`:
|
|
35
40
|
|
|
36
41
|
```json
|
|
37
42
|
{
|
|
@@ -47,6 +52,18 @@ pi install npm:@yandy0725/pi-memory
|
|
|
47
52
|
"sessionSearch": {
|
|
48
53
|
"maxSessions": 10,
|
|
49
54
|
"maxMatches": 5
|
|
55
|
+
},
|
|
56
|
+
"autoSurfacing": {
|
|
57
|
+
"enabled": true,
|
|
58
|
+
"model": "auto",
|
|
59
|
+
"maxFiles": 5,
|
|
60
|
+
"maxTopicBytes": 4096,
|
|
61
|
+
"maxInjectionBytes": 20480
|
|
62
|
+
},
|
|
63
|
+
"extractMemories": {
|
|
64
|
+
"enabled": true,
|
|
65
|
+
"model": "auto",
|
|
66
|
+
"maxContextTokens": 2000
|
|
50
67
|
}
|
|
51
68
|
}
|
|
52
69
|
```
|
|
@@ -55,43 +72,113 @@ pi install npm:@yandy0725/pi-memory
|
|
|
55
72
|
|--------|--------|------|
|
|
56
73
|
| `enabled` | `true` | 开关整个记忆系统 |
|
|
57
74
|
| `memoryDir` | `~/.pi/memory` | 所有记忆数据的根目录 |
|
|
58
|
-
| `memIndexMaxLines` | `200` | `MEMORY.md`
|
|
59
|
-
| `memIndexMaxBytes` | `25600` | `MEMORY.md`
|
|
60
|
-
| `dream.nudgeAfterSessions` | `5` |
|
|
61
|
-
| `dream.nudgeAfterHours` | `24` |
|
|
62
|
-
| `dream.model` | `"auto"` | 整理使用的模型(`"auto"`
|
|
63
|
-
| `sessionSearch.maxSessions` | `10` |
|
|
75
|
+
| `memIndexMaxLines` | `200` | `MEMORY.md` 最大行数 |
|
|
76
|
+
| `memIndexMaxBytes` | `25600` | `MEMORY.md` 最大字节数 |
|
|
77
|
+
| `dream.nudgeAfterSessions` | `5` | 触发提醒需经过的会话数 |
|
|
78
|
+
| `dream.nudgeAfterHours` | `24` | 触发提醒需经过的小时数 |
|
|
79
|
+
| `dream.model` | `"auto"` | 整理使用的模型(`"auto"` = 当前模型,或 `"provider/id"`) |
|
|
80
|
+
| `sessionSearch.maxSessions` | `10` | 搜索历史时最多扫描的会话数 |
|
|
64
81
|
| `sessionSearch.maxMatches` | `5` | 历史搜索最多返回的匹配数 |
|
|
82
|
+
| `autoSurfacing.enabled` | `true` | ⭐ 启用 per-turn topic 文件自动注入 |
|
|
83
|
+
| `autoSurfacing.model` | `"auto"` | ⭐ side-query 相关性选择模型 |
|
|
84
|
+
| `autoSurfacing.maxFiles` | `5` | ⭐ 每轮最多注入的 topic 文件数 |
|
|
85
|
+
| `autoSurfacing.maxTopicBytes` | `4096` | ⭐ 单个注入 topic 文件最大字节数(截断) |
|
|
86
|
+
| `autoSurfacing.maxInjectionBytes` | `20480` | ⭐ 每轮注入内容总字节数上限 |
|
|
87
|
+
| `extractMemories.enabled` | `true` | ⭐ 启用 per-turn 记忆自动提取 |
|
|
88
|
+
| `extractMemories.model` | `"auto"` | ⭐ 提取子 agent 使用的模型 |
|
|
89
|
+
| `extractMemories.maxContextTokens` | `2000` | ⭐ 分析对话的最大 token 数 |
|
|
90
|
+
|
|
91
|
+
项目级配置(`.pi/memory.json`)仅在项目受信任时加载。
|
|
92
|
+
|
|
93
|
+
## 工作原理
|
|
94
|
+
|
|
95
|
+
### MEMORY.md 索引
|
|
96
|
+
|
|
97
|
+
MEMORY.md 是一个**紧凑指针索引** — 每个 topic 文件一行,而非每个条目一行:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
- [Debugging](debugging.md) — SSH 使用 2222 端口;MySQL staging 上 30s 超时
|
|
101
|
+
- [API Conventions](api.md) — REST handlers 在 src/api/handlers/;使用标准错误格式
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
每次会话只有索引被注入系统提示(前 200 行 / 25KB)。Topic 文件内容**不会**在启动时加载 — 通过 auto-surfacing 按需注入或显式 `memory read`。
|
|
105
|
+
|
|
106
|
+
### Topic 文件格式
|
|
107
|
+
|
|
108
|
+
每个 topic 文件使用四字段 YAML frontmatter:
|
|
65
109
|
|
|
66
|
-
|
|
110
|
+
```yaml
|
|
111
|
+
---
|
|
112
|
+
name: 调试技巧
|
|
113
|
+
description: 常见调试模式、SSH 端口、MySQL 超时配置
|
|
114
|
+
type: feedback
|
|
115
|
+
updated: 2026-07-13
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## SSH 踩坑
|
|
119
|
+
staging 使用 2222 端口,密钥在 ~/.ssh/staging
|
|
120
|
+
|
|
121
|
+
## MySQL 超时
|
|
122
|
+
staging 上连接超时 30s
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`description` 字段至关重要 — auto-surfacing 的 side-query 根据它判断相关性。描述要具体。
|
|
126
|
+
|
|
127
|
+
### 记忆类型
|
|
128
|
+
|
|
129
|
+
| 类型 | 含义 | 示例 |
|
|
130
|
+
|------|------|------|
|
|
131
|
+
| `user` | 用户角色、偏好、知识背景 | "用户是数据科学家,关注可观测性" |
|
|
132
|
+
| `feedback` | 教训/纠正/确认(默认) | "用真实 DB 不用 mock—上次踩过坑" |
|
|
133
|
+
| `project` | 项目状态、deadline、incident | "merge freeze 从 3 月 5 日开始" |
|
|
134
|
+
| `reference` | 外部系统指针 | "bug tracker = Linear INGEST project" |
|
|
135
|
+
|
|
136
|
+
### Auto-surfacing
|
|
137
|
+
|
|
138
|
+
每次用户发消息时(`before_agent_start` hook):
|
|
139
|
+
1. 扫描所有 topic 文件,提取 frontmatter 元数据
|
|
140
|
+
2. Side-query LLM 根据用户问题选出最多 `maxFiles` 个相关 topic 文件
|
|
141
|
+
3. 已注入过的 topic 跳过(session 内去重)
|
|
142
|
+
4. 选中文件内容注入 context — agent 自动看到相关记忆
|
|
143
|
+
|
|
144
|
+
### Extract memories
|
|
145
|
+
|
|
146
|
+
每次 agent 运行结束后(`agent_end` hook):
|
|
147
|
+
1. 异步 fork 子 agent,传入本轮对话内容
|
|
148
|
+
2. 分析是否有值得持久化的 learnings
|
|
149
|
+
3. 如有,直接写入 memory 文件 — 偏好、约定、调试心法等
|
|
150
|
+
4. 子 agent 独立运行,结果为未来会话所用
|
|
151
|
+
|
|
152
|
+
记忆提取有选择性:忽略一次性任务、可从项目推导的代码、已在 CLAUDE.md 中的内容。
|
|
67
153
|
|
|
68
154
|
## 工具参考
|
|
69
155
|
|
|
70
156
|
```
|
|
71
157
|
memory(action: "add" | "remove" | "search" | "read",
|
|
72
|
-
content?, topic?, title?,
|
|
158
|
+
content?, topic?, title?, type?,
|
|
73
159
|
entry?, query?, scope?)
|
|
74
160
|
```
|
|
75
161
|
|
|
76
162
|
### `add`
|
|
77
163
|
|
|
78
|
-
|
|
164
|
+
向 topic 文件追加 `## 条目` 区块。若该 topic 已在索引中存在,则更新 MEMORY.md 的 hook 摘要。新建 topic 时在索引中新增一行。
|
|
79
165
|
|
|
80
166
|
- **`content`**(必填)— 要持久化的知识文本
|
|
81
167
|
- **`topic`**(必填)— 目标文件名,如 `"debugging.md"`,不存在则自动创建
|
|
82
|
-
- **`title`**(必填)—
|
|
168
|
+
- **`title`**(必填)— 描述性、自包含的条目标题。只有 MEMORY.md 索引行被注入未来 prompt(topic 文件内容不会),所以标题必须自成一体地传达信息
|
|
169
|
+
- **`type`**(可选)— 记忆分类:`"user"`、`"feedback"`(默认)、`"project"`、`"reference"`
|
|
83
170
|
|
|
84
171
|
### `remove`
|
|
85
172
|
|
|
86
|
-
|
|
173
|
+
按标题删除条目。遍历所有 topic 文件查找匹配的 `##` 区块。更新受影响 topic 的 MEMORY.md hook。当 topic 文件中最后一条被删除后,topic 文件及其索引行均被清理。
|
|
87
174
|
|
|
88
175
|
- **`entry`**(必填)— 要删除的条目标题
|
|
89
176
|
|
|
90
177
|
### `read`
|
|
91
178
|
|
|
92
|
-
|
|
179
|
+
加载记忆内容。可以是整个 topic 文件或单个条目区块。
|
|
93
180
|
|
|
94
|
-
- **`topic`**(可选)— 主题名称,如 `"debugging"` 或 `"debugging.md"
|
|
181
|
+
- **`topic`**(可选)— 主题名称,如 `"debugging"` 或 `"debugging.md"`。加载整个 topic 文件
|
|
95
182
|
- **`entry`**(可选)— 条目标题。返回对应的 `## 条目标题` 区块
|
|
96
183
|
|
|
97
184
|
### `search`
|
|
@@ -99,13 +186,13 @@ memory(action: "add" | "remove" | "search" | "read",
|
|
|
99
186
|
查询记忆文件或会话历史。记忆搜索返回匹配条目的完整区块(整个 `##` 区域)。
|
|
100
187
|
|
|
101
188
|
- **`query`**(必填)— 搜索关键词
|
|
102
|
-
- **`scope`**(可选)— `"memory"
|
|
189
|
+
- **`scope`**(可选)— `"memory"`(默认,扫描 topic 文件)或 `"sessions"`(扫描会话历史)
|
|
103
190
|
|
|
104
191
|
## 命令
|
|
105
192
|
|
|
106
193
|
### `/memory`
|
|
107
194
|
|
|
108
|
-
|
|
195
|
+
显示记忆状态(启用/禁用、目录、索引行数、topic 文件、上次整理时间戳)。
|
|
109
196
|
|
|
110
197
|
```
|
|
111
198
|
/memory — 显示状态
|
|
@@ -115,26 +202,31 @@ memory(action: "add" | "remove" | "search" | "read",
|
|
|
115
202
|
|
|
116
203
|
### `/dream`
|
|
117
204
|
|
|
118
|
-
|
|
205
|
+
启动无头代理,四阶段整理全部记忆文件:
|
|
119
206
|
|
|
120
|
-
|
|
207
|
+
1. **Orient** — 列出文件、读取 MEMORY.md、浏览 topic 文件
|
|
208
|
+
2. **Gather Signal** — 发现重复、矛盾、过时条目
|
|
209
|
+
3. **Consolidate** — 合并重复、解决矛盾、更新日期
|
|
210
|
+
4. **Prune & Index** — 重建 frontmatter、生成 hook、重建 MEMORY.md
|
|
121
211
|
|
|
122
|
-
|
|
212
|
+
使用的模型通过 `memory.json` 的 `dream.model` 配置。开始前弹出确认对话框,完成时通知摘要。
|
|
123
213
|
|
|
124
214
|
## 文件布局
|
|
125
215
|
|
|
126
216
|
```
|
|
127
217
|
~/.pi/memory/
|
|
128
218
|
<12位sha256哈希>/
|
|
129
|
-
MEMORY.md —
|
|
219
|
+
MEMORY.md — 紧凑索引:每个 topic 文件一行
|
|
130
220
|
.dream-meta.json — 上次整理的时间戳和会话计数
|
|
131
|
-
debugging.md —
|
|
221
|
+
debugging.md — topic 文件(frontmatter + ## 条目)
|
|
132
222
|
preferences.md
|
|
133
223
|
...
|
|
134
224
|
```
|
|
135
225
|
|
|
136
|
-
哈希值由项目的 git
|
|
226
|
+
哈希值由项目的 git 根目录(或绝对路径)派生,每个项目拥有独立记忆命名空间。
|
|
137
227
|
|
|
138
228
|
## 快照语义
|
|
139
229
|
|
|
140
|
-
每次 `session_start`
|
|
230
|
+
每次 `session_start` 读取 `MEMORY.md` 索引,通过 `before_agent_start` 追加到系统提示中。超限则截断并标记 `[truncated]`。此快照是会话开始时的静态副本。
|
|
231
|
+
|
|
232
|
+
Topic 文件内容通过 **auto-surfacing**(自动、per-turn、基于相关性)或显式 `memory read` 按需加载。
|
package/index.ts
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { ensureAgentTypes } from "./src/agent-types";
|
|
2
6
|
import { loadConfig, type MemoryConfig } from "./src/config";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
7
|
+
import { runDream } from "./src/dream";
|
|
8
|
+
import { runExtract } from "./src/extract";
|
|
9
|
+
import {
|
|
10
|
+
buildInjection,
|
|
11
|
+
buildSurfacingPrompt,
|
|
12
|
+
injectSurfacedContent,
|
|
13
|
+
loadIndexSnapshot,
|
|
14
|
+
runSideQuery,
|
|
15
|
+
scanTopics,
|
|
16
|
+
} from "./src/inject";
|
|
5
17
|
import { createMemoryTool } from "./src/memory-tool";
|
|
18
|
+
import { readDreamMeta, shouldNudge, writeDreamMeta } from "./src/nudge";
|
|
19
|
+
import { resolveMemoryDir } from "./src/paths";
|
|
6
20
|
import { searchSessions } from "./src/session-search";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
11
|
-
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
// Register custom agent type on load (zero setup, auto-fallback on first session).
|
|
23
|
+
ensureAgentTypes();
|
|
12
24
|
|
|
13
25
|
export default function (pi: ExtensionAPI) {
|
|
14
26
|
let memoryDir: string | null = null;
|
|
15
27
|
let config: MemoryConfig | null = null;
|
|
16
28
|
let indexSnapshot = "";
|
|
17
29
|
let toolRegistered = false;
|
|
30
|
+
const injectedTopics = new Set<string>();
|
|
18
31
|
|
|
19
32
|
pi.on("session_start", async (_event, ctx) => {
|
|
20
33
|
config = await loadConfig(ctx);
|
|
@@ -27,10 +40,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
27
40
|
pi.registerTool(
|
|
28
41
|
createMemoryTool({
|
|
29
42
|
getMemoryDir: () => memoryDir,
|
|
43
|
+
// biome-ignore lint/style/noNonNullAssertion: config assigned in guard above
|
|
30
44
|
getConfig: () => config!,
|
|
31
45
|
getEnabled: () => config?.enabled ?? false,
|
|
32
46
|
searchSessions,
|
|
33
47
|
cwd: () => ctx.cwd,
|
|
48
|
+
// biome-ignore lint/suspicious/noExplicitAny: pi registerTool type cast
|
|
34
49
|
}) as any,
|
|
35
50
|
);
|
|
36
51
|
toolRegistered = true;
|
|
@@ -38,14 +53,108 @@ export default function (pi: ExtensionAPI) {
|
|
|
38
53
|
|
|
39
54
|
// nudge
|
|
40
55
|
if (ctx.hasUI) {
|
|
41
|
-
const { nudge, message } = await shouldNudge(memoryDir, config, ctx.cwd);
|
|
42
|
-
if (nudge)
|
|
56
|
+
const { nudge, message, sessions } = await shouldNudge(memoryDir, config, ctx.cwd);
|
|
57
|
+
if (nudge) {
|
|
58
|
+
const ok = await ctx.ui.confirm("Memory Consolidation", `${message}\n\nConsolidate memory files now?`);
|
|
59
|
+
if (ok) {
|
|
60
|
+
// Fire-and-forget: defers past the current macrotask so all
|
|
61
|
+
// session_start handlers (including pi-subagents') have completed.
|
|
62
|
+
// Does not block session_start.
|
|
63
|
+
const dreamModel = config.dream.model;
|
|
64
|
+
const dir = memoryDir;
|
|
65
|
+
ctx.ui.setStatus("dream", "Consolidating memory...");
|
|
66
|
+
setTimeout(async () => {
|
|
67
|
+
try {
|
|
68
|
+
const summary = await runDream({
|
|
69
|
+
model: dreamModel,
|
|
70
|
+
memoryDir: dir,
|
|
71
|
+
events: pi.events,
|
|
72
|
+
});
|
|
73
|
+
await writeDreamMeta(dir, sessions);
|
|
74
|
+
ctx.ui.notify(summary, "info");
|
|
75
|
+
// biome-ignore lint/suspicious/noExplicitAny: error catch
|
|
76
|
+
} catch (e: any) {
|
|
77
|
+
ctx.ui.notify(`Dream failed: ${e.message}`, "error");
|
|
78
|
+
} finally {
|
|
79
|
+
ctx.ui.setStatus("dream", undefined);
|
|
80
|
+
}
|
|
81
|
+
}, 0);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
43
84
|
}
|
|
44
85
|
});
|
|
45
86
|
|
|
46
|
-
pi.on("before_agent_start", async (event) => {
|
|
47
|
-
if (!config?.enabled || !indexSnapshot) return;
|
|
48
|
-
|
|
87
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
88
|
+
if (!config?.enabled || !indexSnapshot || !memoryDir) return;
|
|
89
|
+
|
|
90
|
+
// Auto-surfacing: select relevant topic files via LLM side-query and inject as message
|
|
91
|
+
const autoSurfacing = config.autoSurfacing;
|
|
92
|
+
// biome-ignore lint/suspicious/noExplicitAny: message injection result
|
|
93
|
+
let injectedMessage: any;
|
|
94
|
+
if (autoSurfacing?.enabled && event.prompt) {
|
|
95
|
+
try {
|
|
96
|
+
if (ctx.hasUI) ctx.ui.setStatus("surfacing", "Searching relevant memories…");
|
|
97
|
+
const manifest = await scanTopics(memoryDir);
|
|
98
|
+
if (manifest.length > 0) {
|
|
99
|
+
const queryPrompt = buildSurfacingPrompt(manifest, event.prompt.slice(0, 4000), injectedTopics);
|
|
100
|
+
const selected = await runSideQuery(queryPrompt, manifest, autoSurfacing.maxFiles, pi.events);
|
|
101
|
+
if (selected.length > 0) {
|
|
102
|
+
const content = await injectSurfacedContent(
|
|
103
|
+
memoryDir,
|
|
104
|
+
selected,
|
|
105
|
+
autoSurfacing.maxTopicBytes,
|
|
106
|
+
autoSurfacing.maxInjectionBytes,
|
|
107
|
+
);
|
|
108
|
+
if (content) {
|
|
109
|
+
// Track injected topics for session-level dedup
|
|
110
|
+
for (const f of selected) injectedTopics.add(f);
|
|
111
|
+
// Inject as a custom message (NOT systemPrompt)
|
|
112
|
+
injectedMessage = { customType: "memory-auto-surfacing", content, display: false };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
/* silently skip auto-surfacing on error */
|
|
118
|
+
} finally {
|
|
119
|
+
if (ctx.hasUI) ctx.ui.setStatus("surfacing", undefined);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// MEMORY.md index injection (always last after auto-surfacing)
|
|
124
|
+
return {
|
|
125
|
+
systemPrompt: buildInjection(event.systemPrompt, indexSnapshot),
|
|
126
|
+
...(injectedMessage ? { message: injectedMessage } : {}),
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
pi.on("agent_end", async (event) => {
|
|
131
|
+
if (!config?.enabled || !memoryDir) return;
|
|
132
|
+
const extractConfig = config.extractMemories;
|
|
133
|
+
if (!extractConfig?.enabled) return;
|
|
134
|
+
if (!event.messages || event.messages.length === 0) return;
|
|
135
|
+
// Fire-and-forget: extract memories in background
|
|
136
|
+
runExtract({
|
|
137
|
+
model: extractConfig.model,
|
|
138
|
+
memoryDir,
|
|
139
|
+
messages: event.messages.map((m) => ({
|
|
140
|
+
// biome-ignore lint/suspicious/noExplicitAny: pi event message union type
|
|
141
|
+
role: String((m as any).role ?? ""),
|
|
142
|
+
content:
|
|
143
|
+
// biome-ignore lint/suspicious/noExplicitAny: pi event message union type
|
|
144
|
+
typeof (m as any).content === "string"
|
|
145
|
+
? // biome-ignore lint/suspicious/noExplicitAny: pi event message union type
|
|
146
|
+
(m as any).content
|
|
147
|
+
: // biome-ignore lint/suspicious/noExplicitAny: pi event message union type
|
|
148
|
+
typeof (m as any).output === "string"
|
|
149
|
+
? // biome-ignore lint/suspicious/noExplicitAny: pi event message union type
|
|
150
|
+
(m as any).output
|
|
151
|
+
: // biome-ignore lint/suspicious/noExplicitAny: pi event message union type
|
|
152
|
+
JSON.stringify((m as any).content ?? ""),
|
|
153
|
+
})),
|
|
154
|
+
maxContextTokens: extractConfig.maxContextTokens,
|
|
155
|
+
}).catch(() => {
|
|
156
|
+
/* silently ignore extract errors */
|
|
157
|
+
});
|
|
49
158
|
});
|
|
50
159
|
|
|
51
160
|
pi.registerCommand("memory", {
|
|
@@ -84,17 +193,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
84
193
|
}
|
|
85
194
|
const ok = await ctx.ui.confirm("Dream", "Consolidate all memory files? This rewrites them in-place.");
|
|
86
195
|
if (!ok) return;
|
|
87
|
-
const model = resolveDreamModel(config, ctx);
|
|
88
|
-
if (!model) {
|
|
89
|
-
ctx.ui.notify("No model available for dream (check dream.model config / API key).", "error");
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
196
|
ctx.ui.setStatus("dream", "Consolidating memory...");
|
|
93
197
|
try {
|
|
94
|
-
const summary = await runDream({
|
|
198
|
+
const summary = await runDream({
|
|
199
|
+
model: config.dream.model,
|
|
200
|
+
memoryDir,
|
|
201
|
+
signal: ctx.signal,
|
|
202
|
+
events: pi.events,
|
|
203
|
+
});
|
|
95
204
|
const sessions = (await SessionManager.list(ctx.cwd)).length;
|
|
96
205
|
await writeDreamMeta(memoryDir, sessions);
|
|
97
206
|
ctx.ui.notify(summary, "info");
|
|
207
|
+
// biome-ignore lint/suspicious/noExplicitAny: command handler ctx
|
|
98
208
|
} catch (e: any) {
|
|
99
209
|
ctx.ui.notify(`Dream failed: ${e.message}`, "error");
|
|
100
210
|
} finally {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "1.0.0",
|
|
7
7
|
"description": "File-system driven persistent memory layer for pi coding agent",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
@@ -35,9 +35,10 @@
|
|
|
35
35
|
"@earendil-works/pi-ai": ">=0.80.2",
|
|
36
36
|
"@earendil-works/pi-coding-agent": ">=0.80.2",
|
|
37
37
|
"@earendil-works/pi-tui": ">=0.80.2",
|
|
38
|
+
"@yandy0725/pi-subagents": "*",
|
|
38
39
|
"typebox": "*"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"typebox": "^1.
|
|
42
|
+
"typebox": "^1.3.3"
|
|
42
43
|
}
|
|
43
44
|
}
|