@morningljn/mnemo 0.1.4 → 0.2.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/dist/retriever.js +30 -42
- package/dist/retriever.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +21 -10
- package/dist/schema.js.map +1 -1
- package/dist/server.js +34 -1
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +37 -0
- package/dist/store.js +166 -9
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/docs/superpowers/plans/2026-05-16-memory-self-learning.md +932 -0
- package/openspec/changes/memory-self-learning/.openspec.yaml +2 -0
- package/openspec/changes/memory-self-learning/design.md +174 -0
- package/openspec/changes/memory-self-learning/proposal.md +35 -0
- package/openspec/changes/memory-self-learning/specs/fact-retrieval/spec.md +35 -0
- package/openspec/changes/memory-self-learning/specs/fact-summary/spec.md +45 -0
- package/openspec/changes/memory-self-learning/specs/length-penalty/spec.md +27 -0
- package/openspec/changes/memory-self-learning/specs/retrieval-log/spec.md +41 -0
- package/openspec/changes/memory-self-learning/specs/self-learning/spec.md +68 -0
- package/openspec/changes/memory-self-learning/tasks.md +56 -0
- package/package.json +1 -1
- package/src/retriever.ts +32 -44
- package/src/schema.ts +21 -10
- package/src/server.ts +36 -1
- package/src/store.ts +215 -9
- package/src/types.ts +4 -1
- package/tests/retriever.test.ts +53 -0
- package/tests/store.test.ts +112 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
## Context
|
|
2
|
+
|
|
3
|
+
mnemo-mcp 是一个纯全局记忆 MCP Server(不含项目记忆),70 条事实存储在 `~/.mnemo/facts.db`(SQLite + FTS5)。当前检索管线:FTS5 BM25 候选 → Jaccard 重排序 → trust_score 加权 → 时间衰减。
|
|
4
|
+
|
|
5
|
+
v0.1.4 引入了动态权重(短查询 FTS 0.7/长查询 0.3)、relevance gate (0.15)、content dedup (Jaccard>0.7) 和 query refinement,但实测检索准确率反而下降。根本原因:
|
|
6
|
+
|
|
7
|
+
1. **动态权重适得其反**:长查询给 FTS5 只留 30%,BM25 正确排序被 Jaccard 覆盖
|
|
8
|
+
2. **"万能条"问题**:29% fact 超 500 字(最长 3921 字),字多容易匹配但 helpful 率极低(0.6%)
|
|
9
|
+
3. **无反馈闭环**:系统不知道哪些检索是好的,无法自我优化
|
|
10
|
+
|
|
11
|
+
约束:
|
|
12
|
+
- 纯 stdio MCP Server,无 HTTP 端点
|
|
13
|
+
- better-sqlite3 同步 API
|
|
14
|
+
- 不引入 ML 依赖(embedding 方案留给后续迭代)
|
|
15
|
+
- 向后兼容:已有 facts.db 不需要迁移
|
|
16
|
+
|
|
17
|
+
## Goals / Non-Goals
|
|
18
|
+
|
|
19
|
+
**Goals:**
|
|
20
|
+
- 回退 v3 检索改动,恢复 v2 准确率水平
|
|
21
|
+
- 通过 length penalty 解决"万能条"霸占检索
|
|
22
|
+
- 通过 summary 字段治理超长 fact 的数据质量
|
|
23
|
+
- 通过 retrieval_log + learn action 建立自学习数据基础
|
|
24
|
+
- 写入端质量控制:限制 content 长度,引导拆分
|
|
25
|
+
|
|
26
|
+
**Non-Goals:**
|
|
27
|
+
- 不引入 embedding/向量检索(后续迭代)
|
|
28
|
+
- 不自动拆分已有超长 fact(需要 LLM,留给手动触发或后续工具)
|
|
29
|
+
- 不修改 fact_feedback 机制(保持现有 helpful/unhelpful)
|
|
30
|
+
- 不改 MCP Resource 预热方案(已实现,保持)
|
|
31
|
+
|
|
32
|
+
## Decisions
|
|
33
|
+
|
|
34
|
+
### D1: 回退动态权重为静态 0.5/0.5
|
|
35
|
+
|
|
36
|
+
**选择**:恢复 v2 静态 FTS/Jaccard 权重各 0.5
|
|
37
|
+
**替代方案**:
|
|
38
|
+
- (a) 只调低动态权重范围(如 0.6/0.4)——治标不治本
|
|
39
|
+
- (b) 完全移除 Jaccard 只用 FTS5——会丢失部分长查询的 token 匹配能力
|
|
40
|
+
|
|
41
|
+
**理由**:v2 的静态 0.5/0.5 经用户确认比 v3 更准。FTS5 BM25 本身是成熟的排序算法,Jaccard 作为辅助校验即可,不应主导排序。
|
|
42
|
+
|
|
43
|
+
### D2: Length Penalty 公式
|
|
44
|
+
|
|
45
|
+
**选择**:`score *= min(1.0, 300 / matchText.length)`
|
|
46
|
+
|
|
47
|
+
其中 `matchText` 为 summary(非空时)或 content(summary 为空时)。即 summary 存在时用 summary 长度计算 penalty,避免有 summary 的 fact 被 content 长度过度惩罚。300 字以内不受影响,超过的线性衰减。
|
|
48
|
+
|
|
49
|
+
**替代方案**:
|
|
50
|
+
- (a) `300 / content.length`(不考虑 summary)——有 summary 时过度惩罚
|
|
51
|
+
- (b) 指数衰减 `score *= 0.99^length`——过度惩罚
|
|
52
|
+
- (c) 硬阈值(超 500 字直接排除)——太粗暴
|
|
53
|
+
|
|
54
|
+
**理由**:既然检索用的是 summary,penalty 也应该基于实际匹配文本的长度。一条有 summary(50字)的 2000 字 fact,penalty = 300/50 = 6 → cap 到 1.0,不受惩罚,符合预期。
|
|
55
|
+
|
|
56
|
+
### D3: retrieval_log 设计
|
|
57
|
+
|
|
58
|
+
**选择**:新建 `retrieval_log` 表,search 时自动插入,保留最近 5000 条
|
|
59
|
+
|
|
60
|
+
```sql
|
|
61
|
+
CREATE TABLE retrieval_log (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
query TEXT NOT NULL,
|
|
64
|
+
results TEXT NOT NULL, -- JSON array of [{id, score}]
|
|
65
|
+
timestamp TEXT DEFAULT (datetime('now'))
|
|
66
|
+
);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
每个返回的 fact 记录 `{id, score}`,便于后续分析"为什么某个 fact 总排第一"。
|
|
70
|
+
|
|
71
|
+
**替代方案**:
|
|
72
|
+
- (a) 只记 fact_ids 数组——粒度太粗,无法分析排序原因
|
|
73
|
+
- (b) 单独 retrieval_log_details 表——过度设计,JSON 数组够用
|
|
74
|
+
|
|
75
|
+
**理由**:`[{id, score}]` 在调试时能直接看到每次检索的评分分布,成本只是多存几个数字。5000 条上限覆盖约 1 个月的高频使用。
|
|
76
|
+
|
|
77
|
+
### D4: learn action 策略
|
|
78
|
+
|
|
79
|
+
**选择**:统计规则 + trust_score 调整 + 数据质量报告
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
对每条 fact:
|
|
83
|
+
rate = helpful_count / max(retrieval_count, 1)
|
|
84
|
+
|
|
85
|
+
if retrieval_count > 30:
|
|
86
|
+
if rate < 0.05: trust_score *= 0.9
|
|
87
|
+
if rate > 0.3: trust_score = min(1.0, trust_score + 0.05)
|
|
88
|
+
|
|
89
|
+
老化: 超过 60 天 last_retrieved_at → trust_score *= 0.95
|
|
90
|
+
|
|
91
|
+
返回:
|
|
92
|
+
{promoted, demoted, aged, unchanged,
|
|
93
|
+
long_facts: [{id, content_length, penalty, has_summary}]}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
aging 基于 `facts.last_retrieved_at` 字段(由 `logRetrieval` 自动更新),不依赖 retrieval_log 查询。
|
|
97
|
+
|
|
98
|
+
**替代方案**:
|
|
99
|
+
- (a) aging 从 retrieval_log 推导——retrieval_log 有 5000 条上限,旧记录可能被删导致判断不准
|
|
100
|
+
- (b) 完全不自动调整,只提供数据让人手动调——不够自动化
|
|
101
|
+
|
|
102
|
+
**理由**:`last_retrieved_at` 字段比从 retrieval_log 查询更可靠(不受日志上限影响)。`long_facts` 报告让用户知道哪些 fact 被 penalty 影响,引导补充 summary。
|
|
103
|
+
|
|
104
|
+
### D5: summary 字段 + 双阈值设计
|
|
105
|
+
|
|
106
|
+
**选择**:facts 表新增 `summary TEXT DEFAULT NULL` 列
|
|
107
|
+
|
|
108
|
+
- summary 非空时,检索用 summary 匹配(FTS5 也索引 summary)
|
|
109
|
+
- summary 为空时,退化为原有 content 匹配
|
|
110
|
+
|
|
111
|
+
两个阈值分工明确:
|
|
112
|
+
- **300 字**:length penalty 的计算阈值(matchText ≤ 300 不惩罚)
|
|
113
|
+
- **500 字**:写入端数据质量警告阈值(content > 500 且无 summary → 返回 warning)
|
|
114
|
+
|
|
115
|
+
**替代方案**:
|
|
116
|
+
- (a) 自动用 LLM 生成 summary——需要额外依赖,不适合 MCP Server
|
|
117
|
+
- (b) 不加 summary,只靠 length penalty——治标不治本
|
|
118
|
+
|
|
119
|
+
**理由**:300 管检索评分(匹配文本质量),500 管写入质量(数据治理提示),职责不重叠。
|
|
120
|
+
|
|
121
|
+
### D7: 保留 refineQuery
|
|
122
|
+
|
|
123
|
+
**选择**:保留 v3 的 `refineQuery()` 查询提炼
|
|
124
|
+
|
|
125
|
+
**理由**:refineQuery 的核心功能是过滤纯操作指令("运行测试"→ return null),避免无效检索。这个功能与权重策略无关,在静态权重下同样有用。移除它会导致"git commit"等操作也触发检索,浪费资源。
|
|
126
|
+
|
|
127
|
+
### D8: 启动时 learn 延迟执行
|
|
128
|
+
|
|
129
|
+
**选择**:server 启动时用 `process.nextTick()` 延迟执行 learn,不阻塞 stdio 初始化
|
|
130
|
+
|
|
131
|
+
**替代方案**:
|
|
132
|
+
- (a) 完全同步执行——可能阻塞 Claude 端初始化(未来上千条 fact 时)
|
|
133
|
+
- (b) 不自动执行,只手动调用——用户容易忘记
|
|
134
|
+
|
|
135
|
+
**理由**:better-sqlite3 是同步 API,70 条 fact 的遍历虽然只需毫秒,但未来数据增长后可能阻塞。`nextTick` 延迟确保 MCP 握手先完成,learn 在下一个事件循环执行。
|
|
136
|
+
|
|
137
|
+
### D9: 现有超长 fact 治理路径
|
|
138
|
+
|
|
139
|
+
**选择**:learn action 返回 `long_facts` 数据质量报告 + 新增 `audit` action
|
|
140
|
+
|
|
141
|
+
- learn 返回 `long_facts: [{id, content_length, penalty, has_summary}]`,列出被 length penalty 严重影响的 fact
|
|
142
|
+
- 新增 `fact_store(action="audit")` 返回完整数据质量报告:超长 fact 列表、无 summary 的长 fact、低 helpful 率 fact、老化候选
|
|
143
|
+
|
|
144
|
+
**理由**:用户需要知道为什么某些记忆突然排不上来(length penalty 影响),也需要工具引导治理。audit action 比 learn 更全面,专门做数据质量分析不改数据。
|
|
145
|
+
|
|
146
|
+
### D6: 移除 relevance gate
|
|
147
|
+
|
|
148
|
+
**选择**:完全移除 v3 的 `RELEVANCE_THRESHOLD = 0.15` 过滤
|
|
149
|
+
|
|
150
|
+
**理由**:0.15 阈值在短查询时经常误杀相关结果。length penalty 已经能更优雅地解决低质量问题。
|
|
151
|
+
|
|
152
|
+
## Risks / Trade-offs
|
|
153
|
+
|
|
154
|
+
- [Risk] summary 字段增加写入复杂度 → 回退策略:summary 为空时完全退化,零影响
|
|
155
|
+
- [Risk] learn action 误降核心记忆的 trust → 缓解:rate < 0.05 阈值很保守,需 30+ 次检索才能触发;trust 下降是渐进的(×0.9)
|
|
156
|
+
- [Risk] retrieval_log 表增长 → 缓解:保留最近 5000 条,超出自动删除最旧记录
|
|
157
|
+
- [Risk] length penalty 对无 summary 的超长 fact 影响过大 → 缓解:learn 返回 long_facts 报告 + audit action 引导用户补充 summary
|
|
158
|
+
- [Trade-off] 回退动态权重意味着放弃了"长查询偏 Jaccard"的策略 → 可接受:静态 0.5/0.5 经验证更准
|
|
159
|
+
- [Risk] 启动时 learn 在大数据量下阻塞 → 缓解:process.nextTick 延迟执行,MCP 握手先完成
|
|
160
|
+
|
|
161
|
+
## Migration Plan
|
|
162
|
+
|
|
163
|
+
1. ALTER TABLE facts ADD COLUMN summary TEXT DEFAULT NULL —— 向后兼容,无数据丢失
|
|
164
|
+
2. ALTER TABLE facts ADD COLUMN last_retrieved_at TEXT DEFAULT NULL —— 用于 aging 判断
|
|
165
|
+
3. CREATE TABLE retrieval_log —— 新表,无影响
|
|
166
|
+
4. 回退 retriever.ts 中的动态权重代码 —— 纯代码变更
|
|
167
|
+
5. 更新 FTS5 索引以包含 summary 列 —— 需要重建虚拟表(数据量 70 条,瞬间完成)
|
|
168
|
+
6. 发布为 minor version(v0.2.0),向后兼容 v0.1.4
|
|
169
|
+
|
|
170
|
+
## Open Questions
|
|
171
|
+
|
|
172
|
+
- ~~summary 为空时,是否应该自动用 content 前 100 字作为 fallback summary?~~ → 已决策:不自动截取,依赖用户/AI 提供或用 audit action 引导
|
|
173
|
+
- ~~learn action 的执行时机~~ → 已决策:启动时 process.nextTick 延迟执行
|
|
174
|
+
- ~~已有超长 fact 是否需要一次性治理~~ → 已决策:learn 返回 long_facts 报告 + audit action 引导
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
## Why
|
|
2
|
+
|
|
3
|
+
mnemo-mcp 当前记忆检索准确率不足,根因有三:(1) **数据质量差**——29% 的 fact 超过 500 字,最长 3921 字的"万能条"像黑洞一样吸走所有检索请求(#144 被检索 628 次仅 4 次 helpful,率 0.6%);(2) **v3 动态权重适得其反**——长查询给 FTS5 只留 30% 权重,把 BM25 的正确排序拉下来了(#60 "暖暖角色设定"FTS5 rank -10.17 排第一,但经 Jaccard 重排后被 #208 挤掉);(3) **没有反馈闭环**——70 条事实累计检索上万次,fact_feedback 不足 100 次,系统无法自我优化。需要从全生命周期角度治理:数据质量 → 检索策略 → 自学习闭环。
|
|
4
|
+
|
|
5
|
+
## What Changes
|
|
6
|
+
|
|
7
|
+
- 回退 v3 动态权重,恢复 v2 静态 FTS/Jaccard 权重(0.5/0.5)
|
|
8
|
+
- 新增 **length penalty**:基于实际匹配文本(summary 或 content)长度的惩罚,超长无 summary 的 fact 自动降权
|
|
9
|
+
- 新增 **retrieval_log 表**:每次 search 自动记录 query + 每条结果的 `{id, score}`,为自学习和调试提供数据
|
|
10
|
+
- 新增 **`learn` action**:基于 rate 规则自动调整 trust_score + 数据质量报告(long_facts 列表)
|
|
11
|
+
- 新增 **`audit` action**:数据质量报告(超长无 summary、低 helpful 率、老化候选),不修改数据
|
|
12
|
+
- 新增 **summary 字段**:facts 表增加 summary + last_retrieved_at 列,超长 fact 存储提炼后的摘要
|
|
13
|
+
- 修改 **content 长度限制**:add/update 时 content 超 500 字且无 summary 返回警告(300-500 字只受 penalty 不警告)
|
|
14
|
+
- 移除 v3 的 **relevance gate**(0.15 阈值误杀有用结果)和 **动态权重逻辑**
|
|
15
|
+
- 保留 v3 的 **refineQuery**(过滤纯操作指令与权重策略无关)
|
|
16
|
+
|
|
17
|
+
## Capabilities
|
|
18
|
+
|
|
19
|
+
### New Capabilities
|
|
20
|
+
- `retrieval-log`: 检索日志自动记录,每次 search 写入 query + [{id, score}] + timestamp,同步更新 last_retrieved_at
|
|
21
|
+
- `self-learning`: learn action(trust 调整 + 数据质量报告)+ audit action(纯报告不改数据)+ 启动时 nextTick 延迟执行
|
|
22
|
+
- `length-penalty`: 基于 matchText(summary 优先于 content)的长度惩罚,有 summary 的 fact 不受 content 长度惩罚
|
|
23
|
+
- `fact-summary`: facts 表增加 summary + last_retrieved_at 字段 + 双阈值(300 penalty / 500 warning)
|
|
24
|
+
|
|
25
|
+
### Modified Capabilities
|
|
26
|
+
- `fact-retrieval`: 回退动态权重为静态 0.5/0.5,移除 relevance gate,集成 length penalty 和 summary 匹配,保留 refineQuery
|
|
27
|
+
|
|
28
|
+
## Impact
|
|
29
|
+
|
|
30
|
+
- `src/retriever.ts`:回退动态权重,移除 relevance gate,新增 length penalty(基于 matchText),summary 匹配
|
|
31
|
+
- `src/store.ts`:新增 retrieval_log 表、facts 表 summary + last_retrieved_at 字段、learn/audit 分析逻辑
|
|
32
|
+
- `src/server.ts`:新增 learn/audit action handler、search 时自动写入 retrieval_log、add/update 长度校验、启动 nextTick learn
|
|
33
|
+
- `src/types.ts`:新增 RetrievalLog 类型、summary/last_retrieved_at 字段、learn/audit 返回类型
|
|
34
|
+
- 数据库:新增 retrieval_log 表、facts 表加 summary + last_retrieved_at 列(ALTER TABLE,向后兼容)
|
|
35
|
+
- 向后兼容:所有变更对现有数据透明,summary 为空时退化为原始 content 匹配
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
## MODIFIED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Static FTS/Jaccard weighting
|
|
4
|
+
检索评分 MUST 使用静态权重 FTS 0.5 / Jaccard 0.5,不再根据查询长度动态调整。
|
|
5
|
+
|
|
6
|
+
#### Scenario: Short query uses static weights
|
|
7
|
+
- **WHEN** 搜索 "深色主题"(2个token)
|
|
8
|
+
- **THEN** 评分使用 ftsWeight=0.5, jaccardWeight=0.5(不再是 0.7/0.3)
|
|
9
|
+
|
|
10
|
+
#### Scenario: Long query uses same static weights
|
|
11
|
+
- **WHEN** 搜索 "为什么 TypeScript 编译报错找不到模块"(8个token)
|
|
12
|
+
- **THEN** 评分使用 ftsWeight=0.5, jaccardWeight=0.5(不再是 0.3/0.7)
|
|
13
|
+
|
|
14
|
+
## REMOVED Requirements
|
|
15
|
+
|
|
16
|
+
### Requirement: Relevance gate threshold
|
|
17
|
+
**Reason**: 0.15 阈值在短查询时误杀相关结果,length penalty 能更优雅地解决低质量问题
|
|
18
|
+
**Migration**: 评分结果不再被 relevance gate 过滤,length penalty 自动惩罚低质量匹配
|
|
19
|
+
|
|
20
|
+
### Requirement: Dynamic scoring weights based on query length
|
|
21
|
+
**Reason**: 动态权重(短查询 FTS 0.7/长查询 0.3)实测降低检索准确率,BM25 正确排序被 Jaccard 覆盖
|
|
22
|
+
**Migration**: 所有查询统一使用静态 FTS 0.5 / Jaccard 0.5
|
|
23
|
+
|
|
24
|
+
## ADDED Requirements
|
|
25
|
+
|
|
26
|
+
### Requirement: Retrieval uses summary for matching when available
|
|
27
|
+
当 fact 的 summary 非空时,检索管线 MUST 使用 summary 进行 FTS5 和 Jaccard 匹配,而非 content。
|
|
28
|
+
|
|
29
|
+
#### Scenario: Search matches on summary
|
|
30
|
+
- **WHEN** 搜索 "VS Code" 且一条 fact 的 summary="用户偏好 VS Code"
|
|
31
|
+
- **THEN** FTS5 和 Jaccard 使用 summary 文本计算匹配度
|
|
32
|
+
|
|
33
|
+
#### Scenario: Returned results include full content
|
|
34
|
+
- **WHEN** 搜索返回一条 fact(summary 非空)
|
|
35
|
+
- **THEN** 返回的 JSON 中 content 字段为完整原始内容,summary 字段为摘要
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: facts table has summary and last_retrieved_at columns
|
|
4
|
+
`facts` 表 MUST 包含 `summary TEXT DEFAULT NULL` 和 `last_retrieved_at TEXT DEFAULT NULL` 列。summary 非空时用于检索匹配,为空时退化为 content 匹配。last_retrieved_at 由检索日志写入时自动更新。
|
|
5
|
+
|
|
6
|
+
#### Scenario: Add fact without summary
|
|
7
|
+
- **WHEN** 调用 `fact_store(action="add", content="用户偏好深色主题")`
|
|
8
|
+
- **THEN** summary 为 NULL,last_retrieved_at 为 NULL,检索时使用 content
|
|
9
|
+
|
|
10
|
+
#### Scenario: Add fact with summary
|
|
11
|
+
- **WHEN** 调用 `fact_store(action="add", content="...", summary="用户偏好深色主题")`
|
|
12
|
+
- **THEN** summary 被存储,检索时优先使用 summary 匹配
|
|
13
|
+
|
|
14
|
+
### Requirement: Write operation warns on long content without summary(500 字阈值)
|
|
15
|
+
当 add/update 的 content 长度超过 500 字且未提供 summary 时,系统 MUST 返回警告提示。
|
|
16
|
+
|
|
17
|
+
#### Scenario: Long fact without summary triggers warning
|
|
18
|
+
- **WHEN** 调用 `fact_store(action="add", content="<600字内容>")` 且未提供 summary
|
|
19
|
+
- **THEN** 操作成功,但返回 JSON 包含 `warnings: ["content 超过 500 字,建议提供 summary 或拆分为多条 fact"]`
|
|
20
|
+
|
|
21
|
+
#### Scenario: Long fact with summary has no warning
|
|
22
|
+
- **WHEN** 调用 `fact_store(action="add", content="<600字内容>", summary="核心摘要")`
|
|
23
|
+
- **THEN** 操作成功,无长度警告
|
|
24
|
+
|
|
25
|
+
#### Scenario: 300-500 字 content without summary has no warning
|
|
26
|
+
- **WHEN** 调用 `fact_store(action="add", content="<400字内容>")` 且未提供 summary
|
|
27
|
+
- **THEN** 操作成功,无警告(300-500 字处于灰色地带,只受 length penalty 影响不受写入警告)
|
|
28
|
+
|
|
29
|
+
### Requirement: FTS5 indexes summary when present
|
|
30
|
+
FTS5 虚拟表 MUST 索引 summary 列。当 summary 非空时,FTS5 使用 summary 进行匹配;summary 为空时使用 content。
|
|
31
|
+
|
|
32
|
+
#### Scenario: FTS5 matches on summary
|
|
33
|
+
- **WHEN** 一条 fact 的 content="很长很长的内容..."(2000字),summary="用户偏好 VS Code"
|
|
34
|
+
- **THEN** 搜索 "VS Code" 时能通过 summary 匹配到该 fact
|
|
35
|
+
|
|
36
|
+
#### Scenario: FTS5 falls back to content when no summary
|
|
37
|
+
- **WHEN** 一条 fact 的 summary=NULL,content="用户偏好 VS Code"
|
|
38
|
+
- **THEN** 搜索 "VS Code" 时通过 content 匹配到该 fact
|
|
39
|
+
|
|
40
|
+
### Requirement: Backward compatible migration
|
|
41
|
+
ALTER TABLE 为已有数据库添加新列时 MUST 不丢失任何现有数据。
|
|
42
|
+
|
|
43
|
+
#### Scenario: Existing database gets new columns
|
|
44
|
+
- **WHEN** mnemo-mcp 启动且 facts 表没有 summary 或 last_retrieved_at 列
|
|
45
|
+
- **THEN** 自动执行 ALTER TABLE 添加缺失列,所有现有 fact 不受影响
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Scoring formula includes length penalty based on match text
|
|
4
|
+
检索评分公式 MUST 对实际匹配文本(summary 非空时用 summary,否则用 content)长度超过 300 字的 fact 施加线性惩罚:`score *= min(1.0, 300 / matchText.length)`。
|
|
5
|
+
|
|
6
|
+
#### Scenario: Short fact gets no penalty
|
|
7
|
+
- **WHEN** 检索到一个 content 长度为 150 字、summary 为 NULL 的 fact,原始 score=0.5
|
|
8
|
+
- **THEN** matchText = content (150字),最终 score = 0.5 × min(1.0, 300/150) = 0.5
|
|
9
|
+
|
|
10
|
+
#### Scenario: Long fact without summary gets penalized
|
|
11
|
+
- **WHEN** 检索到一个 content 长度为 1500 字、summary 为 NULL 的 fact,原始 score=0.5
|
|
12
|
+
- **THEN** matchText = content (1500字),最终 score = 0.5 × min(1.0, 300/1500) = 0.1
|
|
13
|
+
|
|
14
|
+
#### Scenario: Long fact with short summary gets no penalty
|
|
15
|
+
- **WHEN** 检索到一个 content 长度为 2000 字、summary 长度为 50 字的 fact,原始 score=0.5
|
|
16
|
+
- **THEN** matchText = summary (50字),最终 score = 0.5 × min(1.0, 300/50) = 0.5 × 1.0 = 0.5
|
|
17
|
+
|
|
18
|
+
#### Scenario: Boundary at 300 chars of match text
|
|
19
|
+
- **WHEN** 检索到一个 matchText 长度恰好 300 字的 fact
|
|
20
|
+
- **THEN** penalty = min(1.0, 300/300) = 1.0,无惩罚
|
|
21
|
+
|
|
22
|
+
### Requirement: Length penalty applies after all other scoring
|
|
23
|
+
length penalty MUST 在 FTS/Jaccard/trust 评分计算完成后、排序前应用。
|
|
24
|
+
|
|
25
|
+
#### Scenario: Penalty stacks with trust score
|
|
26
|
+
- **WHEN** 一个 fact 的 fts+jaccard relevance=0.6, trust=0.8, matchText 长度=900 字, summary=NULL
|
|
27
|
+
- **THEN** score = 0.6 × 0.8 × min(1.0, 300/900) = 0.48 × 0.333 = 0.16
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Search operations automatically log retrieval results
|
|
4
|
+
每次 `search` 操作 MUST 自动将 query 和返回的 fact 结果写入 `retrieval_log` 表,包含每个 fact 的 id 和 score。
|
|
5
|
+
|
|
6
|
+
#### Scenario: Search logs query and scored results
|
|
7
|
+
- **WHEN** 调用 `fact_store(action="search", query="用户偏好")` 返回 fact_ids [27, 51, 60] 对应 scores [0.45, 0.32, 0.28]
|
|
8
|
+
- **THEN** `retrieval_log` 表新增一条记录,query="用户偏好",results='[{"id":27,"score":0.45},{"id":51,"score":0.32},{"id":60,"score":0.28}]'
|
|
9
|
+
|
|
10
|
+
#### Scenario: Empty search results are still logged
|
|
11
|
+
- **WHEN** 调用 `fact_store(action="search", query="完全不存在的查询")` 返回空结果
|
|
12
|
+
- **THEN** `retrieval_log` 表新增一条记录,query="完全不存在的查询",results="[]"
|
|
13
|
+
|
|
14
|
+
### Requirement: Retrieval log has a size cap
|
|
15
|
+
`retrieval_log` 表 MUST 保留最近 5000 条记录,超出时自动删除最旧记录。
|
|
16
|
+
|
|
17
|
+
#### Scenario: Auto-prune when exceeding 5000 entries
|
|
18
|
+
- **WHEN** `retrieval_log` 已有 5000 条记录,新 search 写入第 5001 条
|
|
19
|
+
- **THEN** 最旧的一条记录被删除,总数保持 5000
|
|
20
|
+
|
|
21
|
+
### Requirement: retrieval_log table schema
|
|
22
|
+
系统 MUST 创建以下表结构:
|
|
23
|
+
```sql
|
|
24
|
+
CREATE TABLE retrieval_log (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
query TEXT NOT NULL,
|
|
27
|
+
results TEXT NOT NULL,
|
|
28
|
+
timestamp TEXT DEFAULT (datetime('now'))
|
|
29
|
+
);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
#### Scenario: Table exists after database initialization
|
|
33
|
+
- **WHEN** mnemo-mcp 启动并打开 `~/.mnemo/facts.db`
|
|
34
|
+
- **THEN** `retrieval_log` 表存在且符合上述 schema
|
|
35
|
+
|
|
36
|
+
### Requirement: logRetrieval updates last_retrieved_at
|
|
37
|
+
每次写入 retrieval_log 时 MUST 同步更新返回的每条 fact 的 `last_retrieved_at` 字段。
|
|
38
|
+
|
|
39
|
+
#### Scenario: last_retrieved_at is updated on retrieval
|
|
40
|
+
- **WHEN** 搜索返回 fact_ids [27, 51]
|
|
41
|
+
- **THEN** fact 27 和 51 的 `last_retrieved_at` 被更新为当前时间
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: learn action adjusts trust scores automatically
|
|
4
|
+
系统 SHALL 提供 `fact_store(action="learn")` 操作,基于检索日志统计自动调整各 fact 的 trust_score。
|
|
5
|
+
|
|
6
|
+
#### Scenario: High retrieval low helpful fact gets demoted
|
|
7
|
+
- **WHEN** 调用 `fact_store(action="learn")` 且某 fact 的 retrieval_count=100, helpful_count=2 (rate=2%)
|
|
8
|
+
- **THEN** 该 fact 的 trust_score 被乘以 0.9
|
|
9
|
+
|
|
10
|
+
#### Scenario: High helpful rate fact gets promoted
|
|
11
|
+
- **WHEN** 调用 `fact_store(action="learn")` 且某 fact 的 retrieval_count=50, helpful_count=20 (rate=40%)
|
|
12
|
+
- **THEN** 该 fact 的 trust_score 增加 0.05(上限 1.0)
|
|
13
|
+
|
|
14
|
+
#### Scenario: Low retrieval count facts are not adjusted
|
|
15
|
+
- **WHEN** 调用 `fact_store(action="learn")` 且某 fact 的 retrieval_count=10
|
|
16
|
+
- **THEN** 该 fact 的 trust_score 不受 rate 规则影响(低于 30 次阈值)
|
|
17
|
+
|
|
18
|
+
### Requirement: learn action applies aging decay based on last_retrieved_at
|
|
19
|
+
系统 MUST 对 `last_retrieved_at` 超过 60 天的 fact 施加老化衰减,trust_score 乘以 0.95。
|
|
20
|
+
|
|
21
|
+
#### Scenario: Stale fact gets aged
|
|
22
|
+
- **WHEN** 调用 `fact_store(action="learn")` 且某 fact 的 last_retrieved_at 在 61 天前
|
|
23
|
+
- **THEN** 该 fact 的 trust_score 被乘以 0.95
|
|
24
|
+
|
|
25
|
+
#### Scenario: Recently retrieved fact is not aged
|
|
26
|
+
- **WHEN** 调用 `fact_store(action="learn")` 且某 fact 的 last_retrieved_at 在 10 天前
|
|
27
|
+
- **THEN** 该 fact 不受老化规则影响
|
|
28
|
+
|
|
29
|
+
#### Scenario: Never retrieved fact gets aged
|
|
30
|
+
- **WHEN** 调用 `fact_store(action="learn")` 且某 fact 的 last_retrieved_at 为 NULL
|
|
31
|
+
- **THEN** 该 fact 被视为从未检索,不受老化规则影响(新 fact 保护期)
|
|
32
|
+
|
|
33
|
+
### Requirement: learn returns adjustment summary with quality report
|
|
34
|
+
`learn` action MUST 返回调整摘要,包含被调整的 fact 数量、方向以及数据质量报告。
|
|
35
|
+
|
|
36
|
+
#### Scenario: Learn returns summary with long_facts report
|
|
37
|
+
- **WHEN** 调用 `fact_store(action="learn")`
|
|
38
|
+
- **THEN** 返回 JSON 包含:
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"promoted": 2,
|
|
42
|
+
"demoted": 5,
|
|
43
|
+
"aged": 3,
|
|
44
|
+
"unchanged": 60,
|
|
45
|
+
"long_facts": [
|
|
46
|
+
{"id": 144, "content_length": 3921, "penalty": 0.077, "has_summary": false},
|
|
47
|
+
{"id": 169, "content_length": 3095, "penalty": 0.097, "has_summary": false}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Requirement: Server startup triggers learn with non-blocking delay
|
|
53
|
+
mnemo-mcp 启动时 MUST 通过 `process.nextTick()` 延迟执行 learn,不阻塞 MCP stdio 握手。
|
|
54
|
+
|
|
55
|
+
#### Scenario: Learn runs after MCP handshake
|
|
56
|
+
- **WHEN** mnemo-mcp server 启动完成 MCP 初始化
|
|
57
|
+
- **THEN** 在下一个事件循环中自动执行 learn 并输出调整摘要到 stderr
|
|
58
|
+
|
|
59
|
+
### Requirement: audit action returns data quality report
|
|
60
|
+
系统 SHALL 提供 `fact_store(action="audit")` 操作,返回完整数据质量报告,不修改任何数据。
|
|
61
|
+
|
|
62
|
+
#### Scenario: Audit returns quality report
|
|
63
|
+
- **WHEN** 调用 `fact_store(action="audit")`
|
|
64
|
+
- **THEN** 返回 JSON 包含:超长 fact 列表(>500字无 summary)、低 helpful 率 fact(rate<5%且retrieval>30)、老化候选(>60天未检索)、总统计
|
|
65
|
+
|
|
66
|
+
#### Scenario: Audit does not modify data
|
|
67
|
+
- **WHEN** 调用 `fact_store(action="audit")`
|
|
68
|
+
- **THEN** 不修改任何 fact 的 trust_score、retrieval_count 或其他字段
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
## 1. 存储层:schema 迁移
|
|
2
|
+
|
|
3
|
+
- [ ] 1.1 `src/store.ts` — 新增 `retrieval_log` 表创建(id, query, results JSON, timestamp)
|
|
4
|
+
- [ ] 1.2 `src/store.ts` — `facts` 表 `ALTER TABLE ADD COLUMN summary TEXT DEFAULT NULL`(兼容已有库)
|
|
5
|
+
- [ ] 1.3 `src/store.ts` — `facts` 表 `ALTER TABLE ADD COLUMN last_retrieved_at TEXT DEFAULT NULL`
|
|
6
|
+
- [ ] 1.4 `src/store.ts` — 重建 FTS5 虚拟表以包含 summary 列
|
|
7
|
+
- [ ] 1.5 `src/store.ts` — 新增 `logRetrieval(query, results: [{id, score}])` 方法,写入 retrieval_log + 更新各 fact 的 last_retrieved_at
|
|
8
|
+
- [ ] 1.6 `src/store.ts` — 新增 `pruneRetrievalLog(maxEntries=5000)` 方法,超出上限删除最旧记录
|
|
9
|
+
- [ ] 1.7 `src/types.ts` — 新增 `RetrievalLogEntry` 类型、`FactStoreArgs` 增加 `summary` 字段、Fact 类型增加 `summary` 和 `last_retrieved_at`
|
|
10
|
+
|
|
11
|
+
## 2. 检索层:回退 + length penalty + summary
|
|
12
|
+
|
|
13
|
+
- [ ] 2.1 `src/retriever.ts` — 回退动态权重为静态 `ftsWeight=0.5, jaccardWeight=0.5`
|
|
14
|
+
- [ ] 2.2 `src/retriever.ts` — 移除 relevance gate(`RELEVANCE_THRESHOLD` 相关代码)
|
|
15
|
+
- [ ] 2.3 `src/retriever.ts` — 评分公式末尾新增 length penalty:`score *= min(1.0, 300 / matchText.length)`,matchText = summary(非空时)或 content
|
|
16
|
+
- [ ] 2.4 `src/retriever.ts` — FTS5 候选查询改为优先匹配 summary(非空时用 summary,空时用 content)
|
|
17
|
+
- [ ] 2.5 `src/retriever.ts` — Jaccard tokenization 同样优先使用 summary
|
|
18
|
+
- [ ] 2.6 `src/retriever.ts` — search() 方法末尾调用 `store.logRetrieval(query, results)` 记录检索日志
|
|
19
|
+
- [ ] 2.7 保留 `src/refine.ts` 的 refineQuery(过滤纯操作指令的功能与权重策略无关)
|
|
20
|
+
|
|
21
|
+
## 3. 自学习层:learn + audit
|
|
22
|
+
|
|
23
|
+
- [ ] 3.1 `src/store.ts` — 新增 `runLearning()` 方法:遍历所有 fact,按 rate 规则调整 trust_score
|
|
24
|
+
- `retrieval_count > 30 && rate < 0.05` → `trust_score *= 0.9`
|
|
25
|
+
- `retrieval_count > 30 && rate > 0.3` → `trust_score = min(1.0, trust_score + 0.05)`
|
|
26
|
+
- `last_retrieved_at` 超过 60 天 → `trust_score *= 0.95`
|
|
27
|
+
- `last_retrieved_at` 为 NULL(新 fact)→ 不老化
|
|
28
|
+
- [ ] 3.2 `src/store.ts` — `runLearning()` 返回 `{promoted, demoted, aged, unchanged, long_facts: [{id, content_length, penalty, has_summary}]}`
|
|
29
|
+
- [ ] 3.3 `src/store.ts` — 新增 `runAudit()` 方法:返回数据质量报告(超长无 summary、低 helpful 率、老化候选),不修改数据
|
|
30
|
+
- [ ] 3.4 `src/server.ts` — 新增 `fact_store(action="learn")` handler,调用 `store.runLearning()`
|
|
31
|
+
- [ ] 3.5 `src/server.ts` — 新增 `fact_store(action="audit")` handler,调用 `store.runAudit()`
|
|
32
|
+
- [ ] 3.6 `src/server.ts` — server 启动时通过 `process.nextTick()` 延迟调用 `store.runLearning()`,输出摘要到 stderr
|
|
33
|
+
|
|
34
|
+
## 4. 写入端:质量控制
|
|
35
|
+
|
|
36
|
+
- [ ] 4.1 `src/server.ts` — add handler 支持 `summary` 参数,存入 summary 列
|
|
37
|
+
- [ ] 4.2 `src/server.ts` — add/update 时 content 长度 > 500 且无 summary → 返回 warnings 提示
|
|
38
|
+
- [ ] 4.3 `src/server.ts` — add/update 写操作后调用 `store.pruneRetrievalLog()` 保持日志上限
|
|
39
|
+
- [ ] 4.4 `src/server.ts` — update handler 支持 `summary` 参数更新
|
|
40
|
+
|
|
41
|
+
## 5. 清理:移除 v3 遗留代码
|
|
42
|
+
|
|
43
|
+
- [ ] 5.1 `src/retriever.ts` — 移除动态权重计算逻辑(`tokenCount <= 3` 判断分支)
|
|
44
|
+
- [ ] 5.2 `src/retriever.ts` — 移除 content dedup(Jaccard > 0.7 去重),改为仅 score 排序
|
|
45
|
+
- [ ] 5.3 `src/refine.ts` — 保留 refineQuery(过滤纯操作指令),但移除与动态权重的耦合
|
|
46
|
+
|
|
47
|
+
## 6. 测试 + 验证
|
|
48
|
+
|
|
49
|
+
- [ ] 6.1 `tests/store.test.ts` — 新增 retrieval_log CRUD 测试(写入、查询、自动清理)
|
|
50
|
+
- [ ] 6.2 `tests/store.test.ts` — 新增 summary 列读写测试
|
|
51
|
+
- [ ] 6.3 `tests/store.test.ts` — 新增 `runLearning()` 信任调整测试(promote/demote/aging/新 fact 保护)
|
|
52
|
+
- [ ] 6.4 `tests/store.test.ts` — 新增 `runAudit()` 测试(返回报告不修改数据)
|
|
53
|
+
- [ ] 6.5 `tests/retriever.test.ts` — 新增 length penalty 测试(有/无 summary 两种场景)
|
|
54
|
+
- [ ] 6.6 `tests/retriever.test.ts` — 新增 summary 匹配测试(FTS5 + Jaccard 用 summary)
|
|
55
|
+
- [ ] 6.7 `tests/retriever.test.ts` — 验证静态权重(不再随查询长度变化)
|
|
56
|
+
- [ ] 6.8 端到端验证:`npm run build && npx vitest run`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morningljn/mnemo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Structured fact memory MCP server — SQLite + FTS5, trust scoring, entity graph, bilingual retrieval for Claude Code & Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server.js",
|