@yandy0725/pi-memory 2.3.0 → 2.4.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 CHANGED
@@ -6,6 +6,11 @@ Aligned with Claude Code's auto memory mechanism: **one memory = one file**, a `
6
6
 
7
7
  > ## ⚠️ Breaking changes
8
8
  >
9
+ > **In 2.4.0:**
10
+ >
11
+ > - **The package-level `enabled` switch is gone.** `memory.json` has no top-level `enabled` any more: a leftover key is ignored, and `{"enabled": false}` no longer disables anything — it used to skip model validation too, so a disabled config often had no model configured. Disable the extension the way you disable any pi package — by not loading it — see [Disabling the extension](#disabling-the-extension). `dream` is now a required task in every session.
12
+ > - **`/memory` no longer prints a `Memory: enabled|disabled` line.** The healthy status block is 7 lines starting with `Dir:`, and the old disabled state no longer exists — only *healthy* and *misconfigured* remain. Module switches (`autoSurfacing.enabled`, `extractMemories.enabled`) are unchanged.
13
+ >
9
14
  > **In 2.3.0:**
10
15
  >
11
16
  > - **The injected index window is now the newest 50 lines / 16 KiB.** `memIndexInjectMaxLines` 200 → 50 and `memIndexInjectMaxBytes` 25600 → 16384. The write capacity is unchanged (200 lines / 25600 bytes), so memories older than the newest 50 index lines no longer reach the system prompt — they stay reachable through auto-surfacing and the `memory` tool. Configs that already set `memIndexInjectMax*` are unaffected.
@@ -17,7 +22,7 @@ Aligned with Claude Code's auto memory mechanism: **one memory = one file**, a `
17
22
  > **In 2.1.0:**
18
23
  >
19
24
  > - **Models must be configured explicitly.** There is no shipped default and no parent-model fallback: `defaults.model` (or a per-task `model`) must exist and be resolvable, or `session_start` reports a config error and initialises **nothing**. See [Model configuration](#model-configuration).
20
- > - **`/memory on` and `/memory off` are gone.** `enabled` is a `memory.json` switch read once at session start — changing it needs a session restart.
25
+ > - **`/memory on` and `/memory off` are gone.** `enabled` is a `memory.json` switch read once at session start — changing it needs a session restart. **That key has since been removed** — it is ignored if present; see [Disabling the extension](#disabling-the-extension).
21
26
  > - **Automatic 1.x → 2.0 migration has been removed.** Legacy topic files stay on disk untouched but are **invisible** to the memory system (they fail `parseEntryFile`'s five-field v2 frontmatter check). See [1.x data](#1x-data).
22
27
  >
23
28
  > **In 2.0.0:**
@@ -37,7 +42,7 @@ Aligned with Claude Code's auto memory mechanism: **one memory = one file**, a `
37
42
  - **Extract memories** ⭐ — after each run an async headless agent receives a **structured rendering of the whole conversation** (every user message in full, assistant text and tool calls, tool results with error flags), not just two messages. It writes through the same `memory` primitives, under a whole-round logical lock it never waits for: if a dream is running, that turn is simply skipped. This feature is **off by default** — set `extractMemories.enabled: true` to turn it on.
38
43
  - **`/dream`** — a headless consolidation agent (Orient → Gather Signal → Consolidate → Prune & Index) that merges duplicates, resolves contradictions, renames entries and rebuilds the index. It has **no raw file access**: it only gets the seven `memory` actions, holds the logical lock for the whole round, and snapshots the entire directory on entry.
39
44
  - **Dream nudge** — after N sessions or N hours a notification suggests `/dream`.
40
- - **`/memory`** — full status (switch, directory, index capacity, entry count, last dream, lock state including the holder), plus `unlock`.
45
+ - **`/memory`** — full status (directory, index capacity, entry count, last dream, lock state including the holder), plus `unlock`.
41
46
  - **Two-level locking** — an in-process logical lock carries the *logical* scope (one primitive call, or a whole dream round); the cross-process `.lock` file is held for **milliseconds only** and is **never reclaimed automatically**. There is no TTL, no heartbeat and no takeover, so mutual exclusion is a hard guarantee; the price is that a lock left behind by a crashed process must be removed by a human (`/memory unlock`).
42
47
  - **Snapshots** — every write leaves a rollback point under `.backups/<ts>-<label>/`, keeping the last `lock.snapshotKeep` (directories named `migrate-*` — whole-directory snapshots from an earlier 1.x migration, whose `originals/` subdirectory holds the pre-2.0 topic files — are never pruned). `/dream` is the exception: it snapshots the whole directory **once on entry**, and the primitives inside that round skip their per-file snapshots (one round, one rollback point).
43
48
  - **Session search** — `memory search scope=sessions` queries past conversation history.
@@ -126,7 +131,6 @@ Create `memory.json` in the agent directory (`~/.pi/agent/memory.json`) or the p
126
131
 
127
132
  ```json
128
133
  {
129
- "enabled": true,
130
134
  "memoryDir": "~/.pi/memory",
131
135
  "memIndexMaxLines": 200,
132
136
  "memIndexMaxBytes": 25600,
@@ -157,7 +161,6 @@ Create `memory.json` in the agent directory (`~/.pi/agent/memory.json`) or the p
157
161
 
158
162
  | Key | Default | Description |
159
163
  |-----|---------|-------------|
160
- | `enabled` | `true` | Toggle the entire memory system on/off. Read once at session start — changing it requires restarting the session |
161
164
  | `memoryDir` | `~/.pi/memory` | Root directory for all memory data |
162
165
  | `memIndexMaxLines` | `200` | Write capacity: max non-empty lines in `MEMORY.md` (the `# Memory Index` header and hand-written headings count too, so this is not exactly the memory count) |
163
166
  | `memIndexMaxBytes` | `25600` | Write capacity: max bytes of `MEMORY.md` |
@@ -192,17 +195,29 @@ Create `memory.json` in the agent directory (`~/.pi/agent/memory.json`) or the p
192
195
 
193
196
  Persisted headless sessions default to `<project memory dir>/sessions/` — inside the project's memory directory, not inside your working copy.
194
197
 
198
+ ### Disabling the extension
199
+
200
+ pi-memory has no package-level switch of its own — the former top-level `enabled` key in `memory.json` is **gone** and is now ignored if present. Disable the extension the way you disable any pi package, by not loading it:
201
+
202
+ Project-only (`.pi/settings.json` in the project root — read only after project trust is granted):
203
+
204
+ ```json
205
+ { "packages": [{ "source": "npm:@yandy0725/pi-memory", "extensions": [] }] }
206
+ ```
207
+
208
+ A project entry replaces the personal entry, so the extension is not loaded in that project. Globally: `pi remove npm:@yandy0725/pi-memory`, or toggle the package's resources with `pi config` (project scope writes `autoload: false` plus `extensions: ["-index.ts"]`). Module switches (`autoSurfacing.enabled`, `extractMemories.enabled`) still turn off individual behaviors while the extension stays loaded.
209
+
195
210
  ## Model configuration
196
211
 
197
212
  Every task that will run must resolve a model — **there is no shipped default and no parent-model fallback**. `defaults.model` satisfies all of them; a per-task `model` (`dream.model`, `extractMemories.model`, `autoSurfacing.model`) overrides it.
198
213
 
199
214
  | Task | Required when |
200
215
  |------|---------------|
201
- | `dream` | the memory system is enabled (`enabled: true`) — always required |
216
+ | `dream` | always required — every session validates it at startup |
202
217
  | `extractMemories` | `extractMemories.enabled` is true |
203
218
  | `autoSurfacing` | `autoSurfacing.enabled` is true |
204
219
 
205
- With `enabled: false` nothing runs — not even `/dream` or the nudge — so no model is required. At `session_start` pi-memory resolves every required model against the model registry. If one is missing or cannot be resolved, it initialises **nothing**: it shows an error notification `pi-memory config error:` followed by one `- <error>` line per problem, and `/memory` reports `Memory: misconfigured` and `Dir: not initialized`, followed by the same lines. The two possible messages are:
220
+ At `session_start` pi-memory resolves every required model against the model registry. If one is missing or cannot be resolved, it initialises **nothing**: it shows an error notification `pi-memory config error:` followed by one `- <error>` line per problem, and `/memory` reports `Memory: misconfigured` and `Dir: not initialized`, followed by the same lines. The two possible messages are:
206
221
 
207
222
  - `no model for <task> — set "<task>.model" or "defaults.model" in memory.json`
208
223
  - `model "<value>" for <task> is not resolvable (unknown id or missing credentials)`
@@ -312,7 +327,6 @@ One line per memory: `- name (type, modified …) — description [file]`.
312
327
  Status output:
313
328
 
314
329
  ```
315
- Memory: enabled
316
330
  Dir: /home/you/.pi/memory/git/github.com__owner__repo
317
331
  Index: 38/200 lines, 2841/25600 bytes, 1 unrecognized lines
318
332
  Inject: 39/50 lines, 2841/16384 bytes
@@ -324,9 +338,8 @@ Lock: free
324
338
 
325
339
  - `Index` uses the **write** capacity (`memIndexMax*`) and reports how many non-empty lines could not be parsed as index lines (the `# Memory Index` header and hand-written headings count). CRLF (or lone CR) line endings are normalised to LF before parsing, and the next write emits LF too, so a `MEMORY.md` re-saved by a Windows editor does **not** raise this count.
326
340
  - `Inject` uses the **injection** window (`memIndexInjectMax*`) and counts the window's lines and bytes — the index text that goes into the `memory_index` section, taken from the **newest** end (the truncation marker itself is not counted). It is computed by the same window code that produces the injected value, so the two cannot drift. Note the two lines count different things: `Index` counts **non-empty** lines, `Inject` counts **every** line of the window, so on a canonical index (LF endings, trailing newline, one blank line after the header) `Inject` reports one more line than `Index` and the same byte count. The value in the system prompt is **frozen for the session** (see [Why the index is frozen](#why-the-index-is-frozen)): a memory written after `session_start` appears in `Index` immediately but in `Inject` only after compaction or in the next session.
327
- - `Modules` reports the activation state of the three model-driven features as `on(<effective model>)` / `off`. The effective model is the task's own `model`, otherwise `defaults.model`. `dream` has no switch of its own — it is on whenever the memory system is enabled.
341
+ - `Modules` reports the activation state of the three model-driven features as `on(<effective model>)` / `off`. The effective model is the task's own `model`, otherwise `defaults.model`. `dream` has no switch of its own — it is always available in a healthy session.
328
342
  - `Lock` is `free`, `held by <op> (pid N on <hostname>, started <ISO>)`, or `unreadable — run /memory unlock`. `/memory unlock` shows the same holder line in its confirmation prompt.
329
- - In a session started with `enabled: false`, nothing is initialized at boot: `/memory` reports `Memory: disabled` plus `Dir: not initialized — set "enabled": true in memory.json and restart`, there is no way to enable it mid-session, and `/memory unlock` still works without a store.
330
343
  - If a required model is missing or cannot be resolved, nothing is initialized and `/memory` reports `Memory: misconfigured` and `Dir: not initialized`, followed by one `- <error>` line per problem. The same errors are shown as an error notification at session start.
331
344
 
332
345
  ### `/dream`
package/README.zh.md CHANGED
@@ -6,6 +6,11 @@ pi coding agent 的文件系统持久记忆层。把项目知识(事实、偏
6
6
 
7
7
  > ## ⚠️ 破坏性变更
8
8
  >
9
+ > **2.4.0:**
10
+ >
11
+ > - **包级开关 `enabled` 已删除。** `memory.json` 不再有顶层 `enabled`:残留的键会被忽略,`{"enabled": false}` 不再能禁用任何东西 —— 它以前还会跳过模型校验,所以被禁用的配置往往也没配模型。要禁用整个扩展,请像禁用其他 pi package 一样「不加载它」,见[禁用本扩展](#禁用本扩展)。`dream` 现在是每个会话的必需任务。
12
+ > - **`/memory` 不再输出 `Memory: enabled|disabled` 行。** 健康态状态块为 7 行、以 `Dir:` 起头;旧的「禁用」态已不存在,只剩**健康**与**配置错误**两态。模块级开关(`autoSurfacing.enabled` / `extractMemories.enabled`)不变。
13
+ >
9
14
  > **2.3.0:**
10
15
  >
11
16
  > - **注入的索引窗口改为「最新的 50 行 / 16 KiB」。** `memIndexInjectMaxLines` 200 → 50、`memIndexInjectMaxBytes` 25600 → 16384。写入口径不变(200 行 / 25600 字节),因此比「最新 50 行」更旧的记忆不再进 system prompt —— 它们仍可由 auto-surfacing 与 `memory` 工具检索。已经显式配置 `memIndexInjectMax*` 的用户不受影响。
@@ -17,7 +22,7 @@ pi coding agent 的文件系统持久记忆层。把项目知识(事实、偏
17
22
  > **2.1.0:**
18
23
  >
19
24
  > - **模型必须显式配置。** 没有内置默认值,也没有父会话模型回退:`defaults.model`(或 per-task `model`)必须存在且可解析,否则 `session_start` 会报配置错误并且**什么都不初始化**。详见[模型配置](#模型配置)。
20
- > - **`/memory on` / `/memory off` 已删除。** `enabled` 只是 `memory.json` 里的开关,启动时读一次,改动需要重启会话。
25
+ > - **`/memory on` / `/memory off` 已删除。** `enabled` 只是 `memory.json` 里的开关,启动时读一次,改动需要重启会话。**该键现已移除** —— 写了也会被忽略,见[禁用本扩展](#禁用本扩展)。
21
26
  > - **1.x → 2.0 的自动迁移已删除。** legacy topic 文件原样留在磁盘上,但对记忆系统**不可见**(过不了 `parseEntryFile` 的 v2 五字段校验)。详见 [1.x 数据](#1x-数据)。
22
27
  >
23
28
  > **2.0.0 已包含:**
@@ -37,7 +42,7 @@ pi coding agent 的文件系统持久记忆层。把项目知识(事实、偏
37
42
  - **自动提取(extract memories)** ⭐ —— 每轮结束后一个异步 headless agent 拿到的是**整轮对话的结构化渲染**(user 消息全文、assistant 文本与 tool_call、tool_result 及其错误标记),而不是两条消息。它经同一套 `memory` 原语写入,并且**从不排队等锁**:dream 正在整轮持锁时,本回合直接跳过。该功能**默认关闭**,需要显式设 `extractMemories.enabled: true`。
38
43
  - **`/dream`** —— headless 整理 agent(Orient → Gather Signal → Consolidate → Prune & Index),合并重复、消解矛盾、改名、重建索引。它**没有裸文件权限**:只有七个 `memory` action,整轮持有逻辑锁,进入时先对整个目录拍一次快照。
39
44
  - **Dream 提醒** —— 距上次 dream 超过 N 个会话或 N 小时后提示 `/dream`。
40
- - **`/memory`** —— 完整状态(开关、目录、索引容量、entry 数、上次 dream、锁状态含持有者),以及 `unlock`。
45
+ - **`/memory`** —— 完整状态(目录、索引容量、entry 数、上次 dream、锁状态含持有者),以及 `unlock`。
41
46
  - **两级锁** —— 进程内逻辑锁承担**逻辑作用域**(单次原语,或 dream 的整轮);跨进程 `.lock` **只持毫秒**且**永不自动回收**。没有 TTL、没有心跳、没有接管,所以互斥是硬保证;代价是崩溃遗留的锁必须**人工**清除(`/memory unlock`)。
42
47
  - **快照** —— 每次写入都在 `.backups/<ts>-<label>/` 留下回滚点,保留最近 `lock.snapshotKeep` 份(`migrate-` 开头的目录是旧版迁移留下的整目录快照,其 `originals/` 子目录里才是 2.0 之前的 topic 原文,永不裁剪)。`/dream` 是例外:它**进入时只对整个目录拍一次**快照,该轮内部的原语会跳过逐文件快照(一轮只留一个回滚点)。
43
48
  - **会话检索** —— `memory search scope=sessions` 查历史会话。
@@ -126,7 +131,6 @@ staging 的 SSH 用 2222 端口,密钥在 ~/.ssh/staging。
126
131
 
127
132
  ```json
128
133
  {
129
- "enabled": true,
130
134
  "memoryDir": "~/.pi/memory",
131
135
  "memIndexMaxLines": 200,
132
136
  "memIndexMaxBytes": 25600,
@@ -157,7 +161,6 @@ staging 的 SSH 用 2222 端口,密钥在 ~/.ssh/staging。
157
161
 
158
162
  | 键 | 默认值 | 说明 |
159
163
  |-----|---------|------|
160
- | `enabled` | `true` | 整个记忆系统的开关。**启动时读一次**,改动需重启会话 |
161
164
  | `memoryDir` | `~/.pi/memory` | 所有记忆数据的根目录 |
162
165
  | `memIndexMaxLines` | `200` | 写入口径:`MEMORY.md` 的最大非空行数(`# Memory Index` 头行与手写标题同样占额度,所以并不等于记忆条数) |
163
166
  | `memIndexMaxBytes` | `25600` | 写入口径:`MEMORY.md` 的最大字节数 |
@@ -192,17 +195,29 @@ staging 的 SSH 用 2222 端口,密钥在 ~/.ssh/staging。
192
195
 
193
196
  headless 会话默认落在 `<项目记忆目录>/sessions/` —— 在项目记忆目录里,不在你的工作副本里。
194
197
 
198
+ ### 禁用本扩展
199
+
200
+ pi-memory 没有自己的包级开关 —— `memory.json` 里原来的顶层 `enabled` 键**已移除**,写了也会被忽略。禁用它与其他 pi package 一样,靠「不加载这个扩展」:
201
+
202
+ 只在本项目禁用(项目根目录的 `.pi/settings.json`,仅在项目被信任后读取):
203
+
204
+ ```json
205
+ { "packages": [{ "source": "npm:@yandy0725/pi-memory", "extensions": [] }] }
206
+ ```
207
+
208
+ 项目条目会替换个人条目,因此该项目不会加载本扩展。全局禁用:`pi remove npm:@yandy0725/pi-memory`,或用 `pi config` 关闭该包的资源(项目 scope 会写成 `autoload: false` + `extensions: ["-index.ts"]`)。扩展保持加载时,仍可用模块级开关(`autoSurfacing.enabled` / `extractMemories.enabled`)单独关掉某个行为。
209
+
195
210
  ## 模型配置
196
211
 
197
212
  会执行的任务必须能解析出模型 —— **既没有随包默认值,也没有父会话模型回退**。`defaults.model` 可以满足全部任务;各任务自己的 `model`(`dream.model` / `extractMemories.model` / `autoSurfacing.model`)优先于它。
198
213
 
199
214
  | 任务 | 何时必需 |
200
215
  |------|---------|
201
- | `dream` | 记忆系统开启(`enabled: true`)时**恒**需要 |
216
+ | `dream` | **恒**需要 —— 每个会话启动时都校验 |
202
217
  | `extractMemories` | `extractMemories.enabled` 为真时 |
203
218
  | `autoSurfacing` | `autoSurfacing.enabled` 为真时 |
204
219
 
205
- `enabled: false` 时什么都不跑(`/dream` 与提醒也被挡住),因此不需要任何模型。`session_start` 会把每个必需模型拿到注册表里解析;只要有缺失或解析不出的,就**不初始化任何东西**:弹一条 error 通知 `pi-memory config error:` + 每个问题一行 `- <error>`,`/memory` 则报 `Memory: misconfigured` + `Dir: not initialized` + 同样的行。两条错误文案:
220
+ `session_start` 会把每个必需模型拿到注册表里解析;只要有缺失或解析不出的,就**不初始化任何东西**:弹一条 error 通知 `pi-memory config error:` + 每个问题一行 `- <error>`,`/memory` 则报 `Memory: misconfigured` + `Dir: not initialized` + 同样的行。两条错误文案:
206
221
 
207
222
  - `no model for <task> — set "<task>.model" or "defaults.model" in memory.json`
208
223
  - `model "<value>" for <task> is not resolvable (unknown id or missing credentials)`
@@ -312,7 +327,6 @@ memory(action: "add" | "replace" | "remove" | "list" | "search",
312
327
  状态输出:
313
328
 
314
329
  ```
315
- Memory: enabled
316
330
  Dir: /home/you/.pi/memory/git/github.com__owner__repo
317
331
  Index: 38/200 lines, 2841/25600 bytes, 1 unrecognized lines
318
332
  Inject: 39/50 lines, 2841/16384 bytes
@@ -324,9 +338,8 @@ Lock: free
324
338
 
325
339
  - `Index` 用**写入**口径(`memIndexMax*`),并报告索引里有多少非空行解析不出(`# Memory Index` 头行与手写标题会计入)。CRLF(以及单独的 CR)行尾在解析前就被归一为 LF,下一次写入也一律输出 LF,因此被 Windows 编辑器改过行尾的 `MEMORY.md` **不会**推高这个计数。注入侧同样做归一:CRLF 文件不会把 `\r` 送进 system prompt。
326
340
  - `Inject` 用**注入**口径(`memIndexInjectMax*`),统计窗口内的行数与字节数 —— 即真正会进 `memory_index` section 的索引文本(截断标记本身不计入)。它与真正注入的值由同一份窗口代码算出来,不可能漂移。注意两行的口径不同:`Index` 数的是**非空**行,`Inject` 数的是窗口内的**全部**行,所以规范索引(LF 行尾、以换行结尾、头部后有且仅有一个空行)下 `Inject` 会比 `Index` 多一行而字节数相同。system prompt 里的值是**会话内冻结**的(见[为什么索引是冻结的](#为什么索引是冻结的)):`session_start` 之后写入的记忆会立刻出现在 `Index`,但要等 compaction 或下一个会话才出现在 `Inject`。
327
- - `Modules` 报三个模型驱动功能的激活状态:`on(<生效模型>)` / `off`。生效模型 = 该任务自己的 `model`,没有则用 `defaults.model`。`dream` 没有独立开关 —— memory 系统启用它就可用。
341
+ - `Modules` 报三个模型驱动功能的激活状态:`on(<生效模型>)` / `off`。生效模型 = 该任务自己的 `model`,没有则用 `defaults.model`。`dream` 没有独立开关 —— 健康会话里它始终可用。
328
342
  - `Lock` 有三种:`free`、`held by <op> (pid N on <hostname>, started <ISO>)`、`unreadable — run /memory unlock`。`/memory unlock` 的确认框会显示同一行持有者信息。
329
- - 以 `enabled: false` 启动的会话在启动时不初始化任何东西:`/memory` 报两行(`Memory: disabled` + `Dir: not initialized — set "enabled": true in memory.json and restart`);会话中途无法开启;`/memory unlock` 不需要 store 也能用。
330
343
  - 必需模型缺失或解析不出时不初始化任何东西,`/memory` 报 `Memory: misconfigured` + `Dir: not initialized` + 每行一条 `- <error>`;同样的错误在 session_start 时以 error 通知出现。
331
344
 
332
345
  ### `/dream`
package/index.ts CHANGED
@@ -71,7 +71,7 @@ function countInjectedBlocks(content: string): number {
71
71
  /**
72
72
  * `/memory` 的模块激活状态一行:`dream=on(model) extractMemories=off autoSurfacing=on(model)`。
73
73
  *
74
- * `dream` 没有独立开关 —— memory 系统启用(能走到状态分支)它就可用;另外两个直接反映各自的
74
+ * `dream` 没有独立开关 —— 健康会话里(能走到状态分支)它就可用;另外两个直接反映各自的
75
75
  * `enabled`。模型是**生效值**(per-task 优先,其次 `defaults.model`),关闭的模块不显示模型:
76
76
  * 用户问「为什么没生效」时答案在开关上,而不在模型上。
77
77
  *
@@ -166,11 +166,8 @@ export default function (pi: ExtensionAPI) {
166
166
  */
167
167
  const inFlight = new Set<Promise<unknown>>();
168
168
 
169
- /** `enabled: false` 时给用户的唯一动作。工具文案与 `/memory` 状态共用同一份,避免两处漂移。 */
170
- const ENABLE_HINT = 'set "enabled": true in memory.json and restart';
171
-
172
169
  /**
173
- * 清空本 session 的运行时状态。三条早退路径(disabled / 配置错误 / 初始化失败)共用:
170
+ * 清空本 session 的运行时状态。两条早退路径(配置错误 / 初始化失败)共用:
174
171
  * 残留上一 session 的 store 会让后续写入落到别的项目目录(Plan C ledger R51)。
175
172
  */
176
173
  function resetSessionState(): void {
@@ -207,9 +204,7 @@ export default function (pi: ExtensionAPI) {
207
204
  getUnavailableMessage: () =>
208
205
  configError
209
206
  ? `Memory not initialized — ${configError.split("\n")[0]}; run /memory for details`
210
- : config?.enabled === false
211
- ? `Memory is disabled — ${ENABLE_HINT}`
212
- : null,
207
+ : null,
213
208
  searchSessions,
214
209
  cwd: () => currentCwd,
215
210
  };
@@ -217,12 +212,12 @@ export default function (pi: ExtensionAPI) {
217
212
  /**
218
213
  * 建立本 session 的记忆运行时:目录 → store → 索引来源 → 注册工具。
219
214
  *
220
- * 只在 `session_start` 调用,且调用方已确认 `config.enabled`(中途启用路径已随 `/memory on` 删除)。
215
+ * 只在 `session_start` 调用,且调用方已通过模型校验。
221
216
  * 抛错 = 初始化失败,由调用方转成配置错误态(`configError`)。
222
217
  * `reason` 只有 `session_start` 会传:resume / fork / reload 用 transcript 的录制值(D14)。
223
218
  */
224
219
  async function initMemory(ctx: ExtensionContext, reason?: string): Promise<void> {
225
- // biome-ignore lint/style/noNonNullAssertion: 调用方已确认 enabled
220
+ // biome-ignore lint/style/noNonNullAssertion: 调用方已通过模型校验
226
221
  const cfg = config!;
227
222
  currentCwd = ctx.cwd;
228
223
  const dir = await resolveMemoryDir(cfg, ctx.cwd);
@@ -256,12 +251,14 @@ export default function (pi: ExtensionAPI) {
256
251
  }
257
252
  }
258
253
  pi.on("session_start", async (event, ctx) => {
259
- // 复位必须在**任何可能抛错的调用之前**跑完:冷启动与 disabled 会话都要有干净的一次失败通知
254
+ // 复位必须在**任何可能抛错的调用之前**跑完:冷启动与配置错误会话都要有干净的一次失败通知
260
255
  // 配额、干净的错误态,以及**清空的运行时**。loadConfig(里面的 getAgentDir)与下面的
261
256
  // modelConfigErrors(宿主给的 registry 可能既没有 getAvailable 也没有 getAll)都属于宿主契约
262
257
  // 之外的部分:它们一旦抛出而复位还没跑,上一 session 的 store / memoryDir 就会留在**已经注册**
263
258
  // 的 `memory` 工具背后 —— 项目 B 的 agent 能写进项目 A 的目录(Plan C ledger R51)。
264
259
  extractErrorNotified = false;
260
+ // `configError = null` 是纯防御:所有把 store 置空的路径都会经 failConfig 覆写它,所以这次复位
261
+ // 本身不可观测 —— 没有测试钉它,删掉也不会让任何用例变红(2026-10-03 final review 的结论)。
265
262
  configError = null;
266
263
  resetSessionState();
267
264
  // `loadConfig` 抛错(`getAgentDir` / `isProjectTrusted` 属宿主契约)与初始化失败同一处理:
@@ -275,8 +272,6 @@ export default function (pi: ExtensionAPI) {
275
272
  return;
276
273
  }
277
274
  config = loaded;
278
- // 状态已经干净,disabled 直接早退即可。
279
- if (!loaded.enabled) return;
280
275
  try {
281
276
  // 启动校验(spec §2.3):模型键缺失 / 不可解析 → 本会话**完全不初始化**
282
277
  //(不解析目录、不建 store、不注册工具),错误态由 `/memory` 重复显示。
@@ -355,7 +350,7 @@ export default function (pi: ExtensionAPI) {
355
350
  lastSystemPrompt = event.systemPrompt;
356
351
  // 先拷到 const:`store` 是工厂作用域的 let,在异步回调里 TS 不会保留它的外层收窄。
357
352
  const activeStore = store;
358
- if (!config?.enabled || !memoryDir || !activeStore) return;
353
+ if (!config || !memoryDir || !activeStore) return;
359
354
 
360
355
  // 索引 section:**每一轮无条件**写入冻结值(含 resume / fork / reload)。
361
356
  // 省略这个键 = pi 的 diffSystemPromptSections 生成 { memory_index: null } = 把索引从
@@ -424,7 +419,7 @@ export default function (pi: ExtensionAPI) {
424
419
  // 的边际缓存损失最小,而长会话到这时候往往已经攒下了新记忆(spec §9.1(c) / §10)。
425
420
  pi.on("session_compact", async () => {
426
421
  const activeStore = store;
427
- if (!config?.enabled || !activeStore) return;
422
+ if (!config || !activeStore) return;
428
423
  // compaction 会把已注入的内容挤出上下文:不清空,这些 entry 本会话再也不会浮现。
429
424
  injectedFiles.clear();
430
425
  indexSnapshot = await buildIndexSection(
@@ -456,7 +451,7 @@ export default function (pi: ExtensionAPI) {
456
451
  // 先拷到 const:`store` / `memoryDir` 是工厂作用域的 let,在异步回调里 TS 不保留外层收窄。
457
452
  const activeStore = store;
458
453
  const dir = memoryDir;
459
- if (!config?.enabled || !dir || !activeStore) return;
454
+ if (!config || !dir || !activeStore) return;
460
455
  const extractConfig = config.extractMemories;
461
456
  if (!extractConfig?.enabled) return;
462
457
  if (!event.messages || event.messages.length === 0) return;
@@ -541,7 +536,7 @@ export default function (pi: ExtensionAPI) {
541
536
  return;
542
537
  }
543
538
  if (args === "unlock") {
544
- // `unlock` 只需要**目录**、不需要 store:以 disabled 启动的会话也要能清锁
539
+ // `unlock` 只需要**目录**、不需要 store:以配置错误启动的会话也要能清锁
545
540
  //(它是崩溃遗留 `.lock` 的唯一人工入口,spec §19)。
546
541
  let dir = memoryDir;
547
542
  if (!dir) {
@@ -564,11 +559,10 @@ export default function (pi: ExtensionAPI) {
564
559
  const activeStore = store;
565
560
  const dir = memoryDir;
566
561
  if (!dir || !activeStore) {
567
- // configError 非空 = 校验或初始化失败(`/memory` 是用户重读错误的唯一入口);
568
- // 否则只可能是配置里 enabled 为假。
569
- const lines = configError
570
- ? ["Memory: misconfigured", "Dir: not initialized", ...configError.split("\n").map((e) => `- ${e}`)]
571
- : ["Memory: disabled", `Dir: not initialized — ${ENABLE_HINT}`];
562
+ // `config` 非 null 而 store 为 null 只可能来自 session_start 的两条早退路径,
563
+ // 因此 configError 在这里必非空;`/memory` 是用户重读错误态的唯一入口。
564
+ const lines = ["Memory: misconfigured", "Dir: not initialized"];
565
+ if (configError) lines.push(...configError.split("\n").map((e) => `- ${e}`));
572
566
  ctx.ui.notify(lines.join("\n"), "info");
573
567
  return;
574
568
  }
@@ -581,7 +575,6 @@ export default function (pi: ExtensionAPI) {
581
575
  const cap = indexCapacity(indexRaw, config.memIndexMaxLines, config.memIndexMaxBytes);
582
576
  const inject = indexInjectionCapacity(indexRaw, config.memIndexInjectMaxLines, config.memIndexInjectMaxBytes);
583
577
  const summary = [
584
- `Memory: ${config.enabled ? "enabled" : "disabled"}`,
585
578
  `Dir: ${dir}`,
586
579
  `Index: ${cap.lineCount}/${config.memIndexMaxLines} lines, ${cap.byteLength}/${config.memIndexMaxBytes} bytes, ${parseEntryIndex(indexRaw).unrecognized} unrecognized lines`,
587
580
  `Inject: ${inject.lineCount}/${config.memIndexInjectMaxLines} lines, ${inject.byteLength}/${config.memIndexInjectMaxBytes} bytes`,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "2.3.0",
6
+ "version": "2.4.0",
7
7
  "description": "File-system driven persistent memory layer for pi coding agent",
8
8
  "license": "MIT",
9
9
  "repository": {
package/src/config.ts CHANGED
@@ -47,7 +47,6 @@ export interface ExtractMemoriesConfig {
47
47
  }
48
48
 
49
49
  export interface MemoryConfig {
50
- enabled: boolean;
51
50
  /** Shared defaults for model and sessionPersistence. Per-task configs override. */
52
51
  defaults?: DefaultsConfig;
53
52
  memoryDir: string;
@@ -87,7 +86,6 @@ export interface MemoryConfig {
87
86
  }
88
87
 
89
88
  export const DEFAULT_CONFIG: MemoryConfig = {
90
- enabled: true,
91
89
  // headless 子会话默认只在内存里跑:extract / dream / 侧查询都不该往用户的 sessions 目录里落盘。
92
90
  // **没有模型默认值**:model 必须由用户显式配置(defaults.model 或 per-task),否则 session_start 报错。
93
91
  defaults: { sessionPersistence: { enabled: false } },
@@ -128,12 +126,8 @@ export function taskModel(cfg: MemoryConfig, task: ModelTask): string | undefine
128
126
  return cfg[task]?.model ?? cfg.defaults?.model;
129
127
  }
130
128
 
131
- /**
132
- * 会执行的任务及其模型值。`enabled: false` 的会话不执行任何一个任务 —— 包括 dream,
133
- * 因为 `/dream` 命令与 nudge 都被 `config.enabled` 挡住。
134
- */
129
+ /** 会执行的任务及其模型值。dream 恒在执行集合内(没有包级开关可以让它不跑)。 */
135
130
  export function requiredModels(cfg: MemoryConfig): Array<{ task: ModelTask; value: string | undefined }> {
136
- if (!cfg.enabled) return [];
137
131
  const out: Array<{ task: ModelTask; value: string | undefined }> = [
138
132
  { task: "dream", value: taskModel(cfg, "dream") },
139
133
  ];
@@ -69,7 +69,7 @@ export interface MemoryToolDeps {
69
69
  getConfig: () => MemoryToolConfig;
70
70
  /**
71
71
  * `getStore()` 为 null 时的完整可读原因(由 index.ts 依会话状态拼好),null = 还没 `session_start`。
72
- * 三种状态:未启动 / 配置错误(模型校验或初始化失败)/ `enabled: false` 的禁用会话。
72
+ * 两种状态:未启动 / 配置错误(模型校验或初始化失败)。
73
73
  */
74
74
  getUnavailableMessage: () => string | null;
75
75
  searchSessions: (cwd: string, query: string, cfg: { maxSessions: number; maxMatches: number }) => Promise<string>;
@@ -200,7 +200,7 @@ export function createMemoryTool(deps: MemoryToolDeps, options: MemoryToolOption
200
200
  async execute(_id: string, params: any, _signal: AbortSignal | undefined, _onUpdate: any, ctx: any) {
201
201
  const store = deps.getStore();
202
202
  if (!store) {
203
- // 健康会话之后,同一进程内可能又起了一个配置错误或禁用的会话(pi 没有 unregister API),
203
+ // 健康会话之后,同一进程内可能又起了一个配置错误的会话(pi 没有 unregister API),
204
204
  // 工具仍在注册表里:此时「no session_start yet」是假话。原因文案由 index.ts 依状态拼好 ——
205
205
  // 工具只负责原样抛出,不猜状态。
206
206
  throw new Error(deps.getUnavailableMessage() ?? "Memory not initialized (no session_start yet)");