memory-eternal 0.22.0 → 0.24.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memory-eternal",
4
- "version": "0.22.0",
4
+ "version": "0.24.0",
5
5
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
6
6
  "author": {
7
7
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![GitHub stars](https://img.shields.io/github/stars/EternalNight996/memory-eternal?style=flat)](https://github.com/EternalNight996/memory-eternal/stargazers)
4
4
  [![npm](https://img.shields.io/npm/v/memory-eternal)](https://www.npmjs.com/package/memory-eternal)
5
- [![release](https://img.shields.io/badge/release-v0.22.0-FF7A45)](https://github.com/EternalNight996/memory-eternal/releases)
5
+ [![release](https://img.shields.io/badge/release-v0.24.0-FF7A45)](https://github.com/EternalNight996/memory-eternal/releases)
6
6
  [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
7
 
8
8
  **English (default)** · [简体中文](README.zh-CN.md)
@@ -54,6 +54,75 @@ dsh-memory open # http://127.0.0.1:7999
54
54
  npx -y -p memory-eternal dsh-memory tui # live queue in the terminal
55
55
  ```
56
56
 
57
+ ## 🧬 How memory actually flows
58
+
59
+ One SQLite vault, one write gate, two read paths. Nothing enters recall without passing the gate.
60
+
61
+ ```mermaid
62
+ flowchart TD
63
+ subgraph S1["① Input"]
64
+ A1["Conversation + tool events<br/>captured when a turn ends"]
65
+ A2["Installed skills<br/>scanned from the host"]
66
+ A3["News follows<br/>RSS · GitHub · HN · arXiv · DSH plugins"]
67
+ A4["Manual cards / imports"]
68
+ end
69
+ subgraph S2["② Distill — the only LLM step"]
70
+ B1["LLM distills the session<br/>on failure: raw card, nothing lost"]
71
+ B2["Dedup: lexical Jaccard<br/>optional LLM merge decision"]
72
+ B3["Kind classification"]
73
+ end
74
+ subgraph S3["③ Review gate — human only"]
75
+ C1[("Quarantine<br/>pending / rejected")]
76
+ C2{"approve / reject"}
77
+ C3[("Main store<br/>approved only")]
78
+ end
79
+ subgraph S4["④ Storage"]
80
+ D1[("SQLite: cards + FTS5 index")]
81
+ D2["Markdown mirror<br/>git-able"]
82
+ D3["audit_log + token ledger"]
83
+ end
84
+ subgraph S5["⑤ Recall — every session"]
85
+ E1["Core memory → system prompt"]
86
+ E2["memory_recall → FTS5 + BM25"]
87
+ E3["memory_revise → pending revision"]
88
+ end
89
+ A1 --> B1 --> B2 --> B3 --> C1
90
+ A2 --> B2
91
+ A3 --> C1
92
+ A4 --> B2
93
+ C1 --> C2
94
+ C2 -->|approve| C3
95
+ C2 -->|reject| C1
96
+ C3 --> D1
97
+ D1 --> D2
98
+ C3 --> D3
99
+ C3 --> E1
100
+ C3 --> E2
101
+ E2 -->|hits + chars| D3
102
+ E3 --> C1
103
+ ```
104
+
105
+ The diagram is only true because three invariants are enforced in code, not by convention:
106
+
107
+ | Invariant | Enforced by |
108
+ |---|---|
109
+ | The main store **only** ever contains approved cards | Two physical tables; every read path queries the main one |
110
+ | An agent can never approve its own memory | Agent writes land in quarantine; approve/reject is a human action in the UI / TUI / CLI |
111
+ | Recall costs **zero model calls** | FTS5 + BM25; the LLM runs only while distilling |
112
+
113
+ ## 🔌 How agents talk to the memory
114
+
115
+ | Channel | Direction | When | Gate | Cost |
116
+ |---|---|---|---|---|
117
+ | `memory_recall` | agent → memory | on demand | approved only | **0 model calls** (p50 2.3 ms) |
118
+ | Core memory | memory → agent | every session, automatic | approved only | ~0.2k chars |
119
+ | Session capture / `memory_write` | conversation → memory | turn end | **queued for review** | 1 LLM call |
120
+ | `memory_revise` | agent → memory | on demand | **queued as a revision** | 0 |
121
+ | Review (Web UI · TUI · CLI) | human → memory | manual | — | 0 |
122
+ | Hooks (Claude Code / Codex / Cursor) | host → memory | session end | **queued** | 1 LLM call |
123
+ | MCP server | any MCP client | on demand | same gate | same |
124
+ | News follows | web → memory | interval / 24h | isolated from cards | network only |
125
+
57
126
  ## 🧾 Does it actually save tokens?
58
127
 
59
128
  Most memory products <em>say</em> they save tokens. We think you should check. The plugin ships a built-in **memory ledger** (Runtime status → Memory ledger) that records:
@@ -77,6 +146,42 @@ net benefit ≈ avoided context cost − (injected characters + distillation cal
77
146
 
78
147
  > Illustrative numbers, not a benchmark — measure your own with the ledger. If recall usefulness stays below ~60%, tune it down (fewer results, shorter summaries) or turn memory off.
79
148
 
149
+ ## 📊 How it compares — measured on a real vault
150
+
151
+ Snapshot **2026-11-07**. Our figures are measured on a real **2,321-card vault** on Windows (not a toy corpus); competitor figures come from their public README / npm / GitHub on the same date. `—` means *not advertised or not verifiable*, not "absent".
152
+
153
+ | Capability | **memory-eternal v0.24.0** | 灵枢 dsh-memory v0.9.23 | SAG-Wiki v0.2.7 | CLAUDE.md / AGENTS.md |
154
+ |---|---|---|---|---|
155
+ | Storage | SQLite + Markdown mirror | Markdown cognitive graph | SQLite FTS5 over your files | flat file(s) |
156
+ | Write gate | **human review queue** (3 stores) | deterministic rules | none — index is truth | none |
157
+ | Immutable audit log | ✅ enforced at the SQLite layer | ✅ claimed | — | — |
158
+ | Retrieval | FTS5 + **BM25/IDF**, column weights | rule-based graph walk | BM25 → JEV (LLM judge) | whole file injected |
159
+ | Recall latency | **p50 2.33 ms · p95 6.18 ms** | — | — | grows with file size |
160
+ | Recall model calls | **0** | 0 | 1–2 per query | 0 |
161
+ | Token ledger (measured usefulness) | ✅ | — | — | — |
162
+ | Multi-agent | DSH + MCP + hooks + TUI + browser | DSH + MCP | CLI + skill | per tool |
163
+ | npm downloads / month | 14,370 | **31,421** | 119 | n/a |
164
+ | GitHub stars | 147 | **612** | 124 | n/a |
165
+ | License | MIT | MIT | MIT | n/a |
166
+
167
+ **We are not the biggest here.** 灵枢 has more stars and downloads; SAG-Wiki retrieves over your own documents better than we do. What we do differently is the *write path*: a human gate, a three-store separation, and an audit trail you can point at — plus a token ledger so you can check whether recall is worth its cost.
168
+
169
+ Reproduce our numbers on your own vault:
170
+
171
+ ```bash
172
+ node scripts/bench-search.mjs --rebuild # rebuild time + p50/p95/max latency
173
+ dsh-memory ledger --days 30 --json # recall injection / capture / usefulness
174
+ ```
175
+
176
+ | Our measured baseline | Value |
177
+ |---|---|
178
+ | Cards indexed | 2,321 |
179
+ | Index rebuild (cold) | 869 ms |
180
+ | Recall p50 / p95 / max | 2.33 / 6.18 / 7.60 ms |
181
+ | Recall throughput | ~431 queries/s |
182
+ | Recall model calls | 0 |
183
+ | Injection per recall (60-day ledger) | ~1,394 chars |
184
+
80
185
  ## 🛡️ Review first, prove later
81
186
 
82
187
  A memory system that silently ingests everything will eventually recall a wrong fact as truth. memory-eternal defaults to **review-before-commit**:
@@ -156,3 +261,30 @@ Not everything should be injected into every session. Core memory is the small,
156
261
  ## 📄 License
157
262
 
158
263
  MIT — use it, self-host it, build on it.
264
+
265
+ ## 🖋 Author's Quotes
266
+
267
+ _Verses from 《蛊真人》(Reverend Insanity). Kept in the original Chinese — a translation would lose the meter. Also rotating at the bottom of the panel's left rail._
268
+
269
+ > 落魄谷中寒风吹,春秋蝉鸣少年归。<br>
270
+ > 荡魂山处石人泪,定仙游走魔向北。<br>
271
+ > 逆流河上万仙退,爱情不敌坚持泪。<br>
272
+ > 宿命天成命中败,仙尊悔而我不悔。
273
+
274
+ > 早岁已知世事艰,仍许飞鸿荡云间。
275
+
276
+ > 男儿不展凌云志,空负天生八尺躯。
277
+
278
+ > 千古地仙随风逝,昔日三王归青冢。<br>
279
+ > 阳莽悍陨谁无败?卷土重来再称王。
280
+
281
+ > 天河一挂淘龙鱼,逆天独行顾八荒。<br>
282
+ > 今朝暂且展翼去,明朝飞仙笞凤凰!
283
+
284
+ > 独帜入渊深未知,身似浮萍命难持。<br>
285
+ > 千舟皆朝归海处,一苇青拨戏浪巅。<br>
286
+ > 惊鸿四散鱼逃尽,唯有残帆傲此间。<br>
287
+ > 待到天开云雾散,负手直望笑苍天。
288
+
289
+ <p align="right"><sub>——《蛊真人》· 大爱仙尊</sub></p>
290
+
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![GitHub stars](https://img.shields.io/github/stars/EternalNight996/memory-eternal?style=flat)](https://github.com/EternalNight996/memory-eternal/stargazers)
4
4
  [![npm](https://img.shields.io/npm/v/memory-eternal)](https://www.npmjs.com/package/memory-eternal)
5
- [![release](https://img.shields.io/badge/release-v0.22.0-FF7A45)](https://github.com/EternalNight996/memory-eternal/releases)
5
+ [![release](https://img.shields.io/badge/release-v0.24.0-FF7A45)](https://github.com/EternalNight996/memory-eternal/releases)
6
6
  [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
7
 
8
8
  [English (default)](README.md) · **简体中文(镜像)**
@@ -54,6 +54,75 @@ dsh-memory open # http://127.0.0.1:7999
54
54
  npx -y -p memory-eternal dsh-memory tui # 终端里的实时待审队列
55
55
  ```
56
56
 
57
+ ## 🧬 底层记忆是怎么流动的
58
+
59
+ 一个 SQLite 库、一道写入闸门、两条读取路径。**没过闸门的东西,永远不会被召回。**
60
+
61
+ ```mermaid
62
+ flowchart TD
63
+ subgraph S1["① 输入"]
64
+ A1["对话 + 工具调用事件<br/>一轮结束后采集"]
65
+ A2["本机已装技能<br/>从宿主技能注册表扫描"]
66
+ A3["新闻关注<br/>RSS · GitHub · HN · arXiv · DSH 插件"]
67
+ A4["手动建卡 / 导入"]
68
+ end
69
+ subgraph S2["② 沉淀 —— 唯一消耗 LLM 的一步"]
70
+ B1["LLM 蒸馏成知识卡<br/>失败则降级原文卡,不丢内容"]
71
+ B2["去重:词法 Jaccard<br/>可选 LLM 判「新建 vs 追加」"]
72
+ B3["归类:knowledge / project / skill …"]
73
+ end
74
+ subgraph S3["③ 审核闸门 —— 只能由人操作"]
75
+ C1[("隔离区<br/>pending / rejected")]
76
+ C2{"批准 / 驳回"}
77
+ C3[("主库<br/>只存 approved")]
78
+ end
79
+ subgraph S4["④ 存储"]
80
+ D1[("SQLite:cards + FTS5 索引")]
81
+ D2["Markdown 镜像<br/>可 git 管理"]
82
+ D3["审计日志 + 记忆账本"]
83
+ end
84
+ subgraph S5["⑤ 召回 —— 每轮会话"]
85
+ E1["核心记忆 → systemPrompt"]
86
+ E2["memory_recall → FTS5 + BM25"]
87
+ E3["memory_revise → 待审修改副本"]
88
+ end
89
+ A1 --> B1 --> B2 --> B3 --> C1
90
+ A2 --> B2
91
+ A3 --> C1
92
+ A4 --> B2
93
+ C1 --> C2
94
+ C2 -->|批准| C3
95
+ C2 -->|驳回| C1
96
+ C3 --> D1
97
+ D1 --> D2
98
+ C3 --> D3
99
+ C3 --> E1
100
+ C3 --> E2
101
+ E2 -->|命中 + 注入字符| D3
102
+ E3 --> C1
103
+ ```
104
+
105
+ 这张图之所以成立,是因为三条不变量**写在代码里**,不是写在文档里:
106
+
107
+ | 不变量 | 由什么保证 |
108
+ |---|---|
109
+ | 主库**物理上只存 approved** | 两张物理表;所有读取路径只查主库 |
110
+ | Agent **永远不能批准自己的记忆** | Agent 只能写进隔离区;批准/驳回是 UI / TUI / CLI 里的人工动作 |
111
+ | 召回**零模型调用** | FTS5 + BM25;LLM 只在沉淀时运行 |
112
+
113
+ ## 🔌 Agent 与记忆的交互通道
114
+
115
+ | 通道 | 方向 | 何时 | 闸门 | 成本 |
116
+ |---|---|---|---|---|
117
+ | `memory_recall` | Agent → 记忆 | 按需 | 只读已批准 | **0 次模型调用**(p50 2.3ms) |
118
+ | 核心记忆 | 记忆 → Agent | 每轮会话自动 | 只读已批准 | 约 0.2k 字符 |
119
+ | 会话沉淀 / `memory_write` | 对话 → 记忆 | 一轮结束 | **进待审队列** | 1 次 LLM |
120
+ | `memory_revise` | Agent → 记忆 | 按需 | **进待审「修改副本」** | 0 |
121
+ | 审核(Web / TUI / CLI) | 人 → 记忆 | 手动 | — | 0 |
122
+ | Hook(Claude Code / Codex / Cursor) | 宿主 → 记忆 | 会话结束 | **进待审** | 1 次 LLM |
123
+ | MCP 服务 | 任意 MCP 客户端 | 按需 | 同上 | 同上 |
124
+ | 新闻关注 | 网络 → 记忆 | 按间隔 / 24h | 与知识卡隔离 | 仅网络 |
125
+
57
126
  ## 🧾 真的省 token 吗?
58
127
 
59
128
  大多数记忆产品都说自己省 token。我们认为你应该自己验证。插件内置**记忆账本**(运行状态 → 记忆账本),记录:
@@ -77,6 +146,42 @@ npx -y -p memory-eternal dsh-memory tui # 终端里的实时待审队列
77
146
 
78
147
  > 以上是示意数字,不是 benchmark —— 用账本测你自己的。如果召回有用率长期低于约 60%,就调小(更少条数、更短摘要)或直接关掉记忆。
79
148
 
149
+ ## 📊 与同类产品的实测对比
150
+
151
+ 数据快照:**2026-11-07**。我们的数字在**真实 2,321 张卡的库**上实测(不是玩具语料);竞品数据取自同一日期的公开 README / npm / GitHub。`—` 表示**未公开或无法核实**,不代表"没有"。
152
+
153
+ | 能力 | **memory-eternal v0.24.0** | 灵枢 dsh-memory v0.9.23 | SAG-Wiki v0.2.7 | CLAUDE.md / AGENTS.md |
154
+ |---|---|---|---|---|
155
+ | 存储 | SQLite + Markdown 镜像 | Markdown 认知图 | 对本地文件的 SQLite FTS5 索引 | 单个文件 |
156
+ | 写入闸门 | **人工审核队列**(三库分离) | 确定性规则裁决 | 无(索引即真相) | 无 |
157
+ | 不可变审计日志 | ✅ SQLite 层强制 | ✅ 官方声称 | — | — |
158
+ | 检索 | FTS5 + **BM25/IDF**,多列加权 | 规则化图谱遍历 | BM25 → JEV(LLM 判定) | 整文件注入 |
159
+ | 召回延迟 | **p50 2.33ms · p95 6.18ms** | — | — | 随文件体积增长 |
160
+ | 召回模型调用 | **0** | 0 | 每次查询 1–2 次 | 0 |
161
+ | 记忆账本(可量化有用率) | ✅ | — | — | — |
162
+ | 多 Agent | DSH + MCP + Hook + TUI + 浏览器 | DSH + MCP | CLI + Skill | 各工具各自为政 |
163
+ | npm 月下载 | 14,370 | **31,421** | 119 | 不适用 |
164
+ | GitHub Star | 147 | **612** | 124 | 不适用 |
165
+ | 许可证 | MIT | MIT | MIT | 不适用 |
166
+
167
+ **我们不是这里最大的。** 灵枢的 star 和下载都比我们多;SAG-Wiki 检索你自己文档的能力也比我们强。我们做的不一样的是**写入路径**:人审闸门、三库分离、可指认的审计链条,外加一本让你能自己核对"召回到底值不值"的记忆账本。
168
+
169
+ 在我们的库上自己复跑:
170
+
171
+ ```bash
172
+ node scripts/bench-search.mjs --rebuild # 重建耗时 + p50/p95/max 延迟
173
+ dsh-memory ledger --days 30 --json # 召回注入 / 沉淀调用 / 有用率
174
+ ```
175
+
176
+ | 我们实测的基线 | 数值 |
177
+ |---|---|
178
+ | 索引卡片数 | 2,321 |
179
+ | 冷启动全量重建 | 869 ms |
180
+ | 召回 p50 / p95 / max | 2.33 / 6.18 / 7.60 ms |
181
+ | 召回吞吐 | 约 431 次/秒 |
182
+ | 召回模型调用 | 0 |
183
+ | 每次召回注入(60 天账本) | 约 1,394 字符 |
184
+
80
185
  ## 🛡️ 先审核,再入库
81
186
 
82
187
  一个「什么都自动吞进去」的记忆系统,迟早会把错误事实当成真相召回。memory-eternal 默认**先审后入**:
@@ -156,3 +261,30 @@ npx -y -p memory-eternal dsh-memory tui # 终端里的实时待审队列
156
261
  ## 📄 License
157
262
 
158
263
  MIT —— 随便用、随便自托管、随便二次开发。
264
+
265
+ ## 🖋 作者语录
266
+
267
+ _「大爱」二字,是全篇最大的反讽。面板左栏底部同步轮播。_
268
+
269
+ > 落魄谷中寒风吹,春秋蝉鸣少年归。<br>
270
+ > 荡魂山处石人泪,定仙游走魔向北。<br>
271
+ > 逆流河上万仙退,爱情不敌坚持泪。<br>
272
+ > 宿命天成命中败,仙尊悔而我不悔。
273
+
274
+ > 早岁已知世事艰,仍许飞鸿荡云间。
275
+
276
+ > 男儿不展凌云志,空负天生八尺躯。
277
+
278
+ > 千古地仙随风逝,昔日三王归青冢。<br>
279
+ > 阳莽悍陨谁无败?卷土重来再称王。
280
+
281
+ > 天河一挂淘龙鱼,逆天独行顾八荒。<br>
282
+ > 今朝暂且展翼去,明朝飞仙笞凤凰!
283
+
284
+ > 独帜入渊深未知,身似浮萍命难持。<br>
285
+ > 千舟皆朝归海处,一苇青拨戏浪巅。<br>
286
+ > 惊鸿四散鱼逃尽,唯有残帆傲此间。<br>
287
+ > 待到天开云雾散,负手直望笑苍天。
288
+
289
+ <p align="right"><sub>——《蛊真人》· 大爱仙尊</sub></p>
290
+
@@ -2,7 +2,7 @@
2
2
  "name": "memory-eternal",
3
3
  "displayName": "Memory Atlas",
4
4
  "description": "Memory-eternal: open the local Memory Atlas library, review queue and knowledge graph in a Cursor/VS Code webview.",
5
- "version": "0.22.0",
5
+ "version": "0.24.0",
6
6
  "publisher": "EternalNight996",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -2,7 +2,7 @@
2
2
  "$schema": "urn:dsh-std:community-draft:dsh-plugin:0.15",
3
3
  "id": "memory-eternal-tui",
4
4
  "name": "memory-eternal-tui",
5
- "version": "0.22.0",
5
+ "version": "0.24.0",
6
6
  "manifestVersion": "0.15",
7
7
  "description": "Open Memory Atlas from dsh-TUI: native /audit scene plus browser launcher.",
8
8
  "facets": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal-tui",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Memory Atlas for dsh-TUI: /audit scene with reason/severity, batch confirmation and reject reasons.",
5
5
  "type": "module",
6
6
  "main": "main.js",
package/index.js CHANGED
@@ -58,6 +58,11 @@ export const Config = z.object({
58
58
  coreMemoryEnabled: z.boolean().default(true),
59
59
  coreMemoryLimit: z.number().min(1).max(20).default(6),
60
60
  coreMemoryMaxChars: z.number().min(200).max(10000).default(2000),
61
+ // 关注摘要推送:开启后按间隔抓取「新闻关注」,有新内容时注入 systemPrompt,
62
+ // 让 Agent 在下一轮主动告诉用户。**默认关闭** —— 这是会打扰人的功能,必须用户显式开启。
63
+ newsDigestEnabled: z.boolean().default(false),
64
+ newsDigestIntervalMinutes: z.number().min(30).max(10080).default(720),
65
+ newsDigestMaxItems: z.number().min(1).max(10).default(5),
61
66
  // 技能卡(kind=skill):从「本机实际安装/可用的技能」生成卡,强制待审。
62
67
  // 不由 agent 对话沉淀(见 lib/capture.js AGENT_CARD_KINDS 不含 skill);
63
68
  // 默认开:宿主启动 / 技能安装变化时后台同步,新技能自动进审核中心。
@@ -1082,6 +1087,75 @@ export function apply(ctx, config) {
1082
1087
  }
1083
1088
  }, 'memory-eternal: core memory section')
1084
1089
 
1090
+ // -- 2c. 关注摘要:默认关闭;开启后按间隔抓取,只把「还没送过」的那一份推一轮 ----
1091
+ // 送达方式与核心记忆一致:systemPrompt 分段。不新增宿主能力,也不往会话里插消息。
1092
+ ctx.effect(() => {
1093
+ let disposeSection = null
1094
+ let timer = null
1095
+ let alive = true
1096
+ let lastText = ''
1097
+ const clearSection = () => {
1098
+ if (!disposeSection) return
1099
+ const dispose = disposeSection
1100
+ disposeSection = null
1101
+ dispose()
1102
+ }
1103
+ const render = async () => {
1104
+ try {
1105
+ const cfg = settings.get() ?? {}
1106
+ if (cfg.enabled === false || cfg.newsDigestEnabled !== true) { clearSection(); lastText = ''; return }
1107
+ const { readDigest, digestPrompt } = await import('./lib/news-digest.js')
1108
+ const d = await readDigest(vaultDir())
1109
+ if (!alive) return
1110
+ // 没有新内容、或这一份已经被某一轮消费过 → 不注入(宁可不提,也不反复念旧闻)
1111
+ if (!d || !Array.isArray(d.items) || !d.items.length || Number(d.consumedAt) >= Number(d.generatedAt)) {
1112
+ clearSection(); lastText = ''
1113
+ return
1114
+ }
1115
+ const text = digestPrompt(d, { maxItems: Number(cfg.newsDigestMaxItems) || 5 })
1116
+ if (!text) { clearSection(); lastText = ''; return }
1117
+ if (text === lastText) return
1118
+ lastText = text
1119
+ clearSection()
1120
+ disposeSection = ctx.systemPrompt.section({ name: 'memory-eternal: news-digest', order: 130, text })
1121
+ } catch { /* 摘要失败绝不能影响正常对话 */ }
1122
+ }
1123
+ const tick = async () => {
1124
+ const cfg = settings.get() ?? {}
1125
+ if (cfg.enabled === false || cfg.newsDigestEnabled !== true) { await render(); return }
1126
+ try {
1127
+ const { readDigest, buildDigest } = await import('./lib/news-digest.js')
1128
+ const d = await readDigest(vaultDir())
1129
+ const intervalMs = Math.max(30, Number(cfg.newsDigestIntervalMinutes) || 720) * 60000
1130
+ if (!d || Date.now() - (Number(d.generatedAt) || 0) >= intervalMs) {
1131
+ await buildDigest(vaultDir(), {
1132
+ windowHours: Math.max(1, Math.round(intervalMs / 3600000)),
1133
+ maxItems: Number(cfg.newsDigestMaxItems) || 5,
1134
+ })
1135
+ }
1136
+ } catch { /* 抓取失败就等下一个周期,不打扰用户 */ }
1137
+ await render()
1138
+ }
1139
+ tick()
1140
+ timer = setInterval(tick, 60000)
1141
+ if (timer && typeof timer.unref === 'function') timer.unref()
1142
+ // 一轮会话开始 = 这份摘要已经进过 systemPrompt,标记消费,下一轮不再重复。
1143
+ const onClaim = async () => {
1144
+ try {
1145
+ const { readDigest, consumeDigest } = await import('./lib/news-digest.js')
1146
+ const d = await readDigest(vaultDir())
1147
+ if (d && Number(d.generatedAt) > Number(d.consumedAt || 0)) await consumeDigest(vaultDir(), d.generatedAt)
1148
+ } catch { /* ignore */ }
1149
+ }
1150
+ if (typeof ctx.on === 'function') ctx.on('agent/inbox/claimed', onClaim)
1151
+ return () => {
1152
+ alive = false
1153
+ if (timer) clearInterval(timer)
1154
+ if (typeof ctx.off === 'function') { try { ctx.off('agent/inbox/claimed', onClaim) } catch { /* 已卸载 */ } }
1155
+ clearSection()
1156
+ }
1157
+ }, 'memory-eternal: news digest section')
1158
+
1085
1159
  const tools = ctx.get('tools')
1086
1160
  if (tools !== undefined) {
1087
1161
  tools.register(defineTool({
@@ -1360,6 +1434,10 @@ export function apply(ctx, config) {
1360
1434
  recallEmbedding: cfg.recallEmbedding || '',
1361
1435
  sessionBudgetChars: cfg.sessionBudgetChars ?? 80000,
1362
1436
  skillCardsAutoSync: cfg.skillCardsAutoSync !== false,
1437
+ // 关注摘要推送(默认关;覆盖测试要求每个 Config 字段都能被 /config 读出)
1438
+ newsDigestEnabled: cfg.newsDigestEnabled === true,
1439
+ newsDigestIntervalMinutes: cfg.newsDigestIntervalMinutes ?? 720,
1440
+ newsDigestMaxItems: cfg.newsDigestMaxItems ?? 5,
1363
1441
  }
1364
1442
  const descriptor = (ctx.get('settings') ?? {}).describe?.({ redactSecrets: true }) ?? []
1365
1443
  const me = descriptor.find((d) => d.ns === 'memory-eternal')