@yandy0725/pi-memory 0.3.1 → 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 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** — auto-generated table of contents with line/byte capacity limits
10
- - **Snapshot injection**: on every new session, the memory index is appended to the system prompt, keeping the agent aware of past work
11
- - **`/dream` command**: launches a headless agent to deduplicate, merge, and consolidate all memory files
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 `pi-memory.json` in the agent directory (`~/.pi/agent/pi-memory.json`) or the project `.pi/` directory (if trusted):
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
- Project-level config (`.pi/pi-memory.json`) is only loaded when the project is trusted.
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 an entry to a topic file and adds a new line to the MEMORY.md index (no upsert — multiple entries per topic).
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) — short title for the index line and entry heading
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 exact title match on the MEMORY.md index. Removes both the index line and the corresponding `##` block from the topic file. When the last entry in a topic is removed, the topic file is deleted.
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, deduplicates entries, merges contradictions, updates outdated info, and reorganizes `MEMORY.md` to be concise. The model used can be configured via `dream.model` in `pi-memory.json` (`"auto"` uses the current conversation model; `"provider/id"` picks a specific model).
205
+ Launch a headless agent that reads all memory files and consolidates them in four phases:
119
206
 
120
- A confirmation dialog is shown before the consolidation begins. The result summary is shown as a notification when done.
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
- Dream meta (timestamp, session count at dream) is persisted in `.dream-meta.json` inside the memory directory.
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 — user-created topic files
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; changes made via the `memory` tool during a session do not update the snapshot for that session, but take effect on the next one.
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` 都会向项目记忆目录下指定名称的 `.md` 文件写入内容
9
- - **`MEMORY.md` 索引**:自动生成的目录,带有行数/字节容量限制
10
- - **快照注入**:每个新会话启动时,记忆索引会追加到系统提示中,让代理始终感知过往工作
11
- - **`/dream` 命令**:启动无头代理,对记忆文件进行去重、合并和整理
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/pi-memory.json`)或项目的 `.pi/` 目录(需受信任)中创建 `pi-memory.json`:
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"` 使用当前模型,或 `"provider/id"` 指定模型) |
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
- 项目级配置(`.pi/pi-memory.json`)仅在项目受信任时加载。
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
- 向主题文件追加条目,并向 MEMORY.md 索引添加新行(不更新已有行——一个主题可以有多个条目)。
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"`(默认,扫描主题文件)或 `"sessions"`(扫描会话历史)
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
- 启动无头代理,读取所有记忆文件,去重、合并矛盾、更新过时信息、重组 `MEMORY.md`。使用的模型可通过 `pi-memory.json` 的 `dream.model` 配置(`"auto"` 使用当前会话模型;`"provider/id"` 指定特定模型)。
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
- 整理元数据(时间戳、整理时会话计数)持久化在记忆目录下的 `.dream-meta.json` 中。
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` 时读取 `MEMORY.md` 索引,通过 `before_agent_start` 追加到系统提示中。如果索引超过 `memIndexMaxLines` 或 `memIndexMaxBytes`,将被截断并添加 `[truncated]` 标记——代理仍能获取到最相关的部分。此快照是会话开始时的静态副本;会话中通过 `memory` 工具所做的变更不会影响当次会话的快照,而是在下次会话中生效。
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 { resolveMemoryDir } from "./src/paths";
4
- import { loadIndexSnapshot, buildInjection } from "./src/inject";
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
- import { runDream, resolveDreamModel } from "./src/dream";
8
- import { shouldNudge, writeDreamMeta, readDreamMeta } from "./src/nudge";
9
- import { SessionManager } from "@earendil-works/pi-coding-agent";
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,114 @@ 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) ctx.ui.notify(message, "info");
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
- return { systemPrompt: buildInjection(event.systemPrompt, indexSnapshot) };
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
+ // Skip for subagents: pi-subagents strips "subagent" from all children's
92
+ // tool sets, so its absence reliably identifies subagents. Without this guard,
93
+ // runSideQuery's subagent spawn → before_agent_start → re-enter here → OOM.
94
+ const agentTools = event.systemPromptOptions?.selectedTools;
95
+ const isSubagent = agentTools && !agentTools.includes("subagent");
96
+
97
+ const autoSurfacing = config.autoSurfacing;
98
+ // biome-ignore lint/suspicious/noExplicitAny: message injection result
99
+ let injectedMessage: any;
100
+ if (autoSurfacing?.enabled && event.prompt && !isSubagent) {
101
+ try {
102
+ if (ctx.hasUI) ctx.ui.setStatus("surfacing", "Searching relevant memories…");
103
+ const manifest = await scanTopics(memoryDir);
104
+ if (manifest.length > 0) {
105
+ const queryPrompt = buildSurfacingPrompt(manifest, event.prompt.slice(0, 4000), injectedTopics);
106
+ const selected = await runSideQuery(queryPrompt, manifest, autoSurfacing.maxFiles, pi.events);
107
+ if (selected.length > 0) {
108
+ const content = await injectSurfacedContent(
109
+ memoryDir,
110
+ selected,
111
+ autoSurfacing.maxTopicBytes,
112
+ autoSurfacing.maxInjectionBytes,
113
+ );
114
+ if (content) {
115
+ // Track injected topics for session-level dedup
116
+ for (const f of selected) injectedTopics.add(f);
117
+ // Inject as a custom message (NOT systemPrompt)
118
+ injectedMessage = { customType: "memory-auto-surfacing", content, display: false };
119
+ }
120
+ }
121
+ }
122
+ } catch {
123
+ /* silently skip auto-surfacing on error */
124
+ } finally {
125
+ if (ctx.hasUI) ctx.ui.setStatus("surfacing", undefined);
126
+ }
127
+ }
128
+
129
+ // MEMORY.md index injection (always last after auto-surfacing)
130
+ return {
131
+ systemPrompt: buildInjection(event.systemPrompt, indexSnapshot),
132
+ ...(injectedMessage ? { message: injectedMessage } : {}),
133
+ };
134
+ });
135
+
136
+ pi.on("agent_end", async (event) => {
137
+ if (!config?.enabled || !memoryDir) return;
138
+ const extractConfig = config.extractMemories;
139
+ if (!extractConfig?.enabled) return;
140
+ if (!event.messages || event.messages.length === 0) return;
141
+ // Fire-and-forget: extract memories in background
142
+ runExtract({
143
+ model: extractConfig.model,
144
+ memoryDir,
145
+ messages: event.messages.map((m) => ({
146
+ // biome-ignore lint/suspicious/noExplicitAny: pi event message union type
147
+ role: String((m as any).role ?? ""),
148
+ content:
149
+ // biome-ignore lint/suspicious/noExplicitAny: pi event message union type
150
+ typeof (m as any).content === "string"
151
+ ? // biome-ignore lint/suspicious/noExplicitAny: pi event message union type
152
+ (m as any).content
153
+ : // biome-ignore lint/suspicious/noExplicitAny: pi event message union type
154
+ typeof (m as any).output === "string"
155
+ ? // biome-ignore lint/suspicious/noExplicitAny: pi event message union type
156
+ (m as any).output
157
+ : // biome-ignore lint/suspicious/noExplicitAny: pi event message union type
158
+ JSON.stringify((m as any).content ?? ""),
159
+ })),
160
+ maxContextTokens: extractConfig.maxContextTokens,
161
+ }).catch(() => {
162
+ /* silently ignore extract errors */
163
+ });
49
164
  });
50
165
 
51
166
  pi.registerCommand("memory", {
@@ -84,17 +199,18 @@ export default function (pi: ExtensionAPI) {
84
199
  }
85
200
  const ok = await ctx.ui.confirm("Dream", "Consolidate all memory files? This rewrites them in-place.");
86
201
  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
202
  ctx.ui.setStatus("dream", "Consolidating memory...");
93
203
  try {
94
- const summary = await runDream({ model, memoryDir, cwd: memoryDir, signal: ctx.signal });
204
+ const summary = await runDream({
205
+ model: config.dream.model,
206
+ memoryDir,
207
+ signal: ctx.signal,
208
+ events: pi.events,
209
+ });
95
210
  const sessions = (await SessionManager.list(ctx.cwd)).length;
96
211
  await writeDreamMeta(memoryDir, sessions);
97
212
  ctx.ui.notify(summary, "info");
213
+ // biome-ignore lint/suspicious/noExplicitAny: command handler ctx
98
214
  } catch (e: any) {
99
215
  ctx.ui.notify(`Dream failed: ${e.message}`, "error");
100
216
  } finally {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.1",
6
+ "version": "1.0.1",
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.1.38"
42
+ "typebox": "^1.3.3"
42
43
  }
43
44
  }