sage-mem 2.10.1 → 2.10.2

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.en.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  **Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.**
14
14
 
15
- **Current release: `2.10.1`** — the npm badge above is the dynamic source of truth. **Contract baseline: `1.0.0`** — the first stable release, whose public commitments, the capability boundaries and the list of things it explicitly cannot do are frozen in the
15
+ **Current release: `2.10.2`** — the npm badge above is the dynamic source of truth. **Contract baseline: `1.0.0`** — the first stable release, whose public commitments, the capability boundaries and the list of things it explicitly cannot do are frozen in the
16
16
  [frozen contract](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md).
17
17
 
18
18
  If it is useful to you, a Star is the most direct feedback.
@@ -75,7 +75,7 @@ Most memory schemes take the "SQLite + a long-running worker" route: once memori
75
75
  | **The model can look things up too** | Three tools are also given to the model (`search_memory` / `read_memory`, plus the disk-writing `trim_memory`), so it can go through memories on its own, read the original text by line, and cut a stale section out into the archive |
76
76
  | **Writes are not overwritten** | While you are editing and the model is changing things at the same time, the later write does not silently clobber the earlier one; the conflict is left as a pending record for you to decide which side to keep |
77
77
  | **Zero extra services** | No worker, no SQLite, no port, no long-running process, and no extra API key or cloud service (the only runtime dependency is the ordinary npm library `zod`) |
