@morningljn/mnemo 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dream.d.ts +2 -0
- package/dist/dream.js +20 -0
- package/dist/dream.js.map +1 -0
- package/dist/init.js +4 -24
- package/dist/init.js.map +1 -1
- package/dist/resources.d.ts +22 -8
- package/dist/resources.js +66 -20
- package/dist/resources.js.map +1 -1
- package/dist/retriever.js +12 -5
- package/dist/retriever.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +2 -2
- package/dist/server.js +40 -6
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +22 -1
- package/dist/store.js +145 -4
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +27 -1
- package/docs/superpowers/plans/2026-05-16-memory-dreaming.md +626 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/design.md +71 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/proposal.md +32 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/specs/compact-search/spec.md +16 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/specs/dream-cycle/spec.md +38 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/tasks.md +27 -0
- package/openspec/specs/compact-search/spec.md +16 -0
- package/openspec/specs/dream-cycle/spec.md +38 -0
- package/package.json +3 -2
- package/src/dream.ts +20 -0
- package/src/init.ts +4 -24
- package/src/resources.ts +77 -21
- package/src/retriever.ts +9 -5
- package/src/schema.ts +2 -2
- package/src/server.ts +46 -7
- package/src/store.ts +166 -5
- package/src/types.ts +25 -1
- package/tests/resource.test.ts +25 -23
- package/tests/store.test.ts +129 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
## Why
|
|
2
|
+
|
|
3
|
+
mnemo 的记忆库随着长期使用会积累大量冗余、重叠、过长的 fact,导致检索精度下降、token 浪费。当前 `runLearning()` 只调 trust 分,不整理内容。需要一套后台自动整理机制(类似 OpenClaw Dreaming),定期合并去重、压缩摘要、分类修正,保持数据库精炼。
|
|
4
|
+
|
|
5
|
+
## What Changes
|
|
6
|
+
|
|
7
|
+
- 新增 `dream` action(`fact_store(action="dream")`),执行三阶段整理:Collect → Consolidate → Evaluate
|
|
8
|
+
- 新增 `mnemo dream` CLI 命令,支持 cron 定时触发
|
|
9
|
+
- 合并 Jaccard > 0.6 的重叠 fact(保留最完整的,其余删除)
|
|
10
|
+
- 长 fact(content > 200 字且无 summary)自动提取关键句生成 summary
|
|
11
|
+
- 分类自动修正:按关键词规则将误分类的 fact 挪到正确 category
|
|
12
|
+
- 输出 dream report:合并/删除/压缩了什么,健康评分多少
|
|
13
|
+
- 搜索结果精简格式:返回 summary(优先)或 content 前 100 字,减少 token 消耗
|
|
14
|
+
|
|
15
|
+
## Capabilities
|
|
16
|
+
|
|
17
|
+
### New Capabilities
|
|
18
|
+
- `dream-cycle`: 后台定期整理记忆库(合并去重、摘要压缩、分类修正、健康评分)
|
|
19
|
+
- `compact-search`: 搜索结果精简格式(summary 优先、content 截断、限制条数)
|
|
20
|
+
|
|
21
|
+
### Modified Capabilities
|
|
22
|
+
|
|
23
|
+
(无已有 spec 需要修改)
|
|
24
|
+
|
|
25
|
+
## Impact
|
|
26
|
+
|
|
27
|
+
- `src/store.ts` — 新增 `runDream()` 方法(合并、压缩、分类修正、报告)
|
|
28
|
+
- `src/retriever.ts` — 搜索结果格式精简(返回 summary 而非完整 content)
|
|
29
|
+
- `src/server.ts` — `fact_store` 新增 `dream` action
|
|
30
|
+
- `src/init.ts` — `mnemo init` 可选配置 cron 定时 dream
|
|
31
|
+
- `tests/store.test.ts` — dream 相关测试
|
|
32
|
+
- 数据库 — dream 可能删除/合并 fact,不可逆操作需谨慎
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: 搜索结果精简格式
|
|
4
|
+
搜索返回结果 SHALL 优先返回 summary 而非完整 content,减少 token 消耗。
|
|
5
|
+
|
|
6
|
+
#### Scenario: 有 summary 的 fact
|
|
7
|
+
- **WHEN** 搜索结果中的 fact 有 summary 字段且非空
|
|
8
|
+
- **THEN** 返回 summary 作为 display 字段,不返回完整 content
|
|
9
|
+
|
|
10
|
+
#### Scenario: 无 summary 的 fact
|
|
11
|
+
- **WHEN** 搜索结果中的 fact 的 summary 为 NULL
|
|
12
|
+
- **THEN** 返回 content 前 100 字 + "..." 作为 display 字段
|
|
13
|
+
|
|
14
|
+
#### Scenario: 返回字段精简
|
|
15
|
+
- **WHEN** 搜索结果返回给调用方
|
|
16
|
+
- **THEN** 每条结果包含 factId、display(精简内容)、category、trustScore、score,不包含完整 content、keywords、tags 等冗余字段
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Dream action 整理记忆库
|
|
4
|
+
系统 SHALL 提供 `fact_store(action="dream")` 操作,执行三阶段整理:Collect → Consolidate → Evaluate。
|
|
5
|
+
|
|
6
|
+
#### Scenario: 合并重叠 fact
|
|
7
|
+
- **WHEN** 同 category 内两条 fact 的 Jaccard 相似度 > 0.6
|
|
8
|
+
- **THEN** 系统保留 content 更长的 fact,将另一条标记删除,并在 dream report 中记录合并对
|
|
9
|
+
|
|
10
|
+
#### Scenario: 压缩长 fact
|
|
11
|
+
- **WHEN** fact 的 content 长度 > 200 字且 summary 为 NULL
|
|
12
|
+
- **THEN** 系统从 content 提取前 2 个完整句子(总长 ≤ 150 字)写入 summary 字段
|
|
13
|
+
|
|
14
|
+
#### Scenario: 分类修正
|
|
15
|
+
- **WHEN** fact 的 category 与内容不匹配(如 identity 类 fact 内容包含"编码规范")
|
|
16
|
+
- **THEN** 系统根据关键词规则表将 fact 挪到正确 category
|
|
17
|
+
|
|
18
|
+
#### Scenario: Dream 前备份
|
|
19
|
+
- **WHEN** dream action 被触发
|
|
20
|
+
- **THEN** 系统在执行任何修改前,自动将数据库备份到 `~/.mnemo/backup/dream-<timestamp>.db`
|
|
21
|
+
|
|
22
|
+
#### Scenario: 输出 dream report
|
|
23
|
+
- **WHEN** dream 整理完成
|
|
24
|
+
- **THEN** 系统返回 JSON 报告,包含 merged、compressed、reclassified、deleted 计数和 health 统计
|
|
25
|
+
|
|
26
|
+
### Requirement: CLI dream 命令
|
|
27
|
+
系统 SHALL 提供 `mnemo dream` CLI 命令,手动触发 dream 整理。
|
|
28
|
+
|
|
29
|
+
#### Scenario: 手动执行 dream
|
|
30
|
+
- **WHEN** 用户运行 `npx mnemo dream` 或 `mnemo dream`
|
|
31
|
+
- **THEN** 系统执行完整 dream cycle 并输出 report 到 stdout
|
|
32
|
+
|
|
33
|
+
### Requirement: 高频 fact 保护
|
|
34
|
+
Dream 整理 SHALL 保护检索次数 > 100 的 fact 不被删除。
|
|
35
|
+
|
|
36
|
+
#### Scenario: 高频 fact 不被合并删除
|
|
37
|
+
- **WHEN** 两条 fact 满足合并条件,但其中一条 retrieval_count > 100
|
|
38
|
+
- **THEN** 系统保留高频 fact,仅删除另一条低频 fact
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
## 1. 搜索结果精简
|
|
2
|
+
|
|
3
|
+
- [ ] 1.1 修改 `ScoredFact` 类型定义,新增 `display` 字段(精简内容)
|
|
4
|
+
- [ ] 1.2 修改 `retriever.ts` search 方法:有 summary 用 summary,无 summary 截取 content 前 100 字
|
|
5
|
+
- [ ] 1.3 修改 `server.ts` search/probe/related/reason 响应格式:返回 display 而非完整 content
|
|
6
|
+
- [ ] 1.4 更新 `tests/retriever.test.ts` 验证精简格式
|
|
7
|
+
|
|
8
|
+
## 2. Dream Cycle - Store 层
|
|
9
|
+
|
|
10
|
+
- [ ] 2.1 实现 `mergeOverlappingFacts()` — 同 category 内 Jaccard > 0.6 的 fact 合并,高频(retrieval > 100)保护
|
|
11
|
+
- [ ] 2.2 实现 `compressLongFacts()` — content > 200 字且无 summary 的 fact 自动提取前 2 句
|
|
12
|
+
- [ ] 2.3 实现 `reclassifyFacts()` — 按关键词规则表修正 category
|
|
13
|
+
- [ ] 2.4 实现 `backupDatabase()` — dream 前备份到 `~/.mnemo/backup/dream-<timestamp>.db`
|
|
14
|
+
- [ ] 2.5 实现 `runDream()` — 编排以上步骤,生成 dream report(merged/compressed/reclassified/deleted + health)
|
|
15
|
+
- [ ] 2.6 新增 `dream` action 到 `server.ts` 的 fact_store handler
|
|
16
|
+
|
|
17
|
+
## 3. CLI 命令
|
|
18
|
+
|
|
19
|
+
- [ ] 3.1 新增 `src/dream.ts` CLI 入口,执行 `store.runDream()` 并输出 report
|
|
20
|
+
- [ ] 3.2 在 `package.json` 添加 `mnemo dream` bin 入口
|
|
21
|
+
|
|
22
|
+
## 4. 测试
|
|
23
|
+
|
|
24
|
+
- [ ] 4.1 `tests/store.test.ts` — mergeOverlappingFacts 合并 + 高频保护测试
|
|
25
|
+
- [ ] 4.2 `tests/store.test.ts` — compressLongFacts 提取 summary 测试
|
|
26
|
+
- [ ] 4.3 `tests/store.test.ts` — reclassifyFacts 分类修正测试
|
|
27
|
+
- [ ] 4.4 `tests/store.test.ts` — runDream 端到端测试(备份数据库 + 整理 + report)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: 搜索结果精简格式
|
|
4
|
+
搜索返回结果 SHALL 优先返回 summary 而非完整 content,减少 token 消耗。
|
|
5
|
+
|
|
6
|
+
#### Scenario: 有 summary 的 fact
|
|
7
|
+
- **WHEN** 搜索结果中的 fact 有 summary 字段且非空
|
|
8
|
+
- **THEN** 返回 summary 作为 display 字段,不返回完整 content
|
|
9
|
+
|
|
10
|
+
#### Scenario: 无 summary 的 fact
|
|
11
|
+
- **WHEN** 搜索结果中的 fact 的 summary 为 NULL
|
|
12
|
+
- **THEN** 返回 content 前 100 字 + "..." 作为 display 字段
|
|
13
|
+
|
|
14
|
+
#### Scenario: 返回字段精简
|
|
15
|
+
- **WHEN** 搜索结果返回给调用方
|
|
16
|
+
- **THEN** 每条结果包含 factId、display(精简内容)、category、trustScore、score,不包含完整 content、keywords、tags 等冗余字段
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Dream action 整理记忆库
|
|
4
|
+
系统 SHALL 提供 `fact_store(action="dream")` 操作,执行三阶段整理:Collect → Consolidate → Evaluate。
|
|
5
|
+
|
|
6
|
+
#### Scenario: 合并重叠 fact
|
|
7
|
+
- **WHEN** 同 category 内两条 fact 的 Jaccard 相似度 > 0.6
|
|
8
|
+
- **THEN** 系统保留 content 更长的 fact,将另一条标记删除,并在 dream report 中记录合并对
|
|
9
|
+
|
|
10
|
+
#### Scenario: 压缩长 fact
|
|
11
|
+
- **WHEN** fact 的 content 长度 > 200 字且 summary 为 NULL
|
|
12
|
+
- **THEN** 系统从 content 提取前 2 个完整句子(总长 ≤ 150 字)写入 summary 字段
|
|
13
|
+
|
|
14
|
+
#### Scenario: 分类修正
|
|
15
|
+
- **WHEN** fact 的 category 与内容不匹配(如 identity 类 fact 内容包含"编码规范")
|
|
16
|
+
- **THEN** 系统根据关键词规则表将 fact 挪到正确 category
|
|
17
|
+
|
|
18
|
+
#### Scenario: Dream 前备份
|
|
19
|
+
- **WHEN** dream action 被触发
|
|
20
|
+
- **THEN** 系统在执行任何修改前,自动将数据库备份到 `~/.mnemo/backup/dream-<timestamp>.db`
|
|
21
|
+
|
|
22
|
+
#### Scenario: 输出 dream report
|
|
23
|
+
- **WHEN** dream 整理完成
|
|
24
|
+
- **THEN** 系统返回 JSON 报告,包含 merged、compressed、reclassified、deleted 计数和 health 统计
|
|
25
|
+
|
|
26
|
+
### Requirement: CLI dream 命令
|
|
27
|
+
系统 SHALL 提供 `mnemo dream` CLI 命令,手动触发 dream 整理。
|
|
28
|
+
|
|
29
|
+
#### Scenario: 手动执行 dream
|
|
30
|
+
- **WHEN** 用户运行 `npx mnemo dream` 或 `mnemo dream`
|
|
31
|
+
- **THEN** 系统执行完整 dream cycle 并输出 report 到 stdout
|
|
32
|
+
|
|
33
|
+
### Requirement: 高频 fact 保护
|
|
34
|
+
Dream 整理 SHALL 保护检索次数 > 100 的 fact 不被删除。
|
|
35
|
+
|
|
36
|
+
#### Scenario: 高频 fact 不被合并删除
|
|
37
|
+
- **WHEN** 两条 fact 满足合并条件,但其中一条 retrieval_count > 100
|
|
38
|
+
- **THEN** 系统保留高频 fact,仅删除另一条低频 fact
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morningljn/mnemo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"mnemo": "dist/server.js",
|
|
9
|
-
"mnemo-init": "dist/init.js"
|
|
9
|
+
"mnemo-init": "dist/init.js",
|
|
10
|
+
"mnemo-dream": "dist/dream.js"
|
|
10
11
|
},
|
|
11
12
|
"publishConfig": {
|
|
12
13
|
"access": "public"
|
package/src/dream.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { MemoryStore } from './store.js'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
const dbPath = join(homedir(), '.mnemo', 'facts.db')
|
|
8
|
+
const store = new MemoryStore(dbPath)
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
console.log('[mnemo dream] 开始整理记忆库...\n')
|
|
12
|
+
const report = await store.runDream()
|
|
13
|
+
console.log(JSON.stringify(report, null, 2))
|
|
14
|
+
console.log(`\n[mnemo dream] 完成: merged=${report.merged} compressed=${report.compressed} reclassified=${report.reclassified} deleted=${report.deleted}`)
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.error('[mnemo dream] error:', err)
|
|
17
|
+
process.exit(1)
|
|
18
|
+
} finally {
|
|
19
|
+
store.close()
|
|
20
|
+
}
|
package/src/init.ts
CHANGED
|
@@ -19,32 +19,12 @@ const CLAUDE_MD_PATH = join(CLAUDE_DIR, 'CLAUDE.md')
|
|
|
19
19
|
const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json')
|
|
20
20
|
|
|
21
21
|
const MEMORY_RULES = `
|
|
22
|
-
# 记忆系统使用规则
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
# mnemo 记忆系统
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## 规则 2:按需补充查询
|
|
31
|
-
仅在以下情况调用 \`fact_store(action="search")\`:
|
|
32
|
-
- 用户消息涉及个人偏好/习惯/工具选择且预热中未覆盖
|
|
33
|
-
- 用户明确查询记忆("我之前说过什么""按我的习惯")
|
|
34
|
-
- 技术选型时需要确认用户偏好
|
|
35
|
-
|
|
36
|
-
不触发查询的情况:
|
|
37
|
-
- 纯操作指令("运行测试""git commit")
|
|
38
|
-
- 通用技术问题("Promise 怎么用")
|
|
39
|
-
- 代码审查/解释请求
|
|
40
|
-
|
|
41
|
-
## 规则 3:写入记忆
|
|
42
|
-
用户说"记住"、"记下来"时,调用 \`fact_store(action="add", content="...", category="...")\`。
|
|
43
|
-
- 先 search 检查是否已有相似事实,有则 update
|
|
44
|
-
- category:identity / coding_style / tool_pref / workflow / general
|
|
45
|
-
|
|
46
|
-
## 规则 4:反馈强化
|
|
47
|
-
成功使用某条记忆时,调用 \`fact_feedback(action="helpful", fact_id=...)\`。
|
|
25
|
+
- 身份问题("你是谁"等)→ 先 fact_store(search, query="角色设定"),按设定回答
|
|
26
|
+
- 用户说"记住"→ fact_store(add),先 search 去重
|
|
27
|
+
- 成功使用记忆 → fact_feedback(helpful, fact_id)
|
|
48
28
|
`
|
|
49
29
|
|
|
50
30
|
const MCP_TOOLS = [
|
package/src/resources.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP Resource manager for mnemo-mcp.
|
|
3
|
-
* Exposes per-category memory
|
|
3
|
+
* Exposes per-category memory as MCP Resources for session warmup injection.
|
|
4
|
+
*
|
|
5
|
+
* identity → 指令格式(Claude 应遵循的行为设定)
|
|
6
|
+
* 其他 → 参考格式(供 Claude 查阅的用户偏好)
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
@@ -8,7 +11,7 @@ import type { MemoryStore } from './store.js'
|
|
|
8
11
|
import type { FactCategory } from './types.js'
|
|
9
12
|
|
|
10
13
|
const CATEGORIES: FactCategory[] = ['identity', 'coding_style', 'tool_pref', 'workflow', 'general']
|
|
11
|
-
const RESOURCE_LIMIT =
|
|
14
|
+
const RESOURCE_LIMIT = 15
|
|
12
15
|
|
|
13
16
|
export interface ResourceFact {
|
|
14
17
|
fact_id: number
|
|
@@ -17,13 +20,12 @@ export interface ResourceFact {
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export class ResourceManager {
|
|
20
|
-
private cache = new Map<FactCategory,
|
|
23
|
+
private cache = new Map<FactCategory, string>()
|
|
21
24
|
|
|
22
25
|
constructor(
|
|
23
26
|
private store: MemoryStore,
|
|
24
27
|
) {}
|
|
25
28
|
|
|
26
|
-
/** Register all category resources with the MCP server */
|
|
27
29
|
registerResources(server: McpServer): void {
|
|
28
30
|
for (const category of CATEGORIES) {
|
|
29
31
|
const uri = `mnemo://global/${category}`
|
|
@@ -32,46 +34,100 @@ export class ResourceManager {
|
|
|
32
34
|
uri,
|
|
33
35
|
{
|
|
34
36
|
description: `${category} category global facts (top ${RESOURCE_LIMIT} by trust)`,
|
|
35
|
-
mimeType: '
|
|
37
|
+
mimeType: 'text/markdown',
|
|
36
38
|
},
|
|
37
39
|
async () => this.readCategory(category),
|
|
38
40
|
)
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
/** Read handler for a specific category */
|
|
43
|
-
|
|
44
|
-
const
|
|
44
|
+
/** Read handler for a specific category (public for server instructions) */
|
|
45
|
+
readCategory(category: FactCategory): { contents: Array<{ uri: string; mimeType: string; text: string }> } {
|
|
46
|
+
const text = this.getFormattedFacts(category)
|
|
45
47
|
return {
|
|
46
48
|
contents: [{
|
|
47
49
|
uri: `mnemo://global/${category}`,
|
|
48
|
-
mimeType: '
|
|
49
|
-
text
|
|
50
|
+
mimeType: 'text/markdown',
|
|
51
|
+
text,
|
|
50
52
|
}],
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
getFacts(category: FactCategory): ResourceFact[] {
|
|
56
|
+
private getFormattedFacts(category: FactCategory): string {
|
|
56
57
|
const cached = this.cache.get(category)
|
|
57
58
|
if (cached) return cached
|
|
58
59
|
|
|
59
|
-
const facts = this.store.listFacts(category, 0.0, RESOURCE_LIMIT)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}))
|
|
60
|
+
const facts = this.store.listFacts(category, 0.0, RESOURCE_LIMIT)
|
|
61
|
+
const text = category === 'identity'
|
|
62
|
+
? this.formatAsInstructions(facts)
|
|
63
|
+
: this.formatAsReference(facts, category)
|
|
64
64
|
|
|
65
|
-
this.cache.set(category,
|
|
66
|
-
return
|
|
65
|
+
this.cache.set(category, text)
|
|
66
|
+
return text
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* identity 类事实格式化为指令——Claude 应直接遵循这些设定。
|
|
71
|
+
* 角色设定排在最前面,用祈使句。
|
|
72
|
+
*/
|
|
73
|
+
private formatAsInstructions(facts: ReturnType<MemoryStore['listFacts']>): string {
|
|
74
|
+
const lines: string[] = ['# 身份与行为设定', '', '以下是你的身份设定和用户偏好,请直接遵循:', '']
|
|
75
|
+
|
|
76
|
+
// 角色/身份相关的 fact 排在最前面
|
|
77
|
+
const roleFacts = facts.filter(f =>
|
|
78
|
+
f.content.includes('角色设定') ||
|
|
79
|
+
f.content.includes('你是') ||
|
|
80
|
+
f.content.includes('身份是') ||
|
|
81
|
+
f.content.includes('女朋友')
|
|
82
|
+
)
|
|
83
|
+
const otherFacts = facts.filter(f => !roleFacts.includes(f))
|
|
84
|
+
|
|
85
|
+
if (roleFacts.length > 0) {
|
|
86
|
+
lines.push('## 你的身份')
|
|
87
|
+
for (const f of roleFacts) {
|
|
88
|
+
// 把描述性语句转为指令
|
|
89
|
+
const content = f.content
|
|
90
|
+
.replace(/^AI角色设定[::]/, '')
|
|
91
|
+
.replace(/^你是/, '')
|
|
92
|
+
.trim()
|
|
93
|
+
lines.push(`- ${content}`)
|
|
94
|
+
}
|
|
95
|
+
lines.push('')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (otherFacts.length > 0) {
|
|
99
|
+
lines.push('## 用户信息')
|
|
100
|
+
for (const f of otherFacts) {
|
|
101
|
+
lines.push(`- ${f.content}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return lines.join('\n')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 非 identity 类事实格式化为参考——供 Claude 查阅但不强制遵循。
|
|
110
|
+
*/
|
|
111
|
+
private formatAsReference(facts: ReturnType<MemoryStore['listFacts']>, category: string): string {
|
|
112
|
+
const title: Record<string, string> = {
|
|
113
|
+
coding_style: '编码风格偏好',
|
|
114
|
+
tool_pref: '工具偏好',
|
|
115
|
+
workflow: '工作流偏好',
|
|
116
|
+
general: '通用知识',
|
|
117
|
+
}
|
|
118
|
+
const lines: string[] = [`# ${title[category] ?? category}`, '']
|
|
119
|
+
|
|
120
|
+
for (const f of facts) {
|
|
121
|
+
lines.push(`- ${f.content}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return lines.join('\n')
|
|
67
125
|
}
|
|
68
126
|
|
|
69
|
-
/** Invalidate all caches — call after any write operation */
|
|
70
127
|
invalidate(): void {
|
|
71
128
|
this.cache.clear()
|
|
72
129
|
}
|
|
73
130
|
|
|
74
|
-
/** Get cache entry count for debugging */
|
|
75
131
|
cacheSize(): number {
|
|
76
132
|
return this.cache.size
|
|
77
133
|
}
|
package/src/retriever.ts
CHANGED
|
@@ -437,15 +437,19 @@ export class FactRetriever {
|
|
|
437
437
|
const ftsParts: string[] = []
|
|
438
438
|
|
|
439
439
|
for (const word of parts) {
|
|
440
|
-
|
|
441
|
-
// 对中文部分追加 bigram
|
|
442
|
-
const cnChars = word.match(/[\u4e00-\u9fff]+/g)
|
|
440
|
+
const cnChars = word.match(/[一-鿿]+/g)
|
|
443
441
|
if (cnChars) {
|
|
442
|
+
// 中文部分:trigram tokenizer 需要至少 3 字符
|
|
444
443
|
for (const seg of cnChars) {
|
|
445
|
-
|
|
446
|
-
|
|
444
|
+
if (seg.length >= 3) ftsParts.push(seg)
|
|
445
|
+
// 提取 trigram(3 字符子串)
|
|
446
|
+
for (let i = 0; i <= seg.length - 3; i++) {
|
|
447
|
+
ftsParts.push(seg.slice(i, i + 3))
|
|
447
448
|
}
|
|
448
449
|
}
|
|
450
|
+
} else {
|
|
451
|
+
// 非中文部分:用引号包裹(短语匹配),至少 1 字符
|
|
452
|
+
if (word.length >= 1) ftsParts.push(`"${word}"`)
|
|
449
453
|
}
|
|
450
454
|
}
|
|
451
455
|
|
package/src/schema.ts
CHANGED
|
@@ -46,9 +46,9 @@ CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
|
|
46
46
|
CREATE INDEX IF NOT EXISTS idx_fact_entities_entity ON fact_entities(entity_id);
|
|
47
47
|
CREATE INDEX IF NOT EXISTS idx_retrieval_log_ts ON retrieval_log(timestamp);
|
|
48
48
|
|
|
49
|
-
-- FTS5
|
|
49
|
+
-- FTS5 全文索引(trigram tokenizer 支持中文子串匹配)
|
|
50
50
|
CREATE VIRTUAL TABLE IF NOT EXISTS facts_fts
|
|
51
|
-
USING fts5(content, tags, summary, content=facts, content_rowid=fact_id);
|
|
51
|
+
USING fts5(content, tags, summary, content=facts, content_rowid=fact_id, tokenize='trigram');
|
|
52
52
|
|
|
53
53
|
-- FTS5 同步触发器:插入
|
|
54
54
|
CREATE TRIGGER IF NOT EXISTS facts_ai AFTER INSERT ON facts BEGIN
|
package/src/server.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { MemoryStore } from './store.js'
|
|
|
9
9
|
import { FactRetriever } from './retriever.js'
|
|
10
10
|
import { ResourceManager } from './resources.js'
|
|
11
11
|
import { fullSecurityScan } from './security.js'
|
|
12
|
-
import type { FactStoreArgs, FactFeedbackArgs, FactCategory } from './types.js'
|
|
12
|
+
import type { FactStoreArgs, FactFeedbackArgs, FactCategory, ScoredFact, CompactFactResult } from './types.js'
|
|
13
13
|
|
|
14
14
|
const FACT_STORE_DESCRIPTION = `结构化事实记忆系统(SQLite+FTS5 索引)。支持读写。
|
|
15
15
|
|
|
@@ -27,7 +27,7 @@ const FACT_STORE_DESCRIPTION = `结构化事实记忆系统(SQLite+FTS5 索引
|
|
|
27
27
|
写入时先 search 检查是否已存在相似事实。identity/coding_style/tool_pref/workflow/general → 全局库,project → 项目库。`
|
|
28
28
|
|
|
29
29
|
const factStoreSchema = {
|
|
30
|
-
action: z.enum(['add', 'search', 'probe', 'related', 'reason', 'contradict', 'update', 'remove', 'list', 'learn', 'audit']),
|
|
30
|
+
action: z.enum(['add', 'search', 'probe', 'related', 'reason', 'contradict', 'update', 'remove', 'list', 'learn', 'audit', 'dream']),
|
|
31
31
|
content: z.union([z.string(), z.array(z.string())]).optional().describe("事实内容('add' 必需,支持批量)"),
|
|
32
32
|
summary: z.string().optional().describe('超长事实的摘要(检索用 summary 匹配)'),
|
|
33
33
|
query: z.string().optional().describe("搜索查询('search' 必需)"),
|
|
@@ -52,6 +52,16 @@ function resolveCategory(category?: string): FactCategory {
|
|
|
52
52
|
return valid.includes(category as FactCategory) ? (category as FactCategory) : 'general'
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function toCompactResult(f: ScoredFact): CompactFactResult {
|
|
56
|
+
return {
|
|
57
|
+
factId: f.factId,
|
|
58
|
+
display: f.summary ?? (f.content.length > 100 ? f.content.slice(0, 100) + '...' : f.content),
|
|
59
|
+
category: f.category,
|
|
60
|
+
trustScore: Math.round(f.trustScore * 100) / 100,
|
|
61
|
+
score: Math.round(f.score * 1000) / 1000,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
const minTrust = 0.3
|
|
56
66
|
|
|
57
67
|
// -- Initialize store + retriever --
|
|
@@ -76,7 +86,25 @@ process.nextTick(() => {
|
|
|
76
86
|
})
|
|
77
87
|
|
|
78
88
|
// -- MCP Server --
|
|
79
|
-
|
|
89
|
+
// 动态生成 instructions:将 identity resource 中的角色设定作为 system prompt 指令注入
|
|
90
|
+
function buildInstructions(): string {
|
|
91
|
+
try {
|
|
92
|
+
const rm = new ResourceManager(store)
|
|
93
|
+
const result = rm.readCategory('identity')
|
|
94
|
+
const identityText = result.contents[0]?.text ?? ''
|
|
95
|
+
if (identityText.length > 10) {
|
|
96
|
+
return identityText
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// fallback:无 identity 数据时不注入
|
|
100
|
+
}
|
|
101
|
+
return ''
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const server = new McpServer(
|
|
105
|
+
{ name: 'mnemo-mcp', version: '0.1.0' },
|
|
106
|
+
{ instructions: buildInstructions() },
|
|
107
|
+
)
|
|
80
108
|
|
|
81
109
|
// -- MCP Resources: 会话预热注入 --
|
|
82
110
|
const resourceManager = new ResourceManager(store)
|
|
@@ -136,26 +164,30 @@ server.tool(
|
|
|
136
164
|
case 'search': {
|
|
137
165
|
if (!a.query) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: query' }) }] }
|
|
138
166
|
const results = retriever.search(a.query, { category: a.category ? category : undefined, minTrust: a.min_trust ?? minTrust, limit: a.limit ?? 10 })
|
|
139
|
-
|
|
167
|
+
const compact = results.map(toCompactResult)
|
|
168
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ results: compact, count: compact.length }) }] }
|
|
140
169
|
}
|
|
141
170
|
|
|
142
171
|
case 'probe': {
|
|
143
172
|
if (!a.entity) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: entity' }) }] }
|
|
144
173
|
const results = retriever.probe(a.entity, { minTrust: a.min_trust ?? minTrust, limit: a.limit ?? 10 })
|
|
145
|
-
|
|
174
|
+
const compact = results.map(toCompactResult)
|
|
175
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ results: compact, count: compact.length }) }] }
|
|
146
176
|
}
|
|
147
177
|
|
|
148
178
|
case 'related': {
|
|
149
179
|
if (!a.entity) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: entity' }) }] }
|
|
150
180
|
const results = retriever.related(a.entity, { minTrust: a.min_trust ?? minTrust, limit: a.limit ?? 10 })
|
|
151
|
-
|
|
181
|
+
const compact = results.map(toCompactResult)
|
|
182
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ results: compact, count: compact.length }) }] }
|
|
152
183
|
}
|
|
153
184
|
|
|
154
185
|
case 'reason': {
|
|
155
186
|
const entities = a.entities ?? []
|
|
156
187
|
if (entities.length === 0) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: "reason requires 'entities' list" }) }] }
|
|
157
188
|
const results = retriever.reason(entities, { minTrust: a.min_trust ?? minTrust, limit: a.limit ?? 10 })
|
|
158
|
-
|
|
189
|
+
const compact = results.map(toCompactResult)
|
|
190
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ results: compact, count: compact.length }) }] }
|
|
159
191
|
}
|
|
160
192
|
|
|
161
193
|
case 'contradict': {
|
|
@@ -194,6 +226,13 @@ server.tool(
|
|
|
194
226
|
return { content: [{ type: 'text' as const, text: JSON.stringify(report) }] }
|
|
195
227
|
}
|
|
196
228
|
|
|
229
|
+
case 'dream': {
|
|
230
|
+
const report = await store.runDream()
|
|
231
|
+
retriever.getCache().clear()
|
|
232
|
+
resourceManager.invalidate()
|
|
233
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(report) }] }
|
|
234
|
+
}
|
|
235
|
+
|
|
197
236
|
case 'list': {
|
|
198
237
|
const facts = store.listFacts(category, a.min_trust ?? 0.0, a.limit ?? 10)
|
|
199
238
|
return { content: [{ type: 'text' as const, text: JSON.stringify({ facts, count: facts.length }) }] }
|