78
- | **Anti-bloat** | Rules guide the model in judging "is this worth storing": nothing without substance is stored, nothing derivable by reading the code / git is stored; the ones that did grow are flagged and dealt with along the 8 KB / 16 KB lines (see [size governance](#size-governance-what-to-do-when-a-memory-grows-too-big)) |
78
+ | **Anti-bloat** | Rules guide the model in judging "is this worth storing": nothing without substance is stored, nothing derivable by reading the code / git is stored; the ones that did grow are flagged and dealt with along the 16 KB / 40 KB lines (see [size governance](#size-governance-what-to-do-when-a-memory-grows-too-big)) |
79
79
  | **Tidyable** | The optional "autodream" reads your memories through once: merges duplicates, fixes the index and broken links, and creates a rollback point before changing anything |
80
80
  | **Migration from Claude Code** | The persona (`CLAUDE.md` → `AGENTS.md`) is copied as-is; memories just need organizing into `.md` files under four types (see [the next section](#migrating-from-claude-code)) |
81
81
  | **Ships its own usage guide** | The package carries a skill (`skills/sage-mem/`): what a memory looks like, the four types, what not to store, the frontmatter pitfalls, how retrieval actually works, how to audit and fix a broken index. Installing the plugin is enough — the agent gets the instructions, no manual setup |
@@ -192,7 +192,7 @@ Since 1.9.0 the memory-management surface follows the harness language (**Settin
192
192
 
193
193
  ![Health: conflicts that need your call on top, machine-scanned structural problems below](https://raw.githubusercontent.com/gezi-wen/sage-mem/main/docs/images/audit.png)
194
194
 
195
- *Health — one screen answers "is anything wrong with this library": the **conflict queue on top** (a finite list that needs your decision: which of two contradictory statements is void) and the **audit below** (dangling index entries, missing index entries, broken links and the rest, reported in red by category). "Points to an archived memory" is only a hint — it does not count as a problem, and there is **no one-click fix**: which one to change is your call. The audit only reports and never modifies. Since 2.10.1 it has two size categories: **over-size memories** (16 KB or more counts as a problem; 8–16 KB is only a hint) and an **over-long index** (report only, see [size governance](#size-governance-what-to-do-when-a-memory-grows-too-big)). When there is really something to deal with, the badge gets a **「复制修复提示词」(copy fix prompt)** button next to it (2.6.0): it turns what this screen needs into one prompt in your clipboard — **the audit problems and the pending conflicts are written as two separate sections** (fix these / only advise on those), so you can paste it to the agent and let it work. A receipt appears at the bottom afterwards — it closes itself in 10 seconds, closes as soon as you switch tabs, and you can dismiss it by hand.*
195
+ *Health — one screen answers "is anything wrong with this library": the **conflict queue on top** (a finite list that needs your decision: which of two contradictory statements is void) and the **audit below** (dangling index entries, missing index entries, broken links and the rest, reported in red by category). "Points to an archived memory" is only a hint — it does not count as a problem, and there is **no one-click fix**: which one to change is your call. The audit only reports and never modifies. Since 2.10.1 it has two size categories: **over-size memories** (40 KB or more counts as a problem; 16–40 KB is only a hint — 2.10.2 raised both lines from 8/16 KB) and an **over-long index** (report only, see [size governance](#size-governance-what-to-do-when-a-memory-grows-too-big)). When there is really something to deal with, the badge gets a **「复制修复提示词」(copy fix prompt)** button next to it (2.6.0): it turns what this screen needs into one prompt in your clipboard — **the audit problems and the pending conflicts are written as two separate sections** (fix these / only advise on those), so you can paste it to the agent and let it work. A receipt appears at the bottom afterwards — it closes itself in 10 seconds, closes as soon as you switch tabs, and you can dismiss it by hand.*
196
196
 
197
197
  *Lab (2.9.0, sits after Health) — only switches that change **which memories are picked**, all off by default. Today it holds one: **MMR de-duplication**. When one topic takes every injection slot, it subtracts each candidate's similarity to what is already picked before choosing. It only changes **which items are picked** (never the score, never a file, never a pinned baseline memory) and returns candidates as-is when they fit the slots. Why it is off by default: in the offline comparison it never fired (the IDF gate already keeps candidates under 10), and in a stress run with the gate disabled duplication fell only 4.5% while wrong picks rose 24% — it knows "different", not "irrelevant". Read the three notes on that page before turning it on; **if memories seem to go missing, switch it off and restart**.
198
198
 
@@ -350,14 +350,26 @@ If the memory directory cannot be read, or permissions are wrong → **it degrad
350
350
 
351
351
  ## Size Governance: What to Do When a Memory Grows Too Big
352
352
 
353
- Memories are files, and files only ever grow — while injection is capped (only the first 1500 characters of a memory are read by default), so whatever is written past that cap is, for your future self, as good as absent. "Too big" therefore needs a way out, and it cannot rely on anyone remembering to take it.
353
+ Memories are files, and files only ever grow — so "too big" needs a way out, and it cannot rely on anyone remembering to take it.
354
+
355
+ **First, a correction to something the previous version got wrong.** The 2.10.1 notes said injection only reads the first 1500 characters, so whatever is written past that cap is, for your future self, as good as absent. That holds in exactly two cases: `baseline` memories (they never go through paragraph replacement, so they are still read from the start and still cut off at the single-item character limit), and an injection that only hits the summary and never the body. Otherwise the body is injected **in 600-character blocks**, selected by relevance — the relevant passage of a large file is still picked, so **size does not affect what gets matched** (the mechanism is in [Retrieval and Injection](#retrieval-and-injection)).
356
+
357
+ What size does affect is three other things:
358
+
359
+ - **Reading the full text means putting all of it into the context.** `read_memory` has no paging, so a large file arrives in one piece.
360
+ - **Autodream costs more.** The more over-line entries a run has to read through, the more time and tokens that run spends.
361
+ - **Editing gets riskier.** The longer the file, the greater the chance a trim or a compression changes the wrong place or drops a fact that is still in force.
362
+
363
+ And one more thing that is not a cost but is in itself a signal: **the longer a memory is, the more likely it is an un-tidied pile.**
364
+
365
+ So the point of the lines is not "letting the tail into the context" — the relevant part of the tail gets in anyway. The point is those three costs, and that signal. 2.10.2 raising them to 16 KB / 40 KB uses the same reasoning: at 8 KB, an ordinary long memory trips the warning on nearly every write, and that is not a hint, it is noise.
354
366
 
355
367
  **Three lines** (defined in exactly one place, `lib/shared/limits.js`; the UI, the audit and the tools all read that one copy):
356
368
 
357
369
  | Quantity | Value | Who acts on it |
358
370
  |---|---|---|
359
- | Warning line | **8 KB** per memory | one line in the write log, one `ℹ` line in the `read_memory` receipt, listed as a hint by the audit |
360
- | Force line | **16 KB** per memory | counts as an audit problem; autodream has to deal with it in that run |
371
+ | Warning line | **16 KB** per memory | one line in the write log, one `ℹ` line in the `read_memory` receipt, listed as a hint by the audit |
372
+ | Force line | **40 KB** per memory | counts as an audit problem; autodream has to deal with it in that run |
361
373
  | Index line | `MEMORY.md` **240 characters** per line / **24 KB** whole file | **report only**: the index should be slimmed, but which entry to drop is your decision |
362
374
 
363
375
  **Two remedies, both leaving a trail**:
@@ -598,7 +610,7 @@ Module boundaries (since 1.2.0):
598
610
  - `lib/recall/` — the injection chain: selection (`select`) / rendering and budget (`compose`) / text (`text`) / session state (`state`)
599
611
  - `lib/assembly/` — the four mounts: autodream / retrieval tools / observation / write gate
600
612
  - `lib/memory/` — pure memory-layer functions and file operations. Size governance is three separate things inside it: `memory/size-policy` (the **pure verdict**: three tiers + advice + that one sentence of plain language), `memory/trim-plan` (the **pure split**: turning a selection into "archived file + remaining body + pointer", with six invariants), and `memory/trim-archive` (the **single** place that writes a cut: exclusive write of the archived file first, then a revision-guarded write of the main file, and if that fails the archived copy is retired to `.retired-*` rather than deleted); `lib/autodream/` — the autodream engine, where `autodream/size-tools` is the thin model-facing adapter (it only forwards `archive_segment` to `trim-archive` and never re-implements the disk order)
601
- - `lib/shared/` — zero-dependency cross-layer rules: file-name rule / local timestamps / the size lines (the 512 KB hard ceiling plus the three governance lines: 8 KB, 16 KB, and the index's 240 characters / 24 KB. **Exactly one copy each**; `test/duplication-test.mjs` turns red otherwise)
613
+ - `lib/shared/` — zero-dependency cross-layer rules: file-name rule / local timestamps / the size lines (the 512 KB hard ceiling plus the three governance lines: 16 KB, 40 KB, and the index's 240 characters / 24 KB. **Exactly one copy each**; `test/duplication-test.mjs` turns red otherwise)
602
614
  - UI side: `client/*.js` is split per screen (`05-shared.js` holds the cross-screen primitives and type tables); `npm run build:client` concatenates them into `lib/client.js`
603
615
  - Dependency direction: `memory` never imports `autodream`, `recall` depends only on `memory`, `assembly` never imports the composition root (`test/structure-test.mjs` turns red if that changes)
604
616
 
@@ -643,7 +655,7 @@ No. Serialized writes guarantee only **in-process** ordering; there is no file l
643
655
  Yes, it is off by default. It and archiving are two different things: archiving is a deterministic rule (by idle days), calls no model and costs nothing.
644
656
 
645
657
  **What do I do when a memory file keeps growing?**
646
- Start with the **over-size memories** section under Health: 16 KB or more must be dealt with, 8–16 KB is only a hint. There are exactly two remedies — if it splits, **archive a segment** (the stale section moves into `archive/` and the main file keeps one pointer line with the reason at the original spot, recoverable at any time); if it does not split, **compress** it (conclusion first, de-duplicate, drop what has been overturned). The model has `trim_memory` to cut directly, and autodream handles anything over 16 KB on every run. If you do not want certain files touched, check them under 「自动整理 → 设置 → 不参与整理的文件」(Automation → Settings → files excluded from tidying). The index `MEMORY.md` is report-only and never rewritten automatically; `session-log.md` is out of scope. Details in [size governance](#size-governance-what-to-do-when-a-memory-grows-too-big).
658
+ Start with the **over-size memories** section under Health: 40 KB or more must be dealt with, 16–40 KB is only a hint. There are exactly two remedies — if it splits, **archive a segment** (the stale section moves into `archive/` and the main file keeps one pointer line with the reason at the original spot, recoverable at any time); if it does not split, **compress** it (conclusion first, de-duplicate, drop what has been overturned). The model has `trim_memory` to cut directly, and autodream handles anything over 40 KB on every run. If you do not want certain files touched, check them under 「自动整理 → 设置 → 不参与整理的文件」(Automation → Settings → files excluded from tidying). The index `MEMORY.md` is report-only and never rewritten automatically; `session-log.md` is out of scope. Details in [size governance](#size-governance-what-to-do-when-a-memory-grows-too-big).
647
659
 
648
660
  **The plugin market says "the registry's latest is older than the installed version, updating would downgrade"?**
649
661
  That is a **stale `/latest` cache on the download-region mirror**, not a mis-published package: the mirror's full metadata (the document pnpm reads) already has the new version, while the `/<pkg>/latest` shortcut still serves an old one. Retry after a minute or two; if it persists, temporarily switch the market's download region to "Global" (the origin registry, registry.npmjs.org), update, then switch back. To check what is actually published: `npm view sage-mem dist-tags`.
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  > File-based cross-session memory for DeepSeek Harness — every memory is a plain markdown file you can open, edit and delete. No database, no worker, no port.
16
16
 
17
- **当前发布版本:`2.10.1`** —— 顶部 npm 徽章是动态权威。**契约基线:`1.0.0`** —— 第一个稳定版,公开承诺、能力边界与「明确做不到」的清单冻结在
17
+ **当前发布版本:`2.10.2`** —— 顶部 npm 徽章是动态权威。**契约基线:`1.0.0`** —— 第一个稳定版,公开承诺、能力边界与「明确做不到」的清单冻结在
18
18
  [契约冻结](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md)。
19
19
 
20
20
  如果它对你有用,给个 Star 是最直接的反馈。
@@ -77,7 +77,7 @@ sage-mem 补上这一层,做法是:**把该记住的事写成一张张卡片
77
77
  | **模型也能自己查** | 另给模型三个工具(`search_memory` / `read_memory`,以及会写盘的 `trim_memory`),它可以主动翻记忆、按行读原文、把已经过时的一节裁去归档 |
78
78
  | **写入不被覆盖** | 你正在编辑、模型同时在改的时候,后写的不会静默盖掉先写的;冲突留成待处理记录,由你决定保留哪边 |
79
79
  | **零额外服务** | 无 worker、无 SQLite、无端口、无常驻进程,不需要额外的 API key 或云服务(运行时唯一的依赖是普通 npm 库 `zod`) |
80
- | **防膨胀** | 靠规则引导模型判断「值不值得存」:无实质内容的不存,靠读代码 / git 能推出来的不存;已经长大的那几条按 8 KB / 16 KB 两条线提醒与处置(见[体积治理](#体积治理超线了怎么办)) |
80
+ | **防膨胀** | 靠规则引导模型判断「值不值得存」:无实质内容的不存,靠读代码 / git 能推出来的不存;已经长大的那几条按 16 KB / 40 KB 两条线提醒与处置(见[体积治理](#体积治理超线了怎么办)) |
81
81
  | **可整理** | 可选的「自动做梦」把记忆通读一遍:合并重复、修索引与断链,改动前先建回滚点 |
82
82
  | **从 Claude Code 迁移** | 人格(`CLAUDE.md` → `AGENTS.md`)照搬;记忆按四类整理成 `.md` 即可(见[下一节](#从-claude-code-迁移)) |
83
83
  | **自带使用指南** | 包内携带一份技能(`skills/sage-mem/`):记忆长什么样、四类怎么分、什么不该存、frontmatter 的坑、检索怎么工作、索引坏了怎么修。装上插件就够了 —— Agent 拿到说明书,不需要手工配置 |
@@ -192,7 +192,7 @@ SAGE_MEM_DIR=/path/to/your/memory
192
192
 
193
193
  ![体检:独立一页,硬问题按类报红分节](https://raw.githubusercontent.com/gezi-wen/sage-mem/main/docs/images/audit.png)
194
194
 
195
- *库健康 —— 一屏回答「这个库有没有事」:**冲突队列在上**(要你裁决的有限队列:两条说法互斥,选哪一方作废),**体检在下**(索引悬空、漏索引、双链断链等结构问题按类报红)。「指向已归档」只作提示、不计入问题;也**不提供一键修补** —— 改哪条由你决定。体检只报告、不修改。2.10.1 起体检多两类体积审计:**超线体积**(单条 ≥ 16 KB 计入问题、8–16 KB 只列提示)与**索引过长**(只报不改,见[体积治理](#体积治理超线了怎么办))。真有事要处理时,徽标后面会多一颗**「复制修复提示词」**(2.6.0):把这一屏要处理的事整理成一段提示词放进剪贴板 —— **体检问题与待裁决冲突分两段写**(前者照清单修、后者只给建议),粘给智能体就能照着办;复制成功后下方弹一条提示,10 秒自收、换页签即收、也能自己关掉。*
195
+ *库健康 —— 一屏回答「这个库有没有事」:**冲突队列在上**(要你裁决的有限队列:两条说法互斥,选哪一方作废),**体检在下**(索引悬空、漏索引、双链断链等结构问题按类报红)。「指向已归档」只作提示、不计入问题;也**不提供一键修补** —— 改哪条由你决定。体检只报告、不修改。2.10.1 起体检多两类体积审计:**超线体积**(单条 ≥ 40 KB 计入问题、16–40 KB 只列提示;这两条线 2.10.2 从 8/16 KB 抬上来)与**索引过长**(只报不改,见[体积治理](#体积治理超线了怎么办))。真有事要处理时,徽标后面会多一颗**「复制修复提示词」**(2.6.0):把这一屏要处理的事整理成一段提示词放进剪贴板 —— **体检问题与待裁决冲突分两段写**(前者照清单修、后者只给建议),粘给智能体就能照着办;复制成功后下方弹一条提示,10 秒自收、换页签即收、也能自己关掉。*
196
196
 
197
197
  *实验性(2.9.0,排在库健康之后)—— 只给「会改变选出哪些记忆」的开关,默认全关。当前只有一项 **MMR 去重**:注入名额被同一件事的多份记忆占满时,先扣掉候选与已选中那些的相似度再挑。它**只改挑哪几条**(不改打分、不改文件、不碰锁定的 baseline 记忆),候选不够名额时原样返回。为什么默认关:离线对照里它一次都没触发(证据门已把候选压到 10 条以内),而关掉证据门的压力测试中重复度只降 4.5%、误注入却涨了 24% —— 它只知道「不像」,不知道「不相关」。打开前请读那一页的三条声明;**若觉得记忆变少了,关掉它、重启即可恢复**。*
198
198
 
@@ -350,14 +350,26 @@ sage-mem 给记忆目录的写入加了**版本守卫**:写入前记下当前
350
350
 
351
351
  ## 体积治理:超线了怎么办
352
352
 
353
- 记忆是文件,而文件只会越写越长 —— 注入却是有上限的(每条默认只读前 1500 字),所以超过上限之后写进去的内容,对未来的你等于不存在。「太大了」因此必须有出口,而且不能指望谁记得住。
353
+ 记忆是文件,而文件只会越写越长 —— 「太大了」因此必须有出口,而且不能指望谁记得住。
354
+
355
+ **先把上一版写错的一句话改对。** 2.10.1 的说明里写着「注入只读前 1500 字,所以超过上限之后写进去的内容,对未来的你等于不存在」。这句话只在两种情况下成立:`baseline` 记忆(它不吃段落替换,仍从开头读、受单条字数上限截断),以及某次注入只命中摘要、没命中正文的时候。其余情况下正文是**按 600 字一块选段**注入的 —— 一条大文件里相关的那段照样会被选中,**体积不影响命中**(机制见[检索与注入](#检索与注入))。
356
+
357
+ 体积真正影响的是另外三笔:
358
+
359
+ - **读全文就是整份进上下文。** `read_memory` 没有分页,一条大文件读一次就是整份。
360
+ - **自动做梦更贵。** 一趟要通读的超线条目越多,那趟花的时间与 token 越多。
361
+ - **编辑更容易出错。** 越长的文件,裁段与压缩时改错地方、碰掉还在生效的事实的风险越大。
362
+
363
+ 还有一条不算成本,但它本身就是信号:**越长,越可能是没整理过的堆积。**
364
+
365
+ 所以线的意义不是「让尾部能进上下文」—— 尾部的相关内容本来就能进。线的意义是上面这三笔成本,和这个信号。2.10.2 把线抬到 16 KB / 40 KB 也是同一个理由:8 KB 时一条正常的长条目几乎每次写入都在提醒,那种提醒不叫提示,叫噪音。
354
366
 
355
367
  **三条线**(唯一定义处 `lib/shared/limits.js`,界面、审计、工具读的是同一份):
356
368
 
357
369
  | 量 | 值 | 谁在管 |
358
370
  |---|---|---|
359
- | 提醒线 | 单条 **8 KB** | 写入日志里一行提醒、`read_memory` 回执里一行 `ℹ`、体检里列为提示 |
360
- | 强制线 | 单条 **16 KB** | 计入体检问题;自动做梦这一趟必须处置 |
371
+ | 提醒线 | 单条 **16 KB** | 写入日志里一行提醒、`read_memory` 回执里一行 `ℹ`、体检里列为提示 |
372
+ | 强制线 | 单条 **40 KB** | 计入体检问题;自动做梦这一趟必须处置 |
361
373
  | 索引线 | `MEMORY.md` 单行 **240 字符** / 整份 **24 KB** | **只报不改**:它该瘦身,但删哪个条目由你决定 |
362
374
 
363
375
  **两条处置,都必须留痕**:
@@ -600,7 +612,7 @@ memory/ 目录(markdown 文件,4 类)
600
612
  - `lib/recall/` —— 注入链:选材(`select`)/ 渲染与预算(`compose`)/ 文本(`text`)/ 会话状态(`state`)
601
613
  - `lib/assembly/` —— 四块挂载:autodream / 检索工具 / 观测 / 写入门
602
614
  - `lib/memory/` —— 记忆层的纯函数与文件操作。体积治理在其中是三件分开的东西:`memory/size-policy`(**纯判据**:三档 + 建议 + 那一句人话)、`memory/trim-plan`(**纯切分**:选段翻译成「归档件 + 剩余正文 + 指针」,六条不变量)、`memory/trim-archive`(**唯一**的裁段写盘处:先排他写归档件、再 revision 守卫改主文件、失败把归档件退役成 `.retired-*` 而不删);`lib/autodream/` —— 自动做梦引擎,其中 `autodream/size-tools` 是模型面的薄适配(只把 `archive_segment` 转调 `trim-archive`,自己不拼写盘逻辑)
603
- - `lib/shared/` —— 跨层零依赖的小判据:文件名规则 / 本地时间戳 / 体积线(512 KB 硬顶,加上体积治理的三条线:8 KB、16 KB、索引 240 字符与 24 KB。**只许一份**,`test/duplication-test.mjs` 会红)
615
+ - `lib/shared/` —— 跨层零依赖的小判据:文件名规则 / 本地时间戳 / 体积线(512 KB 硬顶,加上体积治理的三条线:16 KB、40 KB、索引 240 字符与 24 KB。**只许一份**,`test/duplication-test.mjs` 会红)
604
616
  - 界面侧:`client/*.js` 按屏分片(`05-shared.js` 放跨屏小基元与类型词表),`npm run build:client` 拼成 `lib/client.js`
605
617
  - 依赖方向:`memory` 不认识 `autodream`,`recall` 只依赖 `memory`,`assembly` 不反向依赖组合根(`test/structure-test.mjs` 会红)
606
618
 
@@ -646,7 +658,7 @@ npm test # 全部断言(含三条守卫)
646
658
  能,它默认就是关闭的。它和归档是两件事:归档是确定性规则(按闲置天数),不调模型、不花钱。
647
659
 
648
660
  **记忆文件越写越大怎么办?**
649
- 先看「库健康」里的**超线体积**一节:单条 ≥ 16 KB 必须处置,8–16 KB 只提示。处置只有两条路 —— 拆得开就**裁段归档**(旧的一节搬进 `archive/`,主文件原位置留一行带缘由的指针,随时捞得回),拆不开就**压缩**(结论前置、去重、删掉被推翻的)。模型手上有 `trim_memory` 可以直接裁,自动做梦每趟也会自己处理超过 16 KB 的条目。不想让某几个文件被碰,就在「自动整理 → 设置 → 不参与整理的文件」里勾上。索引 `MEMORY.md` 只报告、不自动改;`session-log.md` 不参与。详见[体积治理](#体积治理超线了怎么办)。
661
+ 先看「库健康」里的**超线体积**一节:单条 ≥ 40 KB 必须处置,16–40 KB 只提示。处置只有两条路 —— 拆得开就**裁段归档**(旧的一节搬进 `archive/`,主文件原位置留一行带缘由的指针,随时捞得回),拆不开就**压缩**(结论前置、去重、删掉被推翻的)。模型手上有 `trim_memory` 可以直接裁,自动做梦每趟也会自己处理超过 40 KB 的条目。不想让某几个文件被碰,就在「自动整理 → 设置 → 不参与整理的文件」里勾上。索引 `MEMORY.md` 只报告、不自动改;`session-log.md` 不参与。详见[体积治理](#体积治理超线了怎么办)。
650
662
 
651
663
  **插件市场报「registry 上的最新版比已装还旧,更新会降级」?**
652
664
  那是**下载地区镜像的 `/latest` 缓存滞后**,不是包发错了:镜像的全量元数据(pnpm 读的那份)已经是新版,但 `/<包>/latest` 这个快捷入口还缓存着旧值。先等一两分钟重试;仍然不行就把插件市场的「下载地区」临时切到「全球」(直连 registry.npmjs.org)更新,完成后再切回。核对实际发布版本:`npm view sage-mem dist-tags`。
@@ -423,7 +423,7 @@ export async function auditMemoryDir(dir, opts = {}) {
423
423
  * 两条判据上的硬约束,都是「不加就会催人去干坏事」的那种:
424
424
  * · **保护名单与非条目一律跳过**(`isReserved` / `isNonEntry`):它们本来就不许改,
425
425
  * 把它们算成「超线待处置」等于让人去动 `MEMORY.md` / `session-log.md` / 自己声明的单副本;
426
- * · **只有强制线(≥16 KB)进 problems**(见下面 `problems` 汇总):8 KB 只是提醒线,
426
+ * · **只有强制线(≥40 KB)进 problems**(见下面 `problems` 汇总):16 KB 只是提醒线,
427
427
  * 掺进去会让 `problems` 这个唯一健康数字变成「永远大于 0」的噪音 —— 与 validityHints
428
428
  * 同一条理由(issue #8 的教训)。
429
429
  * 索引单列:它压根不许改(本版只报不改),进 problems 只会挂一盏关不掉的红灯。
@@ -523,7 +523,7 @@ export function formatAudit(report, limit = 12) {
523
523
  */
524
524
  const overs = report.oversize ?? []
525
525
  const oversizeRows = [...overs.filter((r) => r.level === 'force'), ...overs.filter((r) => r.level !== 'force')]
526
- section('超线体积(≥16 KB 必须处置:能拆一节就 archive_segment,拆不开就 write_memory 压缩;8 KB 起只提醒)', oversizeRows, (r) =>
526
+ section('超线体积(≥40 KB 必须处置:能拆一节就 archive_segment,拆不开就 write_memory 压缩;16 KB 起只提醒)', oversizeRows, (r) =>
527
527
  `${r.file} ${sizeText(r.bytes)} · ${r.level === 'force' ? '必须处置' : '提醒'}`,
528
528
  )
529
529
  section('索引过长(只报不改:MEMORY.md 在保护名单里)', report.indexOversize ?? [], (r) =>
@@ -2,7 +2,8 @@
2
2
  * sage-mem autodream — 提示词。
3
3
  *
4
4
  * 四阶段结构:定位 → 收集 → 整合 → 修剪与索引。2.10.1 在「整合」与「修剪」之间插了
5
- * **阶段 3.5 体积治理**(超 16 KB 的条目必须裁段归档或压缩),所以实际是五节。
5
+ * **阶段 3.5 体积治理**(超 40 KB 的条目必须裁段归档或压缩,2.10.2 起线从 16 KB 抬到 40 KB),
6
+ * 所以实际是五节。
6
7
  *
7
8
  * 两个非显然的取舍:
8
9
  * 1. **写记忆的规范必须随 prompt 自带。** autodream 是一次独立调用,带不上外部的
@@ -98,8 +99,8 @@ export function buildAutodreamUserPrompt(opts) {
98
99
  const stage3_5 = apply
99
100
  ? `- **能拆出一个完整小节** → \`archive_segment\`:旧段进 \`archive/\`(可人工捞回),主文件原位置自动留一行指针
100
101
  - **拆不开** → \`write_memory\` 就地压缩:结论前置、去重、删掉已被推翻的说法;压缩后宿主会自动补压缩留痕,缘由用你自己的 \`reason\`
101
- - 动手前先跑 \`audit_memory\` —— 它会直接列出超过 16 KB 的条目,别自己拿眼睛估体积`
102
- : `- 逐条列出超过 16 KB 的文件,并给**能照着动手**的处置:裁哪一节(写出小节标题或行区间),或压成哪几句要点
102
+ - 动手前先跑 \`audit_memory\` —— 它会直接列出超过 40 KB 的条目,别自己拿眼睛估体积`
103
+ : `- 逐条列出超过 40 KB 的文件,并给**能照着动手**的处置:裁哪一节(写出小节标题或行区间),或压成哪几句要点
103
104
  - 不要只写「建议压缩」—— 那句话在下一趟里等于什么都没说`
104
105
 
105
106
  return `# Autodream:一次记忆整理
@@ -133,7 +134,7 @@ ${apply ? '你有写工具,直接落盘。**每次 `write_memory` / `archive_m
133
134
 
134
135
  ## 阶段 3.5 — 体积治理
135
136
 
136
- 体积线:单条 **8 KB 提醒、16 KB 强制**。**超过 16 KB 的条目本趟必须处置**,二选一:
137
+ 体积线:单条 **16 KB 提醒、40 KB 强制**。**超过 40 KB 的条目本趟必须处置**,二选一:
137
138
 
138
139
  ${stage3_5}
139
140
 
@@ -85,7 +85,7 @@ export async function writeDeclaration(payload) {
85
85
  * 从「这一趟的变更账」+「运行前后的审计」推出 manifest 的 `trim` 段(契约 §5.4)。
86
86
  *
87
87
  * 三个字段的口径,都写在能被人复核的地方:
88
- * · `requested` = 运行**前**超过 16 KB 强制线的条数 —— 这是政策要求的量,不是模型报告的量;
88
+ * · `requested` = 运行**前**超过 40 KB 强制线的条数 —— 这是政策要求的量,不是模型报告的量;
89
89
  * · `deferred` = 跑完再点一遍超线条数,**没下降**就是有该处置的没处置(计划 §6 的核对项)。
90
90
  * 运行后审计没做(中途失败)时按「没下降」算:不确定就报出来,别报一个好看的数字;
91
91
  * · `skipped` 两来源:超线但本趟没动的条目(deferred)、索引超长(protected ——
@@ -485,7 +485,7 @@ function trimReportLines(auditBefore, auditAfter) {
485
485
  const forced = (audit) => (Array.isArray(audit?.oversize) ? audit.oversize.filter((o) => o.level === 'force').length : 0)
486
486
  const before = forced(auditBefore)
487
487
  const afterText = auditAfter ? `${forced(auditAfter)} 条` : '(未做运行后审计)'
488
- const lines = [`- 超过 16 KB(必须处置):${before} → ${afterText}`]
488
+ const lines = [`- 超过 40 KB(必须处置):${before} → ${afterText}`]
489
489
  const idx = Array.isArray(auditBefore?.indexOversize) ? auditBefore.indexOversize.length : 0
490
490
  if (idx) lines.push(`- 索引超长(只报不改,MEMORY.md 不在整理范围内):${idx} 份`)
491
491
  if (before > 0 && auditAfter && forced(auditAfter) >= before) {
@@ -43,7 +43,7 @@ export function archiveSegmentToolSchema() {
43
43
  name: 'archive_segment',
44
44
  description:
45
45
  '把一条记忆里的**一整段**裁下来(旧内容移进 archive/,主文件原位置留一行指针)。' +
46
- '用于超过 16 KB、但能拆出一个完整小节的条目 —— 拆得开就别整体压缩,旧内容进档案馆还能人工捞回。' +
46
+ '用于超过 40 KB、但能拆出一个完整小节的条目 —— 拆得开就别整体压缩,旧内容进档案馆还能人工捞回。' +
47
47
  '参数给 heading(小节标题,不含 #)或 startLine/endLine(1-based 闭区间)**二选一**,reason 必填。' +
48
48
  '归档件命名、frontmatter、指针都由宿主写,你只需要说清「裁哪段、为什么」。',
49
49
  parameters: {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 为什么要有这个模块:2.10.1 之前「一条记忆算不算太大」这件事根本没有判据 ——
5
5
  * 阈值只以数字的形式散在文案与提示词里(`read_memory` 的一句提醒、自动整理的
6
- * 「超过 16 KB 必须处置」、审计的统计),谁也没法把它们对同一份内容问一遍。
6
+ * 「超过 40 KB 必须处置」、审计的统计),谁也没法把它们对同一份内容问一遍。
7
7
  * 结果是同一份 20 KB 的记忆,工具面说「没超」,审计说「超了」—— 这种自相矛盾
8
8
  * 最容易被读成「插件坏了」。判据收到这里,别处只调用,不再自己写数字。
9
9
  *
@@ -152,7 +152,7 @@ export function indexFindings(text) {
152
152
  return { bytes, lines: lines.length, longLines, overDoc, level, advice: level === 'ok' ? 'none' : 'index-slim' }
153
153
  }
154
154
 
155
- /** 去掉 `toFixed` 留下的 `.0`:`8 KB` 比 `8.0 KB` 更像人话(文案里就是「超过 16 KB」)。 */
155
+ /** 去掉 `toFixed` 留下的 `.0`:`16 KB` 比 `16.0 KB` 更像人话(文案里就是「超过 40 KB」)。 */
156
156
  function trimZero(x) {
157
157
  return String(Number(x.toFixed(1)))
158
158
  }
@@ -162,7 +162,7 @@ function trimZero(x) {
162
162
  *
163
163
  * 规则有意写得简单,因为界面、工具回执、审计文案都靠它:
164
164
  * - `< 1 KB` → 整数字节 + `B`;
165
- * - `< 1 MB` → KB,保留一位小数,`.0` 去掉(`8 KB`,不是 `8.0 KB`);
165
+ * - `< 1 MB` → KB,保留一位小数,`.0` 去掉(`16 KB`,不是 `16.0 KB`);
166
166
  * - 再往上 → MB,同一套进位。
167
167
  *
168
168
  * 非法输入(undefined / NaN / 负数)一律当 0:**文案里绝不能出现 `undefined KB`**
@@ -181,10 +181,10 @@ export function sizeText(bytes) {
181
181
  *
182
182
  * 为什么不是 `classifySize().advice` 那句枚举:枚举是给代码做分支的(`'index-slim'`
183
183
  * 对模型没有信息量),而模型与用户读到的是这一句 —— 它必须同时说清**多少、超了哪条
184
- * 线、下一步怎么做**。`read_memory` 回执里那句「ℹ 这条 26.4 KB,超过 16 KB 强制线:
184
+ * 线、下一步怎么做**。`read_memory` 回执里那句「ℹ 这条 43.1 KB,超过 40 KB 强制线:
185
185
  * 按 sage-mem 技能的「体积治理」处置(裁段归档 / 压缩)」就是本函数的产物。
186
186
  *
187
- * 阈值一律经 `sizeText()` 生成(`16 KB` 而不是 `16384`):一句话里混两种单位,
187
+ * 阈值一律经 `sizeText()` 生成(`40 KB` 而不是 `40960`):一句话里混两种单位,
188
188
  * 读的人要多做一次除法。
189
189
  *
190
190
  * `longLines` 是**可选**补充:索引「单行超长」光看字节数看不出来(一行 500 字的索引
@@ -12,7 +12,7 @@
12
12
  * · 写:可选 `opts.create`(createIfAbsent)—— 新建撞名一律 `{ok:false, code:'already_exists'}`,
13
13
  * **绝不**走覆盖入口;
14
14
  * · 不传 `opts` 的旧调用方照旧覆盖,但会落一条 warn(静默无守卫覆盖不再允许)。
15
- * · 2.10.1:**成功**回执可能多一个 `softLimit: {bytes, level}`(越过 8 KB / 16 KB 软线时)。
15
+ * · 2.10.1:**成功**回执可能多一个 `softLimit: {bytes, level}`(越过软线时;线在 2.10.2 起是 16 KB / 40 KB)。
16
16
  * 只提醒、绝不拒写;判据在 `memory/size-policy.js`,这里不写阈值。
17
17
  *
18
18
  * 为什么单列一个模块而不是长在 `lib/index.js`:组合根有 ≤1000 行的结构守卫
@@ -146,7 +146,7 @@ export async function prepareWrite(spec) {
146
146
  }
147
147
 
148
148
  /**
149
- * ⑥.5 软线(2.10.1):过了硬闸门**不等于没话说**。8 KB / 16 KB 这两条提醒线只提醒、
149
+ * ⑥.5 软线(2.10.1 引入;2.10.2 起是 16 KB / 40 KB):过了硬闸门**不等于没话说**。这两条提醒线只提醒、
150
150
  * 绝不拒写 —— 「你这条有点大」与「不许写」是两件事,混在一起就等于让治理变成丢数据。
151
151
  *
152
152
  * ⚠️ 字段为什么同时要塞进回执**又**落一行日志:`writeFile` / `writeRaw` 拿到的是
@@ -28,11 +28,31 @@ export const MAX_RAW_BYTES = 4 * 1024 * 1024
28
28
  * 数据行为变更,软线调一下只影响文案。
29
29
  */
30
30
 
31
+ /**
32
+ * **为什么是 16 KB / 40 KB(2.10.2 抬线,含一处依据更正)**
33
+ *
34
+ * 2.10.1 定线时的说法是「注入每条只读前 1500 字,所以大文件的后段对未来的自己静默失效」。
35
+ * 那句话只在两种情况下成立:① `baseline` 条目(不吃段落替换,仍读开头并受单条字数上限
36
+ * 截断,契约 2.12 标「尽力」);② 某次注入只命中摘要、没命中正文时,给的是文件开头。
37
+ *
38
+ * **普通条目是按 600 字块选段的**(`memory/chunks.js` → `recall/select.js` →
39
+ * `recall/compose.js`):一条 40 KB 的记忆里那段相关的内容照样会被选中注入 ——
40
+ * **体积不影响命中**。所以线要管的不是「让尾部能进上下文」,而是另外三笔:
41
+ * · 读全文的成本:`read_memory` 一次返回整份,没有分页;
42
+ * · 自动做梦的成本:一轮要通读的超线条目越多,token 越贵;
43
+ * · 编辑风险:裁段 / 压缩都是在一份长文上动手,越长越容易切错段、漏掉被推翻的旧说法。
44
+ * 再加上「一条记忆越长越可能是没整理的堆积」这个信号本身。
45
+ *
46
+ * 于是线的位置由「要不要催」决定,不由「注入覆盖到哪」决定:8 KB 时一条正常的长条目
47
+ * (带几段现状与历史)几乎每次写入都在提醒,那是噪音;16 KB 才是「长,但仍是单条」的
48
+ * 典型上限,40 KB 则基本是两三个主题叠在一起 —— 到那儿必须处置才说得过去。
49
+ */
50
+
31
51
  /** 单条记忆的**提醒**线:到这个量级开始建议处置(裁段归档 / 压缩)。 */
32
- export const TRIM_WARN_BYTES = 8 * 1024
52
+ export const TRIM_WARN_BYTES = 16 * 1024
33
53
 
34
54
  /** 单条记忆的**强制**线:自动整理这一趟必须处置,否则审计算一条问题。 */
35
- export const TRIM_FORCE_BYTES = 16 * 1024
55
+ export const TRIM_FORCE_BYTES = 40 * 1024
36
56
 
37
57
  /**
38
58
  * 索引单行字符上限。超了基本等于「一行里塞了正文」—— 索引只该有一行一条的指针
@@ -197,7 +197,7 @@ const auditReportSchema = z.object({
197
197
  }).readonly()).readonly(),
198
198
  // 2.10.1 体积治理:超线条目(warn / force)与超长索引各一个数组。
199
199
  // ⚠️ strict 模式会把**没声明的字段剥掉** —— 不在这里声明,面板与设置页就永远看不到
200
- // 「哪几条超过 16 KB」,而模型面(audit_memory 工具)却看得到:两处口径会当场分叉。
200
+ // 「哪几条超过 40 KB」,而模型面(audit_memory 工具)却看得到:两处口径会当场分叉。
201
201
  // 这两项是**传输契约**,所以宁可放宽单文件行数上限(test/structure-test.mjs)也不搬走。
202
202
  'oversize': z.array(z.object({
203
203
  'file': z.string().readonly(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sage-mem",
3
3
  "description": "Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.",
4
- "version": "2.10.1",
4
+ "version": "2.10.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "icon": "./icon.png",
@@ -43,7 +43,7 @@ metadata:
43
43
  - `name`:稳定标识(小写、连字符),改名等于换一条记忆。
44
44
  - `description`:**最重要的一行**,检索主要靠它(见第八节)。
45
45
  - `metadata.type`:四类之一(见第三节)。
46
- - 正文:随便写,但**前 1500 字以外基本不会被注入**,硬货放前面。
46
+ - 正文:随便写 —— 注入是**按 600 字一块挑最相关的那段**,尾部的内容照样可能被选中,不存在「写了等于没写」。但整份越长,读全文越贵、模型越容易失焦,所以硬货仍放前面。
47
47
 
48
48
  文件名同样参与检索打分,所以文件名要有实义词:`feedback_dangerous-ops.md` 远好于 `feedback_f1.md`。
49
49
 
@@ -135,9 +135,11 @@ metadata:
135
135
 
136
136
  ### 超线了怎么办(体积治理)
137
137
 
138
- 一条记忆会越写越长,而注入只读它的开头(通常是前 1500 字)。**到线不处理 ≠ 只是占空间 —— 是后面的内容对未来的你静默失效。**
138
+ 一条记忆会越写越长。**到线不处理 ≠ 只是占空间** —— 代价是另外三笔:读全文时整份进上下文(`read_memory` 没有分页)、自动做梦一轮要通读的条目越多越贵、条目越长裁段与压缩越容易改错地方。再加一条信号:**越长,越可能是没整理过的堆积。**
139
139
 
140
- 插件有两条线(具体数值随版本变,**以插件回执 / 注入文本为准,别抄死**):提醒线(约 8 KB,写的时候回执会提示)与强制线(约 16 KB,审计会报「必须处置」)。到强制线,**必须二选一**:
140
+ > 别被旧说法带偏:注入**不是**只读开头。普通条目按 600 字块选段,大文件里相关的那段照样会被选中 —— **体积不影响命中**。只有 `baseline` 条目(从开头读、受单条字数上限截断)和「只命中摘要、没命中正文」的那次注入,拿到的才是文件开头。
141
+
142
+ 插件有两条线(具体数值随版本变,**以插件回执 / 注入文本为准,别抄死**):提醒线(约 16 KB,写的时候回执会提示)与强制线(约 40 KB,审计会报「必须处置」)。到强制线,**必须二选一**:
141
143
 
142
144
  1. **裁段归档**(能拆就拆)—— 把已经终结的旧段整段挪进 `archive/`,主文件在原位置留**一行指针**。
143
145
  - 归档件命名:`archive/<原名>-<片段>-<日期>.md`,frontmatter 里带 `archived_at` / `archived_reason` / `archived_from`;**缘由与日期是硬要求**,否则三个月后没人知道为什么裁。